mainwindow.cc 26.3 KB
Newer Older
1 2
/* $Id$
 *
3
 * Copyright (c) 2004  Daniel Elstner  <daniel.elstner@gmx.net>
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License VERSION 2 as
 * published by the Free Software Foundation.  You are not allowed to
 * use any other version of the license; unless you got the explicit
 * permission from the author to do so.
 *
 * 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
 */
daniel_e's avatar
daniel_e committed
20 21

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

31
#include <glib.h>
32
#include <gtk/gtktooltips.h>  /* XXX: see load_xml() */
daniel_e's avatar
daniel_e committed
33
#include <gtkmm.h>
34
#include <gtkmm/comboboxentry.h>
35 36
#include <gconfmm/client.h>
#include <libglademm/xml.h>
37
#include <algorithm>
38
#include <functional>
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 50
typedef Glib::RefPtr<Regexxer::FileBuffer> FileBufferPtr;


51
class FileErrorDialog : public Gtk::MessageDialog
52 53
{
public:
54
  FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
55
                  Gtk::MessageType type, const Regexxer::FileTree::Error& error);
56
  virtual ~FileErrorDialog();
57 58
};

59
FileErrorDialog::FileErrorDialog(Gtk::Window& parent, const Glib::ustring& message,
60
                                 Gtk::MessageType type, const Regexxer::FileTree::Error& error)
61
:
62
  Gtk::MessageDialog(parent, message, false, type, Gtk::BUTTONS_OK, true)
63 64 65 66 67 68
{
  using namespace Gtk;

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

69
  typedef std::list<Glib::ustring> ErrorList;
70 71
  const ErrorList& error_list = error.get_error_list();

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

75
  Box& box = *get_vbox();
76
  Frame *const frame = new Frame();
77
  box.pack_start(*manage(frame), PACK_EXPAND_WIDGET);
78
  frame->set_border_width(6); // HIG spacing
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
  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();
}

96
FileErrorDialog::~FileErrorDialog()
97 98
{}

daniel_e's avatar
daniel_e committed
99 100 101 102 103 104
} // anonymous namespace


namespace Regexxer
{

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
/**** 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
125 126
MainWindow::MainWindow()
:
127
  toolbar_                (0),
128 129 130 131 132 133 134 135
  entry_folder_           (0),
  entry_pattern_          (0),
  button_recursive_       (0),
  button_hidden_          (0),
  entry_regex_            (0),
  entry_substitution_     (0),
  button_multiple_        (0),
  button_caseless_        (0),
136
  filetree_               (0),
137 138 139 140 141 142
  textview_               (0),
  entry_preview_          (0),
  statusline_             (0),
  busy_action_running_    (false),
  busy_action_cancel_     (false),
  busy_action_iteration_  (0),
143
  undo_stack_             (new UndoStack())
daniel_e's avatar
daniel_e committed
144
{
145 146 147
  load_xml();

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

150 151
  entry_folder_->set_text(
      Util::filename_to_utf8_fallback(Util::shorten_pathname(Glib::get_current_dir())));
152

153
  connect_signals();
154

155
  entry_pattern_->set_text("*");
daniel_e's avatar
daniel_e committed
156 157 158 159 160
}

MainWindow::~MainWindow()
{}

161 162
/**** Regexxer::MainWindow -- private **************************************/

163
void MainWindow::load_xml()
164
{
165
  using Gnome::Glade::Xml;
166

167
  const Glib::RefPtr<Xml> xml = Xml::create(glade_mainwindow_filename);
168

169 170
  Gtk::Window* mainwindow = 0;
  window_.reset(xml->get_widget("mainwindow", mainwindow));
171

172 173 174 175 176 177 178 179 180 181 182 183
  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_);
184

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
  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);
  }

202 203 204
  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));
205

206
  controller_.load_xml(xml);
207 208
}

209
void MainWindow::connect_signals()
daniel_e's avatar
daniel_e committed
210
{
211 212
  using sigc::bind;
  using sigc::mem_fun;
daniel_e's avatar
daniel_e committed
213

214 215 216
  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
217

218 219
  entry_folder_ ->signal_activate().connect(controller_.find_files.slot());
  entry_pattern_->signal_activate().connect(controller_.find_files.slot());
220
  entry_pattern_->signal_changed ().connect(mem_fun(*this, &MainWindow::on_entry_pattern_changed));
daniel_e's avatar
daniel_e committed
221

222 223 224 225 226 227 228
  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));
229 230 231 232
  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));
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
  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));
251

252 253
  filetree_->signal_switch_buffer.connect(
      mem_fun(*this, &MainWindow::on_filetree_switch_buffer));
daniel_e's avatar
daniel_e committed
254

255 256
  filetree_->signal_bound_state_changed.connect(
      mem_fun(*this, &MainWindow::on_bound_state_changed));
daniel_e's avatar
daniel_e committed
257

258 259
  filetree_->signal_file_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_file_count_changed));
daniel_e's avatar
daniel_e committed
260

261 262
  filetree_->signal_match_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_match_count_changed));
daniel_e's avatar
daniel_e committed
263

264 265
  filetree_->signal_modified_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_modified_count_changed));
266

267 268
  filetree_->signal_pulse.connect(
      mem_fun(*this, &MainWindow::on_busy_action_pulse));
daniel_e's avatar
daniel_e committed
269

270 271
  filetree_->signal_undo_stack_push.connect(
      mem_fun(*this, &MainWindow::on_undo_stack_push));
daniel_e's avatar
daniel_e committed
272 273
}

274
void MainWindow::on_hide()
daniel_e's avatar
daniel_e committed
275
{
276
  on_busy_action_cancel();
daniel_e's avatar
daniel_e committed
277

278 279 280 281
  // 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
282

283 284 285 286 287 288 289 290
  {
    // 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_);
  }
}
291

292 293 294 295
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
296

297 298 299
bool MainWindow::on_delete_event(GdkEventAny*)
{
  return !confirm_quit_request();
daniel_e's avatar
daniel_e committed
300 301
}

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
void MainWindow::on_cut()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
    buffer->cut_clipboard(textview_->get_clipboard("CLIPBOARD"), textview_->get_editable());
}

void MainWindow::on_copy()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
    buffer->copy_clipboard(textview_->get_clipboard("CLIPBOARD"));
}

void MainWindow::on_paste()
{
  if (const Glib::RefPtr<Gtk::TextBuffer> buffer = textview_->get_buffer())
    buffer->paste_clipboard(textview_->get_clipboard("CLIPBOARD"), textview_->get_editable());
}

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

326 327
void MainWindow::on_quit()
{
328 329
  if (confirm_quit_request())
    window_->hide();
330 331 332 333
}

bool MainWindow::confirm_quit_request()
{
334
  if (filetree_->get_modified_count() == 0)
335 336
    return true;

337 338
  Gtk::MessageDialog dialog (*window_, _("Some files haven't been saved yet.\nQuit anyway?"),
                             false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);
339 340 341 342 343 344 345

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

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

346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
/*
 * 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
375 376
void MainWindow::on_select_folder()
{
377
  using namespace Gtk;
daniel_e's avatar
daniel_e committed
378

379 380 381 382 383
  const std::string folder = get_folder_fullname();

  if (folder.empty())
    return;

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

386 387 388 389 390
  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);
391
  chooser.set_current_folder(folder);
daniel_e's avatar
daniel_e committed
392

393
  if (chooser.run() == RESPONSE_OK)
daniel_e's avatar
daniel_e committed
394
  {
395 396
    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
397 398 399 400 401
  }
}

void MainWindow::on_find_files()
{
402
  if (filetree_->get_modified_count() > 0)
403
  {
404 405
    Gtk::MessageDialog dialog (*window_, _("Some files haven't been saved yet.\nContinue anyway?"),
                               false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);
406

407
    if (dialog.run() != Gtk::RESPONSE_OK)
408 409 410
      return;
  }

411
  const std::string folder = get_folder_fullname();
412

413
  if (folder.empty())
414 415 416
    return;

  undo_stack_clear();
417

418 419
  BusyAction busy (*this);

420 421
  try
  {
422
    Pcre::Pattern pattern (Util::shell_pattern_to_regex(entry_pattern_->get_text()), Pcre::DOTALL);
423

424 425 426
    filetree_->find_files(folder, pattern,
                          button_recursive_->get_active(),
                          button_hidden_->get_active());
427
  }
428
  catch (const Pcre::Error&)
429
  {
430 431
    Gtk::MessageDialog dialog (*window_, _("The file search pattern is invalid."),
                               false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
432 433
    dialog.run();
  }
434
  catch (const FileTree::Error& error)
435
  {
436 437
    FileErrorDialog dialog (*window_, _("The following errors occurred during search:"),
                            Gtk::MESSAGE_WARNING, error);
438 439
    dialog.run();
  }
440

441
  statusline_->set_file_count(filetree_->get_file_count());
daniel_e's avatar
daniel_e committed
442 443 444 445
}

void MainWindow::on_exec_search()
{
446 447
  BusyAction busy (*this);

448 449 450 451
  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
452 453
  try
  {
454
    Pcre::Pattern pattern (regex, (caseless) ? Pcre::CASELESS : Pcre::CompileOptions(0));
455
    filetree_->find_matches(pattern, multiple);
daniel_e's avatar
daniel_e committed
456
  }
457
  catch (const Pcre::Error& error)
daniel_e's avatar
daniel_e committed
458
  {
459
    const int offset = error.offset();
460 461 462 463
    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());
464

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

468
    if (offset >= 0 && offset < entry_regex_->get_text_length())
469 470 471 472
    {
      entry_regex_->grab_focus();
      entry_regex_->select_region(offset, offset + 1);
    }
473

daniel_e's avatar
daniel_e committed
474 475 476
    return;
  }

477
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
478 479 480 481 482
  {
    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
  }

483
  if (filetree_->get_match_count() > 0)
daniel_e's avatar
daniel_e committed
484
  {
485 486 487 488
    // 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(
489
        sigc::mem_fun(*this, &MainWindow::after_exec_search),
490
        Glib::PRIORITY_HIGH_IDLE + 25); // slightly less than redraw (+20)
daniel_e's avatar
daniel_e committed
491 492 493
  }
}

494
bool MainWindow::after_exec_search()
495
{
496
  filetree_->select_first_file();
497
  on_go_next(true);
498

499
  return false;
500 501
}

502
void MainWindow::on_filetree_switch_buffer(FileInfoPtr fileinfo, int file_index)
daniel_e's avatar
daniel_e committed
503
{
504
  const FileBufferPtr old_buffer = FileBufferPtr::cast_static(textview_->get_buffer());
daniel_e's avatar
daniel_e committed
505

506
  if (fileinfo && fileinfo->buffer == old_buffer)
daniel_e's avatar
daniel_e committed
507 508
    return;

509
  if (old_buffer)
daniel_e's avatar
daniel_e committed
510
  {
511
    std::for_each(buffer_connections_.begin(), buffer_connections_.end(),
512
                  std::mem_fun_ref(&sigc::connection::disconnect));
daniel_e's avatar
daniel_e committed
513

514
    buffer_connections_.clear();
daniel_e's avatar
daniel_e committed
515 516 517
    old_buffer->forget_current_match();
  }

518
  if (fileinfo)
daniel_e's avatar
daniel_e committed
519
  {
520
    const FileBufferPtr buffer = fileinfo->buffer;
521
    g_return_if_fail(buffer);
522 523

    textview_->set_buffer(buffer);
524
    textview_->set_editable(!fileinfo->load_failed);
525
    textview_->set_cursor_visible(!fileinfo->load_failed);
526

527
    if (!fileinfo->load_failed)
528 529
    {
      buffer_connections_.push_back(buffer->signal_modified_changed().
530
          connect(sigc::mem_fun(*this, &MainWindow::on_buffer_modified_changed)));
531

532
      buffer_connections_.push_back(buffer->signal_bound_state_changed.
533
          connect(sigc::mem_fun(*this, &MainWindow::on_bound_state_changed)));
534

535
      buffer_connections_.push_back(buffer->signal_preview_line_changed.
536
          connect(sigc::mem_fun(*this, &MainWindow::update_preview)));
537
    }
daniel_e's avatar
daniel_e committed
538

539
    set_title_filename(fileinfo->fullname);
daniel_e's avatar
daniel_e committed
540

541 542
    controller_.replace_file.set_enabled(buffer->get_match_count() > 0);
    controller_.save_file.set_enabled(buffer->get_modified());
543
    controller_.edit_actions.set_enabled(!fileinfo->load_failed);
544 545 546

    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
547
    statusline_->set_file_encoding(fileinfo->encoding);
daniel_e's avatar
daniel_e committed
548 549 550 551
  }
  else
  {
    textview_->set_buffer(FileBuffer::create());
552
    textview_->set_editable(false);
553
    textview_->set_cursor_visible(false);
554

555
    window_->set_title(PACKAGE_NAME);
daniel_e's avatar
daniel_e committed
556

557 558
    controller_.replace_file.set_enabled(false);
    controller_.save_file.set_enabled(false);
559
    controller_.edit_actions.set_enabled(false);
560 561 562

    statusline_->set_match_count(0);
    statusline_->set_match_index(0);
563
    statusline_->set_file_encoding("");
daniel_e's avatar
daniel_e committed
564 565
  }

566
  statusline_->set_file_index(file_index);
daniel_e's avatar
daniel_e committed
567 568 569
  update_preview();
}

570
void MainWindow::on_bound_state_changed()
571
{
572
  BoundState bound = filetree_->get_bound_state();
573

574 575
  controller_.prev_file.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_file.set_enabled((bound & BOUND_LAST)  == 0);
576

577
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
578 579
    bound &= buffer->get_bound_state();

580 581
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
582 583
}

584
void MainWindow::on_filetree_file_count_changed()
585
{
586 587 588 589
  const int file_count = filetree_->get_file_count();

  statusline_->set_file_count(file_count);
  controller_.find_matches.set_enabled(file_count > 0);
590 591
}

592
void MainWindow::on_filetree_match_count_changed()
593
{
594
  controller_.replace_all.set_enabled(filetree_->get_match_count() > 0);
595 596 597

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

600
void MainWindow::on_filetree_modified_count_changed()
601
{
602
  controller_.save_all.set_enabled(filetree_->get_modified_count() > 0);
603 604
}

605 606
void MainWindow::on_buffer_modified_changed()
{
607
  controller_.save_file.set_enabled(textview_->get_buffer()->get_modified());
608 609
}

daniel_e's avatar
daniel_e committed
610 611
void MainWindow::on_go_next_file(bool move_forward)
{
612
  filetree_->select_next_file(move_forward);
daniel_e's avatar
daniel_e committed
613 614 615 616 617
  on_go_next(move_forward);
}

void MainWindow::on_go_next(bool move_forward)
{
618
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
619
  {
620
    if (const Glib::RefPtr<Gtk::TextMark> mark = buffer->get_next_match(move_forward))
daniel_e's avatar
daniel_e committed
621
    {
622
      textview_->scroll_to(mark, 0.125);
623
      statusline_->set_match_index(buffer->get_match_index());
daniel_e's avatar
daniel_e committed
624 625 626 627
      return;
    }
  }

628
  if (filetree_->select_next_file(move_forward))
daniel_e's avatar
daniel_e committed
629 630 631 632 633 634 635
  {
    on_go_next(move_forward); // recursive call
  }
}

void MainWindow::on_replace()
{
636
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
637 638
  {
    buffer->replace_current_match(entry_substitution_->get_text());
639
    on_go_next(true);
daniel_e's avatar
daniel_e committed
640 641 642 643 644
  }
}

void MainWindow::on_replace_file()
{
645
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
646 647
  {
    buffer->replace_all_matches(entry_substitution_->get_text());
648
    statusline_->set_match_index(0);
649
  }
daniel_e's avatar
daniel_e committed
650 651 652 653
}

void MainWindow::on_replace_all()
{
654
  BusyAction busy (*this);
655

656
  filetree_->replace_all_matches(entry_substitution_->get_text());
657
  statusline_->set_match_index(0);
daniel_e's avatar
daniel_e committed
658 659
}

660 661 662 663
void MainWindow::on_save_file()
{
  try
  {
664
    filetree_->save_current_file();
665
  }
666
  catch (const FileTree::Error& error)
667
  {
668 669 670
    const std::list<Glib::ustring>& error_list = error.get_error_list();
    g_assert(error_list.size() == 1);

671 672
    Gtk::MessageDialog dialog (*window_, error_list.front(), false,
                               Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
673 674 675 676 677 678 679 680
    dialog.run();
  }
}

void MainWindow::on_save_all()
{
  try
  {
681
    filetree_->save_all_files();
682
  }
683
  catch (const FileTree::Error& error)
684
  {
685 686
    FileErrorDialog dialog (*window_, _("The following errors occurred during save:"),
                            Gtk::MESSAGE_ERROR, error);
687 688 689 690
    dialog.run();
  }
}

691 692 693 694 695 696 697 698
void MainWindow::on_undo_stack_push(UndoActionPtr action)
{
  undo_stack_->push(action);
  controller_.undo.set_enabled(true);
}

void MainWindow::on_undo()
{
699 700 701
  BusyAction busy (*this);

  undo_stack_->undo_step(sigc::mem_fun(*this, &MainWindow::on_busy_action_pulse));
702 703 704
  controller_.undo.set_enabled(!undo_stack_->empty());
}

705 706 707 708 709 710
void MainWindow::undo_stack_clear()
{
  controller_.undo.set_enabled(false);
  undo_stack_.reset(new UndoStack());
}

711 712
void MainWindow::on_entry_pattern_changed()
{
713
  controller_.find_files.set_enabled(entry_pattern_->get_text_length() > 0);
714 715
}

daniel_e's avatar
daniel_e committed
716 717
void MainWindow::update_preview()
{
718
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
719 720 721 722 723
  {
    Glib::ustring preview;
    const int pos = buffer->get_line_preview(entry_substitution_->get_text(), preview);
    entry_preview_->set_text(preview);

724
    controller_.replace.set_enabled(pos >= 0);
daniel_e's avatar
daniel_e committed
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744

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

745
    if (pos > 0)
daniel_e's avatar
daniel_e committed
746
    {
747
      using namespace sigc;
daniel_e's avatar
daniel_e committed
748 749

      Glib::signal_idle().connect(
750
          bind_return(bind(mem_fun(*entry_preview_, &Gtk::Editable::set_position), pos), false),
daniel_e's avatar
daniel_e committed
751 752 753 754 755
          Glib::PRIORITY_HIGH_IDLE + 17); // between scroll update (+ 15) and redraw (+ 20)
    }
  }
}

756
void MainWindow::set_title_filename(const std::string& filename)
daniel_e's avatar
daniel_e committed
757
{
758
  Glib::ustring title = Util::filename_to_utf8_fallback(Glib::path_get_basename(filename));
daniel_e's avatar
daniel_e committed
759

760 761
  title += " (";
  title += Util::filename_to_utf8_fallback(Util::shorten_pathname(Glib::path_get_dirname(filename)));
762
  title += ") \342\200\223 " PACKAGE_NAME; // U+2013 EN DASH
daniel_e's avatar
daniel_e committed
763

764
  window_->set_title(title);
daniel_e's avatar
daniel_e committed
765 766
}

767 768 769 770
void MainWindow::busy_action_enter()
{
  g_return_if_fail(!busy_action_running_);

771
  controller_.match_actions.set_enabled(false);
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788

  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;
  busy_action_cancel_  = false;

  statusline_->pulse_stop();

789
  controller_.match_actions.set_enabled(true);
790 791 792 793 794 795
}

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

796
  if (!busy_action_cancel_ && (++busy_action_iteration_ % BUSY_GUI_UPDATE_INTERVAL) == 0)
797 798 799
  {
    statusline_->pulse();

800
    const Glib::RefPtr<Glib::MainContext> context = Glib::MainContext::get_default();
801

802
    do {}
803
    while (context->iteration(false) && !busy_action_cancel_);
804 805
  }

806
  return busy_action_cancel_;
807 808 809 810
}

void MainWindow::on_busy_action_cancel()
{
811
  if (busy_action_running_)
812 813 814
    busy_action_cancel_ = true;
}

815
void MainWindow::on_about()
816
{
817
  if (about_dialog_.get())
818 819 820 821 822
  {
    about_dialog_->present();
  }
  else
  {
823
    std::auto_ptr<Gtk::Dialog> dialog = AboutDialog::create(*window_);
824

825
    dialog->signal_hide().connect(sigc::mem_fun(*this, &MainWindow::on_about_dialog_hide));
826 827 828 829 830 831 832 833
    dialog->show();

    about_dialog_ = dialog;
  }
}

void MainWindow::on_about_dialog_hide()
{
834
  // Play safe and transfer ownership, and let the dtor do the delete.
835
  const std::auto_ptr<Gtk::Dialog> temp (about_dialog_);
836 837
}

838 839
void MainWindow::on_preferences()
{
840
  if (pref_dialog_.get())
841
  {
842
    pref_dialog_->get_dialog()->present();
843 844 845
  }
  else
  {
846
    std::auto_ptr<PrefDialog> dialog (new PrefDialog(*window_));
847

848 849
    dialog->get_dialog()->signal_hide().connect(sigc::mem_fun(*this, &MainWindow::on_pref_dialog_hide));
    dialog->get_dialog()->show();
850 851

    pref_dialog_ = dialog;
852 853 854 855 856
  }
}

void MainWindow::on_pref_dialog_hide()
{
857 858
  // Play safe and transfer ownership, and let the dtor do the delete.
  const std::auto_ptr<PrefDialog> temp (pref_dialog_);
859 860
}

861
void MainWindow::on_conf_value_changed(const Glib::ustring& key, const Gnome::Conf::Value& value)
862
{
863 864 865 866 867 868 869 870 871 872
  if (value.get_type() == Gnome::Conf::VALUE_STRING)
  {
    if (key.raw() == conf_key_textview_font)
    {
      const Pango::FontDescription font (value.get_string());
      textview_     ->modify_font(font);
      entry_preview_->modify_font(font);
    }
    else if (key.raw() == conf_key_toolbar_style)
    {
873
      toolbar_->set_toolbar_style(Util::enum_from_nick<Gtk::ToolbarStyle>(value.get_string()));
874 875
    }
  }
876 877
}

daniel_e's avatar
daniel_e committed
878 879
} // namespace Regexxer