mainwindow.cc 29.3 KB
Newer Older
1 2
/*
 * Copyright (c) 2002-2007  Daniel Elstner  <daniel.kitta@gmail.com>
3
 *
4
 * This file is part of regexxer.
5
 *
6 7 8 9
 * regexxer 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.
10
 *
11
 * regexxer is distributed in the hope that it will be useful,
12 13 14 15 16
 * 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
17 18
 * along with regexxer; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
 */
daniel_e's avatar
daniel_e committed
20 21

#include "mainwindow.h"
22
#include "filetree.h"
23
#include "globalstrings.h"
daniel_e's avatar
daniel_e committed
24
#include "pcreshell.h"
25
#include "prefdialog.h"
26
#include "statusline.h"
daniel_e's avatar
daniel_e committed
27
#include "stringutils.h"
28
#include "translation.h"
daniel_e's avatar
daniel_e committed
29

30
#include <glib.h>
daniel_e's avatar
daniel_e committed
31
#include <gtkmm.h>
32
#include <gtksourceviewmm.h>
33
#include <gconfmm/client.h>
34
#include <algorithm>
35
#include <functional>
36
#include <iostream>
daniel_e's avatar
daniel_e committed
37 38 39 40 41 42

#include <config.h>

namespace
{

43
enum { BUSY_GUI_UPDATE_INTERVAL = 16 };
44

daniel_e's avatar
daniel_e committed
45 46
typedef Glib::RefPtr<Regexxer::FileBuffer> FileBufferPtr;

47
static const char *const selection_clipboard = "CLIPBOARD";
48

49 50 51
/*
 * List of authors to be displayed in the about dialog.
 */
52
static const char *const program_authors[] =
53 54 55 56 57
{
  "Daniel Elstner <daniel.kitta@gmail.com>",
  "Murray Cumming <murrayc@murrayc.com>",
  0
};
daniel_e's avatar
daniel_e committed
58

59
static const char *const program_license =
60 61 62 63 64 65 66 67 68 69 70 71
  "regexxer 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.\n"
  "\n"
  "regexxer 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.\n"
  "\n"
  "You should have received a copy of the GNU General Public License "
  "along with regexxer; if not, write to the Free Software Foundation, "
72
  "Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n";
73

74
class FileErrorDialog : public Gtk::MessageDialog
75 76
{
public:
77
  FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
78
                  Gtk::MessageType type, const Regexxer::FileTree::Error& error);
79
  virtual ~FileErrorDialog();
80 81
};

82
FileErrorDialog::FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
83
                                 Gtk::MessageType type, const Regexxer::FileTree::Error& error)
84
:
85
  Gtk::MessageDialog(parent, message, false, type, Gtk::BUTTONS_OK, true)
86 87 88 89 90 91
{
  using namespace Gtk;

  const Glib::RefPtr<TextBuffer> buffer = TextBuffer::create();
  TextBuffer::iterator buffer_end = buffer->end();

92
  typedef std::list<Glib::ustring> ErrorList;
93 94
  const ErrorList& error_list = error.get_error_list();

95 96
  for (ErrorList::const_iterator perr = error_list.begin(); perr != error_list.end(); ++perr)
    buffer_end = buffer->insert(buffer_end, *perr + '\n');
97

98
  Box& box = *get_vbox();
99
  Frame *const frame = new Frame();
100
  box.pack_start(*manage(frame), PACK_EXPAND_WIDGET);
101
  frame->set_border_width(6); // HIG spacing
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  frame->set_shadow_type(SHADOW_IN);

  ScrolledWindow *const scrollwin = new ScrolledWindow();
  frame->add(*manage(scrollwin));
  scrollwin->set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC);

  TextView *const textview = new TextView(buffer);
  scrollwin->add(*manage(textview));
  textview->set_editable(false);
  textview->set_cursor_visible(false);
  textview->set_wrap_mode(WRAP_WORD);
  textview->set_pixels_below_lines(8);

  set_default_size(400, 250);
  frame->show_all();
}

119
FileErrorDialog::~FileErrorDialog()
120 121
{}

122
static
123 124 125 126 127 128 129 130 131 132 133 134 135 136
void print_location(int linenumber, const Glib::ustring& subject, Regexxer::FileInfoPtr fileinfo)
{
  std::cout << fileinfo->fullname << ':' << linenumber + 1 << ':';

  std::string charset;

  if (Glib::get_charset(charset))
    std::cout << subject.raw(); // charset is UTF-8
  else
    std::cout << Glib::convert_with_fallback(subject.raw(), charset, "UTF-8");

  std::cout << std::endl;
}

daniel_e's avatar
daniel_e committed
137 138 139 140 141
} // anonymous namespace

namespace Regexxer
{

142 143 144 145
/**** Regexxer::InitState **************************************************/

InitState::InitState()
:
146 147
  folder        (),
  pattern       (),
148 149
  regex         (),
  substitution  (),
150
  no_recursive  (false),
151
  hidden        (false),
152
  no_global     (false),
153
  ignorecase    (false),
154
  feedback      (false),
155
  no_autorun    (false)
156 157 158 159 160
{}

InitState::~InitState()
{}

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
/**** Regexxer::MainWindow::BusyAction *************************************/

class MainWindow::BusyAction
{
private:
  MainWindow& object_;

  BusyAction(const BusyAction&);
  BusyAction& operator=(const BusyAction&);

public:
  explicit BusyAction(MainWindow& object)
    : object_ (object) { object_.busy_action_enter(); }

  ~BusyAction() { object_.busy_action_leave(); }
};

/**** Regexxer::MainWindow *************************************************/

daniel_e's avatar
daniel_e committed
180 181
MainWindow::MainWindow()
:
182
  toolbar_                (0),
183
  table_file_             (0),
184
  button_folder_          (0),
185
  combo_entry_pattern_    (Gtk::manage(new Gtk::ComboBoxEntryText())),
186 187 188 189 190 191
  button_recursive_       (0),
  button_hidden_          (0),
  entry_regex_            (0),
  entry_substitution_     (0),
  button_multiple_        (0),
  button_caseless_        (0),
192 193
  filetree_               (Gtk::manage(new FileTree())),
  scrollwin_filetree_     (0),
194 195
  scrollwin_textview_     (0),
  textview_               (Gtk::manage(new gtksourceview::SourceView())),
196
  entry_preview_          (0),
197
  statusline_             (Gtk::manage(new StatusLine())),
198 199 200
  busy_action_running_    (false),
  busy_action_cancel_     (false),
  busy_action_iteration_  (0),
201
  undo_stack_             (new UndoStack())
daniel_e's avatar
daniel_e committed
202
{
203 204 205
  load_xml();

  textview_->set_buffer(FileBuffer::create());
206
  window_->set_title(PACKAGE_NAME);
daniel_e's avatar
daniel_e committed
207

208 209 210 211
  vbox_main_->pack_start(*statusline_, Gtk::PACK_SHRINK);
  scrollwin_filetree_->add(*filetree_);
  table_file_->attach(*combo_entry_pattern_, 1, 2, 1, 2);
  
212 213
  scrollwin_textview_->add(*textview_);
  
214 215 216
  statusline_->show_all();
  filetree_->show_all();
  combo_entry_pattern_->show_all();
217
  scrollwin_textview_->show_all();
218
  
219
  connect_signals();
daniel_e's avatar
daniel_e committed
220 221 222 223 224
}

MainWindow::~MainWindow()
{}

225
void MainWindow::initialize(const InitState& init)
226
{
227 228
  std::string folder;

229 230
  if (!init.folder.empty())
    folder = init.folder.front();
231 232 233 234
  if (!Glib::path_is_absolute(folder))
    folder = Glib::build_filename(Glib::get_current_dir(), folder);

  const bool folder_exists = button_folder_->set_current_folder(folder);
235

236
  combo_entry_pattern_->get_entry()->set_text((init.pattern.empty()) ? Glib::ustring(1, '*') : init.pattern);
237 238
  entry_regex_  ->set_text(init.regex);
  entry_substitution_->set_text(init.substitution);
239

240 241 242 243 244 245 246 247 248 249 250
  combo_entry_pattern_->append_text("*.[ch]");
  combo_entry_pattern_->append_text("*.{c,cc,cpp,cxx,c++,C,h,hh,hpp,hxx,h++}");
  combo_entry_pattern_->append_text("*.{ccg,hg}");
  combo_entry_pattern_->append_text("*.idl");
  combo_entry_pattern_->append_text("*.{java,jsp}");
  combo_entry_pattern_->append_text("*.{pl,pm,cgi}");
  combo_entry_pattern_->append_text("*.py");
  combo_entry_pattern_->append_text("*.php[0-9]?");
  combo_entry_pattern_->append_text("*.{html,htm,shtml,js,wml}");
  combo_entry_pattern_->append_text("*.{xml,xsl,css,dtd,xsd}");

251 252 253 254
  button_recursive_->set_active(!init.no_recursive);
  button_hidden_   ->set_active(init.hidden);
  button_multiple_ ->set_active(!init.no_global);
  button_caseless_ ->set_active(init.ignorecase);
255

256
  if (init.feedback)
257 258
    filetree_->signal_feedback.connect(&print_location);

259 260 261 262
  // Strangely, folder_exists seems to be always true, probably because the
  // file chooser works asynchronously but the GLib main loop isn't running
  // yet.  As a work-around, explicitely check whether the directory exists
  // on the file system as well.
263 264
  if (folder_exists && !init.no_autorun
      && !init.folder.empty() && !init.pattern.empty()
265 266
      && Glib::file_test(folder, Glib::FILE_TEST_IS_DIR))
  {
267
    Glib::signal_idle().connect(sigc::mem_fun(*this, &MainWindow::autorun_idle));
268
  }
269 270
}

271 272
/**** Regexxer::MainWindow -- private **************************************/

273
void MainWindow::load_xml()
274
{
275
  const Glib::RefPtr<Gtk::Builder> xml = Gtk::Builder::create_from_file(ui_mainwindow_filename);
276

277
  Gtk::Window* mainwindow = 0;
278 279
  xml->get_widget("mainwindow", mainwindow);
  window_.reset(mainwindow);
280

281
  xml->get_widget("toolbar",             toolbar_);
282
  xml->get_widget("button_folder",       button_folder_);
283 284 285 286 287 288
  xml->get_widget("button_recursive",    button_recursive_);
  xml->get_widget("button_hidden",       button_hidden_);
  xml->get_widget("entry_regex",         entry_regex_);
  xml->get_widget("entry_substitution",  entry_substitution_);
  xml->get_widget("button_multiple",     button_multiple_);
  xml->get_widget("button_caseless",     button_caseless_);
289
  xml->get_widget("scrollwin_textview",  scrollwin_textview_);
290
  xml->get_widget("entry_preview",       entry_preview_);
291 292 293
  xml->get_widget("vbox_main",           vbox_main_);
  xml->get_widget("scrollwin_filetree",  scrollwin_filetree_);
  xml->get_widget("table_file",          table_file_);
294

295
  controller_.load_xml(xml);
296 297
}

298
void MainWindow::connect_signals()
daniel_e's avatar
daniel_e committed
299
{
300 301
  using sigc::bind;
  using sigc::mem_fun;
daniel_e's avatar
daniel_e committed
302

303 304 305
  window_->signal_hide         ().connect(mem_fun(*this, &MainWindow::on_hide));
  window_->signal_style_changed().connect(mem_fun(*this, &MainWindow::on_style_changed));
  window_->signal_delete_event ().connect(mem_fun(*this, &MainWindow::on_delete_event));
daniel_e's avatar
daniel_e committed
306

307 308
  combo_entry_pattern_->get_entry()->signal_activate().connect(controller_.find_files.slot());
  combo_entry_pattern_->signal_changed ().connect(mem_fun(*this, &MainWindow::on_entry_pattern_changed));
daniel_e's avatar
daniel_e committed
309

310 311 312 313 314 315 316
  entry_regex_       ->signal_activate().connect(controller_.find_matches.slot());
  entry_substitution_->signal_activate().connect(controller_.find_matches.slot());
  entry_substitution_->signal_changed ().connect(mem_fun(*this, &MainWindow::update_preview));

  controller_.save_file   .connect(mem_fun(*this, &MainWindow::on_save_file));
  controller_.save_all    .connect(mem_fun(*this, &MainWindow::on_save_all));
  controller_.undo        .connect(mem_fun(*this, &MainWindow::on_undo));
317 318 319 320
  controller_.cut         .connect(mem_fun(*this, &MainWindow::on_cut));
  controller_.copy        .connect(mem_fun(*this, &MainWindow::on_copy));
  controller_.paste       .connect(mem_fun(*this, &MainWindow::on_paste));
  controller_.erase       .connect(mem_fun(*this, &MainWindow::on_erase));
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
  controller_.preferences .connect(mem_fun(*this, &MainWindow::on_preferences));
  controller_.quit        .connect(mem_fun(*this, &MainWindow::on_quit));
  controller_.about       .connect(mem_fun(*this, &MainWindow::on_about));
  controller_.find_files  .connect(mem_fun(*this, &MainWindow::on_find_files));
  controller_.find_matches.connect(mem_fun(*this, &MainWindow::on_exec_search));
  controller_.next_file   .connect(bind(mem_fun(*this, &MainWindow::on_go_next_file), true));
  controller_.prev_file   .connect(bind(mem_fun(*this, &MainWindow::on_go_next_file), false));
  controller_.next_match  .connect(bind(mem_fun(*this, &MainWindow::on_go_next), true));
  controller_.prev_match  .connect(bind(mem_fun(*this, &MainWindow::on_go_next), false));
  controller_.replace     .connect(mem_fun(*this, &MainWindow::on_replace));
  controller_.replace_file.connect(mem_fun(*this, &MainWindow::on_replace_file));
  controller_.replace_all .connect(mem_fun(*this, &MainWindow::on_replace_all));

  Gnome::Conf::Client::get_default_client()
      ->signal_value_changed().connect(mem_fun(*this, &MainWindow::on_conf_value_changed));

  statusline_->signal_cancel_clicked.connect(
      mem_fun(*this, &MainWindow::on_busy_action_cancel));
339

340 341
  filetree_->signal_switch_buffer.connect(
      mem_fun(*this, &MainWindow::on_filetree_switch_buffer));
daniel_e's avatar
daniel_e committed
342

343 344
  filetree_->signal_bound_state_changed.connect(
      mem_fun(*this, &MainWindow::on_bound_state_changed));
daniel_e's avatar
daniel_e committed
345

346 347
  filetree_->signal_file_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_file_count_changed));
daniel_e's avatar
daniel_e committed
348

349 350
  filetree_->signal_match_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_match_count_changed));
daniel_e's avatar
daniel_e committed
351

352 353
  filetree_->signal_modified_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_modified_count_changed));
354

355 356
  filetree_->signal_pulse.connect(
      mem_fun(*this, &MainWindow::on_busy_action_pulse));
daniel_e's avatar
daniel_e committed
357

358 359
  filetree_->signal_undo_stack_push.connect(
      mem_fun(*this, &MainWindow::on_undo_stack_push));
daniel_e's avatar
daniel_e committed
360 361
}

362 363 364 365 366 367 368 369 370 371
bool MainWindow::autorun_idle()
{
  controller_.find_files.activate();

  if (!busy_action_cancel_ && entry_regex_->get_text_length() > 0)
    controller_.find_matches.activate();

  return false;
}

372
void MainWindow::on_hide()
daniel_e's avatar
daniel_e committed
373
{
374
  on_busy_action_cancel();
daniel_e's avatar
daniel_e committed
375

376 377 378 379 380 381 382 383 384 385 386 387
  // Kill the dialogs if they're mapped right now.  This isn't strictly
  // necessary since they'd be deleted in the destructor anyway.  But if we
  // have to do a lot of cleanup the dialogs would stay open for that time,
  // which doesn't look neat.
  {
    // Play safe and transfer ownership, and let the dtor do the delete.
    const std::auto_ptr<Gtk::Dialog> temp (about_dialog_);
  }
  {
    const std::auto_ptr<PrefDialog> temp (pref_dialog_);
  }
}
388

389 390 391 392
void MainWindow::on_style_changed(const Glib::RefPtr<Gtk::Style>&)
{
  FileBuffer::pango_context_changed(window_->get_pango_context());
}
daniel_e's avatar
daniel_e committed
393

394 395 396
bool MainWindow::on_delete_event(GdkEventAny*)
{
  return !confirm_quit_request();
daniel_e's avatar
daniel_e committed
397 398
}

399 400
void MainWindow::on_cut()
{
401 402 403 404 405 406 407 408 409
  if (textview_->is_focus())
  {
    if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
      buffer->cut_clipboard(textview_->get_clipboard(selection_clipboard),
                            textview_->get_editable());
  }
  else
  {
    const int noEntries = 3;
410
    Gtk::Entry *entries[noEntries] = { combo_entry_pattern_->get_entry(), entry_regex_,
411 412 413 414 415 416 417 418 419 420
                                       entry_substitution_ };
    for (int i = 0; i < 3; i++)
    {
      if (entries[i]->is_focus())
      {
        ((Gtk::Editable *)entries[i])->cut_clipboard();
        return ;
      }
    }
  }
421 422 423 424
}

void MainWindow::on_copy()
{
425 426 427 428 429 430 431 432
  if (textview_->is_focus())
  {
    if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
      buffer->copy_clipboard(textview_->get_clipboard(selection_clipboard));
  }
  else
  {
    const int noEntries = 3;
433
    Gtk::Entry *entries[noEntries] = { combo_entry_pattern_->get_entry(), entry_regex_,
434 435 436 437 438 439 440 441 442 443
                                       entry_substitution_ };
    for (int i = 0; i < 3; i++)
    {
      if (entries[i]->is_focus())
      {
        ((Gtk::Editable *)entries[i])->copy_clipboard();
        return ;
      }
    }
  }
444 445 446 447
}

void MainWindow::on_paste()
{
448 449 450 451 452 453 454 455 456
  if (textview_->is_focus())
  {
    if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
      buffer->paste_clipboard(textview_->get_clipboard(selection_clipboard),
                              textview_->get_editable());
  }
  else
  {
    const int noEntries = 3;
457
    Gtk::Entry *entries[noEntries] = { combo_entry_pattern_->get_entry(), entry_regex_,
458 459 460 461 462 463 464 465 466 467
                                       entry_substitution_ };
    for (int i = 0; i < 3; i++)
    {
      if (entries[i]->is_focus())
      {
        ((Gtk::Editable *)entries[i])->paste_clipboard();
        return ;
      }
    }
  }
468 469 470 471 472 473 474 475
}

void MainWindow::on_erase()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
    buffer->erase_selection(true, textview_->get_editable());
}

476 477
void MainWindow::on_quit()
{
478 479
  if (confirm_quit_request())
    window_->hide();
480 481 482 483
}

bool MainWindow::confirm_quit_request()
{
484
  if (filetree_->get_modified_count() == 0)
485 486
    return true;

487 488
  Gtk::MessageDialog dialog (*window_,
                             _("Some files haven\342\200\231t been saved yet.\nQuit anyway?"),
489
                             false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);
490 491 492 493 494 495 496

  dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
  dialog.add_button(Gtk::Stock::QUIT,   Gtk::RESPONSE_OK);

  return (dialog.run() == Gtk::RESPONSE_OK);
}

daniel_e's avatar
daniel_e committed
497 498
void MainWindow::on_find_files()
{
499
  if (filetree_->get_modified_count() > 0)
500
  {
501 502
    Gtk::MessageDialog dialog (*window_,
                               _("Some files haven\342\200\231t been saved yet.\nContinue anyway?"),
503
                               false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);
504

505
    if (dialog.run() != Gtk::RESPONSE_OK)
506 507 508
      return;
  }

509
  std::string folder = button_folder_->get_filename();
510

511
  if (folder.empty())
512 513 514
    folder = Glib::get_current_dir();

  g_return_if_fail(Glib::path_is_absolute(folder));
515 516

  undo_stack_clear();
517

518 519
  BusyAction busy (*this);

520 521
  try
  {
522
    Pcre::Pattern pattern (Util::shell_pattern_to_regex(combo_entry_pattern_->get_entry()->get_text()), Pcre::DOTALL);
523

524 525 526
    filetree_->find_files(folder, pattern,
                          button_recursive_->get_active(),
                          button_hidden_->get_active());
527
  }
528
  catch (const Pcre::Error&)
529
  {
530 531
    Gtk::MessageDialog dialog (*window_, _("The file search pattern is invalid."),
                               false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
532 533
    dialog.run();
  }
534
  catch (const FileTree::Error& error)
535
  {
536 537
    FileErrorDialog dialog (*window_, _("The following errors occurred during search:"),
                            Gtk::MESSAGE_WARNING, error);
538 539
    dialog.run();
  }
540

541
  statusline_->set_file_count(filetree_->get_file_count());
daniel_e's avatar
daniel_e committed
542 543 544 545
}

void MainWindow::on_exec_search()
{
546 547
  BusyAction busy (*this);

548 549 550 551
  const Glib::ustring regex = entry_regex_->get_text();
  const bool caseless = button_caseless_->get_active();
  const bool multiple = button_multiple_->get_active();

daniel_e's avatar
daniel_e committed
552 553
  try
  {
554
    Pcre::Pattern pattern (regex, (caseless) ? Pcre::CASELESS : Pcre::CompileOptions(0));
555

556
    filetree_->find_matches(pattern, multiple);
daniel_e's avatar
daniel_e committed
557
  }
558
  catch (const Pcre::Error& error)
daniel_e's avatar
daniel_e committed
559
  {
560 561
    Gtk::MessageDialog dialog (*window_, error.what(), false,
                               Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
562 563
    dialog.run();

564 565
    const int offset = error.offset();

566
    if (offset >= 0 && offset < entry_regex_->get_text_length())
567 568 569 570
    {
      entry_regex_->grab_focus();
      entry_regex_->select_region(offset, offset + 1);
    }
571

daniel_e's avatar
daniel_e committed
572 573 574
    return;
  }

575
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
576 577 578 579 580
  {
    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
  }

581
  if (filetree_->get_match_count() > 0)
daniel_e's avatar
daniel_e committed
582
  {
583 584 585 586
    // Scrolling has to be post-poned after the redraw, otherwise we might
    // not end up where we want to.  So do that by installing an idle handler.

    Glib::signal_idle().connect(
587
        sigc::mem_fun(*this, &MainWindow::after_exec_search),
588
        Glib::PRIORITY_HIGH_IDLE + 25); // slightly less than redraw (+20)
daniel_e's avatar
daniel_e committed
589 590 591
  }
}

592
bool MainWindow::after_exec_search()
593
{
594
  filetree_->select_first_file();
595
  on_go_next(true);
596

597
  return false;
598 599
}

600
void MainWindow::on_filetree_switch_buffer(FileInfoPtr fileinfo, int file_index)
daniel_e's avatar
daniel_e committed
601
{
602
  const FileBufferPtr old_buffer = FileBufferPtr::cast_static(textview_->get_buffer());
daniel_e's avatar
daniel_e committed
603

604
  if (fileinfo && fileinfo->buffer == old_buffer)
daniel_e's avatar
daniel_e committed
605 606
    return;

607
  if (old_buffer)
daniel_e's avatar
daniel_e committed
608
  {
609
    std::for_each(buffer_connections_.begin(), buffer_connections_.end(),
610
                  std::mem_fun_ref(&sigc::connection::disconnect));
daniel_e's avatar
daniel_e committed
611

612
    buffer_connections_.clear();
daniel_e's avatar
daniel_e committed
613 614 615
    old_buffer->forget_current_match();
  }

616
  if (fileinfo)
daniel_e's avatar
daniel_e committed
617
  {
618
    const FileBufferPtr buffer = fileinfo->buffer;
619
    g_return_if_fail(buffer);
620 621

    textview_->set_buffer(buffer);
622
    textview_->set_editable(!fileinfo->load_failed);
623
    textview_->set_cursor_visible(!fileinfo->load_failed);
624

625
    if (!fileinfo->load_failed)
626 627
    {
      buffer_connections_.push_back(buffer->signal_modified_changed().
628
          connect(sigc::mem_fun(*this, &MainWindow::on_buffer_modified_changed)));
629

630
      buffer_connections_.push_back(buffer->signal_bound_state_changed.
631
          connect(sigc::mem_fun(*this, &MainWindow::on_bound_state_changed)));
632

633
      buffer_connections_.push_back(buffer->signal_preview_line_changed.
634
          connect(sigc::mem_fun(*this, &MainWindow::update_preview)));
635
    }
daniel_e's avatar
daniel_e committed
636

637
    set_title_filename(fileinfo->fullname);
daniel_e's avatar
daniel_e committed
638

639 640
    controller_.replace_file.set_enabled(buffer->get_match_count() > 0);
    controller_.save_file.set_enabled(buffer->get_modified());
641
    controller_.edit_actions.set_enabled(!fileinfo->load_failed);
642 643 644

    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
645
    statusline_->set_file_encoding(fileinfo->encoding);
daniel_e's avatar
daniel_e committed
646 647 648 649
  }
  else
  {
    textview_->set_buffer(FileBuffer::create());
650
    textview_->set_editable(false);
651
    textview_->set_cursor_visible(false);
652

653
    window_->set_title(PACKAGE_NAME);
daniel_e's avatar
daniel_e committed
654

655 656
    controller_.replace_file.set_enabled(false);
    controller_.save_file.set_enabled(false);
657
    controller_.edit_actions.set_enabled(false);
658 659 660

    statusline_->set_match_count(0);
    statusline_->set_match_index(0);
661
    statusline_->set_file_encoding("");
daniel_e's avatar
daniel_e committed
662 663
  }

664
  statusline_->set_file_index(file_index);
daniel_e's avatar
daniel_e committed
665 666 667
  update_preview();
}

668
void MainWindow::on_bound_state_changed()
669
{
670
  BoundState bound = filetree_->get_bound_state();
671

672 673
  controller_.prev_file.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_file.set_enabled((bound & BOUND_LAST)  == 0);
674

675
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
676 677
    bound &= buffer->get_bound_state();

678 679
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
680 681
}

682
void MainWindow::on_filetree_file_count_changed()
683
{
684 685 686 687
  const int file_count = filetree_->get_file_count();

  statusline_->set_file_count(file_count);
  controller_.find_matches.set_enabled(file_count > 0);
688 689
}

690
void MainWindow::on_filetree_match_count_changed()
691
{
692
  controller_.replace_all.set_enabled(filetree_->get_match_count() > 0);
693 694 695

  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
    controller_.replace_file.set_enabled(buffer->get_match_count() > 0);
696 697
}

698
void MainWindow::on_filetree_modified_count_changed()
699
{
700
  controller_.save_all.set_enabled(filetree_->get_modified_count() > 0);
701 702
}

703 704
void MainWindow::on_buffer_modified_changed()
{
705
  controller_.save_file.set_enabled(textview_->get_buffer()->get_modified());
706 707
}

daniel_e's avatar
daniel_e committed
708 709
void MainWindow::on_go_next_file(bool move_forward)
{
710
  filetree_->select_next_file(move_forward);
daniel_e's avatar
daniel_e committed
711 712 713 714 715
  on_go_next(move_forward);
}

void MainWindow::on_go_next(bool move_forward)
{
716
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
717
  {
718
    if (const Glib::RefPtr<Gtk::TextMark> mark = buffer->get_next_match(move_forward))
daniel_e's avatar
daniel_e committed
719
    {
720
      textview_->scroll_to(mark, 0.125);
721
      statusline_->set_match_index(buffer->get_match_index());
daniel_e's avatar
daniel_e committed
722 723 724 725
      return;
    }
  }

726
  if (filetree_->select_next_file(move_forward))
daniel_e's avatar
daniel_e committed
727 728 729 730 731 732 733
  {
    on_go_next(move_forward); // recursive call
  }
}

void MainWindow::on_replace()
{
734
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
735 736
  {
    buffer->replace_current_match(entry_substitution_->get_text());
737
    on_go_next(true);
daniel_e's avatar
daniel_e committed
738 739 740 741 742
  }
}

void MainWindow::on_replace_file()
{
743
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
744 745
  {
    buffer->replace_all_matches(entry_substitution_->get_text());
746
    statusline_->set_match_index(0);
747
  }
daniel_e's avatar
daniel_e committed
748 749 750 751
}

void MainWindow::on_replace_all()
{
752
  BusyAction busy (*this);
753

754
  filetree_->replace_all_matches(entry_substitution_->get_text());
755
  statusline_->set_match_index(0);
daniel_e's avatar
daniel_e committed
756 757
}

758 759 760 761
void MainWindow::on_save_file()
{
  try
  {
762
    filetree_->save_current_file();
763
  }
764
  catch (const FileTree::Error& error)
765
  {
766 767 768
    const std::list<Glib::ustring>& error_list = error.get_error_list();
    g_assert(error_list.size() == 1);

769 770
    Gtk::MessageDialog dialog (*window_, error_list.front(), false,
                               Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
771 772 773 774 775 776 777 778
    dialog.run();
  }
}

void MainWindow::on_save_all()
{
  try
  {
779
    filetree_->save_all_files();
780
  }
781
  catch (const FileTree::Error& error)
782
  {
783 784
    FileErrorDialog dialog (*window_, _("The following errors occurred during save:"),
                            Gtk::MESSAGE_ERROR, error);
785 786 787 788
    dialog.run();
  }
}

789 790 791 792 793 794 795 796
void MainWindow::on_undo_stack_push(UndoActionPtr action)
{
  undo_stack_->push(action);
  controller_.undo.set_enabled(true);
}

void MainWindow::on_undo()
{
797 798 799 800 801 802
  if (textview_->is_focus())
  {
    BusyAction busy (*this);
    undo_stack_->undo_step(sigc::mem_fun(*this, &MainWindow::on_busy_action_pulse));
    controller_.undo.set_enabled(!undo_stack_->empty());
  }
803 804
}

805 806 807 808 809 810
void MainWindow::undo_stack_clear()
{
  controller_.undo.set_enabled(false);
  undo_stack_.reset(new UndoStack());
}

811 812
void MainWindow::on_entry_pattern_changed()
{
813
  controller_.find_files.set_enabled(combo_entry_pattern_->get_entry()->get_text_length() > 0);
814 815
}

daniel_e's avatar
daniel_e committed
816 817
void MainWindow::update_preview()
{
818
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
819 820 821 822
  {
    Glib::ustring preview;
    const int pos = buffer->get_line_preview(entry_substitution_->get_text(), preview);

823
    entry_preview_->set_text(preview);
824
    controller_.replace.set_enabled(pos >= 0);
daniel_e's avatar
daniel_e committed
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844

    // Beware, strange code ahead!
    //
    // The goal is to scroll the preview entry so that it shows the entire
    // replaced text if possible.  In order to do that we first move the cursor
    // to 0, forcing scrolling to the left boundary.  Then we set the cursor to
    // the end of the replaced text, thus forcing the entry widget to scroll
    // again.  The replacement should then be entirely visible provided that it
    // fits into the entry.
    //
    // The problem is that Gtk::Entry doesn't update its scroll position
    // immediately but in an idle handler, thus any calls to set_position()
    // but the last one have no effect at all.
    //
    // To workaround that, we install an idle handler that's executed just
    // after the entry updated its scroll position, but before redrawing is
    // done.

    entry_preview_->set_position(0);

845
    if (pos > 0)
daniel_e's avatar
daniel_e committed
846
    {
847
      using namespace sigc;
daniel_e's avatar
daniel_e committed
848 849

      Glib::signal_idle().connect(
850
          bind_return(bind(mem_fun(*entry_preview_, &Gtk::Editable::set_position), pos), false),
daniel_e's avatar
daniel_e committed
851 852 853 854 855
          Glib::PRIORITY_HIGH_IDLE + 17); // between scroll update (+ 15) and redraw (+ 20)
    }
  }
}

856
void MainWindow::set_title_filename(const std::string& filename)
daniel_e's avatar
daniel_e committed
857
{
858
  Glib::ustring title = Glib::filename_display_basename(filename);
daniel_e's avatar
daniel_e committed
859

860
  title += " (";
861
  title += Util::filename_short_display_name(Glib::path_get_dirname(filename));
862