mainwindow.cc 28.5 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>
31
#include <gtk/gtktooltips.h>  /* XXX: see load_xml() */
daniel_e's avatar
daniel_e committed
32
#include <gtkmm.h>
33
#include <gtkmm/comboboxentry.h>
34 35
#include <gconfmm/client.h>
#include <libglademm/xml.h>
36
#include <algorithm>
37
#include <functional>
38
#include <iostream>
daniel_e's avatar
daniel_e committed
39 40 41 42 43 44 45

#include <config.h>


namespace
{

46
enum { BUSY_GUI_UPDATE_INTERVAL = 16 };
47

daniel_e's avatar
daniel_e committed
48 49
typedef Glib::RefPtr<Regexxer::FileBuffer> FileBufferPtr;

50 51
const char *const selection_clipboard = "CLIPBOARD";

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

62
class FileErrorDialog : public Gtk::MessageDialog
63 64
{
public:
65
  FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
66
                  Gtk::MessageType type, const Regexxer::FileTree::Error& error);
67
  virtual ~FileErrorDialog();
68 69
};

70
FileErrorDialog::FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
71
                                 Gtk::MessageType type, const Regexxer::FileTree::Error& error)
72
:
73
  Gtk::MessageDialog(parent, message, false, type, Gtk::BUTTONS_OK, true)
74 75 76 77 78 79
{
  using namespace Gtk;

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

80
  typedef std::list<Glib::ustring> ErrorList;
81 82
  const ErrorList& error_list = error.get_error_list();

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

86
  Box& box = *get_vbox();
87
  Frame *const frame = new Frame();
88
  box.pack_start(*manage(frame), PACK_EXPAND_WIDGET);
89
  frame->set_border_width(6); // HIG spacing
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
  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();
}

107
FileErrorDialog::~FileErrorDialog()
108 109
{}

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124

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
125 126 127 128 129 130
} // anonymous namespace


namespace Regexxer
{

131 132 133 134 135 136 137 138 139 140 141 142
/**** Regexxer::InitState **************************************************/

InitState::InitState()
:
  folder        (Glib::get_current_dir()),
  pattern       ("*"),
  regex         (),
  substitution  (),
  recursive     (true),
  hidden        (false),
  global        (true),
  ignorecase    (false),
143
  feedback      (false),
144 145 146 147 148 149 150
  autorun       (false)
{}

InitState::~InitState()
{}


151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
/**** 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
171 172
MainWindow::MainWindow()
:
173
  toolbar_                (0),
174 175 176 177 178 179 180 181
  entry_folder_           (0),
  entry_pattern_          (0),
  button_recursive_       (0),
  button_hidden_          (0),
  entry_regex_            (0),
  entry_substitution_     (0),
  button_multiple_        (0),
  button_caseless_        (0),
182
  filetree_               (0),
183 184 185 186 187 188
  textview_               (0),
  entry_preview_          (0),
  statusline_             (0),
  busy_action_running_    (false),
  busy_action_cancel_     (false),
  busy_action_iteration_  (0),
189
  undo_stack_             (new UndoStack())
daniel_e's avatar
daniel_e committed
190
{
191 192 193
  load_xml();

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

196
  connect_signals();
daniel_e's avatar
daniel_e committed
197 198 199 200 201
}

MainWindow::~MainWindow()
{}

202 203 204 205 206 207 208 209 210 211 212 213 214 215
void MainWindow::initialize(std::auto_ptr<InitState> init)
{
  entry_folder_->set_text(
      Util::filename_to_utf8_fallback(Util::shorten_pathname(init->folder)));

  entry_pattern_     ->set_text(init->pattern);
  entry_regex_       ->set_text(init->regex);
  entry_substitution_->set_text(init->substitution);

  button_recursive_->set_active(init->recursive);
  button_hidden_   ->set_active(init->hidden);
  button_multiple_ ->set_active(init->global);
  button_caseless_ ->set_active(init->ignorecase);

216 217 218
  if (init->feedback)
    filetree_->signal_feedback.connect(&print_location);

219
  if (init->autorun)
220
    Glib::signal_idle().connect(sigc::mem_fun(*this, &MainWindow::autorun_idle));
221 222
}

223 224
/**** Regexxer::MainWindow -- private **************************************/

225
void MainWindow::load_xml()
226
{
227
  using Gnome::Glade::Xml;
228

229
  const Glib::RefPtr<Xml> xml = Xml::create(glade_mainwindow_filename);
230

231 232
  Gtk::Window* mainwindow = 0;
  window_.reset(xml->get_widget("mainwindow", mainwindow));
233

234 235 236 237 238 239 240 241 242 243 244 245
  xml->get_widget("toolbar",             toolbar_);
  xml->get_widget("entry_folder",        entry_folder_);
  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_);
  xml->get_widget("filetree",            filetree_);
  xml->get_widget("textview",            textview_);
  xml->get_widget("entry_preview",       entry_preview_);
  xml->get_widget("statusline",          statusline_);
246

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
  Gtk::ComboBoxEntry* combo_pattern = 0;
  xml->get_widget("combo_pattern", combo_pattern);
  entry_pattern_ = dynamic_cast<Gtk::Entry*>(combo_pattern->get_child());

  // Current libglade does not yet provide access to the internal child of
  // GtkComboBoxEntry.  However, I want a tooltip to be assigned to the entry
  // and not to the combo box as a whole (by means of GtkEventBox).  I hope
  // libglade will be fixed soon -- until then, this hack assigns the tooltip
  // manually using the group created by libglade.

  if (GtkTooltipsData *const tipdata = gtk_tooltips_data_get(entry_folder_->Gtk::Widget::gobj()))
  {
    gtk_tooltips_set_tip(tipdata->tooltips, entry_pattern_->Gtk::Widget::gobj(),
                         _("A filename pattern as used by the shell. Character classes "
                           "[ab] and csh style brace expressions {a,b} are supported."), 0);
  }

264 265 266
  Gtk::Button* button_folder = 0;
  xml->get_widget("button_folder", button_folder);
  button_folder->signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_select_folder));
267

268
  controller_.load_xml(xml);
269 270
}

271
void MainWindow::connect_signals()
daniel_e's avatar
daniel_e committed
272
{
273 274
  using sigc::bind;
  using sigc::mem_fun;
daniel_e's avatar
daniel_e committed
275

276 277 278
  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
279

280 281
  entry_folder_ ->signal_activate().connect(controller_.find_files.slot());
  entry_pattern_->signal_activate().connect(controller_.find_files.slot());
282
  entry_pattern_->signal_changed ().connect(mem_fun(*this, &MainWindow::on_entry_pattern_changed));
daniel_e's avatar
daniel_e committed
283

284 285 286 287 288 289 290
  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));
291 292 293 294
  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));
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
  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));
313

314 315
  filetree_->signal_switch_buffer.connect(
      mem_fun(*this, &MainWindow::on_filetree_switch_buffer));
daniel_e's avatar
daniel_e committed
316

317 318
  filetree_->signal_bound_state_changed.connect(
      mem_fun(*this, &MainWindow::on_bound_state_changed));
daniel_e's avatar
daniel_e committed
319

320 321
  filetree_->signal_file_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_file_count_changed));
daniel_e's avatar
daniel_e committed
322

323 324
  filetree_->signal_match_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_match_count_changed));
daniel_e's avatar
daniel_e committed
325

326 327
  filetree_->signal_modified_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_modified_count_changed));
328

329 330
  filetree_->signal_pulse.connect(
      mem_fun(*this, &MainWindow::on_busy_action_pulse));
daniel_e's avatar
daniel_e committed
331

332 333
  filetree_->signal_undo_stack_push.connect(
      mem_fun(*this, &MainWindow::on_undo_stack_push));
daniel_e's avatar
daniel_e committed
334 335
}

336 337 338 339 340 341 342 343 344 345
bool MainWindow::autorun_idle()
{
  controller_.find_files.activate();

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

  return false;
}

346
void MainWindow::on_hide()
daniel_e's avatar
daniel_e committed
347
{
348
  on_busy_action_cancel();
daniel_e's avatar
daniel_e committed
349

350 351 352 353
  // 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.
daniel_e's avatar
daniel_e committed
354

355 356 357 358 359 360 361 362
  {
    // 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_);
  }
}
363

364 365 366 367
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
368

369 370 371
bool MainWindow::on_delete_event(GdkEventAny*)
{
  return !confirm_quit_request();
daniel_e's avatar
daniel_e committed
372 373
}

374 375 376
void MainWindow::on_cut()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
377 378
    buffer->cut_clipboard(textview_->get_clipboard(selection_clipboard),
                          textview_->get_editable());
379 380 381 382 383
}

void MainWindow::on_copy()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
384
    buffer->copy_clipboard(textview_->get_clipboard(selection_clipboard));
385 386 387 388 389
}

void MainWindow::on_paste()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
390 391
    buffer->paste_clipboard(textview_->get_clipboard(selection_clipboard),
                            textview_->get_editable());
392 393 394 395 396 397 398 399
}

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

400 401
void MainWindow::on_quit()
{
402 403
  if (confirm_quit_request())
    window_->hide();
404 405 406 407
}

bool MainWindow::confirm_quit_request()
{
408
  if (filetree_->get_modified_count() == 0)
409 410
    return true;

411 412
  Gtk::MessageDialog dialog (*window_, _("Some files haven't been saved yet.\nQuit anyway?"),
                             false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);
413 414 415 416 417 418 419

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

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

420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
/*
 * Expand the pathname entered into the folder entry, and convert it to
 * the local filename encoding.  If the conversion fails, show an error
 * message to the user and return an empty string.
 */
std::string MainWindow::get_folder_fullname() const
{
  const Glib::ustring folder = entry_folder_->get_text();

  if (folder.empty())
    return Glib::get_current_dir();

  try
  {
    return Util::expand_pathname(Glib::filename_from_utf8(folder));
  }
  catch (const Glib::ConvertError&)
  {
    const Glib::ustring message = Util::compose(
        _("The folder name \"%1\" contains characters not representable "
          "in the encoding of the local file system."), folder);

    Gtk::MessageDialog dialog (*window_, message, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
    dialog.run();

    return std::string();
  }
}

daniel_e's avatar
daniel_e committed
449 450
void MainWindow::on_select_folder()
{
451
  using namespace Gtk;
daniel_e's avatar
daniel_e committed
452

453 454 455 456 457
  const std::string folder = get_folder_fullname();

  if (folder.empty())
    return;

458
  FileChooserDialog chooser (*window_, _("Select a folder"), FILE_CHOOSER_ACTION_SELECT_FOLDER);
daniel_e's avatar
daniel_e committed
459

460 461 462 463 464
  chooser.add_button(Stock::CANCEL, RESPONSE_CANCEL);
  chooser.add_button(Stock::OK,     RESPONSE_OK);
  chooser.set_default_response(RESPONSE_OK);
  chooser.set_modal(true);
  chooser.set_local_only(true);
465
  chooser.set_current_folder(folder);
daniel_e's avatar
daniel_e committed
466

467
  if (chooser.run() == RESPONSE_OK)
daniel_e's avatar
daniel_e committed
468
  {
469 470
    const std::string shortname = Util::shorten_pathname(chooser.get_filename());
    entry_folder_->set_text(Util::filename_to_utf8_fallback(shortname));
daniel_e's avatar
daniel_e committed
471 472 473 474 475
  }
}

void MainWindow::on_find_files()
{
476
  if (filetree_->get_modified_count() > 0)
477
  {
478 479
    Gtk::MessageDialog dialog (*window_, _("Some files haven't been saved yet.\nContinue anyway?"),
                               false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);
480

481
    if (dialog.run() != Gtk::RESPONSE_OK)
482 483 484
      return;
  }

485
  const std::string folder = get_folder_fullname();
486

487
  if (folder.empty())
488 489 490
    return;

  undo_stack_clear();
491

492 493
  BusyAction busy (*this);

494 495
  try
  {
496
    Pcre::Pattern pattern (Util::shell_pattern_to_regex(entry_pattern_->get_text()), Pcre::DOTALL);
497

498 499 500
    filetree_->find_files(folder, pattern,
                          button_recursive_->get_active(),
                          button_hidden_->get_active());
501
  }
502
  catch (const Pcre::Error&)
503
  {
504 505
    Gtk::MessageDialog dialog (*window_, _("The file search pattern is invalid."),
                               false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
506 507
    dialog.run();
  }
508
  catch (const FileTree::Error& error)
509
  {
510 511
    FileErrorDialog dialog (*window_, _("The following errors occurred during search:"),
                            Gtk::MESSAGE_WARNING, error);
512 513
    dialog.run();
  }
514

515
  statusline_->set_file_count(filetree_->get_file_count());
daniel_e's avatar
daniel_e committed
516 517 518 519
}

void MainWindow::on_exec_search()
{
520 521
  BusyAction busy (*this);

522 523 524 525
  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
526 527
  try
  {
528
    Pcre::Pattern pattern (regex, (caseless) ? Pcre::CASELESS : Pcre::CompileOptions(0));
529
    filetree_->find_matches(pattern, multiple);
daniel_e's avatar
daniel_e committed
530
  }
531
  catch (const Pcre::Error& error)
daniel_e's avatar
daniel_e committed
532
  {
533
    const int offset = error.offset();
534 535 536 537
    const Glib::ustring message = (offset >= 0 && unsigned(offset) < regex.length())
      ? Util::compose(_("Error in regular expression at \"%1\" (index %2):\n%3"),
                      regex.substr(offset, 1), Util::int_to_string(offset + 1), error.what())
      : Util::compose(_("Error in regular expression:\n%1"), error.what());
538

539
    Gtk::MessageDialog dialog (*window_, message, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
540 541
    dialog.run();

542
    if (offset >= 0 && offset < entry_regex_->get_text_length())
543 544 545 546
    {
      entry_regex_->grab_focus();
      entry_regex_->select_region(offset, offset + 1);
    }
547

daniel_e's avatar
daniel_e committed
548 549 550
    return;
  }

551
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
552 553 554 555 556
  {
    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
  }

557
  if (filetree_->get_match_count() > 0)
daniel_e's avatar
daniel_e committed
558
  {
559 560 561 562
    // 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(
563
        sigc::mem_fun(*this, &MainWindow::after_exec_search),
564
        Glib::PRIORITY_HIGH_IDLE + 25); // slightly less than redraw (+20)
daniel_e's avatar
daniel_e committed
565 566 567
  }
}

568
bool MainWindow::after_exec_search()
569
{
570
  filetree_->select_first_file();
571
  on_go_next(true);
572

573
  return false;
574 575
}

576
void MainWindow::on_filetree_switch_buffer(FileInfoPtr fileinfo, int file_index)
daniel_e's avatar
daniel_e committed
577
{
578
  const FileBufferPtr old_buffer = FileBufferPtr::cast_static(textview_->get_buffer());
daniel_e's avatar
daniel_e committed
579

580
  if (fileinfo && fileinfo->buffer == old_buffer)
daniel_e's avatar
daniel_e committed
581 582
    return;

583
  if (old_buffer)
daniel_e's avatar
daniel_e committed
584
  {
585
    std::for_each(buffer_connections_.begin(), buffer_connections_.end(),
586
                  std::mem_fun_ref(&sigc::connection::disconnect));
daniel_e's avatar
daniel_e committed
587

588
    buffer_connections_.clear();
daniel_e's avatar
daniel_e committed
589 590 591
    old_buffer->forget_current_match();
  }

592
  if (fileinfo)
daniel_e's avatar
daniel_e committed
593
  {
594
    const FileBufferPtr buffer = fileinfo->buffer;
595
    g_return_if_fail(buffer);
596 597

    textview_->set_buffer(buffer);
598
    textview_->set_editable(!fileinfo->load_failed);
599
    textview_->set_cursor_visible(!fileinfo->load_failed);
600

601
    if (!fileinfo->load_failed)
602 603
    {
      buffer_connections_.push_back(buffer->signal_modified_changed().
604
          connect(sigc::mem_fun(*this, &MainWindow::on_buffer_modified_changed)));
605

606
      buffer_connections_.push_back(buffer->signal_bound_state_changed.
607
          connect(sigc::mem_fun(*this, &MainWindow::on_bound_state_changed)));
608

609
      buffer_connections_.push_back(buffer->signal_preview_line_changed.
610
          connect(sigc::mem_fun(*this, &MainWindow::update_preview)));
611
    }
daniel_e's avatar
daniel_e committed
612

613
    set_title_filename(fileinfo->fullname);
daniel_e's avatar
daniel_e committed
614

615 616
    controller_.replace_file.set_enabled(buffer->get_match_count() > 0);
    controller_.save_file.set_enabled(buffer->get_modified());
617
    controller_.edit_actions.set_enabled(!fileinfo->load_failed);
618 619 620

    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
621
    statusline_->set_file_encoding(fileinfo->encoding);
daniel_e's avatar
daniel_e committed
622 623 624 625
  }
  else
  {
    textview_->set_buffer(FileBuffer::create());
626
    textview_->set_editable(false);
627
    textview_->set_cursor_visible(false);
628

629
    window_->set_title(PACKAGE_NAME);
daniel_e's avatar
daniel_e committed
630

631 632
    controller_.replace_file.set_enabled(false);
    controller_.save_file.set_enabled(false);
633
    controller_.edit_actions.set_enabled(false);
634 635 636

    statusline_->set_match_count(0);
    statusline_->set_match_index(0);
637
    statusline_->set_file_encoding("");
daniel_e's avatar
daniel_e committed
638 639
  }

640
  statusline_->set_file_index(file_index);
daniel_e's avatar
daniel_e committed
641 642 643
  update_preview();
}

644
void MainWindow::on_bound_state_changed()
645
{
646
  BoundState bound = filetree_->get_bound_state();
647

648 649
  controller_.prev_file.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_file.set_enabled((bound & BOUND_LAST)  == 0);
650

651
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
652 653
    bound &= buffer->get_bound_state();

654 655
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
656 657
}

658
void MainWindow::on_filetree_file_count_changed()
659
{
660 661 662 663
  const int file_count = filetree_->get_file_count();

  statusline_->set_file_count(file_count);
  controller_.find_matches.set_enabled(file_count > 0);
664 665
}

666
void MainWindow::on_filetree_match_count_changed()
667
{
668
  controller_.replace_all.set_enabled(filetree_->get_match_count() > 0);
669 670 671

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

674
void MainWindow::on_filetree_modified_count_changed()
675
{
676
  controller_.save_all.set_enabled(filetree_->get_modified_count() > 0);
677 678
}

679 680
void MainWindow::on_buffer_modified_changed()
{
681
  controller_.save_file.set_enabled(textview_->get_buffer()->get_modified());
682 683
}

daniel_e's avatar
daniel_e committed
684 685
void MainWindow::on_go_next_file(bool move_forward)
{
686
  filetree_->select_next_file(move_forward);
daniel_e's avatar
daniel_e committed
687 688 689 690 691
  on_go_next(move_forward);
}

void MainWindow::on_go_next(bool move_forward)
{
692
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
693
  {
694
    if (const Glib::RefPtr<Gtk::TextMark> mark = buffer->get_next_match(move_forward))
daniel_e's avatar
daniel_e committed
695
    {
696
      textview_->scroll_to(mark, 0.125);
697
      statusline_->set_match_index(buffer->get_match_index());
daniel_e's avatar
daniel_e committed
698 699 700 701
      return;
    }
  }

702
  if (filetree_->select_next_file(move_forward))
daniel_e's avatar
daniel_e committed
703 704 705 706 707 708 709
  {
    on_go_next(move_forward); // recursive call
  }
}

void MainWindow::on_replace()
{
710
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
711 712
  {
    buffer->replace_current_match(entry_substitution_->get_text());
713
    on_go_next(true);
daniel_e's avatar
daniel_e committed
714 715 716 717 718
  }
}

void MainWindow::on_replace_file()
{
719
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
720 721
  {
    buffer->replace_all_matches(entry_substitution_->get_text());
722
    statusline_->set_match_index(0);
723
  }
daniel_e's avatar
daniel_e committed
724 725 726 727
}

void MainWindow::on_replace_all()
{
728
  BusyAction busy (*this);
729

730
  filetree_->replace_all_matches(entry_substitution_->get_text());
731
  statusline_->set_match_index(0);
daniel_e's avatar
daniel_e committed
732 733
}

734 735 736 737
void MainWindow::on_save_file()
{
  try
  {
738
    filetree_->save_current_file();
739
  }
740
  catch (const FileTree::Error& error)
741
  {
742 743 744
    const std::list<Glib::ustring>& error_list = error.get_error_list();
    g_assert(error_list.size() == 1);

745 746
    Gtk::MessageDialog dialog (*window_, error_list.front(), false,
                               Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
747 748 749 750 751 752 753 754
    dialog.run();
  }
}

void MainWindow::on_save_all()
{
  try
  {
755
    filetree_->save_all_files();
756
  }
757
  catch (const FileTree::Error& error)
758
  {
759 760
    FileErrorDialog dialog (*window_, _("The following errors occurred during save:"),
                            Gtk::MESSAGE_ERROR, error);
761 762 763 764
    dialog.run();
  }
}

765 766 767 768 769 770 771 772
void MainWindow::on_undo_stack_push(UndoActionPtr action)
{
  undo_stack_->push(action);
  controller_.undo.set_enabled(true);
}

void MainWindow::on_undo()
{
773 774 775
  BusyAction busy (*this);

  undo_stack_->undo_step(sigc::mem_fun(*this, &MainWindow::on_busy_action_pulse));
776 777 778
  controller_.undo.set_enabled(!undo_stack_->empty());
}

779 780 781 782 783 784
void MainWindow::undo_stack_clear()
{
  controller_.undo.set_enabled(false);
  undo_stack_.reset(new UndoStack());
}

785 786
void MainWindow::on_entry_pattern_changed()
{
787
  controller_.find_files.set_enabled(entry_pattern_->get_text_length() > 0);
788 789
}

daniel_e's avatar
daniel_e committed
790 791
void MainWindow::update_preview()
{
792
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
793 794 795 796 797
  {
    Glib::ustring preview;
    const int pos = buffer->get_line_preview(entry_substitution_->get_text(), preview);
    entry_preview_->set_text(preview);

798
    controller_.replace.set_enabled(pos >= 0);
daniel_e's avatar
daniel_e committed
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818

    // 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);

819
    if (pos > 0)
daniel_e's avatar
daniel_e committed
820
    {
821
      using namespace sigc;
daniel_e's avatar
daniel_e committed
822 823

      Glib::signal_idle().connect(
824
          bind_return(bind(mem_fun(*entry_preview_, &Gtk::Editable::set_position), pos), false),
daniel_e's avatar
daniel_e committed
825 826 827 828 829
          Glib::PRIORITY_HIGH_IDLE + 17); // between scroll update (+ 15) and redraw (+ 20)
    }
  }
}

830
void MainWindow::set_title_filename(const std::string& filename)
daniel_e's avatar
daniel_e committed
831
{
832
  Glib::ustring title = Util::filename_to_utf8_fallback(Glib::path_get_basename(filename));
daniel_e's avatar
daniel_e committed
833

834 835
  title += " (";
  title += Util::filename_to_utf8_fallback(Util::shorten_pathname(Glib::path_get_dirname(filename)));
836
  title += ") \342\200\223 " PACKAGE_NAME; // U+2013 EN DASH
daniel_e's avatar
daniel_e committed
837

838
  window_->set_title(title);
daniel_e's avatar
daniel_e committed
839 840
}

841 842 843 844
void MainWindow::busy_action_enter()
{
  g_return_if_fail(!busy_action_running_);

845
  controller_.match_actions.set_enabled(false);
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861

  statusline_->pulse_start();

  busy_action_running_    = true;
  busy_action_cancel_     = false;
  busy_action_iteration_  = 0;
}

void MainWindow::busy_action_leave()
{
  g_return_if_fail(busy_action_running_);

  busy_action_running_ = false;

  statusline_->pulse_stop();

862
  controller_.match_actions.set_enabled(true);
863 864 865 866 867 868
}

bool MainWindow::on_busy_action_pulse()
{
  g_return_val_if_fail(busy_action_running_, true);

869
  if (!busy_action_cancel_ && (++busy_action_iteration_ % BUSY_GUI_UPDATE_INTERVAL) == 0)
870 871 872
  {
    statusline_->pulse();

873
    const Glib::RefPtr<Glib::MainContext> context = Glib::MainContext::get_default();
874

875
    do {}
876
    while (context->iteration(false) && !busy_action_cancel_);
877 878
  }

879
  return busy_action_cancel_;
880 881 882 883
}

void MainWindow::on_busy_action_cancel()
{
884
  if (busy_action_running_)
885 886 887
    busy_action_cancel_ = true;
}

888
void MainWindow::on_about()
889
{
890
  if (about_dialog_.get())