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.
32 #include <sys/types.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.
51 file_changed_since (const gchar
*filename
,
52 gboolean has_known_mtime
,
53 struct timespec known_mtime
)
57 if (stat (filename
, &buf
) == -1)
58 /* file currently doesn't exist */
62 /* file has been created on disk */
65 if (buf
.st_mtim
.tv_sec
> known_mtime
.tv_sec
)
66 /* disk seconds are more recent than known seconds */
68 if (buf
.st_mtim
.tv_sec
< known_mtime
.tv_sec
)
69 /* disk seconds are older than known seconds (file went back in time?) */
72 if (buf
.st_mtim
.tv_nsec
> known_mtime
.tv_nsec
)
73 /* disk nanoseconds are more recent than known nanoseconds */
75 /* disk nanoseconds are equal or older than known nanoseconds */
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
95 x_highlevel_new_page (GschemToplevel
*w_current
, const gchar
*filename
)
97 PAGE
*page
= x_lowlevel_new_page (w_current
, filename
);
100 x_window_set_current_page (w_current
, page
);
106 /*! \brief Show "File changed. Reread from disk?" dialog.
109 x_dialog_reread_from_disk (GschemToplevel
*w_current
, PAGE
*page
)
111 const gchar
*fmt
, *secondary_text
;
112 GtkWidget
*dialog
, *button
;
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?");
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
),
129 GTK_DIALOG_DESTROY_WITH_PARENT
,
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
,
137 button
= gtk_button_new_with_mnemonic (page
->CHANGED
? _("_Revert") :
138 page
->exists_on_disk
? _("_Reload") :
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
:
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
),
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
);
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
181 open_or_create_page (GschemToplevel
*w_current
, const gchar
*filename
,
182 gboolean already_confirmed
)
184 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
186 gchar
*full_filename
;
188 /* If a page has already been created for this filename, return the
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
);
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
);
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?"),
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
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
);
258 x_window_set_current_page (w_current
, page
);
260 /* if there were any symbols which had major changes, put up an
262 major_changed_dialog (w_current
);
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
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
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
;
299 for (GSList
*l
= filenames
; l
!= NULL
; l
= l
->next
) {
300 PAGE
*page
= open_or_create_page (w_current
, (gchar
*) l
->data
,
304 else if (first_page
== NULL
)
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
318 major_changed_dialog (w_current
);
324 /*! \brief Show "File changed. Save anyway?" dialog.
327 x_dialog_save_anyway (GschemToplevel
*w_current
, PAGE
*page
)
333 basename
= g_path_get_basename (page
->page_filename
);
334 dialog
= gtk_message_dialog_new (GTK_WINDOW (w_current
->main_window
),
336 GTK_DIALOG_DESTROY_WITH_PARENT
,
339 _("\"%s\" changed since visited or "
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
,
348 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog
),
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
);
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
367 * \param [in] w_current the toplevel environment
368 * \param [in] page the page to save, or \c NULL to save the
371 * \returns \c TRUE if the page was saved, \c FALSE otherwise
374 x_highlevel_save_page (GschemToplevel
*w_current
, PAGE
*page
)
376 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
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
)
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
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
;
419 /* ignore unchanged pages */
422 if (x_highlevel_save_page (w_current
, page
))
429 i_set_state_msg (w_current
, SELECT
, _("Failed to Save All"));
431 i_set_state_msg (w_current
, SELECT
, _("Saved All"));
433 i_set_state_msg (w_current
, SELECT
, _("No files need saving"));
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
449 * \returns \c TRUE if the page was successfully reloaded, \c FALSE otherwise
452 x_highlevel_revert_page (GschemToplevel
*w_current
, PAGE
*page
)
454 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
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 */
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
,
478 _("<big><b>Really revert \"%s\"?</b></big>"),
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
,
490 /* Set the alternative button order (ok, cancel, help) for other systems */
491 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog
),
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
)
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
522 * \returns \c TRUE if the page was closed, \c FALSE otherwise
525 x_highlevel_close_page (GschemToplevel
*w_current
, PAGE
*page
)
527 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
530 page
= toplevel
->page_current
;
531 g_return_val_if_fail (page
!= NULL
, FALSE
);
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
))
546 x_lowlevel_close_page (w_current
, page
);