mainwindow.cc 31.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* $Id$
 *
 * Copyright (c) 2002  Daniel Elstner  <daniel.elstner@gmx.net>
 *
 * 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 "imagebutton.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
29
#include "stringutils.h"

30
#include <glib.h>
daniel_e's avatar
daniel_e committed
31
#include <gtkmm.h>
32
#include <algorithm>
33
#include <functional>
daniel_e's avatar
daniel_e committed
34
35
36
37
38
39
40

#include <config.h>


namespace
{

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const char *const builtin_filename_patterns[] =
{
  "*.[ch]",                                   // C
  "*.{c,cc,cpp,cxx,c++,C,h,hh,hpp,hxx,h++}",  // C/C++
  "*.{ccg,hg}",                               // gtkmmproc
  "*.idl",                                    // CORBA/COM
  "*.{java,jsp}",                             // Java/JSP
  "*.{pl,pm,cgi}",                            // Perl
  "*.py",                                     // Python
  "*.php3",                                   // PHP
  "*.{html,htm,shtml,js,wml}",                // HTML
  "*.{xml,xsl,css,dtd,xsd}",                  // XML/XSLT
  0
};

56
enum { BUSY_GUI_UPDATE_INTERVAL = 16 };
57

daniel_e's avatar
daniel_e committed
58
59
60
typedef Glib::RefPtr<Regexxer::FileBuffer> FileBufferPtr;


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

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

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

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

  for(ErrorList::const_iterator perr = error_list.begin(); perr != error_list.end(); ++perr)
  {
84
    buffer_end = buffer->insert(buffer_end, *perr);
85
86
87
    buffer_end = buffer->insert(buffer_end, "\n");
  }

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

109
FileErrorDialog::~FileErrorDialog()
110
111
{}

daniel_e's avatar
daniel_e committed
112
113
114
115
116
117
} // anonymous namespace


namespace Regexxer
{

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**** 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
138
139
MainWindow::MainWindow()
:
140
141
  menubar_handle_         (0),
  toolbar_handle_         (0),
142
  toolbar_                (0),
143
144
145
146
147
148
149
150
  entry_folder_           (0),
  entry_pattern_          (0),
  button_recursive_       (0),
  button_hidden_          (0),
  entry_regex_            (0),
  entry_substitution_     (0),
  button_multiple_        (0),
  button_caseless_        (0),
151
  filetree_               (0),
152
153
154
155
156
157
  textview_               (0),
  entry_preview_          (0),
  statusline_             (0),
  busy_action_running_    (false),
  busy_action_cancel_     (false),
  busy_action_iteration_  (0),
158
  undo_stack_             (new UndoStack()),
159
  fileview_font_          ("mono")
daniel_e's avatar
daniel_e committed
160
{
161
162
  using SigC::bind;
  using SigC::slot;
daniel_e's avatar
daniel_e committed
163
164

  set_title_filename();
165
  set_default_size(640, 450);
daniel_e's avatar
daniel_e committed
166

167
  add(*Gtk::manage(create_main_vbox()));
daniel_e's avatar
daniel_e committed
168

169
170
  controller_.save_file   .connect(slot(*this, &MainWindow::on_save_file));
  controller_.save_all    .connect(slot(*this, &MainWindow::on_save_all));
171
  controller_.undo        .connect(slot(*this, &MainWindow::on_undo));
172
173
  controller_.preferences .connect(slot(*this, &MainWindow::on_preferences));
  controller_.quit        .connect(slot(*this, &MainWindow::on_quit));
174
  controller_.info        .connect(slot(*this, &MainWindow::on_info));
175
176
177
178
179
180
181
182
183
184
  controller_.find_files  .connect(slot(*this, &MainWindow::on_find_files));
  controller_.find_matches.connect(slot(*this, &MainWindow::on_exec_search));
  controller_.next_file   .connect(bind(slot(*this, &MainWindow::on_go_next_file), true));
  controller_.prev_file   .connect(bind(slot(*this, &MainWindow::on_go_next_file), false));
  controller_.next_match  .connect(bind(slot(*this, &MainWindow::on_go_next), true));
  controller_.prev_match  .connect(bind(slot(*this, &MainWindow::on_go_next), false));
  controller_.replace     .connect(slot(*this, &MainWindow::on_replace));
  controller_.replace_file.connect(slot(*this, &MainWindow::on_replace_file));
  controller_.replace_all .connect(slot(*this, &MainWindow::on_replace_all));

185
  show_all_children();
186
  load_configuration();
daniel_e's avatar
daniel_e committed
187

188
189
  entry_folder_->set_text(Util::shorten_pathname(
      Util::filename_to_utf8_fallback(Glib::get_current_dir())));
daniel_e's avatar
daniel_e committed
190
191
192
  entry_pattern_->set_text("*");
  button_recursive_->set_active(true);

193
  statusline_->signal_cancel_clicked.connect(slot(*this, &MainWindow::on_busy_action_cancel));
daniel_e's avatar
daniel_e committed
194

195
  filetree_->signal_switch_buffer.connect(
196
      slot(*this, &MainWindow::on_filetree_switch_buffer));
197

198
  filetree_->signal_bound_state_changed.connect(
199
      slot(*this, &MainWindow::on_filetree_bound_state_changed));
200

201
  filetree_->signal_file_count_changed.connect(
202
      slot(*this, &MainWindow::on_filetree_file_count_changed));
203

204
  filetree_->signal_match_count_changed.connect(
205
      slot(*this, &MainWindow::on_filetree_match_count_changed));
206

207
  filetree_->signal_modified_count_changed.connect(
208
      slot(*this, &MainWindow::on_filetree_modified_count_changed));
209

210
  filetree_->signal_pulse.connect(
211
      slot(*this, &MainWindow::on_busy_action_pulse));
212
213
214

  filetree_->signal_undo_stack_push.connect(
      slot(*this, &MainWindow::on_undo_stack_push));
daniel_e's avatar
daniel_e committed
215
216
217
218
219
}

MainWindow::~MainWindow()
{}

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
void MainWindow::set_menutool_mode(MenuToolMode menutool_mode)
{
  switch(menutool_mode)
  {
    case MODE_MENU_AND_TOOL: menubar_handle_->show(); toolbar_handle_->show(); break;
    case MODE_MENU_ONLY:     toolbar_handle_->hide(); menubar_handle_->show(); break;
    case MODE_TOOL_ONLY:     menubar_handle_->hide(); toolbar_handle_->show(); break;
  }
}

MenuToolMode MainWindow::get_menutool_mode() const
{
  const bool menubar_shown = menubar_handle_->is_visible();
  const bool toolbar_shown = toolbar_handle_->is_visible();

  if(menubar_shown && toolbar_shown) return MODE_MENU_AND_TOOL;
  else if(menubar_shown)             return MODE_MENU_ONLY;
  else if(toolbar_shown)             return MODE_TOOL_ONLY;

  g_return_val_if_reached(MODE_MENU_AND_TOOL);
}

242
243
/**** Regexxer::MainWindow -- protected ************************************/

244
245
246
247
void MainWindow::on_hide()
{
  on_busy_action_cancel();

248
249
250
251
252
253
254
  // Hide the dialogs if they're mapped right now.  This isn't really 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.

  if(about_dialog_.get())
    about_dialog_->hide();
255
256
257

  if(pref_dialog_.get())
    pref_dialog_->hide();
258
259
260
261

  Gtk::Window::on_hide();
}

262
263
264
265
266
267
268
void MainWindow::on_style_changed(const Glib::RefPtr<Gtk::Style>& previous_style)
{
  Gtk::Window::on_style_changed(previous_style);

  FileBuffer::pango_context_changed(get_pango_context());
}

269
270
271
272
273
bool MainWindow::on_delete_event(GdkEventAny*)
{
  return !confirm_quit_request();
}

274
275
/**** Regexxer::MainWindow -- private **************************************/

276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
Gtk::Widget* MainWindow::create_main_vbox()
{
  using namespace Gtk;

  std::auto_ptr<Box> vbox_main (new VBox());

  menubar_handle_ = new HandleBox();
  vbox_main->pack_start(*manage(menubar_handle_), PACK_SHRINK);

  MenuBar *const menubar = controller_.create_menubar();
  menubar_handle_->add(*manage(menubar));

  // We have to call accelerate() manually, because otherwise the menu
  // accelerators wouldn't be available if the menu bar is never shown
  // due to user preference.
  menubar->accelerate(*this);

  toolbar_handle_ = new HandleBox();
  vbox_main->pack_start(*manage(toolbar_handle_), PACK_SHRINK);

  toolbar_ = controller_.create_toolbar();
  toolbar_handle_->add(*manage(toolbar_));

  Box *const vbox_interior = new VBox();
  vbox_main->pack_start(*manage(vbox_interior), PACK_EXPAND_WIDGET);
  vbox_interior->set_border_width(2);

  statusline_ = new StatusLine();
  vbox_main->pack_start(*manage(statusline_), PACK_SHRINK);

  Paned *const paned = new HPaned();
  vbox_interior->pack_start(*manage(paned), PACK_EXPAND_WIDGET);

  paned->pack1(*manage(create_left_pane()),  EXPAND);
  paned->pack2(*manage(create_right_pane()), EXPAND);

  vbox_interior->pack_start(*manage(controller_.create_action_area()), PACK_SHRINK);

  return vbox_main.release();
}

daniel_e's avatar
daniel_e committed
317
318
319
320
Gtk::Widget* MainWindow::create_left_pane()
{
  using namespace Gtk;

321
  std::auto_ptr<Box> vbox (new VBox(false, 3));
daniel_e's avatar
daniel_e committed
322
323
324
325
326
327
328
  vbox->set_border_width(1);

  Table *const table = new Table(3, 2, false);
  vbox->pack_start(*manage(table), PACK_SHRINK);
  table->set_border_width(1);
  table->set_spacings(2);

329
  Button *const button_folder = new ImageLabelButton(Stock::OPEN, "Fol_der:", true);
daniel_e's avatar
daniel_e committed
330
331
332
  table->attach(*manage(button_folder), 0, 1, 0, 1, FILL, AttachOptions(0));
  button_folder->signal_clicked().connect(SigC::slot(*this, &MainWindow::on_select_folder));

333
334
  Label *const label_pattern = new Label("Pattern:", 0.0, 0.5);
  table->attach(*manage(label_pattern), 0, 1, 1, 2, FILL, AttachOptions(0));
335
336
337
338
339
340
341
342
343

  entry_folder_ = new Entry();
  table->attach(*manage(entry_folder_), 1, 2, 0, 1, EXPAND|FILL, AttachOptions(0));

  Combo *const combo_pattern = new Combo();
  table->attach(*manage(combo_pattern), 1, 2, 1, 2, EXPAND|FILL, AttachOptions(0));
  combo_pattern->set_popdown_strings(builtin_filename_patterns);

  entry_pattern_ = combo_pattern->get_entry();
344
  label_pattern->set_mnemonic_widget(*entry_pattern_);
daniel_e's avatar
daniel_e committed
345

346
347
  entry_folder_ ->signal_activate().connect(controller_.find_files.slot());
  entry_pattern_->signal_activate().connect(controller_.find_files.slot());
348
  entry_pattern_->signal_changed ().connect(SigC::slot(*this, &MainWindow::on_entry_pattern_changed));
349

350
  Box *const hbox = new HBox(false, 5);
daniel_e's avatar
daniel_e committed
351
352
353
354
355
356
357
358
  table->attach(*manage(hbox), 0, 2, 2, 3, EXPAND|FILL, AttachOptions(0));

  button_recursive_ = new CheckButton("recursive");
  hbox->pack_start(*manage(button_recursive_), PACK_SHRINK);

  button_hidden_ = new CheckButton("hidden");
  hbox->pack_start(*manage(button_hidden_), PACK_SHRINK);

359
360
361
362
363
  Button *const button_find_files = new Button(Stock::FIND);
  hbox->pack_end(*manage(button_find_files), PACK_SHRINK);

  button_find_files->signal_clicked().connect(controller_.find_files.slot());
  controller_.find_files.add_widget(*button_find_files);
daniel_e's avatar
daniel_e committed
364
365
366
367
368
369
370

  Frame *const frame = new Frame();
  vbox->pack_start(*manage(frame), PACK_EXPAND_WIDGET);

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

371
372
  filetree_ = new FileTree();
  scrollwin->add(*manage(filetree_));
daniel_e's avatar
daniel_e committed
373
374
  scrollwin->set_policy(POLICY_AUTOMATIC, POLICY_ALWAYS);

375
376
377
378
#if REGEXXER_HAVE_GTKMM_22
  entry_folder_->get_accessible()->set_name("Folder");
#endif

379
380
381
382
383
384
385
  tooltips_.set_tip(*entry_folder_,     "The directory to be searched");
  tooltips_.set_tip(*entry_pattern_,    "A filename pattern as used by the shell. "
                                        "Character classes [ab] and csh style "
                                        "brace expressions {a,b} are supported.");
  tooltips_.set_tip(*button_recursive_, "Recurse into subdirectories");
  tooltips_.set_tip(*button_hidden_,    "Also find hidden files");
  tooltips_.set_tip(*button_find_files, "Find all files that match the filename pattern");
daniel_e's avatar
daniel_e committed
386
387
388
389
390
391
392
393

  return vbox.release();
}

Gtk::Widget* MainWindow::create_right_pane()
{
  using namespace Gtk;

394
  std::auto_ptr<Box> vbox (new VBox(false, 3));
daniel_e's avatar
daniel_e committed
395
396
397
398
399
400
401
  vbox->set_border_width(1);

  Table *const table = new Table(2, 3, false);
  vbox->pack_start(*manage(table), PACK_SHRINK);
  table->set_border_width(1);
  table->set_spacings(2);

402
403
404
405
406
407
408
  Label *const label_search = new Label("Search:",  0.0, 0.5);
  table->attach(*manage(label_search), 0, 1, 0, 1, FILL, AttachOptions(0));
  table->attach(*manage(entry_regex_ = new Entry()),  1, 2, 0, 1, EXPAND|FILL, AttachOptions(0));
  label_search->set_mnemonic_widget(*entry_regex_);

  Label *const label_replace = new Label("Replace:",  0.0, 0.5);
  table->attach(*manage(label_replace), 0, 1, 1, 2, FILL, AttachOptions(0));
daniel_e's avatar
daniel_e committed
409
  table->attach(*manage(entry_substitution_ = new Entry()),  1, 2, 1, 2, EXPAND|FILL, AttachOptions(0));
410
  label_replace->set_mnemonic_widget(*entry_substitution_);
daniel_e's avatar
daniel_e committed
411

412
413
  entry_regex_       ->signal_activate().connect(controller_.find_matches.slot());
  entry_substitution_->signal_activate().connect(controller_.find_matches.slot());
414
  entry_substitution_->signal_changed ().connect(SigC::slot(*this, &MainWindow::update_preview));
daniel_e's avatar
daniel_e committed
415

416
  Box *const hbox_options = new HBox(false, 5);
daniel_e's avatar
daniel_e committed
417
418
419
420
  table->attach(*manage(hbox_options), 2, 3, 0, 1, FILL, AttachOptions(0));
  hbox_options->pack_start(*manage(button_multiple_ = new CheckButton("/g")), PACK_SHRINK);
  hbox_options->pack_start(*manage(button_caseless_ = new CheckButton("/i")), PACK_SHRINK);

421
422
423
424
425
  Button *const button_find_matches = new Button(Stock::FIND);
  table->attach(*manage(button_find_matches), 2, 3, 1, 2, FILL, AttachOptions(0));

  controller_.find_matches.add_widget(*button_find_matches);
  button_find_matches->signal_clicked().connect(controller_.find_matches.slot());
daniel_e's avatar
daniel_e committed
426
427
428
429

  Frame *const frame = new Frame();
  vbox->pack_start(*manage(frame), PACK_EXPAND_WIDGET);

430
  Box *const vbox_textview = new VBox(false, 3);
daniel_e's avatar
daniel_e committed
431
432
433
434
435
  frame->add(*manage(vbox_textview));

  ScrolledWindow *const scrollwin = new ScrolledWindow();
  vbox_textview->pack_start(*manage(scrollwin), PACK_EXPAND_WIDGET);

436
  textview_ = new TextView(FileBuffer::create());
daniel_e's avatar
daniel_e committed
437
  scrollwin->add(*manage(textview_));
438
  textview_->set_editable(false);
439
  textview_->set_cursor_visible(false);
daniel_e's avatar
daniel_e committed
440
441
442
443
444
445

  entry_preview_ = new Entry();
  vbox_textview->pack_start(*manage(entry_preview_), PACK_SHRINK);
  entry_preview_->set_has_frame(false);
  entry_preview_->set_editable(false);
  entry_preview_->unset_flags(CAN_FOCUS);
446
  entry_preview_->modify_font(fileview_font_);
daniel_e's avatar
daniel_e committed
447

448
#if REGEXXER_HAVE_GTKMM_22
449
  entry_preview_->get_accessible()->set_name("Preview");
450
#endif
451

452
453
454
455
456
457
458
459
  tooltips_.set_tip(*entry_regex_,        "A regular expression in Perl syntax");
  tooltips_.set_tip(*entry_substitution_, "The new string to substitute. As in Perl, you can "
                                          "refer to parts of the match using $1, $2, etc. "
                                          "or even $+, $&, $` and $'. The operators "
                                          "\\l, \\u, \\L, \\U and \\E are supported as well.");
  tooltips_.set_tip(*button_multiple_,    "Find all possible matches in a line");
  tooltips_.set_tip(*button_caseless_,    "Do case insensitive matching");
  tooltips_.set_tip(*button_find_matches, "Find all matches of the regular expression");
460
  tooltips_.set_tip(*entry_preview_,      "Preview of the substitution in the current line");
daniel_e's avatar
daniel_e committed
461
462
463
464

  return vbox.release();
}

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
void MainWindow::on_quit()
{
  if(confirm_quit_request())
    hide();
}

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

  const Glib::ustring message = "Some files haven't been saved yet.\nQuit anyway?";
  Gtk::MessageDialog dialog (*this, message, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);

  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
485
486
487
488
489
void MainWindow::on_select_folder()
{
  using namespace Glib;

  Gtk::FileSelection filesel ("Select a folder");
490
491

  filesel.set_modal(true);
daniel_e's avatar
daniel_e committed
492
493
  filesel.set_transient_for(*this);
  filesel.hide_fileop_buttons();
494
  filesel.set_has_separator(false);
495
496
  filesel.get_file_list()->get_parent()->hide();
  filesel.set_default_size(350, -1);
daniel_e's avatar
daniel_e committed
497

498
499
  {
    std::string filename = filename_from_utf8(Util::expand_pathname(entry_folder_->get_text()));
daniel_e's avatar
daniel_e committed
500

501
502
    if(!filename.empty() && *filename.rbegin() != G_DIR_SEPARATOR)
      filename += G_DIR_SEPARATOR;
daniel_e's avatar
daniel_e committed
503

504
505
    filesel.set_filename(filename);
  }
daniel_e's avatar
daniel_e committed
506
507
508

  if(filesel.run() == Gtk::RESPONSE_OK)
  {
509
    const ustring filename = filename_to_utf8(filesel.get_filename());
510

511
    entry_folder_->set_text(Util::shorten_pathname(path_get_dirname(filename)));
daniel_e's avatar
daniel_e committed
512

513
514
    if(!filename.empty() && *filename.rbegin() != G_DIR_SEPARATOR)
      entry_pattern_->set_text(path_get_basename(filename));
daniel_e's avatar
daniel_e committed
515
516
517
518
519
  }
}

void MainWindow::on_find_files()
{
520
521
522
523
524
525
526
527
528
  if(filetree_->get_modified_count() > 0)
  {
    const Glib::ustring message = "Some files haven't been saved yet.\nContinue anyway?";
    Gtk::MessageDialog dialog (*this, message, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);

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

529
530
531
532
533
  std::string folder = Glib::filename_from_utf8(Util::expand_pathname(entry_folder_->get_text()));

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

534
535
  BusyAction busy (*this);

536
537
  try
  {
538
539
    Pcre::Pattern pattern (Util::shell_pattern_to_regex(entry_pattern_->get_text()));

540
    filetree_->find_files(
541
        folder, pattern,
542
543
544
545
546
547
548
549
550
        button_recursive_->get_active(),
        button_hidden_->get_active());
  }
  catch(const Pcre::Error& error)
  {
    const Glib::ustring message = "The file search pattern is invalid.";
    Gtk::MessageDialog dialog (*this, message, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
    dialog.run();
  }
551
  catch(const FileTree::Error& error)
552
  {
553
554
    const Glib::ustring message = "The following errors occurred during search:";
    FileErrorDialog dialog (*this, message, Gtk::MESSAGE_WARNING, error);
555
556
    dialog.run();
  }
557

558
  statusline_->set_file_count(filetree_->get_file_count());
daniel_e's avatar
daniel_e committed
559
560
561
562
}

void MainWindow::on_exec_search()
{
563
564
  BusyAction busy (*this);

565
566
567
568
  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
569
570
  try
  {
571
    Pcre::Pattern pattern (regex, (caseless) ? Pcre::CASELESS : Pcre::CompileOptions(0));
572
    filetree_->find_matches(pattern, multiple);
daniel_e's avatar
daniel_e committed
573
  }
574
  catch(const Pcre::Error& error)
daniel_e's avatar
daniel_e committed
575
  {
576
    Glib::ustring message = "Error in regular expression";
577
    const int byte_offset = error.offset();
578
    int       char_index  = entry_regex_->get_position();
579

580
    if(byte_offset >= 0 && unsigned(byte_offset) < regex.bytes())
581
    {
582
      const Glib::ustring::const_iterator pos (regex.begin().base() + byte_offset);
583
      char_index = std::distance(regex.begin(), pos);
584
585

      message += " at \302\273";
586
      message += *pos;
587
      message += "\302\253 (index ";
588
      message += Util::int_to_string(char_index + 1);
589
590
591
592
593
594
595
596
597
      message += ")";
    }

    message += ":\n";
    message += error.what();

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

598
599
600
    entry_regex_->grab_focus(); // XXX: kills PRIMARY selection (GTK+ problem)
    entry_regex_->set_position(char_index);

daniel_e's avatar
daniel_e committed
601
602
603
    return;
  }

604
605
606
607
608
609
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
  {
    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
  }

610
  if(filetree_->get_match_count() > 0)
daniel_e's avatar
daniel_e committed
611
  {
612
613
614
615
616
617
    // 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(
        SigC::slot(*this, &MainWindow::after_exec_search),
        Glib::PRIORITY_HIGH_IDLE + 25); // slightly less than redraw (+20)
daniel_e's avatar
daniel_e committed
618
619
620
  }
}

621
bool MainWindow::after_exec_search()
622
{
623
  filetree_->select_first_file();
624
  on_go_next(true);
625

626
  return false;
627
628
}

629
void MainWindow::on_filetree_switch_buffer(FileInfoPtr fileinfo, int file_index)
daniel_e's avatar
daniel_e committed
630
{
631
  const FileBufferPtr old_buffer = FileBufferPtr::cast_static(textview_->get_buffer());
daniel_e's avatar
daniel_e committed
632
633
634
635
636
637

  if(fileinfo && fileinfo->buffer == old_buffer)
    return;

  if(old_buffer)
  {
638
639
    std::for_each(buffer_connections_.begin(), buffer_connections_.end(),
                  std::mem_fun_ref(&SigC::Connection::disconnect));
daniel_e's avatar
daniel_e committed
640

641
    buffer_connections_.clear();
daniel_e's avatar
daniel_e committed
642
643
644
    old_buffer->forget_current_match();
  }

645
  if(fileinfo)
daniel_e's avatar
daniel_e committed
646
  {
647
    const FileBufferPtr buffer = fileinfo->buffer;
648
    g_return_if_fail(buffer);
649
650

    textview_->set_buffer(buffer);
651
    textview_->set_editable(!fileinfo->load_failed);
652
    textview_->set_cursor_visible(!fileinfo->load_failed);
653

654
655
656
657
658
659
    if(!fileinfo->load_failed)
    {
      textview_->modify_font(fileview_font_);

      buffer_connections_.push_back(buffer->signal_match_count_changed.
          connect(SigC::slot(*this, &MainWindow::on_buffer_match_count_changed)));
daniel_e's avatar
daniel_e committed
660

661
662
      buffer_connections_.push_back(buffer->signal_modified_changed().
          connect(SigC::slot(*this, &MainWindow::on_buffer_modified_changed)));
663

664
665
      buffer_connections_.push_back(buffer->signal_bound_state_changed.
          connect(SigC::slot(*this, &MainWindow::on_buffer_bound_state_changed)));
666

667
668
669
670
671
672
673
      buffer_connections_.push_back(buffer->signal_preview_line_changed.
          connect(SigC::slot(*this, &MainWindow::update_preview)));
    }
    else
    {
      textview_->modify_font(get_pango_context()->get_font_description());
    }
daniel_e's avatar
daniel_e committed
674

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

677
    on_buffer_match_count_changed(buffer->get_match_count());
678
    on_buffer_modified_changed();
679
    on_buffer_bound_state_changed(buffer->get_bound_state());
680
681
682

    statusline_->set_match_count(buffer->get_original_match_count());
    statusline_->set_match_index(buffer->get_match_index());
683
    statusline_->set_file_encoding(fileinfo->encoding);
daniel_e's avatar
daniel_e committed
684
685
686
687
  }
  else
  {
    textview_->set_buffer(FileBuffer::create());
688
    textview_->set_editable(false);
689
    textview_->set_cursor_visible(false);
690
    textview_->modify_font(get_pango_context()->get_font_description());
691

daniel_e's avatar
daniel_e committed
692
693
    set_title_filename();

694
    on_buffer_match_count_changed(0);
695
    on_buffer_modified_changed();
696
    on_buffer_bound_state_changed(BOUND_FIRST | BOUND_LAST);
697
698
699

    statusline_->set_match_count(0);
    statusline_->set_match_index(0);
700
    statusline_->set_file_encoding("");
daniel_e's avatar
daniel_e committed
701
702
  }

703
  statusline_->set_file_index(file_index);
daniel_e's avatar
daniel_e committed
704
705
706
  update_preview();
}

707
void MainWindow::on_filetree_bound_state_changed()
708
{
709
  BoundState bound = filetree_->get_bound_state();
710

711
712
  controller_.prev_file.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_file.set_enabled((bound & BOUND_LAST)  == 0);
713

714
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
715
716
    bound &= buffer->get_bound_state();

717
718
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
719
720
}

721
void MainWindow::on_filetree_file_count_changed()
722
{
723
724
725
726
  const int file_count = filetree_->get_file_count();

  statusline_->set_file_count(file_count);
  controller_.find_matches.set_enabled(file_count > 0);
727
728
}

729
void MainWindow::on_filetree_match_count_changed()
730
{
731
  controller_.replace_all.set_enabled(filetree_->get_match_count() > 0);
732
733
}

734
void MainWindow::on_filetree_modified_count_changed()
735
{
736
  controller_.save_all.set_enabled(filetree_->get_modified_count() > 0);
737
738
}

739
740
void MainWindow::on_buffer_match_count_changed(int match_count)
{
741
  controller_.replace_file.set_enabled(match_count > 0);
742
743
}

744
745
void MainWindow::on_buffer_modified_changed()
{
746
  controller_.save_file.set_enabled(textview_->get_buffer()->get_modified());
747
748
}

749
void MainWindow::on_buffer_bound_state_changed(BoundState bound)
daniel_e's avatar
daniel_e committed
750
{
751
  bound &= filetree_->get_bound_state();
752

753
754
  controller_.prev_match.set_enabled((bound & BOUND_FIRST) == 0);
  controller_.next_match.set_enabled((bound & BOUND_LAST)  == 0);
daniel_e's avatar
daniel_e committed
755
756
757
758
}

void MainWindow::on_go_next_file(bool move_forward)
{
759
  filetree_->select_next_file(move_forward);
daniel_e's avatar
daniel_e committed
760
761
762
763
764
  on_go_next(move_forward);
}

void MainWindow::on_go_next(bool move_forward)
{
765
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
766
767
768
769
  {
    if(const Glib::RefPtr<Gtk::TextMark> mark = buffer->get_next_match(move_forward))
    {
      textview_->scroll_to_mark(mark, 0.2);
770
      statusline_->set_match_index(buffer->get_match_index());
daniel_e's avatar
daniel_e committed
771
772
773
774
      return;
    }
  }

775
  if(filetree_->select_next_file(move_forward))
daniel_e's avatar
daniel_e committed
776
777
778
779
780
781
782
  {
    on_go_next(move_forward); // recursive call
  }
}

void MainWindow::on_replace()
{
783
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
784
785
  {
    buffer->replace_current_match(entry_substitution_->get_text());
786
    on_go_next(true);
daniel_e's avatar
daniel_e committed
787
788
789
790
791
  }
}

void MainWindow::on_replace_file()
{
792
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
793
794
  {
    buffer->replace_all_matches(entry_substitution_->get_text());
795
    statusline_->set_match_index(0);
796
  }
daniel_e's avatar
daniel_e committed
797
798
799
800
}

void MainWindow::on_replace_all()
{
801
  BusyAction busy (*this);
802

803
  filetree_->replace_all_matches(entry_substitution_->get_text());
804
  statusline_->set_match_index(0);
daniel_e's avatar
daniel_e committed
805
806
}

807
808
809
810
void MainWindow::on_save_file()
{
  try
  {
811
    filetree_->save_current_file();
812
  }
813
  catch(const FileTree::Error& error)
814
  {
815
816
817
818
    const std::list<Glib::ustring>& error_list = error.get_error_list();
    g_assert(error_list.size() == 1);

    Gtk::MessageDialog dialog (*this, error_list.front(), Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
819
820
821
822
823
824
825
826
    dialog.run();
  }
}

void MainWindow::on_save_all()
{
  try
  {
827
    filetree_->save_all_files();
828
  }
829
  catch(const FileTree::Error& error)
830
831
832
833
834
835
836
  {
    const Glib::ustring message = "The following errors occurred during save:";
    FileErrorDialog dialog (*this, message, Gtk::MESSAGE_ERROR, error);
    dialog.run();
  }
}

837
838
839
840
841
842
843
844
845
846
847
848
void MainWindow::on_undo_stack_push(UndoActionPtr action)
{
  undo_stack_->push(action);
  controller_.undo.set_enabled(true);
}

void MainWindow::on_undo()
{
  undo_stack_->undo_step();
  controller_.undo.set_enabled(!undo_stack_->empty());
}

849
850
void MainWindow::on_entry_pattern_changed()
{
851
  controller_.find_files.set_enabled(entry_pattern_->get_text_length() > 0);
852
853
}

daniel_e's avatar
daniel_e committed
854
855
void MainWindow::update_preview()
{
856
  if(const FileBufferPtr buffer = FileBufferPtr::cast_static(textview_->get_buffer()))
daniel_e's avatar
daniel_e committed
857
858
859
860
861
  {
    Glib::ustring preview;
    const int pos = buffer->get_line_preview(entry_substitution_->get_text(), preview);
    entry_preview_->set_text(preview);

862
    controller_.replace.set_enabled(pos >= 0);
daniel_e's avatar
daniel_e committed
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912

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

    if(pos > 0)
    {
      using SigC::slot;
      using SigC::bind;
      using SigC::bind_return;

      Glib::signal_idle().connect(
          bind_return(bind(slot(*entry_preview_, &Gtk::Editable::set_position), pos), false),
          Glib::PRIORITY_HIGH_IDLE + 17); // between scroll update (+ 15) and redraw (+ 20)
    }
  }
}

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

  if(!filename.empty())
  {
    title  = Glib::path_get_basename(filename);
    title += " (";
    title += Util::shorten_pathname(Glib::path_get_dirname(filename));
    title += ") - ";
  }

  title += PACKAGE_NAME;

  set_title(title);
}

913
914
915
916
void MainWindow::busy_action_enter()
{
  g_return_if_fail(!busy_action_running_);

917
  controller_.match_actions.set_enabled(false);
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934

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

935
  controller_.match_actions.set_enabled(true);
936
937
938
939
940
941
}

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

942
  if(!busy_action_cancel_ && (++busy_action_iteration_ % BUSY_GUI_UPDATE_INTERVAL) == 0)
943
944
945
946
947
  {
    statusline_->pulse();

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

948
949
    do {}
    while(context->iteration(false) && !busy_action_cancel_);
950
951
  }

952
  return busy_action_cancel_;
953
954
955
956
957
958
959
960
}

void MainWindow::on_busy_action_cancel()
{
  if(busy_action_running_)
    busy_action_cancel_ = true;
}

961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
void MainWindow::on_info()
{
  if(about_dialog_.get())
  {
    about_dialog_->present();
  }
  else
  {
    std::auto_ptr<AboutDialog> dialog (new AboutDialog(*this));

    dialog->signal_hide().connect(SigC::slot(*this, &MainWindow::on_about_dialog_hide));
    dialog->show();

    about_dialog_ = dialog;
  }
}

void MainWindow::on_about_dialog_hide()
{
  about_dialog_.reset();
}

983
984
985
986
void MainWindow::on_preferences()
{
  if(pref_dialog_.get())
  {
987
    pref_dialog_->present();
988
989
990
  }
  else
  {
991
    std::auto_ptr<PrefDialog> dialog (new PrefDialog(*this));
992

993
    dialog->set_pref_menutool_mode(get_menutool_mode());
994
995
    dialog->set_pref_toolbar_style(toolbar_->get_toolbar_style());
    dialog->set_pref_fallback_encoding(filetree_->get_fallback_encoding());
996

997
998
999
    dialog->signal_pref_menutool_mode_changed.connect(
        SigC::slot(*this, &MainWindow::set_menutool_mode));

1000
    dialog->signal_pref_toolbar_style_changed.connect(
1001
1002
        SigC::slot(*toolbar_, &Gtk::Toolbar::set_toolbar_style));

1003
    dialog->signal_pref_fallback_encoding_changed.connect(
1004
        SigC::slot(*filetree_, &FileTree::set_fallback_encoding));
1005

1006
1007
1008
1009
    dialog->signal_hide().connect(SigC::slot(*this, &MainWindow::on_pref_dialog_hide));
    dialog->show();

    pref_dialog_ = dialog;
1010
1011
1012
1013
1014
1015
  }
}

void MainWindow::on_pref_dialog_hide()
{
  pref_dialog_.reset();
1016
1017
1018
1019
1020
1021
1022
1023
1024
  save_configuration();
}

void MainWindow::load_configuration()
{
  ConfigData config;

  config.load();

1025
  set_menutool_mode(config.menutool_mode);
1026
  toolbar_->set_toolbar_style(config.toolbar_style);
1027
  filetree_->set_fallback_encoding(config.fallback_encoding);
1028
1029
1030
1031
1032
1033
}

void MainWindow::save_configuration()
{
  ConfigData config;

1034
  config.menutool_mode = get_menutool_mode();
1035
  config.toolbar_style = toolbar_->get_toolbar_style();
1036
  config.fallback_encoding = filetree_->get_fallback_encoding();
1037
1038

  config.save();
1039
1040
}

daniel_e's avatar
daniel_e committed
1041
1042
} // namespace Regexxer