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