mainwindow.cc 25.4 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() */
33
#include <gconfmm.h>
daniel_e's avatar
daniel_e committed
34
#include <gtkmm.h>
35
#include <gtkmm/comboboxentry.h>
36
#include <libglademm.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());
daniel_e's avatar
daniel_e committed
148 149
  set_title_filename();

150 151
  entry_folder_->set_text(Util::shorten_pathname(
      Util::filename_to_utf8_fallback(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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
  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));
  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));
247

248 249
  filetree_->signal_switch_buffer.connect(
      mem_fun(*this, &MainWindow::on_filetree_switch_buffer));
daniel_e's avatar
daniel_e committed
250

251 252
  filetree_->signal_bound_state_changed.connect(
      mem_fun(*this, &MainWindow::on_bound_state_changed));
daniel_e's avatar
daniel_e committed
253

254 255
  filetree_->signal_file_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_file_count_changed));
daniel_e's avatar
daniel_e committed
256

257 258
  filetree_->signal_match_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_match_count_changed));
daniel_e's avatar
daniel_e committed
259

260 261
  filetree_->signal_modified_count_changed.connect(
      mem_fun(*this, &MainWindow::on_filetree_modified_count_changed));
262

263 264
  filetree_->signal_pulse.connect(
      mem_fun(*this, &MainWindow::on_busy_action_pulse));
daniel_e's avatar
daniel_e committed
265

266 267
  filetree_->signal_undo_stack_push.connect(
      mem_fun(*this, &MainWindow::on_undo_stack_push));
daniel_e's avatar
daniel_e committed
268 269
}

270
void MainWindow::on_hide()
daniel_e's avatar
daniel_e committed
271
{
272
  on_busy_action_cancel();
daniel_e's avatar
daniel_e committed
273

274 275 276 277
  // 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
278

279 280 281 282 283 284 285 286
  {
    // 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_);
  }
}
287

288 289 290 291
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
292

293 294 295
bool MainWindow::on_delete_event(GdkEventAny*)
{
  return !confirm_quit_request();
daniel_e's avatar
daniel_e committed
296 297
}

298 299
void MainWindow::on_quit()
{
300 301
  if (confirm_quit_request())
    window_->hide();
302 303 304 305
}

bool MainWindow::confirm_quit_request()
{
306
  if (filetree_->get_modified_count() == 0)
307 308
    return true;

309 310
  const Glib::ustring message = _("Some files haven't been saved yet.\nQuit anyway?");
  Gtk::MessageDialog dialog (*window_, message, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);
311 312 313 314 315 316 317

  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
318 319 320
void MainWindow::on_select_folder()
{
  using namespace Glib;
321
  using namespace Gtk;
daniel_e's avatar
daniel_e committed
322

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

325 326 327 328
  chooser.add_button(Stock::CANCEL, RESPONSE_CANCEL);
  chooser.add_button(Stock::OK,     RESPONSE_OK);
  chooser.set_default_response(RESPONSE_OK);
  chooser.set_modal(true);
daniel_e's avatar
daniel_e committed
329

330 331
  chooser.set_local_only(true);
  chooser.set_current_folder(filename_from_utf8(Util::expand_pathname(entry_folder_->get_text())));
daniel_e's avatar
daniel_e committed
332

333
  if (chooser.run() == RESPONSE_OK)
daniel_e's avatar
daniel_e committed
334
  {
335
    std::string filename = chooser.get_filename();
336

337 338
    if (!filename.empty() && *filename.rbegin() != G_DIR_SEPARATOR)
    {
339
      // The new GTK+ file chooser doesn't append '/' to directories anymore.
340 341 342 343 344
      if (file_test(filename, FILE_TEST_IS_DIR))
        filename += G_DIR_SEPARATOR;
      else
        entry_pattern_->set_text(filename_to_utf8(path_get_basename(filename)));
    }
daniel_e's avatar
daniel_e committed
345

346
    entry_folder_->set_text(Util::shorten_pathname(filename_to_utf8(path_get_dirname(filename))));
daniel_e's avatar
daniel_e committed
347 348 349 350 351
  }
}

void MainWindow::on_find_files()
{
352
  if (filetree_->get_modified_count() > 0)
353
  {
354 355
    const Glib::ustring message = _("Some files haven't been saved yet.\nContinue anyway?");
    Gtk::MessageDialog dialog (*window_, message, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);
356

357
    if (dialog.run() != Gtk::RESPONSE_OK)
358 359 360
      return;
  }

361 362
  undo_stack_clear();

363 364
  std::string folder = Glib::filename_from_utf8(Util::expand_pathname(entry_folder_->get_text()));

365
  if (folder.empty())
366 367
    folder = Glib::get_current_dir();

368 369
  BusyAction busy (*this);

370 371
  try
  {
372
    Pcre::Pattern pattern (Util::shell_pattern_to_regex(entry_pattern_->get_text()), Pcre::DOTALL);
373

374
    filetree_->find_files(
375
        folder, pattern,
376 377 378
        button_recursive_->get_active(),
        button_hidden_->get_active());
  }
379
  catch (const Pcre::Error& error)
380
  {
381 382
    const Glib::ustring message = _("The file search pattern is invalid.");
    Gtk::MessageDialog dialog (*window_, message, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
383 384
    dialog.run();
  }
385
  catch (const FileTree::Error& error)
386
  {
387 388
    const Glib::ustring message = _("The following errors occurred during search:");
    FileErrorDialog dialog (*window_, message, Gtk::MESSAGE_WARNING, error);
389 390
    dialog.run();
  }
391

392
  statusline_->set_file_count(filetree_->get_file_count());
daniel_e's avatar
daniel_e committed
393 394 395 396
}

void MainWindow::on_exec_search()
{
397 398
  BusyAction busy (*this);

399 400 401 402
  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
403 404
  try
  {
405
    Pcre::Pattern pattern (regex, (caseless) ? Pcre::CASELESS : Pcre::CompileOptions(0));
406
    filetree_->find_matches(pattern, multiple);
daniel_e's avatar
daniel_e committed
407
  }
408
  catch (const Pcre::Error& error)
daniel_e's avatar
daniel_e committed
409
  {
410
    const int offset = error.offset();
411 412 413 414
    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());
415

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

419
    if (offset >= 0 && offset < entry_regex_->get_text_length())
420 421 422 423
    {
      entry_regex_->grab_focus();
      entry_regex_->select_region(offset, offset + 1);
    }
424

daniel_e's avatar
daniel_e committed
425 426 427
    return;
  }

428
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
429 430 431 432 433
  {
    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
  }

434
  if (filetree_->get_match_count() > 0)
daniel_e's avatar
daniel_e committed
435
  {
436 437 438 439
    // 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(
440
        sigc::mem_fun(*this, &MainWindow::after_exec_search),
441
        Glib::PRIORITY_HIGH_IDLE + 25); // slightly less than redraw (+20)
daniel_e's avatar
daniel_e committed
442 443 444
  }
}

445
bool MainWindow::after_exec_search()
446
{
447
  filetree_->select_first_file();
448
  on_go_next(true);
449

450
  return false;
451 452
}

453
void MainWindow::on_filetree_switch_buffer(FileInfoPtr fileinfo, int file_index)
daniel_e's avatar
daniel_e committed
454
{
455
  const FileBufferPtr old_buffer = FileBufferPtr::cast_static(textview_->get_buffer());
daniel_e's avatar
daniel_e committed
456

457
  if (fileinfo && fileinfo->buffer == old_buffer)
daniel_e's avatar
daniel_e committed
458 459
    return;

460
  if (old_buffer)
daniel_e's avatar
daniel_e committed
461
  {
462
    std::for_each(buffer_connections_.begin(), buffer_connections_.end(),
463
                  std::mem_fun_ref(&sigc::connection::disconnect));
daniel_e's avatar
daniel_e committed
464

465
    buffer_connections_.clear();
daniel_e's avatar
daniel_e committed
466 467 468
    old_buffer->forget_current_match();
  }

469
  if (fileinfo)
daniel_e's avatar
daniel_e committed
470
  {
471
    const FileBufferPtr buffer = fileinfo->buffer;
472
    g_return_if_fail(buffer);
473 474

    textview_->set_buffer(buffer);
475
    textview_->set_editable(!fileinfo->load_failed);
476
    textview_->set_cursor_visible(!fileinfo->load_failed);
477

478
    if (!fileinfo->load_failed)
479 480
    {
      buffer_connections_.push_back(buffer->signal_modified_changed().
481
          connect(sigc::mem_fun(*this, &MainWindow::on_buffer_modified_changed)));
482

483
      buffer_connections_.push_back(buffer->signal_bound_state_changed.
484
          connect(sigc::mem_fun(*this, &MainWindow::on_bound_state_changed)));
485

486
      buffer_connections_.push_back(buffer->signal_preview_line_changed.
487
          connect(sigc::mem_fun(*this, &MainWindow::update_preview)));
488
    }
daniel_e's avatar
daniel_e committed
489

490
    set_title_filename(Util::filename_to_utf8_fallback(fileinfo->fullname));
daniel_e's avatar
daniel_e committed
491

492 493
    controller_.replace_file.set_enabled(buffer->get_match_count() > 0);
    controller_.save_file.set_enabled(buffer->get_modified());
494 495 496

    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
497
    statusline_->set_file_encoding(fileinfo->encoding);
daniel_e's avatar
daniel_e committed
498 499 500 501
  }
  else
  {
    textview_->set_buffer(FileBuffer::create());
502
    textview_->set_editable(false);
503
    textview_->set_cursor_visible(false);
504

daniel_e's avatar
daniel_e committed
505 506
    set_title_filename();

507 508
    controller_.replace_file.set_enabled(false);
    controller_.save_file.set_enabled(false);
509 510 511

    statusline_->set_match_count(0);
    statusline_->set_match_index(0);
512
    statusline_->set_file_encoding("");
daniel_e's avatar
daniel_e committed
513 514
  }

515
  statusline_->set_file_index(file_index);
daniel_e's avatar
daniel_e committed
516 517 518
  update_preview();
}

519
void MainWindow::on_bound_state_changed()
520
{
521
  BoundState bound = filetree_->get_bound_state();
522

523 524
  controller_.prev_file.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_file.set_enabled((bound & BOUND_LAST)  == 0);
525

526
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
527 528
    bound &= buffer->get_bound_state();

529 530
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
531 532
}

533
void MainWindow::on_filetree_file_count_changed()
534
{
535 536 537 538
  const int file_count = filetree_->get_file_count();

  statusline_->set_file_count(file_count);
  controller_.find_matches.set_enabled(file_count > 0);
539 540
}

541
void MainWindow::on_filetree_match_count_changed()
542
{
543
  controller_.replace_all.set_enabled(filetree_->get_match_count() > 0);
544 545 546

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

549
void MainWindow::on_filetree_modified_count_changed()
550
{
551
  controller_.save_all.set_enabled(filetree_->get_modified_count() > 0);
552 553
}

554 555
void MainWindow::on_buffer_modified_changed()
{
556
  controller_.save_file.set_enabled(textview_->get_buffer()->get_modified());
557 558
}

daniel_e's avatar
daniel_e committed
559 560
void MainWindow::on_go_next_file(bool move_forward)
{
561
  filetree_->select_next_file(move_forward);
daniel_e's avatar
daniel_e committed
562 563 564 565 566
  on_go_next(move_forward);
}

void MainWindow::on_go_next(bool move_forward)
{
567
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
568
  {
569
    if (const Glib::RefPtr<Gtk::TextMark> mark = buffer->get_next_match(move_forward))
daniel_e's avatar
daniel_e committed
570
    {
571
      textview_->scroll_to_mark(mark, 0.125);
572
      statusline_->set_match_index(buffer->get_match_index());
daniel_e's avatar
daniel_e committed
573 574 575 576
      return;
    }
  }

577
  if (filetree_->select_next_file(move_forward))
daniel_e's avatar
daniel_e committed
578 579 580 581 582 583 584
  {
    on_go_next(move_forward); // recursive call
  }
}

void MainWindow::on_replace()
{
585
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
586 587
  {
    buffer->replace_current_match(entry_substitution_->get_text());
588
    on_go_next(true);
daniel_e's avatar
daniel_e committed
589 590 591 592 593
  }
}

void MainWindow::on_replace_file()
{
594
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
595 596
  {
    buffer->replace_all_matches(entry_substitution_->get_text());
597
    statusline_->set_match_index(0);
598
  }
daniel_e's avatar
daniel_e committed
599 600 601 602
}

void MainWindow::on_replace_all()
{
603
  BusyAction busy (*this);
604

605
  filetree_->replace_all_matches(entry_substitution_->get_text());
606
  statusline_->set_match_index(0);
daniel_e's avatar
daniel_e committed
607 608
}

609 610 611 612
void MainWindow::on_save_file()
{
  try
  {
613
    filetree_->save_current_file();
614
  }
615
  catch (const FileTree::Error& error)
616
  {
617 618 619
    const std::list<Glib::ustring>& error_list = error.get_error_list();
    g_assert(error_list.size() == 1);

620
    Gtk::MessageDialog dialog (*window_, error_list.front(), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
621 622 623 624 625 626 627 628
    dialog.run();
  }
}

void MainWindow::on_save_all()
{
  try
  {
629
    filetree_->save_all_files();
630
  }
631
  catch (const FileTree::Error& error)
632
  {
633 634
    const Glib::ustring message = _("The following errors occurred during save:");
    FileErrorDialog dialog (*window_, message, Gtk::MESSAGE_ERROR, error);
635 636 637 638
    dialog.run();
  }
}

639 640 641 642 643 644 645 646
void MainWindow::on_undo_stack_push(UndoActionPtr action)
{
  undo_stack_->push(action);
  controller_.undo.set_enabled(true);
}

void MainWindow::on_undo()
{
647 648 649
  BusyAction busy (*this);

  undo_stack_->undo_step(sigc::mem_fun(*this, &MainWindow::on_busy_action_pulse));
650 651 652
  controller_.undo.set_enabled(!undo_stack_->empty());
}

653 654 655 656 657 658
void MainWindow::undo_stack_clear()
{
  controller_.undo.set_enabled(false);
  undo_stack_.reset(new UndoStack());
}

659 660
void MainWindow::on_entry_pattern_changed()
{
661
  controller_.find_files.set_enabled(entry_pattern_->get_text_length() > 0);
662 663
}

daniel_e's avatar
daniel_e committed
664 665
void MainWindow::update_preview()
{
666
  if (const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
667 668 669 670 671
  {
    Glib::ustring preview;
    const int pos = buffer->get_line_preview(entry_substitution_->get_text(), preview);
    entry_preview_->set_text(preview);

672
    controller_.replace.set_enabled(pos >= 0);
daniel_e's avatar
daniel_e committed
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692

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

693
    if (pos > 0)
daniel_e's avatar
daniel_e committed
694
    {
695
      using namespace sigc;
daniel_e's avatar
daniel_e committed
696 697

      Glib::signal_idle().connect(
698
          bind_return(bind(mem_fun(*entry_preview_, &Gtk::Editable::set_position), pos), false),
daniel_e's avatar
daniel_e committed
699 700 701 702 703 704 705 706 707
          Glib::PRIORITY_HIGH_IDLE + 17); // between scroll update (+ 15) and redraw (+ 20)
    }
  }
}

void MainWindow::set_title_filename(const Glib::ustring& filename)
{
  Glib::ustring title;

708
  if (!filename.empty())
daniel_e's avatar
daniel_e committed
709 710 711 712
  {
    title  = Glib::path_get_basename(filename);
    title += " (";
    title += Util::shorten_pathname(Glib::path_get_dirname(filename));
713
    title += ") \342\200\223 "; // U+2013 EN DASH
daniel_e's avatar
daniel_e committed
714 715 716 717
  }

  title += PACKAGE_NAME;

718
  window_->set_title(title);
daniel_e's avatar
daniel_e committed
719 720
}

721 722 723 724
void MainWindow::busy_action_enter()
{
  g_return_if_fail(!busy_action_running_);

725
  controller_.match_actions.set_enabled(false);
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

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

743
  controller_.match_actions.set_enabled(true);
744 745 746 747 748 749
}

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

750
  if (!busy_action_cancel_ && (++busy_action_iteration_ % BUSY_GUI_UPDATE_INTERVAL) == 0)
751 752 753
  {
    statusline_->pulse();

754
    const Glib::RefPtr<Glib::MainContext> context = Glib::MainContext::get_default();
755

756
    do {}
757
    while (context->iteration(false) && !busy_action_cancel_);
758 759
  }

760
  return busy_action_cancel_;
761 762 763 764
}

void MainWindow::on_busy_action_cancel()
{
765
  if (busy_action_running_)
766 767 768
    busy_action_cancel_ = true;
}

769
void MainWindow::on_about()
770
{
771
  if (about_dialog_.get())
772 773 774 775 776
  {
    about_dialog_->present();
  }
  else
  {
777
    std::auto_ptr<Gtk::Dialog> dialog = AboutDialog::create(*window_);
778

779
    dialog->signal_hide().connect(sigc::mem_fun(*this, &MainWindow::on_about_dialog_hide));
780 781 782 783 784 785 786 787
    dialog->show();

    about_dialog_ = dialog;
  }
}

void MainWindow::on_about_dialog_hide()
{
788
  // Play safe and transfer ownership, and let the dtor do the delete.
789
  const std::auto_ptr<Gtk::Dialog> temp (about_dialog_);
790 791
}

792 793
void MainWindow::on_preferences()
{
794
  if (pref_dialog_.get())
795
  {
796
    pref_dialog_->get_dialog()->present();
797 798 799
  }
  else
  {
800
    std::auto_ptr<PrefDialog> dialog (new PrefDialog(*window_));
801

802 803
    dialog->get_dialog()->signal_hide().connect(sigc::mem_fun(*this, &MainWindow::on_pref_dialog_hide));
    dialog->get_dialog()->show();
804 805

    pref_dialog_ = dialog;
806 807 808 809 810
  }
}

void MainWindow::on_pref_dialog_hide()
{
811 812
  // Play safe and transfer ownership, and let the dtor do the delete.
  const std::auto_ptr<PrefDialog> temp (pref_dialog_);
813 814
}

815
void MainWindow::on_conf_value_changed(const Glib::ustring& key, const Gnome::Conf::Value& value)
816
{
817
  using namespace Gtk;
818

819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
  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)
    {
      toolbar_->set_toolbar_style(Util::enum_from_nick<ToolbarStyle>(value.get_string()));
    }
  }
  else if (value.get_type() == Gnome::Conf::VALUE_BOOL)
  {
    if (key.raw() == conf_key_override_direction)
    {
      const TextDirection direction = (value.get_bool()) ? TEXT_DIR_LTR : TEXT_DIR_NONE;
      entry_folder_      ->set_direction(direction);
      entry_pattern_     ->set_direction(direction);
      entry_regex_       ->set_direction(direction);
      entry_substitution_->set_direction(direction);
      filetree_          ->set_direction(direction);
      textview_          ->set_direction(direction);
      entry_preview_     ->set_direction(direction);
    }
  }
846 847
}

daniel_e's avatar
daniel_e committed
848 849
} // namespace Regexxer