Bump gEDA version
[geda-gaf.git] / gschem / src / x_highlevel.c
blob4ace05713dfc3fa8f112b1635be296b581f57072
1 /* gEDA - GPL Electronic Design Automation
2 * gschem - gEDA Schematic Capture
3 * Copyright (C) 1998-2010 Ales Hvezda
4 * Copyright (C) 1998-2020 gEDA Contributors (see ChangeLog for details)
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.
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.
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
21 /*! \file x_highlevel.c
22 * \brief High-level page-related functions.
24 * As opposed to the low-level page functions, the high-level page
25 * functions do interact with the user. They switch to a newly
26 * created / opened page, ask for confirmation for potentially
27 * destructive actions (switching pages if necessary), and warn about
28 * major symbol changes.
30 #include <config.h>
32 #include <sys/types.h>
33 #include <sys/stat.h>
35 #include "gschem.h"
38 /*! \brief Check if file on disk is more recent than saved timestamp.
40 * \param [in] filename filename on disk
41 * \param [in] has_known_mtime whether \a known_mtime is assumed to
42 * contain a valid value
43 * \param [in] known_mtime timestamp to compare to
45 * \returns If the file is missing, returns \c FALSE. If no timestamp
46 * was recorded (\a has_known_mtime is \c FALSE) but the file
47 * exists, returns \c TRUE. Otherwise, returns whether the
48 * file on disk is more recent than the saved timestamp.
50 static gboolean
51 file_changed_since (const gchar *filename,
52 gboolean has_known_mtime,
53 struct timespec known_mtime)
55 struct stat buf;
57 if (stat (filename, &buf) == -1)
58 /* file currently doesn't exist */
59 return FALSE;
61 if (!has_known_mtime)
62 /* file has been created on disk */
63 return TRUE;
65 if (buf.st_mtim.tv_sec > known_mtime.tv_sec)
66 /* disk seconds are more recent than known seconds */
67 return TRUE;
68 if (buf.st_mtim.tv_sec < known_mtime.tv_sec)
69 /* disk seconds are older than known seconds (file went back in time?) */
70 return FALSE;
72 if (buf.st_mtim.tv_nsec > known_mtime.tv_nsec)
73 /* disk nanoseconds are more recent than known nanoseconds */
74 return TRUE;
75 /* disk nanoseconds are equal or older than known nanoseconds */
76 return FALSE;
80 /*! \brief Create a new page with a titleblock.
82 * If \a filename is \c NULL, the name of the new page is build from
83 * configuration data ('untitled-name') and a counter for uniqueness.
85 * The page becomes the new current page of \a w_current.
87 * \param [in] w_current the toplevel environment
88 * \param [in] filename the filename for the new page, or \c NULL to
89 * generate an untitled filename
91 * \returns a pointer to the new page, or \c NULL if a file with the
92 * specified filename already exists
94 PAGE *
95 x_highlevel_new_page (GschemToplevel *w_current, const gchar *filename)
97 PAGE *page = x_lowlevel_new_page (w_current, filename);
99 if (page != NULL)
100 x_window_set_current_page (w_current, page);
102 return page;
106 /*! \brief Show "File changed. Reread from disk?" dialog.
108 static gint
109 x_dialog_reread_from_disk (GschemToplevel *w_current, PAGE *page)
111 const gchar *fmt, *secondary_text;
112 GtkWidget *dialog, *button;
113 gint response_id;
115 if (page->exists_on_disk) {
116 fmt = _("The file \"%s\" has changed on disk."),
117 secondary_text = page->CHANGED
118 ? _("Do you want to drop your changes and reload the file?")
119 : _("Do you want to reload it?");
120 } else {
121 fmt = _("The file \"%s\" has been created on disk."),
122 secondary_text = page->CHANGED
123 ? _("Do you want to drop your changes and load the file?")
124 : _("Do you want to open it?");
127 dialog = gtk_message_dialog_new (GTK_WINDOW (w_current->main_window),
128 GTK_DIALOG_MODAL |
129 GTK_DIALOG_DESTROY_WITH_PARENT,
130 GTK_MESSAGE_WARNING,
131 GTK_BUTTONS_NONE,
132 fmt, page->page_filename);
133 g_object_set (dialog, "secondary-text", secondary_text, NULL);
134 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
135 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
136 NULL);
137 button = gtk_button_new_with_mnemonic (page->CHANGED ? _("_Revert") :
138 page->exists_on_disk ? _("_Reload") :
139 _("_Open"));
140 gtk_widget_set_can_default (button, TRUE);
141 gtk_button_set_image (GTK_BUTTON (button),
142 gtk_image_new_from_stock (
143 page->CHANGED ? GTK_STOCK_REVERT_TO_SAVED :
144 page->exists_on_disk ? GTK_STOCK_REFRESH :
145 GTK_STOCK_OPEN,
146 GTK_ICON_SIZE_BUTTON));
147 gtk_widget_show (button);
148 gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
149 GTK_RESPONSE_ACCEPT);
150 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
151 GTK_RESPONSE_ACCEPT,
152 GTK_RESPONSE_CANCEL,
153 -1);
154 gtk_dialog_set_default_response (GTK_DIALOG (dialog),
155 page->CHANGED ? GTK_RESPONSE_CANCEL
156 : GTK_RESPONSE_ACCEPT);
157 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
158 response_id = gtk_dialog_run (GTK_DIALOG (dialog));
159 gtk_widget_destroy (dialog);
161 return response_id;
165 /*! \brief Helper function for \c x_highlevel_open_page(s).
167 * If there is a matching page in \a w_current, returns a pointer to
168 * the existing page. If the file exists, opens a new page from the
169 * file. Otherwise, asks the user for confirmation and, if confirmed,
170 * creates a new page with that name.
172 * \param [in] w_current the toplevel environment
173 * \param [in] filename the name of the file to open/create
174 * \param [in] already_confirmed whether the user has already
175 * confirmed creating the file
177 * \returns a pointer to the page, or \c NULL if the file couldn't be
178 * loaded or the user didn't confirm creating a page
180 static PAGE *
181 open_or_create_page (GschemToplevel *w_current, const gchar *filename,
182 gboolean already_confirmed)
184 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
185 PAGE *page;
186 gchar *full_filename;
188 /* If a page has already been created for this filename, return the
189 existing page.
190 The filename is intentionally not normalized here. If the file
191 doesn't exists, the normalized filename would be NULL; if it does
192 exist, x_lowlevel_open_page() will take care of finding and
193 returning the correct page. */
194 page = s_page_search (toplevel, filename);
195 if (page != NULL) {
196 if (page->page_filename != NULL &&
197 file_changed_since (page->page_filename,
198 page->exists_on_disk,
199 page->last_modified) &&
200 x_dialog_reread_from_disk (w_current, page) == GTK_RESPONSE_ACCEPT) {
201 x_lowlevel_revert_page (w_current, page);
202 return s_page_search (toplevel, filename);
204 return page;
207 /* open page if file exists */
208 full_filename = f_normalize_filename (filename, NULL);
209 if (full_filename != NULL) {
210 g_free (full_filename);
211 return x_lowlevel_open_page (w_current, filename);
214 if (!already_confirmed &&
215 !x_dialog_confirm_create (
216 GTK_WINDOW (w_current->main_window),
217 _("Couldn't find \"%s\".\n\n"
218 "Do you want to create a new file with this name?"),
219 filename))
220 return NULL;
222 return x_lowlevel_new_page (w_current, filename);
226 /*! \brief Open a new page from a file, or find an existing page.
228 * Creates a new page, loads the file in it, and shows the "Major
229 * symbol changes" dialog if major symversion mismatches were found.
230 * If there is already a matching page in \a w_current, returns a
231 * pointer to the existing page instead.
233 * The page becomes the new current page of \a w_current. If there
234 * was exactly one untitled, unchanged page before this operation, the
235 * existing page is closed.
237 * \param [in] w_current the toplevel environment
238 * \param [in] filename the name of the file to open
240 * \returns a pointer to the page, or \c NULL if the file couldn't be
241 * loaded
243 PAGE *
244 x_highlevel_open_page (GschemToplevel *w_current, const gchar *filename)
246 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
247 GList *pages = geda_list_get_glist (toplevel->pages);
248 PAGE *sole_page = g_list_length (pages) == 1 ? (PAGE *) pages->data : NULL;
250 PAGE *page = open_or_create_page (w_current, filename, FALSE);
252 if (sole_page != NULL && page != NULL && page != sole_page &&
253 g_list_find (geda_list_get_glist (toplevel->pages), sole_page) != NULL &&
254 sole_page->is_untitled && !sole_page->CHANGED)
255 x_lowlevel_close_page (w_current, sole_page);
257 if (page != NULL)
258 x_window_set_current_page (w_current, page);
260 /* if there were any symbols which had major changes, put up an
261 error dialog box */
262 major_changed_dialog (w_current);
264 return page;
268 /*! \brief Open multiple pages from files.
270 * For each file specified in \a filenames that is not already opened,
271 * creates a new page and loads the file in it. If major symversion
272 * mismatches were found in any of the files, the "Major symbol
273 * changes" dialog is shown.
275 * The first page that could be opened or already existed becomes the
276 * new current page of \a w_current. If there was exactly one
277 * untitled, unchanged page before this operation, the existing page
278 * is closed.
280 * \param [in] w_current the toplevel environment
281 * \param [in] filenames a GSList of filenames to open
282 * \param [in] already_confirmed whether the user has already
283 * confirmed creating the file(s)
285 * \returns \c TRUE if all files could be opened, \c FALSE otherwise
287 gboolean
288 x_highlevel_open_pages (GschemToplevel *w_current, GSList *filenames,
289 gboolean already_confirmed)
291 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
292 GList *pages = geda_list_get_glist (toplevel->pages);
293 PAGE *sole_page = g_list_length (pages) == 1 ? (PAGE *) pages->data : NULL;
295 PAGE *first_page = NULL;
296 gboolean success = TRUE;
298 /* open each file */
299 for (GSList *l = filenames; l != NULL; l = l->next) {
300 PAGE *page = open_or_create_page (w_current, (gchar *) l->data,
301 already_confirmed);
302 if (page == NULL)
303 success = FALSE;
304 else if (first_page == NULL)
305 first_page = page;
308 if (sole_page != NULL && first_page != NULL && first_page != sole_page &&
309 g_list_find (geda_list_get_glist (toplevel->pages), sole_page) != NULL &&
310 sole_page->is_untitled && !sole_page->CHANGED)
311 x_lowlevel_close_page (w_current, sole_page);
313 if (first_page != NULL)
314 x_window_set_current_page (w_current, first_page);
316 /* if there were any symbols which had major changes, put up an
317 error dialog box */
318 major_changed_dialog (w_current);
320 return success;
324 /*! \brief Show "File changed. Save anyway?" dialog.
326 static gint
327 x_dialog_save_anyway (GschemToplevel *w_current, PAGE *page)
329 gchar *basename;
330 GtkWidget *dialog;
331 gint response_id;
333 basename = g_path_get_basename (page->page_filename);
334 dialog = gtk_message_dialog_new (GTK_WINDOW (w_current->main_window),
335 GTK_DIALOG_MODAL |
336 GTK_DIALOG_DESTROY_WITH_PARENT,
337 GTK_MESSAGE_WARNING,
338 GTK_BUTTONS_NONE,
339 _("\"%s\" changed since visited or "
340 "saved."),
341 basename);
342 g_free (basename);
343 g_object_set (dialog, "secondary-text", _("Save anyway?"), NULL);
344 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
345 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
346 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
347 NULL);
348 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
349 GTK_RESPONSE_ACCEPT,
350 GTK_RESPONSE_CANCEL,
351 -1);
352 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
353 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
354 response_id = gtk_dialog_run (GTK_DIALOG (dialog));
355 gtk_widget_destroy (dialog);
357 return response_id;
361 /*! \brief Save a page.
363 * Saves a page to its current filename. If the page is untitled,
364 * makes it the current page of \a w_current and shows a file chooser
365 * dialog.
367 * \param [in] w_current the toplevel environment
368 * \param [in] page the page to save, or \c NULL to save the
369 * current page
371 * \returns \c TRUE if the page was saved, \c FALSE otherwise
373 gboolean
374 x_highlevel_save_page (GschemToplevel *w_current, PAGE *page)
376 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
378 if (page == NULL) {
379 page = toplevel->page_current;
380 g_return_val_if_fail (page != NULL, FALSE);
383 if (page->is_untitled) {
384 x_window_set_current_page (w_current, page);
385 x_window_present (w_current);
387 return x_fileselect_save (w_current);
390 if (page->page_filename != NULL &&
391 file_changed_since (page->page_filename,
392 page->exists_on_disk,
393 page->last_modified) &&
394 x_dialog_save_anyway (w_current, page) != GTK_RESPONSE_ACCEPT)
395 return FALSE;
397 return x_lowlevel_save_page (w_current, page, page->page_filename);
401 /*! \brief Save all changed pages.
403 * For untitled pages, a file chooser dialog is shown.
405 * \param [in] w_current the toplevel environment
407 * \returns \c TRUE if all changed pages were saved, \c FALSE otherwise
409 gboolean
410 x_highlevel_save_all (GschemToplevel *w_current)
412 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
413 gboolean saved_any = FALSE, success = TRUE;
415 for (const GList *l = geda_list_get_glist (toplevel->pages);
416 l != NULL; l = l->next) {
417 PAGE *page = (PAGE *) l->data;
418 if (!page->CHANGED)
419 /* ignore unchanged pages */
420 continue;
422 if (x_highlevel_save_page (w_current, page))
423 saved_any = TRUE;
424 else
425 success = FALSE;
428 if (!success)
429 i_set_state_msg (w_current, SELECT, _("Failed to Save All"));
430 else if (saved_any)
431 i_set_state_msg (w_current, SELECT, _("Saved All"));
432 else
433 i_set_state_msg (w_current, SELECT, _("No files need saving"));
435 return success;
439 /*! \brief Reload a page from disk.
441 * Closes the page, creates a new page, and reads the file back from
442 * disk. If the page has been changed, makes it the current page of
443 * \a w_current and asks the user for confirmation.
445 * \param [in] w_current the toplevel environment
446 * \param [in] page the page to revert, or \c NULL to revert the
447 * current page
449 * \returns \c TRUE if the page was successfully reloaded, \c FALSE otherwise
451 gboolean
452 x_highlevel_revert_page (GschemToplevel *w_current, PAGE *page)
454 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
456 if (page == NULL) {
457 page = toplevel->page_current;
458 g_return_val_if_fail (page != NULL, FALSE);
461 if (page->is_untitled)
462 /* can't revert untitled page */
463 return FALSE;
465 if (page->CHANGED) {
466 GtkWidget *dialog;
467 int response;
469 x_window_set_current_page (w_current, page);
470 x_window_present (w_current);
472 gchar *basename = g_path_get_basename (page->page_filename);
473 dialog = gtk_message_dialog_new_with_markup (
474 GTK_WINDOW (w_current->main_window),
475 GTK_DIALOG_DESTROY_WITH_PARENT,
476 GTK_MESSAGE_WARNING,
477 GTK_BUTTONS_NONE,
478 _("<big><b>Really revert \"%s\"?</b></big>"),
479 basename);
480 g_free (basename);
481 g_object_set (dialog, "secondary-text",
482 _("By reverting the file to the state saved on disk, "
483 "you will lose your unsaved changes, including all "
484 "undo information."), NULL);
485 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
486 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
487 GTK_STOCK_REVERT_TO_SAVED, GTK_RESPONSE_ACCEPT,
488 NULL);
490 /* Set the alternative button order (ok, cancel, help) for other systems */
491 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
492 GTK_RESPONSE_ACCEPT,
493 GTK_RESPONSE_CANCEL,
494 -1);
496 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
497 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
499 response = gtk_dialog_run (GTK_DIALOG (dialog));
500 gtk_widget_destroy (dialog);
502 if (response != GTK_RESPONSE_ACCEPT)
503 return FALSE;
506 return x_lowlevel_revert_page (w_current, page);
510 /*! \brief Close a page.
512 * If the page has been changed, makes it the current page of \a
513 * toplevel and asks the user for confirmation.
515 * Switches to the next valid page if necessary. If this was the last
516 * page of the toplevel, a new untitled page is created.
518 * \param [in] w_current the toplevel environment
519 * \param [in] page the page to close, or \c NULL to close the
520 * current page
522 * \returns \c TRUE if the page was closed, \c FALSE otherwise
524 gboolean
525 x_highlevel_close_page (GschemToplevel *w_current, PAGE *page)
527 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
529 if (page == NULL) {
530 page = toplevel->page_current;
531 g_return_val_if_fail (page != NULL, FALSE);
534 if (page->CHANGED) {
535 /* Setting the current page is redundant as the close confirmation
536 dialog switches to the current page (is has to, since it may be
537 called when the window is closed while there's a single changed
538 page in the background) but doesn't hurt, either. */
539 x_window_set_current_page (w_current, page);
540 x_window_present (w_current);
542 if (!x_dialog_close_changed_page (w_current, page))
543 return FALSE;
546 x_lowlevel_close_page (w_current, page);
547 return TRUE;