1 /* Lepton EDA Schematic Capture
2  * Copyright (C) 1998-2010 Ales Hvezda
3  * Copyright (C) 1998-2015 gEDA Contributors
4  * Copyright (C) 2017-2021 Lepton EDA Contributors
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19  */
20 #include <config.h>
21 
22 #include "gschem.h"
23 
24 
25 
26 /*! Open/Save dialog file filters
27  */
28 static GtkFileFilter* filter_sch      = NULL;
29 static GtkFileFilter* filter_sym      = NULL;
30 static GtkFileFilter* filter_sch_sym  = NULL;
31 static GtkFileFilter* filter_all      = NULL;
32 
33 /*! Remember the last used filters
34  */
35 static GtkFileFilter* filter_last_opendlg = NULL;
36 
37 static int
38 x_fileselect_load_backup (GschemToplevel *w_current,
39                           GString *message);
40 
41 static void
42 add_filter (GtkFileChooser* filechooser,
43             GtkFileFilter** filter,
44             const gchar*    name,
45             GtkFileFilterFunc pfn);
46 
47 
48 
49 static gboolean
filename_sym(const gchar * fname)50 filename_sym (const gchar* fname)
51 {
52   gchar* str = g_utf8_strdown (fname, -1);
53   gboolean res = g_str_has_suffix (str, ".sym");
54   g_free (str);
55 
56   return res;
57 }
58 
59 
60 
61 static gboolean
filename_sch(const gchar * fname)62 filename_sch (const gchar* fname)
63 {
64   gchar* str = g_utf8_strdown (fname, -1);
65   gboolean res = g_str_has_suffix (str, ".sch");
66   g_free (str);
67 
68   return res;
69 }
70 
71 
72 
73 static gboolean
filter_func_sch(const GtkFileFilterInfo * info,gpointer data)74 filter_func_sch(const GtkFileFilterInfo* info, gpointer data)
75 {
76   return filename_sch (info->filename);
77 }
78 
79 static gboolean
filter_func_sym(const GtkFileFilterInfo * info,gpointer data)80 filter_func_sym(const GtkFileFilterInfo* info, gpointer data)
81 {
82   return filename_sym (info->filename);
83 }
84 
85 static gboolean
filter_func_sch_sym(const GtkFileFilterInfo * info,gpointer data)86 filter_func_sch_sym(const GtkFileFilterInfo* info, gpointer data)
87 {
88   return filename_sch (info->filename) || filename_sym (info->filename);
89 }
90 
91 static gboolean
filter_func_all(const GtkFileFilterInfo * info,gpointer data)92 filter_func_all(const GtkFileFilterInfo* info, gpointer data)
93 {
94   return TRUE;
95 }
96 
97 
98 
99 /*! \brief Creates filter for file chooser.
100  *  \par Function Description
101  *  This function adds file filters to <B>filechooser</B>.
102  *  It also restores the last chosen filters (separate for
103  *  Open and Save dialogs).
104  *
105  *  \param [in] filechooser The file chooser to add filter to.
106  */
107 static void
setup_filters(GtkFileChooser * filechooser)108 setup_filters (GtkFileChooser *filechooser)
109 {
110   add_filter (filechooser, &filter_sch,
111               _("Schematics (*.sch)"), &filter_func_sch);
112 
113   add_filter (filechooser, &filter_sym,
114               _("Symbols (*.sym)"), &filter_func_sym);
115 
116   add_filter (filechooser, &filter_sch_sym,
117               _("Schematics and symbols (*.sch *.sym)"), &filter_func_sch_sym);
118 
119   add_filter (filechooser, &filter_all,
120               _("All files"), &filter_func_all);
121 
122   /* use *.sch filter by default:
123   */
124   if (filter_last_opendlg == NULL)
125   {
126     filter_last_opendlg = filter_sch;
127   }
128 
129 } /* x_fileselect_setup_filechooser_filters() */
130 
131 
132 
133 /*! \brief Replace last 3 chars in filename with \a suffix and get basename
134  *
135  *  \par Function Description
136  *  Example:
137  *  basename_switch_suffix( "/path/to/file.sch", "sym" ) => "file.sym"
138  *  Caller must g_free() the return value.
139  *
140  *  \param  path    Full file path.
141  *  \param  suffix  A new suffix.
142  *
143  *  \return File's basename with suffix replaces with \a suffix
144  */
145 static gchar*
basename_switch_suffix(const gchar * path,const gchar * suffix)146 basename_switch_suffix (const gchar* path, const gchar* suffix)
147 {
148   gchar* bname = g_path_get_basename (path);
149   if (bname == NULL)
150   {
151     return NULL;
152   }
153 
154   gchar* name      = (gchar*) malloc (PATH_MAX);
155   glong  len_bname = g_utf8_strlen (bname, -1);
156 
157   const gsize len_suffix = 3;
158   g_utf8_strncpy (name, bname, len_bname - len_suffix);
159 
160   gchar* bname_new = g_strconcat (name, suffix, NULL);
161 
162 #ifdef DEBUG
163   printf( " .. bname:     [%s]\n",  bname );
164   printf( " .. len_bname: [%lu]\n", len_bname );
165   printf( " .. name:      [%s]\n",  name );
166   printf( " .. bname_new: [%s]\n",  bname_new );
167 #endif
168 
169   g_free (name);
170   g_free (bname);
171 
172   return bname_new;
173 
174 } /* basename_switch_suffix() */
175 
176 
177 
178 /*! \brief Dialog's "filter" property change notification handler
179  *
180  *  \par Function Description
181  *  Change filename's extension (.sch or .sym) in the "Save As"
182  *  dialog according to the currently selected filter.
183  */
184 static void
on_filter_changed(GtkFileChooserDialog * dialog,gpointer data)185 on_filter_changed (GtkFileChooserDialog* dialog, gpointer data)
186 {
187   GtkFileChooser* chooser = GTK_FILE_CHOOSER (dialog);
188   GtkFileFilter*  filter  = gtk_file_chooser_get_filter (chooser);
189 
190   gchar* fname = gtk_file_chooser_get_filename (chooser);
191   if (fname == NULL)
192   {
193     return;
194   }
195 
196 
197   gchar* bname = NULL;
198 
199   if (filter == filter_sch && filename_sym (fname))
200   {
201     bname = basename_switch_suffix (fname, "sch");
202   }
203   else
204   if (filter == filter_sym && filename_sch (fname))
205   {
206     bname = basename_switch_suffix (fname, "sym");
207   }
208 
209   if (bname != NULL)
210   {
211     gtk_file_chooser_set_current_name (chooser, bname);
212     g_free (bname);
213   }
214 
215 } /* on_filter_changed() */
216 
217 
218 
219 /*! \brief Updates the preview when the selection changes.
220  *  \par Function Description
221  *  This is the callback function connected to the 'update-preview'
222  *  signal of the <B>GtkFileChooser</B>.
223  *
224  *  It updates the preview widget with the name of the newly selected
225  *  file.
226  *
227  *  \param [in] chooser   The file chooser to add the preview to.
228  *  \param [in] user_data A pointer on the preview widget.
229  */
230 static void
x_fileselect_callback_update_preview(GtkFileChooser * chooser,gpointer user_data)231 x_fileselect_callback_update_preview (GtkFileChooser *chooser,
232                                       gpointer user_data)
233 {
234   GschemPreview *preview = GSCHEM_PREVIEW (user_data);
235   gchar *filename, *preview_filename = NULL;
236 
237   filename = gtk_file_chooser_get_preview_filename (chooser);
238   if (filename != NULL &&
239       !g_file_test (filename, G_FILE_TEST_IS_DIR)) {
240     preview_filename = filename;
241   }
242 
243   /* update preview */
244   g_object_set (preview,
245                 "width-request",  160,
246                 "height-request", 120,
247                 "filename", preview_filename,
248                 "active", (preview_filename != NULL),
249                 NULL);
250 
251   g_free (filename);
252 }
253 
254 /*! \brief Adds a preview to a file chooser.
255  *  \par Function Description
256  *  This function adds a preview section to the stock
257  *  <B>GtkFileChooser</B>.
258  *
259  *  The <B>Preview</B> object is inserted in a frame and alignment
260  *  widget for accurate positionning.
261  *
262  *  Other widgets can be added to this preview area for example to
263  *  enable/disable the preview. Currently, the preview is always
264  *  active.
265  *
266  *  Function <B>x_fileselect_callback_update_preview()</B> is
267  *  connected to the signal 'update-preview' of <B>GtkFileChooser</B>
268  *  so that it redraws the preview area every time a new file is
269  *  selected.
270  *
271  *  \param [in] filechooser The file chooser to add the preview to.
272  */
273 static void
x_fileselect_add_preview(GtkFileChooser * filechooser)274 x_fileselect_add_preview (GtkFileChooser *filechooser)
275 {
276   GtkWidget *alignment, *frame, *preview;
277 
278   frame = GTK_WIDGET (g_object_new (GTK_TYPE_FRAME,
279                                     "label", _("Preview"),
280                                     NULL));
281   alignment = GTK_WIDGET (g_object_new (GTK_TYPE_ALIGNMENT,
282                                         "right-padding", 5,
283                                         "left-padding", 5,
284                                         "xscale", 0.0,
285                                         "yscale", 0.0,
286                                         "xalign", 0.5,
287                                         "yalign", 0.5,
288                                         NULL));
289 
290   preview = gschem_preview_new ();
291 
292   gtk_container_add (GTK_CONTAINER (alignment), preview);
293   gtk_container_add (GTK_CONTAINER (frame), alignment);
294   gtk_widget_show_all (frame);
295 
296   g_object_set (filechooser,
297                 /* GtkFileChooser */
298                 "use-preview-label", FALSE,
299                 "preview-widget", frame,
300                 NULL);
301 
302   /* connect callback to update preview */
303   g_signal_connect (filechooser,
304                     "update-preview",
305                     G_CALLBACK (x_fileselect_callback_update_preview),
306                     preview);
307 
308 }
309 
310 /*! \brief Opens a file chooser for opening one or more schematics.
311  *  \par Function Description
312  *  This function opens a file chooser dialog and wait for the user to
313  *  select at least one file to load as <B>w_current</B>'s new pages.
314  *
315  *  The function updates the user interface.
316  *
317  *  At the end of the function, the w_current->toplevel's current page
318  *  is set to the page of the last loaded page.
319  *
320  *  \param [in] w_current The GschemToplevel environment.
321  */
322 void
x_fileselect_open(GschemToplevel * w_current)323 x_fileselect_open(GschemToplevel *w_current)
324 {
325   LeptonPage *page = NULL;
326   GtkWidget *dialog;
327   gchar *cwd;
328 
329   dialog = gtk_file_chooser_dialog_new (_("Open"),
330                                         GTK_WINDOW(w_current->main_window),
331                                         GTK_FILE_CHOOSER_ACTION_OPEN,
332                                         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
333                                         GTK_STOCK_OPEN,   GTK_RESPONSE_ACCEPT,
334                                         NULL);
335 
336   /* Set the alternative button order (ok, cancel, help) for other systems */
337   gtk_dialog_set_alternative_button_order(GTK_DIALOG(dialog),
338                                           GTK_RESPONSE_ACCEPT,
339                                           GTK_RESPONSE_CANCEL,
340                                           -1);
341 
342   if (w_current->file_preview)
343   {
344     x_fileselect_add_preview (GTK_FILE_CHOOSER (dialog));
345   }
346 
347   g_object_set (dialog,
348                 /* GtkFileChooser */
349                 "select-multiple", TRUE,
350                 NULL);
351 
352   /* add file filters to dialog */
353   setup_filters (GTK_FILE_CHOOSER (dialog));
354   /* restore last filter: */
355   gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter_last_opendlg);
356 
357   /* force start in current working directory, not in 'Recently Used' */
358   cwd = g_get_current_dir ();
359   gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd);
360   g_free (cwd);
361   gtk_widget_show (dialog);
362   if (gtk_dialog_run ((GtkDialog*)dialog) == GTK_RESPONSE_ACCEPT) {
363 
364     /* remember current filter: */
365     filter_last_opendlg = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (dialog));
366 
367     GSList *tmp, *filenames =
368       gtk_file_chooser_get_filenames (GTK_FILE_CHOOSER (dialog));
369 
370     /* open each file */
371     for (tmp = filenames; tmp != NULL;tmp = g_slist_next (tmp)) {
372       page = x_window_open_page (w_current, (gchar*)tmp->data);
373     }
374     /* Switch to the last page opened */
375     if ( page != NULL )
376       x_window_set_current_page (w_current, page);
377 
378     /* free the list of filenames */
379     g_slist_foreach (filenames, (GFunc)g_free, NULL);
380     g_slist_free (filenames);
381   }
382   gtk_widget_destroy (dialog);
383 
384 }
385 
386 
387 
388 /*! \brief Opens a file chooser for saving the current page.
389  *  \par Function Description
390  *  This function opens a file chooser dialog and wait for the user to
391  *  select a file where the \a page will be saved.
392  *
393  *  If the user cancels the operation (with the cancel button), the
394  *  page is not saved and FALSE is returned.
395  *
396  *  The function updates the user interface. (Actual UI update
397  *  is performed in x_window_save_page(), which is called by this
398  *  function).
399  *
400  *  \param  [in]     w_current The GschemToplevel environment.
401  *  \param  [in]     page      The page to be saved.
402  *  \param  [in,out] result    If not NULL, will be filled with save operation result.
403  *  \return                    TRUE if dialog was closed with ACCEPT response.
404  */
405 gboolean
x_fileselect_save(GschemToplevel * w_current,LeptonPage * page,gboolean * result)406 x_fileselect_save (GschemToplevel *w_current,
407                    LeptonPage* page,
408                    gboolean* result)
409 {
410   g_return_val_if_fail (w_current != NULL, FALSE);
411   g_return_val_if_fail (page != NULL, FALSE);
412 
413   gboolean ret = FALSE;
414   if (result != NULL)
415   {
416     *result = FALSE;
417   }
418 
419   GtkWidget* dialog = gtk_file_chooser_dialog_new(
420     _("Save As"),
421     GTK_WINDOW(w_current->main_window),
422     GTK_FILE_CHOOSER_ACTION_SAVE,
423     GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
424     GTK_STOCK_SAVE,   GTK_RESPONSE_ACCEPT,
425     NULL);
426 
427   /* Set the alternative button order (ok, cancel, help) for other systems:
428   */
429   gtk_dialog_set_alternative_button_order(GTK_DIALOG(dialog),
430                                           GTK_RESPONSE_ACCEPT,
431                                           GTK_RESPONSE_CANCEL,
432                                           -1);
433 
434   /* set default response signal. This is usually triggered by the
435    * "Return" key:
436   */
437   gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
438 
439   g_object_set (dialog,
440                 /* GtkFileChooser */
441                 "select-multiple", FALSE,
442                 /* only in GTK 2.8 */
443                 /* "do-overwrite-confirmation", TRUE, */
444                 NULL);
445 
446   /* add file filters to dialog:
447   */
448   setup_filters (GTK_FILE_CHOOSER (dialog));
449   const gchar* fname = lepton_page_get_filename (page);
450 
451   if (filename_sch (fname))
452   {
453     gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter_sch);
454   }
455   else
456   if (filename_sym (fname))
457   {
458     gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter_sym);
459   }
460   else
461   {
462     gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter_all);
463   }
464 
465   /* set the current filename or directory name if new document:
466   */
467   if (g_file_test (fname, G_FILE_TEST_EXISTS))
468   {
469     gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (dialog), fname);
470   }
471   else
472   {
473     gchar *cwd = g_get_current_dir ();
474 
475     /* force save in current working dir:
476     */
477     gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd);
478     g_free (cwd);
479 
480     /* set page file's basename as the current filename:
481     */
482     gchar* bname = g_path_get_basename (fname);
483     gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), bname);
484     g_free (bname);
485   }
486 
487 
488   /* add handler for dialog's "filter" property change notification:
489   */
490   g_signal_connect (dialog,
491                     "notify::filter",
492                     G_CALLBACK (&on_filter_changed),
493                     NULL);
494 
495 
496   /*
497    * Open "Save As.." dialog:
498   */
499 
500   gtk_widget_show (dialog);
501   if (gtk_dialog_run ((GtkDialog*)dialog) == GTK_RESPONSE_ACCEPT)
502   {
503     gchar *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
504 
505     /* If the file already exists, display a dialog box to check if
506        the user really wants to overwrite it:
507     */
508     if ((filename != NULL) && g_file_test (filename, G_FILE_TEST_EXISTS))
509     {
510       GtkWidget *checkdialog =
511         gtk_message_dialog_new (GTK_WINDOW(dialog),
512                                 (GtkDialogFlags) (GTK_DIALOG_MODAL |
513                                                   GTK_DIALOG_DESTROY_WITH_PARENT),
514                                 GTK_MESSAGE_QUESTION,
515                                 GTK_BUTTONS_YES_NO,
516                                 _("The selected file `%1$s' already exists.\n\n"
517                                   "Would you like to overwrite it?"),
518                                 filename);
519 
520       gtk_window_set_title (GTK_WINDOW (checkdialog), _("Overwrite file?"));
521       gtk_dialog_set_default_response (GTK_DIALOG (checkdialog), GTK_RESPONSE_NO);
522 
523       if (gtk_dialog_run (GTK_DIALOG (checkdialog)) != GTK_RESPONSE_YES)
524       {
525         g_message (_("Save cancelled on user request"));
526         g_free (filename);
527         filename = NULL;
528       }
529 
530       gtk_widget_destroy (checkdialog);
531     }
532 
533 
534     /* try saving the page to file filename:
535     */
536     if (filename != NULL)
537     {
538       ret = TRUE;
539 
540       gboolean res = x_window_save_page (w_current, page, filename);
541 
542       if (result != NULL)
543       {
544         *result = res;
545       }
546     }
547 
548     g_free (filename);
549 
550   } /* if: accept response */
551 
552   gtk_widget_destroy (dialog);
553 
554   return ret;
555 
556 } /* x_fileselect_save() */
557 
558 
559 
560 /*! \brief Load/Backup selection dialog.
561  *  \par Function Description
562  *  This function opens a message dialog and wait for the user to choose
563  *  if load the backup or the original file.
564  *
565  *  \todo Make this a registered callback function with user data,
566  *        as we'd rather be passed a GschemToplevel than a LeptonToplevel.
567  *
568  *  \param [in] user_data The GschemToplevel object.
569  *  \param [in] message   Message to display to user.
570  *  \return TRUE if the user wants to load the backup file, FALSE otherwise.
571  */
572 static int
x_fileselect_load_backup(GschemToplevel * w_current,GString * message)573 x_fileselect_load_backup (GschemToplevel *w_current,
574                           GString *message)
575 {
576   GtkWidget *dialog;
577 
578   g_string_append(message, _(
579 "\n"
580 "If you load the original file, the backup file "
581 "will be overwritten in the next autosave timeout and it will be lost."
582 "\n\n"
583 "Do you want to load the backup file?\n"));
584 
585   dialog = gtk_message_dialog_new (GTK_WINDOW(w_current->main_window),
586                                    GTK_DIALOG_MODAL,
587                                    GTK_MESSAGE_QUESTION,
588                                    GTK_BUTTONS_YES_NO,
589                                    "%s", message->str);
590 
591   gtk_window_set_title (GTK_WINDOW (dialog), "Load Backup");
592   /* Set the alternative button order (ok, cancel, help) for other systems */
593   gtk_dialog_set_alternative_button_order(GTK_DIALOG(dialog),
594                                           GTK_RESPONSE_YES,
595                                           GTK_RESPONSE_NO,
596                                           -1);
597 
598   gtk_widget_show (dialog);
599   if (gtk_dialog_run ((GtkDialog*)dialog) == GTK_RESPONSE_YES) {
600     gtk_widget_destroy(dialog);
601     return TRUE;
602   }
603   else {
604     gtk_widget_destroy(dialog);
605     return FALSE;
606   }
607 }
608 
609 
610 gboolean
schematic_file_open(GschemToplevel * w_current,LeptonPage * page,const gchar * filename,GError ** err)611 schematic_file_open (GschemToplevel *w_current,
612                      LeptonPage *page,
613                      const gchar *filename,
614                      GError **err)
615 {
616   g_return_val_if_fail ((w_current != NULL), FALSE);
617 
618   GError *tmp_err = NULL;
619   gboolean stat_error = FALSE;
620   gint flags = F_OPEN_RC;
621   gboolean active_backup = f_has_active_autosave (filename, &tmp_err);
622 
623   if (tmp_err != NULL) {
624     g_warning ("%s\n", tmp_err->message);
625     g_error_free (tmp_err);
626     stat_error = TRUE;
627   }
628 
629   if (active_backup) {
630     gchar *backup_filename = f_get_autosave_filename (filename);
631     GString *message = f_backup_message (backup_filename, stat_error);
632     if (x_fileselect_load_backup (w_current, message)) {
633       flags |= F_OPEN_FORCE_BACKUP;
634     }
635 
636     g_string_free (message, TRUE);
637     g_free (backup_filename);
638   }
639 
640   return f_open_flags (gschem_toplevel_get_toplevel (w_current),
641                        page, filename, flags, err);
642 }
643 
644 
645 
646 /*! \brief Add a file chooser filter.
647  *
648  *  \param [in]      filechooser  GtkFileChooser
649  *  \param [in, out] filter       filter to set up
650  *  \param [in]      name         filter display name
651  *  \param [in]      pfn          filter function
652  */
653 static void
add_filter(GtkFileChooser * filechooser,GtkFileFilter ** filter,const gchar * name,GtkFileFilterFunc pfn)654 add_filter (GtkFileChooser* filechooser,
655             GtkFileFilter** filter,
656             const gchar*    name,
657             GtkFileFilterFunc pfn)
658 {
659   if (*filter == NULL)
660   {
661     *filter = gtk_file_filter_new();
662 
663     gtk_file_filter_set_name (*filter, name);
664     gtk_file_filter_add_custom (*filter, GTK_FILE_FILTER_FILENAME, pfn, NULL, NULL);
665 
666     /* GtkFileChooser takes ownership of the filter.
667      * ++ ref count to keep it alive after chooser is destroyed.
668     */
669     g_object_ref (G_OBJECT (*filter));
670   }
671 
672   gtk_file_chooser_add_filter (filechooser, *filter);
673 }
674