Logo Search packages:      
Sourcecode: sagasu version File versions  Download package

SagasuApp.cpp

/*  $Id: SagasuApp.cpp,v 1.17 2005/08/09 00:11:28 sarrazip Exp $
    SagasuApp.cpp - Class representing the main window

    sagasu - GNOME tool to find strings in a set of files
    Copyright (C) 2002-2004 Pierre Sarrazin <http://sarrazip.com/>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
    02111-1307, USA.
*/

#include "SagasuApp.h"

#include <assert.h>
#include <libintl.h>
#define _(String) gettext (String)
#define gettext_noop(String) String
#define N_(String) gettext_noop (String)

#include <libgnomeui/libgnomeui.h>
#include <gdk/gdk.h>
#include <libgnome/gnome-config.h>
#include <libgnome/gnome-sound.h>

#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <signal.h>
#include <ctype.h>
#include <fcntl.h>
#include <math.h>
#include <errno.h>

#include <iostream>
#include <algorithm>  /* required by g++ 2.95.3 to define min() */

using namespace std;

#undef sprintf
#define sprintf @  /* forbidden */


///////////////////////////////////////////////////////////////////////////////


static const char *SEARCH_STRING_PATH = "Search/String";
static const char *FILE_PATTERNS_PATH = "Search/FilePatterns";
static const char *SEARCH_DIR_PATH = "Search/Directory";
static const char *EDITOR_COMMAND_PATH = "Commands/Editor";
static const char *MATCH_WHOLE_WORDS_PATH = "Options/MatchWord";
static const char *MATCH_CASE_PATH = "Options/MatchCase";
static const char *USE_PERL_REGEX_PATH = "Options/UsePerlRegex";
static const char *DIR_RECURSION_DEPTH_PATH = "Options/DirRecursionDepth";
static const char *EXCLUDE_CVS_DIRS_PATH = "Options/ExcludeCVSDirs";
static const char *EXCLUDE_SYMLINKED_DIRS_PATH = "Options/ExcludeSymlinkedDirs";

static const guint BORDER = 4;
static const gint SPACING = 4;

static const gint MAX_DIR_RECURSION_DEPTH = 20;


///////////////////////////////////////////////////////////////////////////////


/*static*/ SagasuApp *SagasuApp::instance = NULL;


///////////////////////////////////////////////////////////////////////////////


void
reaper(int)
{
    pid_t pid;
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
      ;
}


#define WIDGET_AND_POINTER_CALLBACK(cb) \
      static void cb(GtkWidget *w = NULL, gpointer data = NULL) \
            { SagasuApp::get_instance().cb(w, data); }
#include "callbacks.h"
#undef WIDGET_AND_POINTER_CALLBACK


/*static*/
gboolean
input_channel_ready_cb(GIOChannel *source,
                  GIOCondition condition,
                  gpointer data)
{
    return ((SagasuApp *) data)->input_channel_ready(condition);
}


gboolean
result_page_button_press_cb(GtkWidget *text_view,
                      GdkEventButton *e,
                      gpointer data)
{
    ResultPage *page = (ResultPage *) data;
    SagasuApp &app = SagasuApp::get_instance();

    if (e->button == 1 && e->type == GDK_2BUTTON_PRESS)
      app.launch_editor_for_position(
                  page->get_text_pos_from_window_coords(e->x, e->y));

    return false;
}


bool
is_entry_empty(GtkWidget *entry)
{
    const gchar *text = gtk_entry_get_text(GTK_ENTRY(entry));
    g_return_val_if_fail(text != NULL, true);
    return text[0] == '\0';
}


gboolean
key_press_in_result_search_entry_cb(GtkWidget *, GdkEventKey *event, gpointer)
{
    g_return_val_if_fail(event != NULL, true);
    return SagasuApp::get_instance().key_press_in_result_search_entry(event);
}


gboolean
key_press_in_search_field_cb(GtkWidget *, GdkEventKey *event, gpointer)
{
    g_return_val_if_fail(event != NULL, true);
    return SagasuApp::get_instance().key_press_in_search_field(event);
}


void
about_cb(GtkWidget *widget, gpointer data)
{
    static GtkWidget *dialog = NULL;

    GtkWidget *app = (GtkWidget *) data;

    if (dialog != NULL) 
    {
      g_assert(GTK_WIDGET_REALIZED(dialog));
      gdk_window_show(dialog->window);
      gdk_window_raise(dialog->window);
    }
    else
    {        
      const gchar *authors[] =
      {
          "Pierre Sarrazin <http://sarrazip.com/>",
          NULL
      };

      string logo_filename = 
            get_dir(PIXMAPDIR, "PIXMAPDIR") + PACKAGE ".png";

      GdkPixbuf *logo = gdk_pixbuf_new_from_file(
                              logo_filename.c_str(), NULL); 
      string copyright = string(
           "Copyright (C) 2002-2004 Pierre Sarrazin <http://sarrazip.com/>\n")
                  + _("Distributed under the GNU General Public License");
      dialog = gnome_about_new(_("Sagasu"), VERSION,
          copyright.c_str(),
          _("GNOME tool to find strings in a set of files"),
          authors, NULL, NULL, logo);

      gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(app));      
      if (logo != NULL)
          gdk_pixbuf_unref(logo);

      g_signal_connect(G_OBJECT(dialog), "destroy",
                        G_CALLBACK(gtk_widget_destroyed), &dialog); 

      gtk_widget_show(dialog);
    }
}


static
GtkWidget *
label_new(const gchar *utf8_text, GtkWidget *mnemonic_widget)
{
    GtkWidget *label = gtk_label_new_with_mnemonic(utf8_text);
    if (mnemonic_widget != NULL)
      gtk_label_set_mnemonic_widget(GTK_LABEL(label), mnemonic_widget);
    return label;
}


///////////////////////////////////////////////////////////////////////////////

#define GNOMEUIINFO_ITEM_ACCEL(label, tooltip, callback, ac_key, ac_mods) \
        { GNOME_APP_UI_ITEM, label, tooltip, (gpointer)callback, NULL, NULL, \
                GNOME_APP_PIXMAP_NONE, NULL, \
            (guint) (ac_key), (GdkModifierType) (ac_mods), NULL }


GnomeUIInfo file_menu[] =
{
    GNOMEUIINFO_MENU_EXIT_ITEM(::exit_cb, NULL),
    GNOMEUIINFO_END
};

GnomeUIInfo tabs_menu[] =
{
    GNOMEUIINFO_MENU_NEW_ITEM(
      N_("_New tab"),
      N_("Create a new tab in which to display search results"),
      ::create_new_result_page_cb,
      NULL),
    GNOMEUIINFO_ITEM_NONE(
      N_("Go to _first tab"),
      N_("Go to the leftmost tab"),
      ::first_tab_cb),
    GNOMEUIINFO_ITEM_ACCEL(
      N_("P_revious tab"),
      N_("Go to the tab at the left of the current one"),
      previous_tab_cb,
      GDK_r,
      GDK_CONTROL_MASK),
    GNOMEUIINFO_ITEM_ACCEL(
      N_("Nex_t tab"),
      N_("Go to the tab at the right of the current one"),
      next_tab_cb,
      GDK_t,
      GDK_CONTROL_MASK),
    GNOMEUIINFO_ITEM_NONE(
      N_("Go to _last tab"),
      N_("Go to the rightmost tab"),
      ::last_tab_cb),
    GNOMEUIINFO_ITEM_ACCEL(
      N_("_Close tab"),
      N_("Close the current result page"),
      ::close_current_tab_cb,
      GDK_w,
      GDK_CONTROL_MASK),
    GNOMEUIINFO_END
};

GnomeUIInfo help_menu[] =
{
    GNOMEUIINFO_HELP(PACKAGE),
    GNOMEUIINFO_ITEM_NONE(
      N_("_Say the word \"sagasu\""),
      N_("Play a sound file that pronounces the word \"sagasu\""),
      ::say_sagasu_cb),
    GNOMEUIINFO_ITEM_NONE(
      N_("Go to Sagasu _Home Page"),
      N_("Open the Sagasu Home Page in a browser"),
      ::home_page_cb),
    GNOMEUIINFO_ITEM_NONE(
      N_("View this Program's _License (GPL)"),
      N_("View the text of the license that covers this program"),
      ::view_license_cb),
    GNOMEUIINFO_MENU_ABOUT_ITEM(about_cb, NULL),
    GNOMEUIINFO_END
};

GnomeUIInfo menu[] =
{
    GNOMEUIINFO_MENU_FILE_TREE(file_menu),
    { GNOME_APP_UI_SUBTREE_STOCK, N_("_Tabs"), NULL, tabs_menu, NULL, NULL,
                (GnomeUIPixmapType) 0, NULL, 0, (GdkModifierType) 0, NULL },
    GNOMEUIINFO_MENU_HELP_TREE(help_menu),
    GNOMEUIINFO_END
};


GnomeUIInfo toolbar[] =
{
    GNOMEUIINFO_ITEM_STOCK(
      _("New tab"),
      _("Create a new tab in which to display search results"),
      ::create_new_result_page_cb,
      GTK_STOCK_NEW),
    GNOMEUIINFO_ITEM_STOCK(
      _("First tab"),
      _("Go to the leftmost tab"),
      ::first_tab_cb,
      GTK_STOCK_GOTO_FIRST),
    GNOMEUIINFO_ITEM_STOCK(
      _("Previous tab"),
      _("Go to the tab at the left of the current one"),
      previous_tab_cb,
      GTK_STOCK_GO_BACK),
    GNOMEUIINFO_ITEM_STOCK(
      _("Next tab"),
      _("Go to the tab at the right of the current one"),
      next_tab_cb,
      GTK_STOCK_GO_FORWARD),
    GNOMEUIINFO_ITEM_STOCK(
      _("Last tab"),
      _("Go to the rightmost tab"),
      ::last_tab_cb,
      GTK_STOCK_GOTO_LAST),
    GNOMEUIINFO_ITEM_STOCK(
      _("New search"),
      _("Empty the search string field"),
      ::erase_search_string_cb,
      GTK_STOCK_CLEAR),
    GNOMEUIINFO_SEPARATOR,
    GNOMEUIINFO_ITEM_STOCK(
      _("Quit"),
      _("Quit the program"),
      ::exit_cb,
      GTK_STOCK_QUIT),
    GNOMEUIINFO_END
};


///////////////////////////////////////////////////////////////////////////////


/*static*/
void
SagasuApp::create_instance(const string &init_search_expr,
                        const string &init_search_dir)
{
    instance = new SagasuApp(init_search_expr, init_search_dir);
}


/*static*/
SagasuApp &
SagasuApp::get_instance()
{
    return *instance;
}


SagasuApp::SagasuApp(const string &init_search_expr,
                  const string &init_search_dir)
  : appwin(NULL),
  
    search_string_entry(NULL),
    file_patterns_entry(NULL),
    search_dir_entry(NULL),
    editor_cmd_entry(NULL),

    search_button(NULL),
    default_file_patterns_button(NULL),
    browse_search_dir_button(NULL),
    default_editor_cmd_button(NULL),

    match_whole_words_button(NULL),
    match_case_button(NULL),
    use_perl_regex_button(NULL),
    dir_recursion_depth_spin_button(NULL),
    exclude_cvs_dirs_button(NULL),
    exclude_symlinked_dirs_button(NULL),

    result_notebook(NULL),
    next_notebook_page_num(1),
    default_file_patterns("*.c *.cc *.C *.cpp *.cxx *.h"),
    default_editor_command("gnome-terminal -e \"vi +%n '%f'\""),
    input_channel(NULL),
    input_fd(-1),
    search_pid(0),
    result_page_of_current_search(NULL),
    pending_input(),
    num_matching_lines(0),
    num_matching_files(0),
    last_matching_filename(),
    status(NULL),

    result_search_entry(NULL),
    result_search_button(NULL),

    search_dir_file_sel_dlg(NULL),
    search_dir_file_sel_label(NULL)
{
    build_user_interface(init_search_expr, init_search_dir);

    /*  Set up a "reaper" callback which will be call when any child of
      this application terminates.  This avoids having <defunct> Perl
      interpreters.
    */
    signal(SIGCHLD, reaper);
}


SagasuApp::~SagasuApp()
{
}


string
SagasuApp::make_next_page_tab_label()
{
    char temp[128];
    snprintf(temp, sizeof(temp), "  %s%lu  ",
            next_notebook_page_num < 10 ? "_" : "",
            next_notebook_page_num);
    next_notebook_page_num++;
    return temp;
}


GtkWidget *
SagasuApp::make_next_page_tab(ResultPage *page)
/*
    Creates an hbox that contains a label and a button.
    The label shows a unique number that identifies that result page.
    The button shows a "close" icon.  When clicked, tab_close_clicked_cb()
    is called with 'page' as the data.
*/
{
    GtkWidget *label = gtk_label_new_with_mnemonic(
                        make_next_page_tab_label().c_str());

    string xpm_filename = get_dir(PIXMAPDIR, "PIXMAPDIR") + "close.xpm";
    GtkWidget *image = gtk_image_new_from_file(xpm_filename.c_str());
    assert(image != NULL);
    GtkWidget *button = gtk_button_new();
    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    gtk_container_add(GTK_CONTAINER(button), image);

    GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
    gtk_widget_show_all(GTK_WIDGET(hbox));

    g_signal_connect(G_OBJECT(button), "clicked",
                  G_CALLBACK(::tab_close_clicked_cb), page);

    return hbox;
}


void
SagasuApp::show_working_state(bool busy)
{
    const char *utf8_label_text = (busy ? _("_Stop") : _("_Search"));

    gtk_button_set_label(GTK_BUTTON(search_button), utf8_label_text);

    gtk_widget_set_sensitive(
            GTK_WIDGET(default_file_patterns_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(browse_search_dir_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(default_editor_cmd_button), !busy);

    gtk_widget_set_sensitive(
            GTK_WIDGET(match_whole_words_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(match_case_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(use_perl_regex_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(dir_recursion_depth_spin_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(exclude_cvs_dirs_button), !busy);
    gtk_widget_set_sensitive(
            GTK_WIDGET(exclude_symlinked_dirs_button), !busy);

    if (busy)
    {
      display_wait_cursor();
      gnome_appbar_push(GNOME_APPBAR(status), _("Searching..."));
    }
    else
    {
      remove_wait_cursor();
      gnome_appbar_pop(GNOME_APPBAR(status));
    }
}


void
SagasuApp::display_wait_cursor()
{
    assert(appwin != NULL);

    if (appwin->window != NULL)
    {
      GdkCursor *cursor = gdk_cursor_new(GDK_WATCH);
      gdk_window_set_cursor(appwin->window, cursor);
      gdk_cursor_unref(cursor);
    }
}


void
SagasuApp::remove_wait_cursor()
{
    assert(appwin != NULL);
    if (appwin->window != NULL)
      gdk_window_set_cursor(appwin->window, NULL);
}


void
SagasuApp::build_user_interface(const std::string &init_search_expr,
                        const std::string &init_search_dir)
{
    // Create the application:

    appwin = gnome_app_new(PACKAGE, _("Sagasu"));
    gtk_window_set_title(GTK_WINDOW(appwin), _("Sagasu"));

    g_signal_connect(appwin, "delete_event", G_CALLBACK(::exit_cb), NULL);

    gtk_window_set_resizable(GTK_WINDOW(appwin), TRUE);
    gtk_window_set_default_size(GTK_WINDOW(appwin), 600, 500);
    gtk_window_set_wmclass(GTK_WINDOW(appwin), PACKAGE, PACKAGE);


    // Fill the main window:

    GtkWidget *vbox = gtk_vbox_new(FALSE, SPACING);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), BORDER);

    //////////////////////////////////////////////////////////////////////
    //
    // Search parameter fields
    //

    GtkWidget *parameters_table = gtk_table_new(4, 3, FALSE);
    gtk_table_set_row_spacings(GTK_TABLE(parameters_table), 2);
    gtk_table_set_col_spacings(GTK_TABLE(parameters_table), 2);

    search_string_entry = gtk_entry_new();
    file_patterns_entry = gtk_entry_new();
    search_dir_entry = gtk_entry_new();
    editor_cmd_entry = gtk_entry_new();

    struct
    {
      GtkWidget **button;
      GtkWidget *entry;
      const char *prompt_text;
      const char *button_text;
      void (*cb)(GtkWidget *, gpointer);
    }
    params[] =
    {
      { &search_button, search_string_entry, _("Search strin_g:"),
                        "", ::search_cb },
      { &default_file_patterns_button, file_patterns_entry,
                                    _("File _patterns:"),
                        _("_Defaults"), ::default_file_patterns_cb },
      { &browse_search_dir_button, search_dir_entry, _("Se_arch directory:"),
                        _("_Browse..."), ::browse_search_dir_cb },
      { &default_editor_cmd_button, editor_cmd_entry, _("Edit_or command:"),
                        _("D_efault"), ::default_editor_cmd_cb },
      { NULL, NULL, NULL, NULL },
    };

    size_t i;
    for (i = 0; params[i].entry != NULL; i++)
    {
      GtkWidget *label = label_new(params[i].prompt_text, params[i].entry);
      gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);

      gtk_entry_set_max_length(GTK_ENTRY(params[i].entry), 1023);

      GtkWidget *button = gtk_button_new_with_mnemonic(params[i].button_text);

      assert(*params[i].button == NULL);
      *params[i].button = button;

      g_signal_connect(G_OBJECT(button), "clicked",
                              G_CALLBACK(params[i].cb), NULL);

      gtk_table_attach(GTK_TABLE(parameters_table), label,
            0, 1, i, i + 1, GTK_FILL, GTK_SHRINK, 0, 0);
      gtk_table_attach(GTK_TABLE(parameters_table), params[i].entry,
            1, 2, i, i + 1,
                GtkAttachOptions(GTK_EXPAND | GTK_FILL), GTK_SHRINK, 0, 0);
      gtk_table_attach(GTK_TABLE(parameters_table), button,
            2, 3, i, i + 1, GTK_FILL, GTK_SHRINK, 0, 0);

      g_signal_connect(G_OBJECT(params[i].entry), "key-press-event",
                  G_CALLBACK(key_press_in_search_field_cb), NULL);
    }


    //////////////////////////////////////////////////////////////////////
    //
    // Search options check buttons
    //

    GtkWidget *options_table = gtk_table_new(2, 3, FALSE);
    gtk_table_set_col_spacings(GTK_TABLE(options_table), SPACING * 4);

    struct
    {
      GtkWidget **check_button;
      const char *button_text;
    }
    buttons[] =
    {
      { &match_whole_words_button, _("Match _whole words") },
      { &match_case_button, _("Match _case") },
      { &use_perl_regex_button, _("Use Perl rege_x") },
      { NULL, "" },
      { &exclude_cvs_dirs_button, _("Exclude C_VS dirs") },
      { &exclude_symlinked_dirs_button, _("Exclude sym_linked dirs") },
      { NULL, NULL }
    };

    for (i = 0; buttons[i].button_text != NULL; i++)
    {
      GtkWidget *w;
      if (buttons[i].check_button == NULL)
      {
          /*
            Exception: in this slot, we now put a spin button that
            allows the user to select the depth of directory recursion,
            instead of just allowing or disallowing recursion.
          */
          GtkAdjustment *adj = (GtkAdjustment *) gtk_adjustment_new(
                  MAX_DIR_RECURSION_DEPTH, 0.0, MAX_DIR_RECURSION_DEPTH,
                  1.0, 5.0, 0.0);
          dir_recursion_depth_spin_button = gtk_spin_button_new(adj, 0, 0);
          gtk_spin_button_set_numeric(
                  GTK_SPIN_BUTTON(dir_recursion_depth_spin_button), true);

          GtkWidget *label = label_new(_("Dir _recursion depth:"),
                              dir_recursion_depth_spin_button);
          gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);

          w = gtk_hbox_new(FALSE, 2);
          gtk_box_pack_start(GTK_BOX(w), label, FALSE, FALSE, 0);
          gtk_box_pack_start(GTK_BOX(w),
                  dir_recursion_depth_spin_button, FALSE, FALSE, 0);
      }
      else
      {
          w = gtk_check_button_new_with_mnemonic(
                                  buttons[i].button_text);
          assert(*buttons[i].check_button == NULL);
          *buttons[i].check_button = w;
      }

      gtk_table_attach(GTK_TABLE(options_table), w,
                  i % 3, i % 3 + 1, i / 3, i / 3 + 1,
                  GTK_FILL, GTK_SHRINK, 0, 0);
    }


    //////////////////////////////////////////////////////////////////////
    //
    // Notebook containing result pages
    //

    result_notebook = gtk_notebook_new();
    gtk_notebook_set_scrollable(GTK_NOTEBOOK(result_notebook), TRUE);
    create_new_result_page_cb();


    //////////////////////////////////////////////////////////////////////
    //
    // Entry and button to search inside a result page
    //

    GtkWidget *result_search_box = gtk_hbox_new(FALSE, SPACING);
    result_search_entry = gtk_entry_new();
    GtkWidget *result_search_prompt =
                  label_new(_("Find in res_ults:"), result_search_entry);
    result_search_button = gtk_button_new_with_mnemonic(_("F_ind"));

    gtk_box_pack_start(GTK_BOX(result_search_box),
                  result_search_prompt, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(result_search_box),
                  result_search_entry, TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(result_search_box),
                  result_search_button, FALSE, FALSE, 0);

    g_signal_connect(G_OBJECT(result_search_entry), "changed",
                G_CALLBACK(::result_search_entry_changed_cb), this);
    g_signal_connect(G_OBJECT(result_search_entry), "key-press-event",
                G_CALLBACK(::key_press_in_result_search_entry_cb), NULL);
    g_signal_connect(G_OBJECT(result_search_button), "clicked",
                G_CALLBACK(::result_search_button_clicked_cb), NULL);

    result_search_entry_changed_cb();


    //////////////////////////////////////////////////////////////////////

    gtk_box_pack_start(GTK_BOX(vbox), parameters_table, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), options_table, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), result_notebook, TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), result_search_box, FALSE, FALSE, 0);

    gnome_app_set_contents(GNOME_APP(appwin), vbox);


    status = gnome_appbar_new(FALSE, TRUE, GNOME_PREFERENCES_NEVER);
    gnome_app_set_statusbar(GNOME_APP(appwin), status);

    // This call needs to appear twice, for some reason:
    gnome_appbar_push(GNOME_APPBAR(status), _("Ready."));
    gnome_appbar_push(GNOME_APPBAR(status), _("Ready."));


    menu[1].label = gettext(menu[1].label);  // PATCH

    gnome_app_create_menus_with_data(GNOME_APP(appwin), menu, appwin);
    gnome_app_create_toolbar_with_data(GNOME_APP(appwin), toolbar, appwin);
    gnome_app_install_menu_hints(GNOME_APP(appwin), menu);


    load_configuration();

    // TODO: do not convert the two parameters if command line is in UTF-8.
    if (!init_search_expr.empty())
      entry_set_text(search_string_entry, u8_string(init_search_expr));
    if (!init_search_dir.empty())
      entry_set_text(search_dir_entry, u8_string(init_search_dir));

    entry_changed_cb();


    // These signals must be connected after the initialization of the entries:
    g_signal_connect(G_OBJECT(search_string_entry), "changed",
                  G_CALLBACK(::entry_changed_cb), NULL);
    g_signal_connect(G_OBJECT(file_patterns_entry), "changed",
                  G_CALLBACK(::entry_changed_cb), NULL);
    g_signal_connect(G_OBJECT(search_dir_entry), "changed",
                  G_CALLBACK(::entry_changed_cb), NULL);


    show_working_state(false);

    gtk_widget_show_all(appwin);
}


ResultPage *
SagasuApp::get_current_result_page() const
{
    gint page_num = gtk_notebook_get_current_page(
                        GTK_NOTEBOOK(result_notebook));
    g_return_val_if_fail(page_num >= 0, NULL);

    GtkWidget *child_widget = gtk_notebook_get_nth_page(
                        GTK_NOTEBOOK(result_notebook), page_num);
    g_return_val_if_fail(child_widget != NULL, NULL);

    ResultPage *page = (ResultPage *)
                  g_object_get_data(G_OBJECT(child_widget), "ResultPage");
    g_return_val_if_fail(page != NULL, NULL);

    return page;
}


void
SagasuApp::search_cb(GtkWidget *, gpointer)
{
    SagasuApp::get_instance().save_configuration();

    string target_expr = latin1_string(gtk_entry_get_text(
                              GTK_ENTRY(search_string_entry)));
    if (target_expr.empty())
      return;

    string raw_target_expr = target_expr;

    string search_dir = latin1_string(gtk_entry_get_text(
                              GTK_ENTRY(search_dir_entry)));
    if (search_dir.empty())
      return;
    if (search_dir != "/")
      chomp(search_dir, '/');

    ResultPage *cur_page = get_current_result_page();
    g_return_if_fail(cur_page != NULL);

    cur_page->set_next_search_pos(0);


    if (input_channel != NULL)  // if a search is already in progress
    {
      cur_page->insert_text("\n\n" "*** "
                + string(_("Interrupted")) + " ***" "\n");

      close_input();
      return;
    }

    cur_page->clear();


    if (use_regex())
      quotechars(target_expr, "#/@");
    else
      quotechars(target_expr);

    if (match_whole_words())
      target_expr = "\\b(" + target_expr + ")\\b";

    input_channel = NULL;
    create_search_process(
                  target_expr,
                  raw_target_expr,
                  search_dir,
                  get_file_patterns(),
                  match_case(),
                  dir_recursion_depth(),
                  exclude_cvs_dirs(),
                  exclude_symlinked_dirs());
    if (input_fd == -1)
      return;

    input_channel = g_io_channel_unix_new(input_fd);
    g_return_if_fail(input_channel != NULL);
    if (g_io_channel_set_encoding(input_channel, NULL, NULL)
                            != G_IO_STATUS_NORMAL)
      g_return_if_reached();

    (void) g_io_add_watch(input_channel,
            GIOCondition(G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL),
            input_channel_ready_cb, this);

    show_working_state(true);
}


std::string
SagasuApp::get_file_patterns() const
{
    return latin1_string(gtk_entry_get_text(
                            GTK_ENTRY(file_patterns_entry)));
}


bool
SagasuApp::match_whole_words() const
{
    return gtk_toggle_button_get_active(
                GTK_TOGGLE_BUTTON(match_whole_words_button));
}


bool
SagasuApp::match_case() const
{
    return gtk_toggle_button_get_active(
                GTK_TOGGLE_BUTTON(match_case_button));
}


bool
SagasuApp::use_regex() const
{
    return gtk_toggle_button_get_active(
                GTK_TOGGLE_BUTTON(use_perl_regex_button));
}


gint
SagasuApp::dir_recursion_depth() const
{
    return gtk_spin_button_get_value_as_int(
            GTK_SPIN_BUTTON(dir_recursion_depth_spin_button));
}


bool
SagasuApp::exclude_cvs_dirs() const
{
    return gtk_toggle_button_get_active(
                GTK_TOGGLE_BUTTON(exclude_cvs_dirs_button));
}


bool
SagasuApp::exclude_symlinked_dirs() const
{
    return gtk_toggle_button_get_active(
                GTK_TOGGLE_BUTTON(exclude_symlinked_dirs_button));
}


void
SagasuApp::create_search_process(const string &target_expr,
                        const string &raw_target_expr,
                        const string &search_dir,
                        const string &file_patterns,
                        bool match_case,
                        gint recursion_depth,
                        bool exclude_cvs_dirs,
                        bool exclude_symlinked_dirs)
/*
    Creates a process that will search for the regular expression
    given in 'target_expr' and returns the file descriptor that can
    be read to obtain the results written by this process.

    'target_expr' must not be empty.

    Sets members 'input_fd' and 'search_pid'.  On error, these members
    become -1 and 0 respectively.
*/
{
    assert(result_page_of_current_search == NULL);
    assert(num_matching_lines == 0);
    assert(num_matching_files == 0);
    assert(!target_expr.empty());

    errno = 0;
    input_fd = -1;
    search_pid = 0;

    /*
      Check that the search directory is accessible.
    */
    if (access(search_dir.c_str(), R_OK | X_OK) != 0)
    {
      int e = errno;
      errno_dialog(appwin, _("Search directory is not accessible"), e);
      return;
    }


    /*
      Create a pipe and receive the reading and writing file descriptors
      in pipefds[0] and pipefds[1] respectively.
    */
    int pipefds[2];
    if (pipe(pipefds) != 0)
    {
      int e = errno;
      errno_dialog(appwin, _("Error when creating a pipe"), e);
      return;
    }

    /*
      Duplicate the current process.
      The child's stdout and stderr are redirected to the pipe,
      and the pipe's reading file description is returned.
      The child executes a Perl script that does the actual searching.
    */
    pid_t pid = fork();
    if (pid == -1)
    {
      int e = errno;
      errno_dialog(appwin, _("Error when creating the search process"), e);
      close(pipefds[0]);
      close(pipefds[1]);
      return;
    }

    if (pid == 0)  // if this is the child
    {
      close(pipefds[0]);  // child does not read from this GNOME app

      // redirect child's stdout and stderr to the writing fd of the pipe:
      if (dup2(pipefds[1], STDOUT_FILENO) == -1)
      {
          int e = errno;
          cerr << "dup2 failed for stdout: " << strerror(e) << endl;
          _exit(127);
      }
      if (dup2(pipefds[1], STDERR_FILENO) == -1)
      {
          int e = errno;
          cerr << "dup2 failed for stderr: " << strerror(e) << endl;
          _exit(127);
      }
      close(pipefds[1]);  // not needed anymore

      string script = get_dir(PKGDATADIR, "PKGDATADIR") + "sagasu-helper.pl";

      char depth[128];
      snprintf(depth, sizeof(depth), "%d", (int) recursion_depth);

      execl("/usr/bin/perl", "perl", script.c_str(),
            target_expr.c_str(), 
            search_dir.c_str(), 
            file_patterns.c_str(),
            match_case ? "1" : "0",
            exclude_cvs_dirs ? "1" : "0",
            exclude_symlinked_dirs ? "1" : "0",
            depth,
            NULL);

      cerr << "exec() returned: " << strerror(errno) << endl;
      exit(EXIT_FAILURE);
    }


    /*
      This is the parent.
      Close the writing end of the pipe.
      Remember in which page the results are to be written.
      Start that page with the search string.
    */
    close(pipefds[1]);
    result_page_of_current_search = get_current_result_page();
    result_page_of_current_search->insert_text(
                _("Search string:") + string(" ") + raw_target_expr + "\n");
    pending_input.erase();

    last_matching_filename.erase();
    num_matching_lines = 0;
    num_matching_files = 0;
    show_num_matching_lines();

    input_fd = pipefds[0];
    search_pid = pid;
}


void
SagasuApp::close_input()
/*
    If there is an active input channel, shuts it down, closes the pipe's
    file descriptor, kills the search process with SIGTERM, sets input_fd
    to -1 and search_pid to 0.

*/
{
    if (input_channel != NULL)
    {
      g_io_channel_shutdown(input_channel, FALSE, NULL);
      g_io_channel_unref(input_channel);
      input_channel = NULL;

      close(input_fd);
      input_fd = -1;

      kill(search_pid, SIGTERM);
      search_pid = 0;
    }
    else
    {
      assert(input_fd == -1);
      assert(search_pid == 0);
    }

    show_num_matching_lines();


    result_page_of_current_search = NULL;
    num_matching_lines = 0;
    num_matching_files = 0;

    show_working_state(false);
}


void
SagasuApp::show_num_matching_lines()
{
    if (result_page_of_current_search == NULL)
      return;
    char status[2048];
    snprintf(status, sizeof(status),
          "%s: %u; %s: %u %s",
            _("Matching lines"), (unsigned) num_matching_lines,
            _("matching files"), (unsigned) num_matching_files,
            (input_channel == NULL ? "" : _("(still searching)"))
            );
    result_page_of_current_search->set_status_label(status);
}


gboolean
SagasuApp::input_channel_ready(GIOCondition condition)
{
    if (input_fd == -1)
    {
      assert(input_channel == NULL);
      return FALSE;  // event source must be removed
    }

    char buffer[1024];
    ssize_t bytes_read = read(input_fd, buffer, sizeof(buffer));
    if (bytes_read <= 0)
    {
      int e = errno;
      if (bytes_read < 0)
      {
          g_warning(
                (string("read() failed on source fd: ")
                + strerror(e)).c_str());
      }

      close_input();
      return FALSE;  // event source must be removed
    }

    pending_input.append(buffer, bytes_read);
    process_pending_input();
    show_num_matching_lines();
    return TRUE;
}


void
SagasuApp::process_pending_input()
/*
    Pass all newline terminated strings in 'pendingInput'.
    Count those that do not start with the error prefix as result lines.
    The passed lines are inserted in the current result buffer
    and then are removed from 'pendingInput'.

    We expect 'pendingInput' to sometimes end with characters that are
    not followed by a newline.  This should mean that more input is
    expected from the pipe.

    In addition, filenames are extracted from result lines and distinct
    filenames are counted as matching files.  This method assumes that
    all occurrences of a filename will be consecutive.
*/
{
    string::size_type pos_newline, search_pos;
    for (search_pos = 0;
      (pos_newline = pending_input.find('\n', search_pos)) != string::npos;
      search_pos = pos_newline + 1)
    {
      if (memcmp(pending_input.data() + search_pos, "*** ", 4) != 0)
      {
          num_matching_lines++;

          string::size_type pos_colon = pending_input.find(':', search_pos);
          if (pos_colon < pos_newline)
          {
            string filename(pending_input,
                        search_pos, pos_colon - search_pos);
            if (filename != last_matching_filename)
            {
                num_matching_files++;
                last_matching_filename = filename;
            }
          }
      }

    }

    if (search_pos > 0)
    {
      string inserted(pending_input, 0, search_pos);
      result_page_of_current_search->insert_text(inserted);
      pending_input.erase(0, search_pos);
    }
}


string
SagasuApp::get_result_text_line_for_pos(gint pos_in_chars) const
/*
    Returns a Latin-1 string.
*/
{
    ResultPage *page = get_current_result_page();
    GtkWidget *view = page->get_text_view();
    GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));

    // Get an iterator on the desired position:
    GtkTextIter it;
    gtk_text_buffer_get_iter_at_offset(buf, &it, pos_in_chars);

    // Get the number (>=0) of the line that contains the desired position:
    gint line_num = gtk_text_iter_get_line(&it);
    if (line_num == 0)  // first line shows search string; it is not a result
      return "";

    // Get an iterator on the start of that line:
    GtkTextIter line_start_it;
    gtk_text_buffer_get_iter_at_line_offset(buf, &line_start_it, line_num, 0);

    // Get an iterator on the end of that line, using the length:
    gint line_length = gtk_text_iter_get_chars_in_line(&it);
    GtkTextIter line_end_it = line_start_it;
    gtk_text_iter_forward_chars(&line_end_it, line_length);

    // Get the line itself, and convert it from UTF-8 to Latin-1:
    GCharPtr utf8_line = gtk_text_buffer_get_text(
                        buf, &line_start_it, &line_end_it, true);
    return latin1_string(utf8_line.get());
}


int
get_filename_and_line_num_from_line(
                        const string &line,
                        string &filename,
                        unsigned long &line_num)
/*
    'line' must be in Latin-1.
*/
{
    string::size_type pos_colon1 = line.find(':');
    if (pos_colon1 == string::npos || pos_colon1 == 0)
      return -1;
    string::size_type pos_colon2 = line.find(':', pos_colon1 + 1);
    if (pos_colon2 == string::npos || pos_colon2 == line.length() - 1)
      return -2;

    // Check that all characters between the two colons are decimal digits:
    if (pos_colon1 + 1 == pos_colon2)
      return -3;
    for (string::size_type i = pos_colon1 + 1; i < pos_colon2; i++)
      if (!isdigit(line[i]))
          return -4;
    errno = 0;
    line_num = strtoul(line.c_str() + pos_colon1 + 1, NULL, 10);
    if (line_num == ULONG_MAX || errno == ERANGE)
      return -5;
    filename = string(line, 0, pos_colon1);
    return 0;
}


void
SagasuApp::launch_editor_for_position(gint pos_in_chars)
{
    string line = get_result_text_line_for_pos(pos_in_chars);

    string filename;
    unsigned long line_num = 0;
    if (get_filename_and_line_num_from_line(line, filename, line_num) != 0)
      return;


    const gchar *utf8_cmd = gtk_entry_get_text(GTK_ENTRY(editor_cmd_entry));
    string cmd = latin1_string(utf8_cmd);
    char ln[128];
    snprintf(ln, sizeof(ln), "%lu", line_num);
    substitute(cmd, "%n", ln);
    substitute(cmd, "%f", filename);


    // Start a child process to run system(), which is blocking:
    pid_t pid = fork();
    if (pid == -1)
    {
      int e = errno;
      errno_dialog(appwin, _("Error when creating the editor process"), e);
      return;
    }
    if (pid == 0)  // if in child
    {
      system(cmd.c_str());

      /*
          We must exit the child with _exit() instead of exit()
          to avoid crashing with the following error message
          when the user forcibly kills the terminal program
          (e.g., xterm) started by system():

          Gdk-ERROR **: X connection to :0.0 broken
          (explicit kill or server shutdown).
      */
      _exit(EXIT_SUCCESS);
    }

    // In parent: nothing
}


string
get_config_var_path(const string &var)
{
    return string("/") + PACKAGE + "/" + var;
}


string
get_config_var(const string &var, const string &utf8_default)
/*
    gnome_config_sync() must be called afterwards to save the changes
    in the configuration file.
    Parameters must be Latin-1 strings.
    Returns a UTF-8 string.
*/
{
    string path = get_config_var_path(var);
    GCharPtr s = gnome_config_get_string(path.c_str());
    if (s.get() != NULL)
      return s.get();
    gnome_config_set_string(path.c_str(), utf8_default.c_str());
    return utf8_default;
}


bool
get_bool_config_var(const string &var, bool deFault)
{
    return get_config_var(var, deFault ? "1" : "0") == "1";
}


void
set_config_var(const string &var, const string &utf8_value)
/*
    gnome_config_sync() must be called afterwards to save the changes
    in the configuration file.
    Parameters must be Latin-1 strings.
*/
{
    gnome_config_set_string(
            get_config_var_path(var).c_str(), utf8_value.c_str());
}


void
SagasuApp::load_configuration()
{
    entry_set_text(search_string_entry,
          get_config_var(SEARCH_STRING_PATH, ""));
    entry_set_text(file_patterns_entry,
          get_config_var(FILE_PATTERNS_PATH, default_file_patterns));
    const char *home = getenv("HOME");
    if (home == NULL)
      home = "/";
    entry_set_text(search_dir_entry,
            get_config_var(SEARCH_DIR_PATH, u8_string(home).c_str()));
    entry_set_text(editor_cmd_entry,
            get_config_var(EDITOR_COMMAND_PATH, default_editor_command));

    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(match_whole_words_button),
            get_bool_config_var(MATCH_WHOLE_WORDS_PATH, false));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(match_case_button),
            get_bool_config_var(MATCH_CASE_PATH, true));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(use_perl_regex_button),
            get_bool_config_var(USE_PERL_REGEX_PATH, false));

    string d = get_config_var(DIR_RECURSION_DEPTH_PATH, "-1");
    long depth = strtol(d.c_str(), NULL, 10);
    if (depth < 0 || depth > MAX_DIR_RECURSION_DEPTH)
      depth = MAX_DIR_RECURSION_DEPTH;
    gtk_spin_button_set_value(
            GTK_SPIN_BUTTON(dir_recursion_depth_spin_button),
            (gint) depth);

    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(exclude_cvs_dirs_button),
            get_bool_config_var(EXCLUDE_CVS_DIRS_PATH, true));
    gtk_toggle_button_set_active(
                      GTK_TOGGLE_BUTTON(exclude_symlinked_dirs_button),
            get_bool_config_var(EXCLUDE_SYMLINKED_DIRS_PATH, true));

    gnome_config_sync();
}


void
SagasuApp::save_configuration()
{
    set_config_var(SEARCH_STRING_PATH,
                  gtk_entry_get_text(GTK_ENTRY(search_string_entry)));
    set_config_var(FILE_PATTERNS_PATH,
                  gtk_entry_get_text(GTK_ENTRY(file_patterns_entry)));
    set_config_var(SEARCH_DIR_PATH,
                  gtk_entry_get_text(GTK_ENTRY(search_dir_entry)));
    set_config_var(EDITOR_COMMAND_PATH,
                  gtk_entry_get_text(GTK_ENTRY(editor_cmd_entry)));

    #define GET_ACTIVE(b) \
          (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(b)) ? "1" : "0")

    char depth[128];
    snprintf(depth, sizeof(depth), "%d", (int) dir_recursion_depth());

    set_config_var(MATCH_WHOLE_WORDS_PATH,
            GET_ACTIVE(match_whole_words_button));
    set_config_var(MATCH_CASE_PATH,
            GET_ACTIVE(match_case_button));
    set_config_var(USE_PERL_REGEX_PATH,
            GET_ACTIVE(use_perl_regex_button));
    set_config_var(DIR_RECURSION_DEPTH_PATH,
            depth);
    set_config_var(EXCLUDE_CVS_DIRS_PATH,
            GET_ACTIVE(exclude_cvs_dirs_button));
    set_config_var(EXCLUDE_SYMLINKED_DIRS_PATH,
            GET_ACTIVE(exclude_symlinked_dirs_button));

    #undef GET_ACTIVE

    gnome_config_sync();
}


void
SagasuApp::entry_changed_cb(GtkWidget *, gpointer)
{
    bool ss_empty = is_entry_empty(search_string_entry);
    bool fp_empty = is_entry_empty(file_patterns_entry);
    bool sd_empty = is_entry_empty(search_dir_entry);
    gtk_widget_set_sensitive(search_button,
                        !ss_empty && !fp_empty && !sd_empty);
}


void
SagasuApp::result_search_entry_changed_cb(GtkWidget *, gpointer)
{
    gtk_widget_set_sensitive(result_search_button,
                              !is_entry_empty(result_search_entry));
}


const gchar *
utf8_stristr(const gchar *haystack, const gchar *needle)
{
    const gunichar needle_start = g_utf8_get_char(needle);
    const gunichar needle_start_upper = g_unichar_toupper(needle_start);
    const gunichar needle_start_lower = g_unichar_tolower(needle_start);

    for (;;)
    {
      // If we are looking for "pizza", then look for a 'P' and for 'p':

      const gchar *found_upper = g_utf8_strchr(
                              haystack, -1, needle_start_upper);
      const gchar *found_lower = g_utf8_strchr(
                              haystack, -1, needle_start_lower);

      // If both searches failed, declare a failure:
      if (found_upper == NULL && found_lower == NULL)
          return NULL;

      // If one of the two searches succeeded, point to the position found;
      // if the two searches succeeded, point to the leftmost position:

      const gchar *found;
      if (found_upper != NULL && found_lower == NULL)
          found = found_upper;
      else if (found_upper == NULL && found_lower != NULL)
          found = found_lower;
      else
          found = min(found_upper, found_lower);

      // Try to match each character of 'needle' with the characters
      // that start at 'found':

      const gchar *f = found;

      const gchar *n;
      for (n = needle; *n != 0; n = g_utf8_find_next_char(n, NULL))
      {
          gunichar needle_char = g_utf8_get_char(n);
          gunichar needle_char_lower = g_unichar_tolower(needle_char);

          gunichar found_char = g_utf8_get_char(f);
          if (found_char == 0)  // if no more text in which to search:
            break;  // failure

          gunichar found_char_lower = g_unichar_tolower(found_char);

          if (needle_char_lower != found_char_lower)
            break;  // failure

          f = g_utf8_find_next_char(f, NULL);
      }

      if (*n == 0)  // if preceding loop succeeded
          return found;

      haystack = g_utf8_find_next_char(found, NULL);
    }
}


void
SagasuApp::result_search_button_clicked_cb(GtkWidget *, gpointer)
{
    const gchar *target = gtk_entry_get_text(GTK_ENTRY(result_search_entry));
    g_return_if_fail(target != NULL);
    if (target[0] == '\0')
      return;


    display_wait_cursor();


    // This call must precede the call to select_region().
    gtk_editable_select_region(GTK_EDITABLE(result_search_entry), 0, -1);


    ResultPage *page = get_current_result_page();
    string contents = page->get_all_text();
    gint search_pos = page->get_next_search_pos();
    gint content_length = page->get_char_count();
    if (search_pos >= content_length)
      search_pos = content_length;

    const gchar *contents_ptr = contents.c_str();
    const gchar *srch = g_utf8_offset_to_pointer(contents_ptr, search_pos);
    const gchar *found = utf8_stristr(srch, target);

    if (found == NULL)
    {
      gnome_appbar_set_status(GNOME_APPBAR(status), _("Not found"));

      page->set_next_search_pos(0);
    }
    else
    {
      glong found_length = g_utf8_strlen(found, -1);
      glong found_pos = content_length - found_length;

      unsigned pct = (unsigned)
                  floor(100.0 * found_pos / content_length + 0.5);
      char p[128];
      snprintf(p, sizeof(p), "%u", pct);
      string t = _("Found at:") + string(" ") + p + "%";

      gnome_appbar_set_status(GNOME_APPBAR(status), t.c_str());

      // Select found string:
      gint found_end = gint(found_pos + g_utf8_strlen(target, -1));
      page->select_text_region(gint(found_pos), found_end);

      // Set the position for the next search:
      page->set_next_search_pos(found_end);
    }

    remove_wait_cursor();
}


void
SagasuApp::default_file_patterns_cb(GtkWidget *, gpointer)
{
    gtk_entry_set_text(GTK_ENTRY(file_patterns_entry),
            latin1_string(default_file_patterns.c_str()).c_str());
}


void
SagasuApp::default_editor_cmd_cb(GtkWidget *, gpointer)
{
    gtk_entry_set_text(GTK_ENTRY(editor_cmd_entry),
            latin1_string(default_editor_command).c_str());
}


void
search_dir_file_sel_ok(GtkWidget *, gpointer)
{
    SagasuApp::get_instance().accept_search_dir();
}


void
search_dir_file_sel_cancel(GtkWidget *, gpointer)
{
    SagasuApp::get_instance().close_search_dir_file_sel();
}


gboolean
search_dir_file_sel_destroy(GtkWidget *, GdkEvent *, gpointer)
{
    SagasuApp::get_instance().close_search_dir_file_sel();
    return true;
}


void
SagasuApp::browse_search_dir_cb(GtkWidget *, gpointer)
{
    if (search_dir_file_sel_dlg != NULL)
      return;

    assert(search_dir_file_sel_label == NULL);

    search_dir_file_sel_dlg = gtk_file_selection_new(
                            _("Select search directory"));
    gtk_window_set_transient_for(
                GTK_WINDOW(search_dir_file_sel_dlg),
                GTK_WINDOW(appwin));

    g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(
                      search_dir_file_sel_dlg)->ok_button),
                "clicked",
                G_CALLBACK(search_dir_file_sel_ok),
                NULL);

    g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(
                      search_dir_file_sel_dlg)->cancel_button),
                "clicked",
                G_CALLBACK(search_dir_file_sel_cancel),
                NULL);

    g_signal_connect(G_OBJECT(search_dir_file_sel_dlg),
                "destroy",
                G_CALLBACK(search_dir_file_sel_destroy),
                NULL);

    search_dir_file_sel_label = gtk_label_new("");
    gtk_box_pack_start(GTK_BOX(GTK_FILE_SELECTION(
                            search_dir_file_sel_dlg)->action_area),
                      search_dir_file_sel_label,
                      FALSE, FALSE, 0);

    const char *utf8_dir = gtk_entry_get_text(GTK_ENTRY(search_dir_entry));
    string latin1_dir = latin1_string(utf8_dir);
    struct stat statbuf;
    if (stat(latin1_dir.c_str(), &statbuf) == 0 && S_ISDIR(statbuf.st_mode))
    {
      if (latin1_dir[latin1_dir.length() - 1] != '/')
          latin1_dir += '/';
      gtk_file_selection_set_filename(
                      GTK_FILE_SELECTION(search_dir_file_sel_dlg),
                      latin1_dir.c_str());
    }

    gtk_widget_show(search_dir_file_sel_dlg);
    gtk_widget_show(search_dir_file_sel_label);

    gtk_widget_set_sensitive(browse_search_dir_button, false);
}


void
SagasuApp::close_search_dir_file_sel()
{
    assert(search_dir_file_sel_dlg != NULL);
    assert(search_dir_file_sel_label != NULL);

    gtk_widget_hide(search_dir_file_sel_dlg);
    gtk_widget_destroy(search_dir_file_sel_dlg);

    search_dir_file_sel_dlg = NULL;
    search_dir_file_sel_label = NULL;

    gtk_widget_set_sensitive(browse_search_dir_button, true);
}


void
SagasuApp::accept_search_dir()
{
    const gchar *filename = gtk_file_selection_get_filename(
                        GTK_FILE_SELECTION(search_dir_file_sel_dlg));
    struct stat statbuf;
    if (stat(filename, &statbuf) != 0)
    {
      gtk_label_set_text(GTK_LABEL(search_dir_file_sel_label),
                        _("Directory not found."));
      gdk_beep();
      return;
    }
    if (!S_ISDIR(statbuf.st_mode))
    {
      gtk_label_set_text(GTK_LABEL(search_dir_file_sel_label),
                        _("Not a directory."));
      gdk_beep();
      return;
    }
    gtk_entry_set_text(GTK_ENTRY(search_dir_entry),
                  u8_string(filename).c_str());
    close_search_dir_file_sel();
}


gboolean
SagasuApp::key_press_in_search_field(GdkEventKey *event)
{
    switch (event->keyval)
    {
      case GDK_Return:
      case GDK_KP_Enter:
          search_cb();
          return true;

      default:
          return false;
    }
}


gboolean
SagasuApp::key_press_in_result_search_entry(GdkEventKey *event)
{
    switch (event->keyval)
    {
      case GDK_Return:
      case GDK_KP_Enter:
          result_search_button_clicked_cb();
          return true;

      default:
          return false;
    }
}


void
SagasuApp::home_page_cb(GtkWidget *, gpointer)
{
    show_url(PACKAGE_HOME_PAGE, appwin,
            _("Error when opening Sagasu Home Page"));
}


void
SagasuApp::view_license_cb(GtkWidget *, gpointer)
{
    string url = "file:" + get_dir(PKGDATADIR, "PKGDATADIR") + "COPYING";
    show_url(url, appwin, _("Error when opening license file"));
}


///////////////////////////////////////////////////////////////////////////////
//
// CALLBACKS METHODS
//

void
SagasuApp::create_new_result_page_cb(GtkWidget *, gpointer)
{
    ResultPage *page = new ResultPage();
    GtkWidget *tab = make_next_page_tab(page);
    gtk_notebook_append_page(GTK_NOTEBOOK(result_notebook),
                        page->get_top_widget(), tab);
    gint page_num = gtk_notebook_page_num(GTK_NOTEBOOK(result_notebook),
                        page->get_top_widget());
    gtk_notebook_set_current_page(GTK_NOTEBOOK(result_notebook), page_num);

    g_signal_connect(G_OBJECT(page->get_text_view()), "button-press-event",
                  G_CALLBACK(result_page_button_press_cb), page);
}


void
SagasuApp::exit_cb(GtkWidget *, gpointer)
{
    close_input();  // terminate any searching subprocess if applicable
    save_configuration();
    gtk_main_quit();
}


void
SagasuApp::first_tab_cb(GtkWidget *, gpointer)
{
    gtk_notebook_set_current_page(GTK_NOTEBOOK(result_notebook), 0);
}


void
SagasuApp::last_tab_cb(GtkWidget *, gpointer)
{
    gtk_notebook_set_current_page(GTK_NOTEBOOK(result_notebook), -1);
}


void
SagasuApp::previous_tab_cb(GtkWidget *, gpointer)
{
    gtk_notebook_prev_page(GTK_NOTEBOOK(result_notebook));
}


void
SagasuApp::next_tab_cb(GtkWidget *, gpointer)
{
    gtk_notebook_next_page(GTK_NOTEBOOK(result_notebook));
}


void
SagasuApp::erase_search_string_cb(GtkWidget *, gpointer)
{
    entry_set_text(search_string_entry, "");
}


void
SagasuApp::tab_close_clicked_cb(GtkWidget *, gpointer data)
{
    ResultPage *page = (ResultPage *) data;
    assert(page != NULL);

    // If trying to close tab of active search, refuse to close it.
    if (page == result_page_of_current_search)
      return;

    GtkWidget *tw = page->get_top_widget();

    gint page_num = gtk_notebook_page_num(GTK_NOTEBOOK(result_notebook), tw);
    assert(page_num >= 0);

    // If only one tab open, refuse to close it.
    if (gtk_notebook_get_nth_page(GTK_NOTEBOOK(result_notebook), 1) == NULL)
      return;

    gtk_notebook_remove_page(GTK_NOTEBOOK(result_notebook), page_num);
    delete page;

    // If only one tab left, make it number 1:
    if (gtk_notebook_get_nth_page(GTK_NOTEBOOK(result_notebook), 1) == NULL)
    {
      // Get the page widget:
      GtkWidget *child = gtk_notebook_get_nth_page(
                            GTK_NOTEBOOK(result_notebook), 0);

      // Get the tab widget of that page:
      GtkWidget *hbox = gtk_notebook_get_tab_label(
                            GTK_NOTEBOOK(result_notebook), child);
      assert(hbox != NULL);

      // Get the label inside that tab widget (it's the 1st child widget):
      GList *hbox_children =
                  gtk_container_get_children(GTK_CONTAINER(hbox));
      gpointer first_child = g_list_nth_data(hbox_children, 0);
      assert(first_child != NULL);
      assert(GTK_IS_LABEL(first_child));

      // Reset the global page number counter:
      next_notebook_page_num = 1;

      // Set the text of the label widget to "1":
      string new_label = make_next_page_tab_label();
      gtk_label_set_text_with_mnemonic(
                      GTK_LABEL(first_child), new_label.c_str());

      // Tell the user about this trick:
      gnome_appbar_set_status(GNOME_APPBAR(status),
                            _("Tab numbers restarted at 1"));
    }
}


void
SagasuApp::close_current_tab_cb(GtkWidget *, gpointer)
{
    tab_close_clicked_cb(NULL, get_current_result_page());
}


void
SagasuApp::say_sagasu_cb(GtkWidget *, gpointer)
{
    string fn = get_dir(PKGSOUNDDIR, "PKGSOUNDDIR") + PACKAGE + ".wav";
    gnome_sound_play(fn.c_str());
}

Generated by  Doxygen 1.6.0   Back to index