Merge branch 'stable-1.10'
[geda-gaf.git] / gschem / src / x_highlevel.c
blob10db725884addd467c5c6c4169cec1810152179a
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 and ask for confirmation for potentially
27 * destructive actions (switching pages if necessary).
29 #include <config.h>
31 #include <sys/types.h>
32 #include <sys/stat.h>
34 #include "gschem.h"
37 /*! \brief Check if file on disk is more recent than saved timestamp.
39 * \param [in] filename filename on disk
40 * \param [in] has_known_mtime whether \a known_mtime is assumed to
41 * contain a valid value
42 * \param [in] known_mtime timestamp to compare to
44 * \returns If the file is missing, returns \c FALSE. If no timestamp
45 * was recorded (\a has_known_mtime is \c FALSE) but the file
46 * exists, returns \c TRUE. Otherwise, returns whether the
47 * file on disk is more recent than the saved timestamp.
49 static gboolean
50 file_changed_since (const gchar *filename,
51 gboolean has_known_mtime,
52 struct timespec known_mtime)
54 struct stat buf;
56 if (stat (filename, &buf) == -1)
57 /* file currently doesn't exist */
58 return FALSE;
60 if (!has_known_mtime)
61 /* file has been created on disk */
62 return TRUE;
64 if (buf.st_mtim.tv_sec > known_mtime.tv_sec)
65 /* disk seconds are more recent than known seconds */
66 return TRUE;
67 if (buf.st_mtim.tv_sec < known_mtime.tv_sec)
68 /* disk seconds are older than known seconds (file went back in time?) */
69 return FALSE;
71 if (buf.st_mtim.tv_nsec > known_mtime.tv_nsec)
72 /* disk nanoseconds are more recent than known nanoseconds */
73 return TRUE;
74 /* disk nanoseconds are equal or older than known nanoseconds */
75 return FALSE;
79 /*! \brief Create a new page with a titleblock.
81 * If \a filename is \c NULL, the name of the new page is build from
82 * configuration data ('untitled-name') and a counter for uniqueness.
84 * The page becomes the new current page of \a w_current.
86 * \param [in] w_current the toplevel environment
87 * \param [in] filename the filename for the new page, or \c NULL to
88 * generate an untitled filename
90 * \returns a pointer to the new page, or \c NULL if a file with the
91 * specified filename already exists
93 PAGE *
94 x_highlevel_new_page (GschemToplevel *w_current, const gchar *filename)
96 PAGE *page = x_lowlevel_new_page (w_current, filename);
98 if (page != NULL)
99 x_window_set_current_page (w_current, page);
101 return page;
105 /*! \brief Show "File changed. Reread from disk?" dialog.
107 static gint
108 x_dialog_reread_from_disk (GschemToplevel *w_current, PAGE *page)
110 const gchar *fmt, *secondary_text;
111 GtkWidget *dialog, *button;
112 gint response_id;
114 if (page->exists_on_disk) {
115 fmt = _("The file \"%s\" has changed on disk."),
116 secondary_text = page->CHANGED
117 ? _("Do you want to drop your changes and reload the file?")
118 : _("Do you want to reload it?");
119 } else {
120 fmt = _("The file \"%s\" has been created on disk."),
121 secondary_text = page->CHANGED
122 ? _("Do you want to drop your changes and load the file?")
123 : _("Do you want to open it?");
126 dialog = gtk_message_dialog_new (GTK_WINDOW (w_current->main_window),
127 GTK_DIALOG_MODAL |
128 GTK_DIALOG_DESTROY_WITH_PARENT,
129 GTK_MESSAGE_WARNING,
130 GTK_BUTTONS_NONE,
131 fmt, page->page_filename);
132 g_object_set (dialog, "secondary-text", secondary_text, NULL);
133 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
134 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
135 NULL);
136 button = gtk_button_new_with_mnemonic (page->CHANGED ? _("_Revert") :
137 page->exists_on_disk ? _("_Reload") :
138 _("_Open"));
139 gtk_widget_set_can_default (button, TRUE);
140 gtk_button_set_image (GTK_BUTTON (button),
141 gtk_image_new_from_stock (
142 page->CHANGED ? GTK_STOCK_REVERT_TO_SAVED :
143 page->exists_on_disk ? GTK_STOCK_REFRESH :
144 GTK_STOCK_OPEN,
145 GTK_ICON_SIZE_BUTTON));
146 gtk_widget_show (button);
147 gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
148 GTK_RESPONSE_ACCEPT);
149 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
150 GTK_RESPONSE_ACCEPT,
151 GTK_RESPONSE_CANCEL,
152 -1);
153 gtk_dialog_set_default_response (GTK_DIALOG (dialog),
154 page->CHANGED ? GTK_RESPONSE_CANCEL
155 : GTK_RESPONSE_ACCEPT);
156 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
157 response_id = gtk_dialog_run (GTK_DIALOG (dialog));
158 gtk_widget_destroy (dialog);
160 return response_id;
164 /*! \brief Helper function for \c x_highlevel_open_page(s).
166 * If there is a matching page in \a w_current, returns a pointer to
167 * the existing page. If the file exists, opens a new page from the
168 * file. Otherwise, asks the user for confirmation and, if confirmed,
169 * creates a new page with that name.
171 * \param [in] w_current the toplevel environment
172 * \param [in] filename the name of the file to open/create
173 * \param [in] already_confirmed whether the user has already
174 * confirmed creating the file
176 * \returns a pointer to the page, or \c NULL if the file couldn't be
177 * loaded or the user didn't confirm creating a page
179 static PAGE *
180 open_or_create_page (GschemToplevel *w_current, const gchar *filename,
181 gboolean already_confirmed)
183 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
184 PAGE *page;
185 gchar *full_filename;
187 /* If a page has already been created for this filename, return the
188 existing page.
189 The filename is intentionally not normalized here. If the file
190 doesn't exists, the normalized filename would be NULL; if it does
191 exist, x_lowlevel_open_page() will take care of finding and
192 returning the correct page. */
193 page = s_page_search (toplevel, filename);
194 if (page != NULL) {
195 if (page->page_filename != NULL &&
196 file_changed_since (page->page_filename,
197 page->exists_on_disk,
198 page->last_modified) &&
199 x_dialog_reread_from_disk (w_current, page) == GTK_RESPONSE_ACCEPT) {
200 x_lowlevel_revert_page (w_current, page);
201 return s_page_search (toplevel, filename);
203 return page;
206 /* open page if file exists */
207 full_filename = f_normalize_filename (filename, NULL);
208 if (full_filename != NULL) {
209 g_free (full_filename);
210 return x_lowlevel_open_page (w_current, filename);
213 if (!already_confirmed &&
214 !x_dialog_confirm_create (
215 GTK_WINDOW (w_current->main_window),
216 _("Couldn't find \"%s\".\n\n"
217 "Do you want to create a new file with this name?"),
218 filename))
219 return NULL;
221 return x_lowlevel_new_page (w_current, filename);
225 /*! \brief Open a new page from a file, or find an existing page.
227 * Creates a new page and loads the file in it. If there is already a
228 * matching page in \a w_current, returns a pointer to the existing
229 * page instead.
231 * The page becomes the new current page of \a w_current. If there
232 * was exactly one untitled, unchanged page before this operation, the
233 * existing page is closed.
235 * \param [in] w_current the toplevel environment
236 * \param [in] filename the name of the file to open
238 * \returns a pointer to the page, or \c NULL if the file couldn't be
239 * loaded
241 PAGE *
242 x_highlevel_open_page (GschemToplevel *w_current, const gchar *filename)
244 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
245 GList *pages = geda_list_get_glist (toplevel->pages);
246 PAGE *sole_page = g_list_length (pages) == 1 ? (PAGE *) pages->data : NULL;
248 PAGE *page = open_or_create_page (w_current, filename, FALSE);
250 if (sole_page != NULL && page != NULL && page != sole_page &&
251 g_list_find (geda_list_get_glist (toplevel->pages), sole_page) != NULL &&
252 sole_page->is_untitled && !sole_page->CHANGED)
253 x_lowlevel_close_page (w_current, sole_page);
255 if (page != NULL)
256 x_window_set_current_page (w_current, page);
258 return page;
262 /*! \brief Open multiple pages from files.
264 * For each file specified in \a filenames that is not already opened,
265 * creates a new page and loads the file in it.
267 * The first page that could be opened or already existed becomes the
268 * new current page of \a w_current. If there was exactly one
269 * untitled, unchanged page before this operation, the existing page
270 * is closed.
272 * \param [in] w_current the toplevel environment
273 * \param [in] filenames a GSList of filenames to open
274 * \param [in] already_confirmed whether the user has already
275 * confirmed creating the file(s)
277 * \returns \c TRUE if all files could be opened, \c FALSE otherwise
279 gboolean
280 x_highlevel_open_pages (GschemToplevel *w_current, GSList *filenames,
281 gboolean already_confirmed)
283 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
284 GList *pages = geda_list_get_glist (toplevel->pages);
285 PAGE *sole_page = g_list_length (pages) == 1 ? (PAGE *) pages->data : NULL;
287 PAGE *first_page = NULL;
288 gboolean success = TRUE;
290 /* open each file */
291 for (GSList *l = filenames; l != NULL; l = l->next) {
292 PAGE *page = open_or_create_page (w_current, (gchar *) l->data,
293 already_confirmed);
294 if (page == NULL)
295 success = FALSE;
296 else if (first_page == NULL)
297 first_page = page;
300 if (sole_page != NULL && first_page != NULL && first_page != sole_page &&
301 g_list_find (geda_list_get_glist (toplevel->pages), sole_page) != NULL &&
302 sole_page->is_untitled && !sole_page->CHANGED)
303 x_lowlevel_close_page (w_current, sole_page);
305 if (first_page != NULL)
306 x_window_set_current_page (w_current, first_page);
308 return success;
312 /*! \brief Show "File changed. Save anyway?" dialog.
314 static gint
315 x_dialog_save_anyway (GschemToplevel *w_current, PAGE *page)
317 gchar *basename;
318 GtkWidget *dialog;
319 gint response_id;
321 basename = g_path_get_basename (page->page_filename);
322 dialog = gtk_message_dialog_new (GTK_WINDOW (w_current->main_window),
323 GTK_DIALOG_MODAL |
324 GTK_DIALOG_DESTROY_WITH_PARENT,
325 GTK_MESSAGE_WARNING,
326 GTK_BUTTONS_NONE,
327 _("\"%s\" changed since visited or "
328 "saved."),
329 basename);
330 g_free (basename);
331 g_object_set (dialog, "secondary-text", _("Save anyway?"), NULL);
332 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
333 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
334 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
335 NULL);
336 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
337 GTK_RESPONSE_ACCEPT,
338 GTK_RESPONSE_CANCEL,
339 -1);
340 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
341 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
342 response_id = gtk_dialog_run (GTK_DIALOG (dialog));
343 gtk_widget_destroy (dialog);
345 return response_id;
349 /*! \brief Save a page.
351 * Saves a page to its current filename. If the page is untitled,
352 * makes it the current page of \a w_current and shows a file chooser
353 * dialog.
355 * \param [in] w_current the toplevel environment
356 * \param [in] page the page to save, or \c NULL to save the
357 * current page
359 * \returns \c TRUE if the page was saved, \c FALSE otherwise
361 gboolean
362 x_highlevel_save_page (GschemToplevel *w_current, PAGE *page)
364 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
366 if (page == NULL) {
367 page = toplevel->page_current;
368 g_return_val_if_fail (page != NULL, FALSE);
371 if (page->is_untitled) {
372 x_window_set_current_page (w_current, page);
373 x_window_present (w_current);
375 return x_fileselect_save (w_current);
378 if (page->page_filename != NULL &&
379 file_changed_since (page->page_filename,
380 page->exists_on_disk,
381 page->last_modified) &&
382 x_dialog_save_anyway (w_current, page) != GTK_RESPONSE_ACCEPT)
383 return FALSE;
385 return x_lowlevel_save_page (w_current, page, page->page_filename);
389 /*! \brief Save all changed pages.
391 * For untitled pages, a file chooser dialog is shown.
393 * \param [in] w_current the toplevel environment
395 * \returns \c TRUE if all changed pages were saved, \c FALSE otherwise
397 gboolean
398 x_highlevel_save_all (GschemToplevel *w_current)
400 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
401 gboolean saved_any = FALSE, success = TRUE;
403 for (const GList *l = geda_list_get_glist (toplevel->pages);
404 l != NULL; l = l->next) {
405 PAGE *page = (PAGE *) l->data;
406 if (!page->CHANGED)
407 /* ignore unchanged pages */
408 continue;
410 if (x_highlevel_save_page (w_current, page))
411 saved_any = TRUE;
412 else
413 success = FALSE;
416 if (!success)
417 i_set_state_msg (w_current, SELECT, _("Failed to Save All"));
418 else if (saved_any)
419 i_set_state_msg (w_current, SELECT, _("Saved All"));
420 else
421 i_set_state_msg (w_current, SELECT, _("No files need saving"));
423 return success;
427 /*! \brief Reload a page from disk.
429 * Closes the page, creates a new page, and reads the file back from
430 * disk. If the page has been changed, makes it the current page of
431 * \a w_current and asks the user for confirmation.
433 * \param [in] w_current the toplevel environment
434 * \param [in] page the page to revert, or \c NULL to revert the
435 * current page
437 * \returns \c TRUE if the page was successfully reloaded, \c FALSE otherwise
439 gboolean
440 x_highlevel_revert_page (GschemToplevel *w_current, PAGE *page)
442 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
444 if (page == NULL) {
445 page = toplevel->page_current;
446 g_return_val_if_fail (page != NULL, FALSE);
449 if (page->is_untitled)
450 /* can't revert untitled page */
451 return FALSE;
453 if (page->CHANGED) {
454 GtkWidget *dialog;
455 int response;
457 x_window_set_current_page (w_current, page);
458 x_window_present (w_current);
460 gchar *basename = g_path_get_basename (page->page_filename);
461 dialog = gtk_message_dialog_new_with_markup (
462 GTK_WINDOW (w_current->main_window),
463 GTK_DIALOG_DESTROY_WITH_PARENT,
464 GTK_MESSAGE_WARNING,
465 GTK_BUTTONS_NONE,
466 _("<big><b>Really revert \"%s\"?</b></big>"),
467 basename);
468 g_free (basename);
469 g_object_set (dialog, "secondary-text",
470 _("By reverting the file to the state saved on disk, "
471 "you will lose your unsaved changes, including all "
472 "undo information."), NULL);
473 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
474 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
475 GTK_STOCK_REVERT_TO_SAVED, GTK_RESPONSE_ACCEPT,
476 NULL);
478 /* Set the alternative button order (ok, cancel, help) for other systems */
479 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
480 GTK_RESPONSE_ACCEPT,
481 GTK_RESPONSE_CANCEL,
482 -1);
484 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
485 gtk_window_set_title (GTK_WINDOW (dialog), _("gschem"));
487 response = gtk_dialog_run (GTK_DIALOG (dialog));
488 gtk_widget_destroy (dialog);
490 if (response != GTK_RESPONSE_ACCEPT)
491 return FALSE;
494 return x_lowlevel_revert_page (w_current, page);
498 /*! \brief Close a page.
500 * If the page has been changed, makes it the current page of \a
501 * toplevel and asks the user for confirmation.
503 * Switches to the next valid page if necessary. If this was the last
504 * page of the toplevel, a new untitled page is created.
506 * \param [in] w_current the toplevel environment
507 * \param [in] page the page to close, or \c NULL to close the
508 * current page
510 * \returns \c TRUE if the page was closed, \c FALSE otherwise
512 gboolean
513 x_highlevel_close_page (GschemToplevel *w_current, PAGE *page)
515 TOPLEVEL *toplevel = gschem_toplevel_get_toplevel (w_current);
517 if (page == NULL) {
518 page = toplevel->page_current;
519 g_return_val_if_fail (page != NULL, FALSE);
522 if (page->CHANGED) {
523 /* Setting the current page is redundant as the close confirmation
524 dialog switches to the current page (is has to, since it may be
525 called when the window is closed while there's a single changed
526 page in the background) but doesn't hurt, either. */
527 x_window_set_current_page (w_current, page);
528 x_window_present (w_current);
530 if (!x_dialog_close_changed_page (w_current, page))
531 return FALSE;
534 x_lowlevel_close_page (w_current, page);
535 return TRUE;