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).
31 #include <sys/types.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.
50 file_changed_since (const gchar
*filename
,
51 gboolean has_known_mtime
,
52 struct timespec known_mtime
)
56 if (stat (filename
, &buf
) == -1)
57 /* file currently doesn't exist */
61 /* file has been created on disk */
64 if (buf
.st_mtim
.tv_sec
> known_mtime
.tv_sec
)
65 /* disk seconds are more recent than known seconds */
67 if (buf
.st_mtim
.tv_sec
< known_mtime
.tv_sec
)
68 /* disk seconds are older than known seconds (file went back in time?) */
71 if (buf
.st_mtim
.tv_nsec
> known_mtime
.tv_nsec
)
72 /* disk nanoseconds are more recent than known nanoseconds */
74 /* disk nanoseconds are equal or older than known nanoseconds */
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
94 x_highlevel_new_page (GschemToplevel
*w_current
, const gchar
*filename
)
96 PAGE
*page
= x_lowlevel_new_page (w_current
, filename
);
99 x_window_set_current_page (w_current
, page
);
105 /*! \brief Show "File changed. Reread from disk?" dialog.
108 x_dialog_reread_from_disk (GschemToplevel
*w_current
, PAGE
*page
)
110 const gchar
*fmt
, *secondary_text
;
111 GtkWidget
*dialog
, *button
;
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?");
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
),
128 GTK_DIALOG_DESTROY_WITH_PARENT
,
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
,
136 button
= gtk_button_new_with_mnemonic (page
->CHANGED
? _("_Revert") :
137 page
->exists_on_disk
? _("_Reload") :
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
:
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
),
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
);
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
180 open_or_create_page (GschemToplevel
*w_current
, const gchar
*filename
,
181 gboolean already_confirmed
)
183 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
185 gchar
*full_filename
;
187 /* If a page has already been created for this filename, return the
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
);
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
);
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?"),
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
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
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
);
256 x_window_set_current_page (w_current
, 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
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
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
;
291 for (GSList
*l
= filenames
; l
!= NULL
; l
= l
->next
) {
292 PAGE
*page
= open_or_create_page (w_current
, (gchar
*) l
->data
,
296 else if (first_page
== NULL
)
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
);
312 /*! \brief Show "File changed. Save anyway?" dialog.
315 x_dialog_save_anyway (GschemToplevel
*w_current
, PAGE
*page
)
321 basename
= g_path_get_basename (page
->page_filename
);
322 dialog
= gtk_message_dialog_new (GTK_WINDOW (w_current
->main_window
),
324 GTK_DIALOG_DESTROY_WITH_PARENT
,
327 _("\"%s\" changed since visited or "
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
,
336 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog
),
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
);
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
355 * \param [in] w_current the toplevel environment
356 * \param [in] page the page to save, or \c NULL to save the
359 * \returns \c TRUE if the page was saved, \c FALSE otherwise
362 x_highlevel_save_page (GschemToplevel
*w_current
, PAGE
*page
)
364 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
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
)
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
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
;
407 /* ignore unchanged pages */
410 if (x_highlevel_save_page (w_current
, page
))
417 i_set_state_msg (w_current
, SELECT
, _("Failed to Save All"));
419 i_set_state_msg (w_current
, SELECT
, _("Saved All"));
421 i_set_state_msg (w_current
, SELECT
, _("No files need saving"));
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
437 * \returns \c TRUE if the page was successfully reloaded, \c FALSE otherwise
440 x_highlevel_revert_page (GschemToplevel
*w_current
, PAGE
*page
)
442 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
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 */
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
,
466 _("<big><b>Really revert \"%s\"?</b></big>"),
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
,
478 /* Set the alternative button order (ok, cancel, help) for other systems */
479 gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog
),
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
)
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
510 * \returns \c TRUE if the page was closed, \c FALSE otherwise
513 x_highlevel_close_page (GschemToplevel
*w_current
, PAGE
*page
)
515 TOPLEVEL
*toplevel
= gschem_toplevel_get_toplevel (w_current
);
518 page
= toplevel
->page_current
;
519 g_return_val_if_fail (page
!= NULL
, FALSE
);
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
))
534 x_lowlevel_close_page (w_current
, page
);