From 7cd4215f58b3f3e0b833f268513a56b183d78f84 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sat, 3 Nov 2018 08:18:46 -0700 Subject: [PATCH] Added GTK+3 GUI. We'll keep the GTK+2 plugin for older machines, but GTK+3 gives us a more modern look, Wayland support, etc. --- CMakeLists.txt | 49 ++- examples/build-example.sh | 2 + gui.c | 3 + gui.h | 1 + gui_gtkplus3.c | 884 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 933 insertions(+), 6 deletions(-) create mode 100644 gui_gtkplus3.c diff --git a/CMakeLists.txt b/CMakeLists.txt index bba3b1e..cf691b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,8 @@ # This file written by Ryan C. Gordon. +# !!! FIXME: modernize this file. + # The "BINARY SIZE +=" comments note about how much bulk, in kilobytes, a # given option adds to the binary on x86 Linux (built with gcc 3.3.6 @@ -501,26 +503,61 @@ IF(NOT BEOS) IF(NOT MACOSX) FIND_PACKAGE(GTK2 COMPONENTS gtk) IF(NOT GTK2_FOUND) - MESSAGE(STATUS "Can't find GTK+v2 headers/libraries. Can't build GTK+ GUI.") + MESSAGE(STATUS "Can't find GTK+v2 headers/libraries. Can't build GTK+-2.0 GUI.") ELSE(NOT GTK2_FOUND) # In order to reduce the GTK2 library dependencies at link time, we only link against 'gtk-x11-2.0'. # This is more portable, as the dynamic linker/loader will take care of the other library dependencies at run time. - SET(LIBGTK_LINK_FLAGS "-lgtk-x11-2.0") + SET(LIBGTK2_LINK_FLAGS "-lgtk-x11-2.0") OPTION(MOJOSETUP_GUI_GTKPLUS2 "Enable GTK+ 2.0 GUI" TRUE) IF(MOJOSETUP_GUI_GTKPLUS2) ADD_DEFINITIONS(-DSUPPORT_GUI_GTKPLUS2=1) - INCLUDE_DIRECTORIES(${GTK2_INCLUDE_DIRS}) - OPTION(MOJOSETUP_GUI_GTKPLUS2_STATIC "Statically link GTK+ GUI" FALSE) + OPTION(MOJOSETUP_GUI_GTKPLUS2_STATIC "Statically link GTK+ 2.0 GUI" FALSE) IF(MOJOSETUP_GUI_GTKPLUS2_STATIC) + INCLUDE_DIRECTORIES(${GTK2_INCLUDE_DIRS}) ADD_DEFINITIONS(-DGUI_STATIC_LINK_GTKPLUS2=1) SET(OPTIONAL_SRCS ${OPTIONAL_SRCS} gui_gtkplus2.c) - SET(OPTIONAL_LIBS ${OPTIONAL_LIBS} ${LIBGTK_LINK_FLAGS}) + SET(OPTIONAL_LIBS ${OPTIONAL_LIBS} ${LIBGTK2_LINK_FLAGS}) ELSE(MOJOSETUP_GUI_GTKPLUS2_STATIC) MOJOSETUP_ADD_LIBRARY(mojosetupgui_gtkplus2 gui_gtkplus2.c) - TARGET_LINK_LIBRARIES(mojosetupgui_gtkplus2 ${LIBGTK_LINK_FLAGS}) + TARGET_LINK_LIBRARIES(mojosetupgui_gtkplus2 ${LIBGTK2_LINK_FLAGS}) + TARGET_INCLUDE_DIRECTORIES(mojosetupgui_gtkplus2 PUBLIC ${GTK2_INCLUDE_DIRS}) ENDIF(MOJOSETUP_GUI_GTKPLUS2_STATIC) ENDIF(MOJOSETUP_GUI_GTKPLUS2) ENDIF(NOT GTK2_FOUND) + +FIND_PACKAGE(PkgConfig) +IF(NOT PkgConfig_FOUND) + MESSAGE(STATUS "Can't find pkgconfig. Can't build GTK+-3.0 GUI.") +ELSE(NOT PkgConfig_FOUND) + PKG_CHECK_MODULES(GTK3 gtk+-3.0) + IF(NOT GTK3_FOUND) + MESSAGE(STATUS "Can't find GTK+v3 headers/libraries. Can't build GTK+-3.0 GUI.") + ELSE(NOT GTK3_FOUND) + # In order to reduce the GTK3 library dependencies at link time, we only link against 'gtk-3.0'. + # This is more portable, as the dynamic linker/loader will take care of the other library dependencies at run time. + SET(LIBGTK3_LINK_FLAGS "-lgtk-3") + OPTION(MOJOSETUP_GUI_GTKPLUS3 "Enable GTK+ 3.0 GUI" TRUE) + IF(MOJOSETUP_GUI_GTKPLUS3) + ADD_DEFINITIONS(-DSUPPORT_GUI_GTKPLUS3=1) + OPTION(MOJOSETUP_GUI_GTKPLUS3_STATIC "Statically link GTK+ 3.0 GUI" FALSE) + IF(MOJOSETUP_GUI_GTKPLUS3_STATIC) + INCLUDE_DIRECTORIES(${GTK3_INCLUDE_DIRS}) + ADD_DEFINITIONS(-DGUI_STATIC_LINK_GTKPLUS3=1) + SET(OPTIONAL_SRCS ${OPTIONAL_SRCS} gui_gtkplus3.c) + SET(OPTIONAL_LIBS ${OPTIONAL_LIBS} ${LIBGTK3_LINK_FLAGS}) + ELSE(MOJOSETUP_GUI_GTKPLUS3_STATIC) + MOJOSETUP_ADD_LIBRARY(mojosetupgui_gtkplus3 gui_gtkplus3.c) + TARGET_LINK_LIBRARIES(mojosetupgui_gtkplus3 ${LIBGTK3_LINK_FLAGS}) + TARGET_INCLUDE_DIRECTORIES(mojosetupgui_gtkplus3 PUBLIC ${GTK3_INCLUDE_DIRS}) + ENDIF(MOJOSETUP_GUI_GTKPLUS3_STATIC) + ENDIF(MOJOSETUP_GUI_GTKPLUS3) + ENDIF(NOT GTK3_FOUND) +ENDIF(NOT PkgConfig_FOUND) + +IF(MOJOSETUP_GUI_GTKPLUS2_STATIC AND MOJOSETUP_GUI_GTKPLUS3_STATIC) + MESSAGE(FATAL_ERROR "You can't statically link both GTK+2 and GTK+3 support.") +ENDIF(MOJOSETUP_GUI_GTKPLUS2_STATIC AND MOJOSETUP_GUI_GTKPLUS3_STATIC) + ENDIF(NOT MACOSX) ENDIF(NOT BEOS) ENDIF(UNIX) diff --git a/examples/build-example.sh b/examples/build-example.sh index 7c91877..23ae0a2 100755 --- a/examples/build-example.sh +++ b/examples/build-example.sh @@ -141,6 +141,8 @@ cmake \ -DMOJOSETUP_ARCHIVE_TAR_GZ=FALSE \ -DMOJOSETUP_GUI_GTKPLUS2=$FALSEIFMINIMAL \ -DMOJOSETUP_GUI_GTKPLUS2_STATIC=FALSE \ + -DMOJOSETUP_GUI_GTKPLUS3=$FALSEIFMINIMAL \ + -DMOJOSETUP_GUI_GTKPLUS3_STATIC=FALSE \ -DMOJOSETUP_GUI_NCURSES=$FALSEIFMINIMAL \ -DMOJOSETUP_GUI_NCURSES_STATIC=FALSE \ -DMOJOSETUP_GUI_STDIO=TRUE \ diff --git a/gui.c b/gui.c index a86d584..7bb5244 100644 --- a/gui.c +++ b/gui.c @@ -31,6 +31,9 @@ static const MojoGuiEntryPoint staticGui[] = #if GUI_STATIC_LINK_COCOA MojoGuiPlugin_cocoa, #endif +#if GUI_STATIC_LINK_GTKPLUS3 + MojoGuiPlugin_gtkplus3, +#endif #if GUI_STATIC_LINK_GTKPLUS2 MojoGuiPlugin_gtkplus2, #endif diff --git a/gui.h b/gui.h index 9ecdc4a..add0dd9 100644 --- a/gui.h +++ b/gui.h @@ -287,6 +287,7 @@ const MojoGui *MOJOGUI_ENTRY_POINT(int rev, const MojoSetupEntryPoints *e) \ const MojoGui *MojoGuiPlugin_stdio(int rev, const MojoSetupEntryPoints *e); const MojoGui *MojoGuiPlugin_ncurses(int rev, const MojoSetupEntryPoints *e); const MojoGui *MojoGuiPlugin_gtkplus2(int rev, const MojoSetupEntryPoints *e); +const MojoGui *MojoGuiPlugin_gtkplus3(int rev, const MojoSetupEntryPoints *e); const MojoGui *MojoGuiPlugin_www(int rev, const MojoSetupEntryPoints *e); const MojoGui *MojoGuiPlugin_cocoa(int rev, const MojoSetupEntryPoints *e); diff --git a/gui_gtkplus3.c b/gui_gtkplus3.c new file mode 100644 index 0000000..9c482f3 --- /dev/null +++ b/gui_gtkplus3.c @@ -0,0 +1,884 @@ +/** + * MojoSetup; a portable, flexible installation application. + * + * Please see the file LICENSE.txt in the source's root directory. + * + * This file written by Ryan C. Gordon. + */ + +#if !SUPPORT_GUI_GTKPLUS3 +#error Something is wrong in the build system. +#endif + +#define BUILDING_EXTERNAL_PLUGIN 1 +#include "gui.h" + +MOJOGUI_PLUGIN(gtkplus3) + +#if !GUI_STATIC_LINK_GTKPLUS3 +CREATE_MOJOGUI_ENTRY_POINT(gtkplus3) +#endif + +#undef format +#include +#define format entry->format + +typedef enum +{ + PAGE_INTRO, + PAGE_README, + PAGE_OPTIONS, + PAGE_DEST, + PAGE_PRODUCTKEY, + PAGE_PROGRESS, + PAGE_FINAL +} WizardPages; + +static WizardPages currentpage = PAGE_INTRO; +static gboolean canfwd = TRUE; +static GtkWidget *gtkwindow = NULL; +static GtkWidget *pagetitle = NULL; +static GtkWidget *notebook = NULL; +static GtkWidget *readme = NULL; +static GtkWidget *cancel = NULL; +static GtkWidget *back = NULL; +static GtkWidget *next = NULL; +static GtkWidget *finish = NULL; +static GtkWidget *msgbox = NULL; +static GtkWidget *destination = NULL; +static GtkWidget *productkey = NULL; +static GtkWidget *progressbar = NULL; +static GtkWidget *progresslabel = NULL; +static GtkWidget *finallabel = NULL; +static GtkWidget *browse = NULL; +static GtkWidget *splash = NULL; + +static volatile enum +{ + CLICK_BACK=-1, + CLICK_CANCEL, + CLICK_NEXT, + CLICK_NONE +} click_value = CLICK_NONE; + + +static void prepare_wizard(const char *name, WizardPages page, + boolean can_back, boolean can_fwd, + boolean can_cancel, boolean fwd_at_start) +{ + char *markup = g_markup_printf_escaped( + "%s", + name); + + currentpage = page; + canfwd = can_fwd; + + gtk_label_set_markup(GTK_LABEL(pagetitle), markup); + g_free(markup); + + gtk_widget_set_sensitive(back, can_back); + gtk_widget_set_sensitive(next, fwd_at_start); + gtk_widget_set_sensitive(cancel, can_cancel); + gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), (gint) page); + + assert(click_value == CLICK_NONE); + assert(gtkwindow != NULL); + assert(msgbox == NULL); +} // prepare_wizard + + +static int wait_event(void) +{ + int retval = 0; + + // signals fired under gtk_main_iteration can change click_value. + gtk_main_iteration(); + if (click_value == CLICK_CANCEL) + { + char *title = xstrdup(_("Cancel installation")); + char *text = xstrdup(_("Are you sure you want to cancel installation?")); + if (!MojoGui_gtkplus3_promptyn(title, text, false)) + click_value = CLICK_NONE; + free(title); + free(text); + } // if + + assert(click_value <= CLICK_NONE); + retval = (int) click_value; + click_value = CLICK_NONE; + return retval; +} // wait_event + + +static int pump_events(void) +{ + int retval = (int) CLICK_NONE; + while ( (retval == ((int) CLICK_NONE)) && (gtk_events_pending()) ) + retval = wait_event(); + return retval; +} // pump_events + + +static int run_wizard(const char *name, WizardPages page, + boolean can_back, boolean can_fwd, boolean can_cancel, + boolean fwd_at_start) +{ + int retval = CLICK_NONE; + prepare_wizard(name, page, can_back, can_fwd, can_cancel, fwd_at_start); + while (retval == ((int) CLICK_NONE)) + retval = wait_event(); + + assert(retval < ((int) CLICK_NONE)); + return retval; +} // run_wizard + + +static gboolean signal_delete(GtkWidget *w, GdkEvent *evt, gpointer data) +{ + click_value = CLICK_CANCEL; + return TRUE; /* eat event: default handler destroys window! */ +} // signal_delete + + +static void signal_clicked(GtkButton *_button, gpointer data) +{ + GtkWidget *button = GTK_WIDGET(_button); + if (button == back) + click_value = CLICK_BACK; + else if (button == cancel) + click_value = CLICK_CANCEL; + else if ((button == next) || (button == finish)) + click_value = CLICK_NEXT; + else + { + assert(0 && "Unknown click event."); + } // else +} // signal_clicked + + +static void signal_browse_clicked(GtkButton *_button, gpointer data) +{ + GtkWidget *dialog = gtk_file_chooser_dialog_new ( + _("Destination"), + GTK_WINDOW(gtkwindow), + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + _("Cancel"),GTK_RESPONSE_CANCEL, + _("OK"), GTK_RESPONSE_ACCEPT, + NULL); + + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), + gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(destination))); + + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { + gchar *filename; + gchar *utfname; + + filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + + utfname = g_filename_to_utf8(filename, -1, NULL, NULL, NULL); + gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(destination), utfname); + gtk_combo_box_set_active(GTK_COMBO_BOX(destination), 0); + + g_free(utfname); + g_free(filename); + } + + // !!! FIXME: should append the package name to the directory they chose? + // !!! FIXME: This is annoying, they might have created the folder + // !!! FIXME: themselves in the dialog. + // !!! FIXME: Could warn when the target directory already contains files? + + gtk_widget_destroy(dialog); +} // signal_browse_clicked + + +static void signal_dest_changed(GtkComboBoxText *combo, gpointer user_data) +{ + // Disable the forward button when the destination entry is blank. + if ((currentpage == PAGE_DEST) && (canfwd)) + { + gchar *str = gtk_combo_box_text_get_active_text(combo); + const gboolean filled_in = ((str != NULL) && (*str != '\0')); + g_free(str); + gtk_widget_set_sensitive(next, filled_in); + } // if +} // signal_dest_changed + + +static void signal_productkey_changed(GtkEditable *edit, gpointer user_data) +{ + // Disable the forward button when the entry is blank. + if ((currentpage == PAGE_PRODUCTKEY) && (canfwd)) + { + const char *fmt = (const char *) user_data; + char *key = (char *) gtk_editable_get_chars(edit, 0, -1); + const gboolean okay = isValidProductKey(fmt, key); + g_free(key); + gtk_widget_set_sensitive(next, okay); + } // if +} // signal_productkey_changed + + +static uint8 MojoGui_gtkplus3_priority(boolean istty) +{ + // gnome-session exports this environment variable since 2002. + if (getenv("GNOME_DESKTOP_SESSION_ID") != NULL) + return MOJOGUI_PRIORITY_TRY_FIRST; + else if (getenv("GNOME_SHELL_SESSION_MODE") != NULL) // this is newer. + return MOJOGUI_PRIORITY_TRY_FIRST; + return MOJOGUI_PRIORITY_TRY_NORMAL; +} // MojoGui_gtkplus3_priority + + +static boolean MojoGui_gtkplus3_init(void) +{ + int tmpargc = 0; + char *args[] = { NULL, NULL }; + char **tmpargv = args; + if (!gtk_init_check(&tmpargc, &tmpargv)) + { + logInfo("gtkplus3: gtk_init_check() failed, use another UI."); + return false; + } // if + return true; +} // MojoGui_gtkplus3_init + + +static void MojoGui_gtkplus3_deinit(void) +{ + // !!! FIXME: GTK+ doesn't have a deinit function...it installs a crappy + // !!! FIXME: atexit() function! +} // MojoGui_gtkplus3_deinit + +/** + * + * @param defbutton The response ID to use when enter is pressed, or 0 + * to leave it up to GTK+. + */ +static gint do_msgbox(const char *title, const char *text, + GtkMessageType mtype, GtkButtonsType btype, + GtkResponseType defbutton, + void (*addButtonCallback)(GtkWidget *_msgbox)) +{ + gint retval = 0; + + // Modal dialog, this will never be called recursively. + assert(msgbox == NULL); + + msgbox = gtk_message_dialog_new(GTK_WINDOW(gtkwindow), GTK_DIALOG_MODAL, + mtype, btype, "%s", title); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(msgbox), + "%s", text); + + if (defbutton) + gtk_dialog_set_default_response(GTK_DIALOG(msgbox), defbutton); + + if (addButtonCallback != NULL) + addButtonCallback(msgbox); + + retval = gtk_dialog_run(GTK_DIALOG(msgbox)); + gtk_widget_destroy(msgbox); + msgbox = NULL; + + return retval; +} // do_msgbox + + +static void MojoGui_gtkplus3_msgbox(const char *title, const char *text) +{ + do_msgbox(title, text, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, 0, NULL); +} // MojoGui_gtkplus3_msgbox + + +static boolean MojoGui_gtkplus3_promptyn(const char *title, const char *text, + boolean defval) +{ + gint rc = do_msgbox(title, text, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + defval ? GTK_RESPONSE_YES : GTK_RESPONSE_NO, + NULL); + + return (rc == GTK_RESPONSE_YES); +} // MojoGui_gtkplus3_promptyn + + +static void promptynanButtonCallback(GtkWidget *_msgbox) +{ + char *always = xstrdup(_("_Always")); + char *never = xstrdup(_("N_ever")); + gtk_dialog_add_buttons(GTK_DIALOG(_msgbox), + _("Yes"), GTK_RESPONSE_YES, + _("No"), GTK_RESPONSE_NO, + always, 1, + never, 0, + NULL); + + free(always); + free(never); +} // promptynanButtonCallback + + +static MojoGuiYNAN MojoGui_gtkplus3_promptynan(const char *title, + const char *text, + boolean defval) +{ + const gint rc = do_msgbox(title, text, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + defval ? GTK_RESPONSE_YES : GTK_RESPONSE_NO, + promptynanButtonCallback); + switch (rc) + { + case GTK_RESPONSE_YES: return MOJOGUI_YES; + case GTK_RESPONSE_NO: return MOJOGUI_NO; + case 1: return MOJOGUI_ALWAYS; + case 0: return MOJOGUI_NEVER; + } // switch + + assert(false && "BUG: unhandled case in switch statement"); + return MOJOGUI_NO; // just in case. +} // MojoGui_gtkplus3_promptynan + + +static GtkWidget *create_button(GtkWidget *box, const char *iconname, + const char *text, + void (*signal_callback) + (GtkButton *button, gpointer data)) +{ + GtkWidget *button = gtk_button_new_with_mnemonic(text); + GtkWidget *image = gtk_image_new_from_icon_name(iconname, GTK_ICON_SIZE_BUTTON); + gtk_button_set_image (GTK_BUTTON(button), image); + gtk_widget_show(button); + gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6); + g_signal_connect(button, "clicked", G_CALLBACK(signal_callback), NULL); + return button; +} // create_button + + +static void free_splash(guchar *pixels, gpointer data) +{ + free(pixels); +} // free_splash + + +static GtkWidget *build_splash(const MojoGuiSplash *splash) +{ + GtkWidget *retval = NULL; + GdkPixbuf *pixbuf = NULL; + guchar *rgba = NULL; + const uint32 splashlen = splash->w * splash->h * 4; + + if (splash->position == MOJOGUI_SPLASH_NONE) + return NULL; + + if ((splash->rgba == NULL) || (splashlen == 0)) + return NULL; + + rgba = (guchar *) xmalloc(splashlen); + memcpy(rgba, splash->rgba, splashlen); + pixbuf = gdk_pixbuf_new_from_data(rgba, GDK_COLORSPACE_RGB, TRUE, 8, + splash->w, splash->h, splash->w * 4, + free_splash, NULL); + if (pixbuf == NULL) + free(rgba); + else + { + retval = gtk_image_new_from_pixbuf(pixbuf); + g_object_unref(pixbuf); // retval adds a ref to pixbuf, so lose our's. + if (retval != NULL) + gtk_widget_show(retval); + } // else + + return retval; +} // build_splash + + +static GtkWidget *create_gtkwindow(const char *title, + const MojoGuiSplash *_splash) +{ + GtkWidget *splashbox = NULL; + GtkWidget *window; + GtkWidget *widget; + GtkWidget *box; + GtkWidget *hbox; + + currentpage = PAGE_INTRO; + canfwd = TRUE; + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_resizable(GTK_WINDOW(window), TRUE); + gtk_window_set_title(GTK_WINDOW(window), title); + gtk_container_set_border_width(GTK_CONTAINER(window), 8); + + GdkPixbuf *icon = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (), + "system-software-installer", + 48, 0, NULL); + if (icon) + gtk_window_set_icon(GTK_WINDOW(window), icon); + + assert(splash == NULL); + splash = build_splash(_splash); + if (splash != NULL) + { + // !!! FIXME: MOJOGUI_SPLASH_BACKGROUND? + const MojoGuiSplashPos pos = _splash->position; + if ((pos == MOJOGUI_SPLASH_LEFT) || (pos == MOJOGUI_SPLASH_RIGHT)) + { + splashbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_show(splashbox); + gtk_container_add(GTK_CONTAINER(window), splashbox); + if (pos == MOJOGUI_SPLASH_LEFT) + gtk_box_pack_start(GTK_BOX(splashbox), splash, FALSE, FALSE, 6); + else + gtk_box_pack_end(GTK_BOX(splashbox), splash, FALSE, FALSE, 6); + } // if + + else if ((pos == MOJOGUI_SPLASH_TOP) || (pos == MOJOGUI_SPLASH_BOTTOM)) + { + splashbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_show(splashbox); + gtk_container_add(GTK_CONTAINER(window), splashbox); + if (pos == MOJOGUI_SPLASH_TOP) + gtk_box_pack_start(GTK_BOX(splashbox), splash, FALSE, FALSE, 6); + else + gtk_box_pack_end(GTK_BOX(splashbox), splash, FALSE, FALSE, 6); + } // else if + } // if + + if (splashbox == NULL) // no splash, use the window for the top container. + splashbox = window; + + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_show(box); + gtk_container_add(GTK_CONTAINER(splashbox), box); + + pagetitle = gtk_label_new(""); + gtk_widget_show(pagetitle); + gtk_box_pack_start(GTK_BOX(box), pagetitle, FALSE, TRUE, 16); + + notebook = gtk_notebook_new(); + gtk_widget_show(notebook); + gtk_container_set_border_width(GTK_CONTAINER(notebook), 0); + gtk_box_pack_start(GTK_BOX(box), notebook, TRUE, TRUE, 0); + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), FALSE); + gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE); + gtk_widget_set_size_request(notebook, + (gint) (((float) gdk_screen_width()) * 0.4), + (gint) (((float) gdk_screen_height()) * 0.4)); + + widget = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(box), widget, FALSE, FALSE, 0); + gtk_button_box_set_layout(GTK_BUTTON_BOX (widget), GTK_BUTTONBOX_END); + //gtk_button_box_set_child_ipadding(GTK_BUTTON_BOX (widget), 6, 0); + //gtk_button_box_set_spacing(GTK_BUTTON_BOX (widget), 6); + + box = widget; + cancel = create_button(box, "gtk-cancel", _("Cancel"), signal_clicked); + back = create_button(box, "gtk-go-back", _("Back"), signal_clicked); + next = create_button(box, "gtk-go-forward", _("Next"), signal_clicked); + finish = create_button(box, "gtk-goto-last", _("Finish"), signal_clicked); + gtk_widget_hide(finish); + + // !!! FIXME: intro page. + widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(widget); + gtk_container_add(GTK_CONTAINER(notebook), widget); + + // README/EULA page. + widget = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy( + GTK_SCROLLED_WINDOW(widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS); + gtk_scrolled_window_set_shadow_type( + GTK_SCROLLED_WINDOW(widget), + GTK_SHADOW_IN); + gtk_widget_show(widget); + gtk_container_add(GTK_CONTAINER(notebook), widget); + + readme = gtk_text_view_new(); + gtk_widget_show(readme); + gtk_container_add(GTK_CONTAINER(widget), readme); + gtk_text_view_set_editable(GTK_TEXT_VIEW(readme), FALSE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(readme), GTK_WRAP_NONE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(readme), FALSE); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(readme), 4); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(readme), 4); + + // !!! FIXME: options page. + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(box); + gtk_container_add(GTK_CONTAINER(notebook), box); + + // Destination page + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(box); + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + widget = gtk_label_new(_("Folder:")); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, TRUE, 0); + gtk_label_set_justify(GTK_LABEL(widget), GTK_JUSTIFY_CENTER); + gtk_label_set_line_wrap(GTK_LABEL(widget), FALSE); + destination = gtk_combo_box_text_new_with_entry(); + g_signal_connect(destination, "changed", G_CALLBACK(signal_dest_changed), NULL); + gtk_widget_set_halign(GTK_WIDGET(destination), GTK_ALIGN_FILL); + gtk_widget_set_valign(GTK_WIDGET(destination), GTK_ALIGN_CENTER); + gtk_box_pack_start(GTK_BOX(hbox), destination, TRUE, TRUE, 0); + browse = create_button(hbox, "gtk-open", + _("B_rowse..."), signal_browse_clicked); + gtk_widget_show_all (hbox); + gtk_box_pack_start(GTK_BOX(box), hbox, FALSE, FALSE, 0); + gtk_container_add(GTK_CONTAINER(notebook), box); + + // Product key page + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(box); + + widget = gtk_label_new(_("Please enter your product key")); + gtk_label_set_justify(GTK_LABEL(widget), GTK_JUSTIFY_CENTER); + gtk_label_set_line_wrap(GTK_LABEL(widget), TRUE); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(box), widget, FALSE, TRUE, 0); + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_show(hbox); + productkey = gtk_entry_new(); + gtk_editable_set_editable(GTK_EDITABLE(productkey), TRUE); + gtk_entry_set_visibility(GTK_ENTRY(productkey), TRUE); + gtk_widget_show(productkey); + gtk_box_pack_start(GTK_BOX(hbox), productkey, TRUE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(box), hbox, FALSE, TRUE, 0); + + gtk_container_add(GTK_CONTAINER(notebook), box); + + // Progress page + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_show(box); + progressbar = gtk_progress_bar_new(); + gtk_widget_show(progressbar); + gtk_widget_set_halign(GTK_WIDGET(progressbar), GTK_ALIGN_FILL); + gtk_widget_set_valign(GTK_WIDGET(progressbar), GTK_ALIGN_CENTER); + gtk_box_pack_start(GTK_BOX(box), progressbar, FALSE, TRUE, 0); + progresslabel = gtk_label_new(""); + gtk_widget_show(progresslabel); + gtk_box_pack_start(GTK_BOX(box), progresslabel, FALSE, TRUE, 0); + gtk_label_set_justify(GTK_LABEL(progresslabel), GTK_JUSTIFY_LEFT); + gtk_label_set_line_wrap(GTK_LABEL(progresslabel), FALSE); + gtk_container_add(GTK_CONTAINER(notebook), box); + + // !!! FIXME: final page. + widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(widget); + gtk_container_add(GTK_CONTAINER(notebook), widget); + finallabel = gtk_label_new(""); + gtk_widget_show(finallabel); + gtk_box_pack_start(GTK_BOX(widget), finallabel, FALSE, TRUE, 0); + gtk_label_set_justify(GTK_LABEL(finallabel), GTK_JUSTIFY_LEFT); + gtk_label_set_line_wrap(GTK_LABEL(finallabel), TRUE); + + g_signal_connect(window, "delete-event", G_CALLBACK(signal_delete), NULL); + + gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + gtk_widget_show(window); + + gtk_widget_grab_focus(next); + + return window; +} // create_gtkwindow + + +static boolean MojoGui_gtkplus3_start(const char *title, + const MojoGuiSplash *splash) +{ + gtkwindow = create_gtkwindow(title, splash); + return (gtkwindow != NULL); +} // MojoGui_gtkplus3_start + + +static void MojoGui_gtkplus3_stop(void) +{ + assert(msgbox == NULL); + if (gtkwindow != NULL) + gtk_widget_destroy(gtkwindow); + + gtkwindow = NULL; + pagetitle = NULL; + finallabel = NULL; + progresslabel = NULL; + progressbar = NULL; + destination = NULL; + productkey = NULL; + notebook = NULL; + readme = NULL; + cancel = NULL; + back = NULL; + next = NULL; + finish = NULL; + splash = NULL; +} // MojoGui_gtkplus3_stop + + +static int MojoGui_gtkplus3_readme(const char *name, const uint8 *data, + size_t datalen, boolean can_back, + boolean can_fwd) +{ + GtkTextBuffer *textbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(readme)); + gtk_text_buffer_set_text(textbuf, (const gchar *) data, datalen); + return run_wizard(name, PAGE_README, can_back, can_fwd, true, can_fwd); +} // MojoGui_gtkplus3_readme + + +static void set_option_tree_sensitivity(MojoGuiSetupOptions *opts, boolean val) +{ + if (opts != NULL) + { + gtk_widget_set_sensitive((GtkWidget *) opts->guiopaque, val); + set_option_tree_sensitivity(opts->next_sibling, val); + set_option_tree_sensitivity(opts->child, val && opts->value); + } // if +} // set_option_tree_sensitivity + + +static void signal_option_toggled(GtkToggleButton *toggle, gpointer data) +{ + MojoGuiSetupOptions *opts = (MojoGuiSetupOptions *) data; + const boolean enabled = gtk_toggle_button_get_active(toggle); + opts->value = enabled; + set_option_tree_sensitivity(opts->child, enabled); +} // signal_option_toggled + + +static GtkWidget *new_option_level(GtkWidget *box) +{ + GtkWidget *retval = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_halign(retval, GTK_ALIGN_START); + gtk_widget_set_valign(retval, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(retval, 15); + gtk_widget_show(retval); + gtk_box_pack_start(GTK_BOX(box), retval, TRUE, TRUE, 0); + return retval; +} // new_option_level + + +static void build_options(MojoGuiSetupOptions *opts, GtkWidget *box, + gboolean sensitive) +{ + if (opts != NULL) + { + GtkWidget *widget = NULL; + + if (opts->is_group_parent) + { + MojoGuiSetupOptions *kids = opts->child; + GtkWidget *childbox = NULL; + widget = gtk_label_new(opts->description); + gtk_widget_set_halign(widget, GTK_ALIGN_START); + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + opts->guiopaque = widget; + gtk_widget_set_sensitive(widget, sensitive); + gtk_widget_show(widget); + gtk_label_set_justify(GTK_LABEL(widget), GTK_JUSTIFY_LEFT); + gtk_label_set_line_wrap(GTK_LABEL(widget), FALSE); + gtk_box_pack_start(GTK_BOX(box), widget, FALSE, TRUE, 0); + + if (opts->tooltip != NULL) + gtk_widget_set_tooltip_text(widget, opts->tooltip); + + widget = NULL; + childbox = new_option_level(box); + while (kids) + { + widget = gtk_radio_button_new_with_label_from_widget( + GTK_RADIO_BUTTON(widget), + kids->description); + kids->guiopaque = widget; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + kids->value); + gtk_widget_set_sensitive(widget, sensitive); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(childbox), widget, FALSE, TRUE, 0); + g_signal_connect(widget, "toggled", G_CALLBACK(signal_option_toggled), kids); + + if (kids->tooltip != NULL) + gtk_widget_set_tooltip_text(widget, kids->tooltip); + + if (kids->child != NULL) + { + build_options(kids->child, new_option_level(childbox), + sensitive); + } // if + kids = kids->next_sibling; + } // while + } // if + + else + { + widget = gtk_check_button_new_with_label(opts->description); + opts->guiopaque = widget; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + opts->value); + gtk_widget_set_sensitive(widget, sensitive); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(box), widget, FALSE, TRUE, 0); + g_signal_connect(widget, "toggled", G_CALLBACK(signal_option_toggled), opts); + + if (opts->tooltip != NULL) + gtk_widget_set_tooltip_text(widget, opts->tooltip); + + if (opts->child != NULL) + { + build_options(opts->child, new_option_level(box), + ((sensitive) && (opts->value)) ); + } // if + } // else + + build_options(opts->next_sibling, box, sensitive); + } // if +} // build_options + + +static int MojoGui_gtkplus3_options(MojoGuiSetupOptions *opts, + boolean can_back, boolean can_fwd) +{ + int retval; + GtkWidget *box; + GtkWidget *page; // this is a vbox. + + page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), PAGE_OPTIONS); + box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(box); + gtk_box_pack_start(GTK_BOX(page), box, FALSE, FALSE, 0); + + build_options(opts, box, TRUE); + retval = run_wizard(_("Options"), PAGE_OPTIONS, + can_back, can_fwd, true, can_fwd); + gtk_widget_destroy(box); + return retval; +} // MojoGui_gtkplus3_options + + +static char *MojoGui_gtkplus3_destination(const char **recommends, int recnum, + int *command, boolean can_back, + boolean can_fwd) +{ + GtkComboBoxText *combo = GTK_COMBO_BOX_TEXT(destination); + const boolean fwd_at_start = ( (recnum > 0) && (*(recommends[0])) ); + gchar *str = NULL; + char *retval = NULL; + int i; + + for (i = 0; i < recnum; i++) + gtk_combo_box_text_append_text(combo, recommends[i]); + gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0); + + *command = run_wizard(_("Destination"), PAGE_DEST, + can_back, can_fwd, true, fwd_at_start); + + str = gtk_combo_box_text_get_active_text(combo); + + // shouldn't ever be empty ("next" should be disabled in that case). + assert( (*command <= 0) || ((str != NULL) && (*str != '\0')) ); + + retval = xstrdup(str); + g_free(str); + + for (i = recnum-1; i >= 0; i--) + gtk_combo_box_text_remove(combo, i); + + return retval; +} // MojoGui_gtkplus3_destination + + +static int MojoGui_gtkplus3_productkey(const char *desc, const char *fmt, + char *buf, const int buflen, + boolean can_back, boolean can_fwd) +{ + gchar *str = NULL; + int retval = 0; + const boolean fwd_at_start = isValidProductKey(fmt, buf); + + gtk_entry_set_max_length(GTK_ENTRY(productkey), buflen - 1); + gtk_entry_set_width_chars(GTK_ENTRY(productkey), buflen - 1); + gtk_entry_set_text(GTK_ENTRY(productkey), (gchar *) buf); + + const guint connid = g_signal_connect(productkey, "changed", + G_CALLBACK(signal_productkey_changed), + (void *) fmt); + retval = run_wizard(desc, PAGE_PRODUCTKEY, + can_back, can_fwd, true, fwd_at_start); + g_signal_handler_disconnect(productkey, connid); + + str = gtk_editable_get_chars(GTK_EDITABLE(productkey), 0, -1); + // should never be invalid ("next" should be disabled in that case). + assert( (retval <= 0) || ((str) && (isValidProductKey(fmt, str))) ); + assert(strlen(str) < buflen); + strcpy(buf, (char *) str); + g_free(str); + gtk_entry_set_text(GTK_ENTRY(productkey), ""); + + return retval; +} // MojoGui_gtkplus3_productkey + + +static boolean MojoGui_gtkplus3_insertmedia(const char *medianame) +{ + gint rc = 0; + // !!! FIXME: Use stock GTK icon for "media"? + // !!! FIXME: better text. + const char *title = xstrdup(_("Media change")); + // !!! FIXME: better text. + const char *fmt = xstrdup(_("Please insert '%0'")); + const char *text = format(fmt, medianame); + rc = do_msgbox(title, text, GTK_MESSAGE_WARNING, + GTK_BUTTONS_OK_CANCEL, GTK_RESPONSE_OK, NULL); + free((void *) text); + free((void *) fmt); + free((void *) title); + return (rc == GTK_RESPONSE_OK); +} // MojoGui_gtkplus3_insertmedia + + +static void MojoGui_gtkplus3_progressitem(void) +{ + // no-op in this UI target. +} // MojoGui_gtkplus3_progressitem + + +static boolean MojoGui_gtkplus3_progress(const char *type, const char *component, + int percent, const char *item, + boolean can_cancel) +{ + static uint32 lastTicks = 0; + const uint32 ticks = ticks(); + int rc; + + if ((ticks - lastTicks) > 200) // just not to spam this... + { + GtkProgressBar *progress = GTK_PROGRESS_BAR(progressbar); + if (percent < 0) + gtk_progress_bar_pulse(progress); + else + gtk_progress_bar_set_fraction(progress, ((gdouble) percent) / 100.0); + + gtk_progress_bar_set_text(progress, component); + gtk_label_set_text(GTK_LABEL(progresslabel), item); + lastTicks = ticks; + } // if + + prepare_wizard(type, PAGE_PROGRESS, false, false, can_cancel, false); + rc = pump_events(); + assert( (rc == ((int) CLICK_CANCEL)) || (rc == ((int) CLICK_NONE)) ); + return (rc != CLICK_CANCEL); +} // MojoGui_gtkplus3_progress + + +static void MojoGui_gtkplus3_final(const char *msg) +{ + gtk_widget_hide(next); + gtk_widget_show(finish); + gtk_label_set_text(GTK_LABEL(finallabel), msg); + run_wizard(_("Finish"), PAGE_FINAL, false, true, false, true); +} // MojoGui_gtkplus3_final + +// end of gui_gtkplus3.c ... +