1 /*
2  * Copyright (c) 2002-2007  Daniel Elstner  <daniel.kitta@gmail.com>
3  *
4  * This file is part of regexxer.
5  *
6  * regexxer is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * regexxer is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with regexxer; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  */
20 
21 #include "filetree.h"
22 #include "filetreeprivate.h"
23 #include "globalstrings.h"
24 #include "stringutils.h"
25 #include "translation.h"
26 #include "settings.h"
27 
28 #include <glibmm.h>
29 #include <gtkmm/stock.h>
30 
31 #include <config.h>
32 
33 namespace Regexxer
34 {
35 
36 using namespace Regexxer::FileTreePrivate;
37 
38 /**** Regexxer::FileTree ***************************************************/
39 
FileTree()40 FileTree::FileTree()
41 :
42   treestore_      (Gtk::TreeStore::create(FileTreeColumns::instance())),
43   color_modified_ ("#DF421E"), // accent red
44   sum_matches_    (0)
45 {
46   using namespace Gtk;
47   using sigc::mem_fun;
48 
49   set_model(treestore_);
50   const FileTreeColumns& model_columns = FileTreeColumns::instance();
51 
52   treestore_->set_default_sort_func(&default_sort_func);
53   treestore_->set_sort_func(model_columns.collatekey, &collatekey_sort_func);
54   treestore_->set_sort_column(TreeStore::DEFAULT_SORT_COLUMN_ID, SORT_ASCENDING);
55 
56   treestore_->signal_rows_reordered()
57       .connect(mem_fun(*this, &FileTree::on_treestore_rows_reordered));
58 
59   {
60     Column *const column = new Column(_("File"));
61     append_column(*manage(column));
62 
63     CellRendererPixbuf *const cell_icon = new CellRendererPixbuf();
64     column->pack_start(*manage(cell_icon), false);
65 
66     CellRendererText *const cell_filename = new CellRendererText();
67     column->pack_start(*manage(cell_filename));
68 
69     column->add_attribute(cell_filename->property_text(), model_columns.filename);
70     column->set_cell_data_func(*cell_icon,     mem_fun(*this, &FileTree::icon_cell_data_func));
71     column->set_cell_data_func(*cell_filename, mem_fun(*this, &FileTree::text_cell_data_func));
72 
73     column->set_resizable(true);
74     column->set_expand(true);
75 
76     column->set_sort_column(model_columns.collatekey);
77   }
78   {
79     Column *const column = new Column(_("#"));
80     append_column(*manage(column));
81 
82     CellRendererText *const cell_matchcount = new CellRendererText();
83     column->pack_start(*manage(cell_matchcount));
84 
85     column->add_attribute(cell_matchcount->property_text(), model_columns.matchcount);
86     column->set_cell_data_func(*cell_matchcount, mem_fun(*this, &FileTree::text_cell_data_func));
87 
88     column->set_alignment(1.0);
89     cell_matchcount->property_xalign() = 1.0;
90 
91     column->set_sort_column(model_columns.matchcount);
92   }
93 
94   set_search_column(model_columns.filename);
95 
96   const Glib::RefPtr<TreeSelection> selection = get_selection();
97 
98   selection->set_select_function(&FileTree::select_func);
99   selection->signal_changed().connect(mem_fun(*this, &FileTree::on_selection_changed));
100 
101   Settings::instance()->signal_changed().connect(mem_fun(*this, &FileTree::on_conf_value_changed));
102 }
103 
~FileTree()104 FileTree::~FileTree()
105 {}
106 
find_files(const std::string & dirname,const Glib::RefPtr<Glib::Regex> & pattern,bool recursive,bool hidden)107 void FileTree::find_files(const std::string& dirname, const Glib::RefPtr<Glib::Regex>& pattern,
108                           bool recursive, bool hidden)
109 {
110   FindData find_data (pattern, recursive, hidden);
111 
112   const bool modified_count_changed = (toplevel_.modified_count != 0);
113 
114   treestore_->clear();
115 
116   toplevel_.file_count     = 0;
117   toplevel_.modified_count = 0;
118   sum_matches_ = 0;
119 
120   signal_bound_state_changed(); // emit
121   signal_match_count_changed(); // emit
122 
123   if (modified_count_changed)
124     signal_modified_count_changed(); // emit
125 
126   find_recursively(dirname, find_data);
127 
128   // Work around a strange misbehavior: the tree is kept sorted while the
129   // file search is in progress, which causes the scroll offset to change
130   // slightly.  This in turn confuses TreeView::set_cursor() -- the first
131   // call after the tree was completely filled just doesn't scroll.
132   if (toplevel_.file_count > 0)
133     scroll_to_row(Gtk::TreeModel::Path(1u, 0));
134 
135   signal_bound_state_changed(); // emit
136 
137   if (!find_data.error_list->empty())
138     throw Error(find_data.error_list);
139 }
140 
get_file_count() const141 int FileTree::get_file_count() const
142 {
143   g_return_val_if_fail(toplevel_.file_count >= 0, 0);
144   return toplevel_.file_count;
145 }
146 
save_current_file()147 void FileTree::save_current_file()
148 {
149   if (const Gtk::TreeModel::iterator selected = get_selection()->get_selected())
150   {
151     Util::SharedPtr<MessageList> error_list (new MessageList());
152 
153     {
154       Util::ScopedBlock block (conn_modified_changed_);
155       save_file_at_iter(selected, error_list);
156     }
157 
158     if (!error_list->empty())
159       throw Error(error_list);
160   }
161 }
162 
save_all_files()163 void FileTree::save_all_files()
164 {
165   Util::SharedPtr<MessageList> error_list (new MessageList());
166 
167   {
168     Util::ScopedBlock block (conn_modified_changed_);
169 
170     treestore_->foreach_iter(sigc::bind(
171         sigc::mem_fun(*this, &FileTree::save_file_at_iter),
172         sigc::ref(error_list)));
173   }
174 
175   if (!error_list->empty())
176     throw Error(error_list);
177 }
178 
select_first_file()179 void FileTree::select_first_file()
180 {
181   if (sum_matches_ > 0)
182     expand_and_select(path_match_first_);
183 }
184 
select_next_file(bool move_forward)185 bool FileTree::select_next_file(bool move_forward)
186 {
187   if (Gtk::TreeModel::iterator iter = get_selection()->get_selected())
188   {
189     Gtk::TreeModel::Path collapse;
190 
191     if ((move_forward) ? next_match_file(iter, &collapse) : prev_match_file(iter, &collapse))
192     {
193       if (!collapse.empty())
194         collapse_row(collapse);
195 
196       expand_and_select(Gtk::TreeModel::Path(iter));
197       return true;
198     }
199   }
200 
201   return false;
202 }
203 
get_bound_state()204 BoundState FileTree::get_bound_state()
205 {
206   BoundState bound = BOUND_FIRST | BOUND_LAST;
207 
208   if (sum_matches_ > 0)
209   {
210     if (const Gtk::TreeModel::iterator iter = get_selection()->get_selected())
211     {
212       Gtk::TreeModel::Path path (iter);
213 
214       if (path > path_match_first_)
215         bound &= ~BOUND_FIRST;
216 
217       if (path < path_match_last_)
218         bound &= ~BOUND_LAST;
219     }
220   }
221 
222   return bound;
223 }
224 
find_matches(const Glib::RefPtr<Glib::Regex> & pattern,bool multiple)225 void FileTree::find_matches(const Glib::RefPtr<Glib::Regex>& pattern, bool multiple)
226 {
227   {
228     Util::ScopedBlock  block_conn (conn_match_count_);
229     ScopedBlockSorting block_sort (*this);
230     FindMatchesData    find_data  (pattern, multiple);
231 
232     treestore_->foreach(sigc::bind(
233         sigc::mem_fun(*this, &FileTree::find_matches_at_path_iter),
234         sigc::ref(find_data)));
235   }
236 
237   signal_bound_state_changed(); // emit
238 }
239 
get_match_count() const240 long FileTree::get_match_count() const
241 {
242   g_return_val_if_fail(sum_matches_ >= 0, 0);
243   return sum_matches_;
244 }
245 
replace_all_matches(const Glib::ustring & substitution)246 void FileTree::replace_all_matches(const Glib::ustring& substitution)
247 {
248   {
249     Util::ScopedBlock block_match_count      (conn_match_count_);
250     Util::ScopedBlock block_modified_changed (conn_modified_changed_);
251     Util::ScopedBlock block_undo_stack_push  (conn_undo_stack_push_);
252     ScopedBlockSorting block_sort   (*this);
253     ReplaceMatchesData replace_data (*this, substitution);
254 
255     treestore_->foreach(sigc::bind(
256         sigc::mem_fun(*this, &FileTree::replace_matches_at_path_iter),
257         sigc::ref(replace_data)));
258 
259     // Adjust the boundary range if the operation has been interrupted.
260     if (sum_matches_ > 0)
261     {
262       Gtk::TreeModel::iterator first = treestore_->get_iter(path_match_first_);
263 
264       if ((*first)[FileTreeColumns::instance().matchcount] == 0 && next_match_file(first))
265         path_match_first_ = first;
266     }
267 
268     signal_undo_stack_push(replace_data.undo_stack); // emit
269   }
270 
271   signal_bound_state_changed(); // emit
272 }
273 
get_modified_count() const274 int FileTree::get_modified_count() const
275 {
276   g_return_val_if_fail(toplevel_.modified_count >= 0, 0);
277   return toplevel_.modified_count;
278 }
279 
280 /**** Regexxer::FileTree -- protected **************************************/
281 
on_style_updated()282 void FileTree::on_style_updated()
283 {
284   pixbuf_directory_   = render_icon_pixbuf(Gtk::Stock::DIRECTORY,     Gtk::ICON_SIZE_MENU);
285   pixbuf_file_        = render_icon_pixbuf(Gtk::Stock::FILE,          Gtk::ICON_SIZE_MENU);
286   pixbuf_load_failed_ = render_icon_pixbuf(Gtk::Stock::MISSING_IMAGE, Gtk::ICON_SIZE_MENU);
287 
288   Gdk::RGBA rgba = get_style_context()->get_color(Gtk::STATE_FLAG_INSENSITIVE);
289   color_load_failed_.set_rgb_p(rgba.get_red(), rgba.get_green(), rgba.get_blue());
290 
291   Gtk::TreeView::on_style_updated();
292 }
293 
294 /**** Regexxer::FileTree -- private ****************************************/
295 
icon_cell_data_func(Gtk::CellRenderer * cell,const Gtk::TreeModel::iterator & iter)296 void FileTree::icon_cell_data_func(Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter)
297 {
298   Gtk::CellRendererPixbuf& renderer = dynamic_cast<Gtk::CellRendererPixbuf&>(*cell);
299   const FileInfoBasePtr    infobase = (*iter)[FileTreeColumns::instance().fileinfo];
300 
301   if (const FileInfoPtr fileinfo = shared_dynamic_cast<FileInfo>(infobase))
302   {
303     renderer.property_pixbuf() = (fileinfo->load_failed) ? pixbuf_load_failed_ : pixbuf_file_;
304   }
305   else if (shared_dynamic_cast<DirInfo>(infobase))
306   {
307     renderer.property_pixbuf() = pixbuf_directory_;
308   }
309   else
310   {
311     renderer.property_pixbuf().reset_value();
312   }
313 }
314 
text_cell_data_func(Gtk::CellRenderer * cell,const Gtk::TreeModel::iterator & iter)315 void FileTree::text_cell_data_func(Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter)
316 {
317   Gtk::CellRendererText& renderer = dynamic_cast<Gtk::CellRendererText&>(*cell);
318   const FileInfoBasePtr  infobase = (*iter)[FileTreeColumns::instance().fileinfo];
319 
320   const Gdk::Color* color = 0;
321 
322   if (const FileInfoPtr fileinfo = shared_dynamic_cast<FileInfo>(infobase))
323   {
324     if (fileinfo->load_failed)
325       color = &color_load_failed_;
326     else if (fileinfo->buffer && fileinfo->buffer->get_modified())
327       color = &color_modified_;
328   }
329   else if (const DirInfoPtr dirinfo = shared_dynamic_cast<DirInfo>(infobase))
330   {
331     if (dirinfo->modified_count > 0)
332       color = &color_modified_;
333   }
334 
335   if (color)
336     renderer.property_foreground_gdk() = *color;
337   else
338     renderer.property_foreground_gdk().reset_value();
339 
340   if (color == &color_modified_)
341     renderer.property_style() = Pango::STYLE_OBLIQUE;
342   else
343     renderer.property_style().reset_value();
344 }
345 
346 // static
select_func(const Glib::RefPtr<Gtk::TreeModel> & model,const Gtk::TreeModel::Path & path,bool)347 bool FileTree::select_func(const Glib::RefPtr<Gtk::TreeModel>& model,
348                            const Gtk::TreeModel::Path& path, bool)
349 {
350   // Don't allow selection of directory nodes.
351   return (get_fileinfo_from_iter(model->get_iter(path)) != 0);
352 }
353 
find_recursively(const std::string & dirname,FindData & find_data)354 void FileTree::find_recursively(const std::string& dirname, FindData& find_data)
355 {
356   using namespace Glib;
357 
358   try
359   {
360     int file_count = 0;
361     Dir dir (dirname);
362 
363     for (Dir::iterator pos = dir.begin(); pos != dir.end(); ++pos)
364     {
365       if (signal_pulse()) // emit
366         break;
367 
368       const std::string filename = *pos;
369 
370       if (!find_data.hidden && *filename.begin() == '.')
371         continue;
372 
373       const std::string fullname = build_filename(dirname, filename);
374 
375       if (file_test(fullname, FILE_TEST_IS_SYMLINK))
376         continue; // ignore symbolic links
377 
378       if (find_data.recursive && file_test(fullname, FILE_TEST_IS_DIR))
379       {
380         // Put the directory name on the stack instead of creating a new node
381         // immediately.  The corresponding node will be created on demand if
382         // there's actually a matching file in the directory or one of its
383         // subdirectories.
384         ScopedPushDir pushdir (find_data.dirstack, fullname);
385         find_recursively(fullname, find_data); // recurse
386       }
387       else if (file_test(fullname, FILE_TEST_IS_REGULAR))
388       {
389         const ustring basename = Glib::filename_display_basename(fullname);
390 
391         if (find_data.pattern->match(basename))
392         {
393           find_add_file(basename, fullname, find_data);
394           ++file_count;
395         }
396       }
397     }
398 
399     find_increment_file_count(find_data, file_count);
400   }
401   catch (const FileError& error)
402   {
403     // Collect errors but don't interrupt the search.
404     find_data.error_list->push_back(error.what());
405   }
406 }
407 
find_add_file(const Glib::ustring & basename,const std::string & fullname,FindData & find_data)408 void FileTree::find_add_file(const Glib::ustring& basename, const std::string& fullname,
409                              FindData& find_data)
410 {
411   // Build the collate key with a leading '1' so that directories always
412   // come first (they have a leading '0').  This is simpler and faster
413   // than explicitely checking for directories in the sort function.
414   std::string collate_key (1, '1');
415   collate_key += basename.collate_key();
416 
417   const FileInfoBasePtr fileinfo (new FileInfo(fullname));
418 
419   Gtk::TreeModel::Row row;
420 
421   if (find_data.dirstack.empty())
422   {
423     row = *treestore_->prepend(); // new toplevel node
424   }
425   else
426   {
427     if (!find_data.dirstack.back().second)
428       find_fill_dirstack(find_data); // build all directory nodes in the stack
429 
430     row = *treestore_->prepend(find_data.dirstack.back().second->children());
431   }
432 
433   const FileTreeColumns& columns = FileTreeColumns::instance();
434 
435   row[columns.filename]   = basename;
436   row[columns.collatekey] = collate_key;
437   row[columns.fileinfo]   = fileinfo;
438 }
439 
find_fill_dirstack(FindData & find_data)440 void FileTree::find_fill_dirstack(FindData& find_data)
441 {
442   const FileTreeColumns& columns = FileTreeColumns::instance();
443 
444   const DirStack::iterator pend  = find_data.dirstack.end();
445   DirStack::iterator       pprev = pend;
446 
447   for (DirStack::iterator pdir = find_data.dirstack.begin(); pdir != pend; pprev = pdir++)
448   {
449     if (pdir->second) // node already created
450       continue;
451 
452     const Glib::ustring dirname = Glib::filename_display_basename(pdir->first);
453 
454     // Build the collate key with a leading '0' so that directories always
455     // come first.  This is simpler and faster than explicitely checking for
456     // directories in the sort function.
457     std::string collate_key (1, '0');
458     collate_key += dirname.collate_key();
459 
460     const FileInfoBasePtr dirinfo (new DirInfo());
461 
462     if (pprev == pend)
463       pdir->second = treestore_->prepend(); // new toplevel node
464     else
465       pdir->second = treestore_->prepend(pprev->second->children());
466 
467     Gtk::TreeModel::Row row = *pdir->second;
468 
469     row[columns.filename]   = dirname;
470     row[columns.collatekey] = collate_key;
471     row[columns.fileinfo]   = dirinfo;
472   }
473 }
474 
find_increment_file_count(FindData & find_data,int file_count)475 void FileTree::find_increment_file_count(FindData& find_data, int file_count)
476 {
477   if (file_count <= 0)
478     return;
479 
480   const FileTreeColumns& columns = FileTreeColumns::instance();
481 
482   const DirStack::iterator pbegin = find_data.dirstack.begin();
483   DirStack::iterator       pdir   = find_data.dirstack.end();
484 
485   while (pdir != pbegin)
486   {
487     const FileInfoBasePtr base = (*(--pdir)->second)[columns.fileinfo];
488     shared_polymorphic_cast<DirInfo>(base)->file_count += file_count;
489   }
490 
491   toplevel_.file_count += file_count;
492   signal_file_count_changed(); // emit
493 }
494 
save_file_at_iter(const Gtk::TreeModel::iterator & iter,const Util::SharedPtr<MessageList> & error_list)495 bool FileTree::save_file_at_iter(const Gtk::TreeModel::iterator& iter,
496                                  const Util::SharedPtr<MessageList>& error_list)
497 {
498   const FileInfoPtr fileinfo = get_fileinfo_from_iter(iter);
499 
500   if (fileinfo && fileinfo->buffer && fileinfo->buffer->get_modified())
501   {
502     try
503     {
504       save_file(fileinfo);
505     }
506     catch (const Glib::Error& error)
507     {
508       error_list->push_back(Util::compose(_("Failed to save file \342\200\234%1\342\200\235: %2"),
509                                           Glib::filename_display_basename(fileinfo->fullname),
510                                           error.what()));
511     }
512 
513     if (!fileinfo->buffer->get_modified())
514       propagate_modified_change(iter, false);
515 
516     if (fileinfo != last_selected_ && fileinfo->buffer->is_freeable())
517       Glib::RefPtr<FileBuffer>().swap(fileinfo->buffer); // reduce memory footprint
518   }
519 
520   return false;
521 }
522 
find_matches_at_path_iter(const Gtk::TreeModel::Path & path,const Gtk::TreeModel::iterator & iter,FindMatchesData & find_data)523 bool FileTree::find_matches_at_path_iter(const Gtk::TreeModel::Path& path,
524                                          const Gtk::TreeModel::iterator& iter,
525                                          FindMatchesData& find_data)
526 {
527   if (signal_pulse()) // emit
528     return true;
529 
530   if (const FileInfoPtr fileinfo = get_fileinfo_from_iter(iter))
531   {
532     if (!fileinfo->buffer)
533       load_file_with_fallback(iter, fileinfo);
534 
535     if (fileinfo->load_failed)
536       return false; // continue
537 
538     const Glib::RefPtr<FileBuffer> buffer = fileinfo->buffer;
539     g_assert(buffer);
540 
541     Util::ScopedConnection conn (buffer->signal_pulse.connect(signal_pulse.make_slot()));
542 
543     const int old_match_count = buffer->get_match_count();
544 
545     // Optimize the common case and construct the feedback slot only if there
546     // are actually any handlers connected to the signal.  find_matches() can
547     // then check whether the slot is empty to avoid providing arguments that
548     // are never going to be used.
549     const int new_match_count =
550         buffer->find_matches(find_data.pattern, find_data.multiple, (signal_feedback.empty())
551                              ? sigc::slot<void, int, const Glib::ustring&>()
552                              : sigc::bind(signal_feedback.make_slot(), fileinfo));
553 
554     if (new_match_count > 0)
555     {
556       if (!find_data.path_match_first_set)
557       {
558         find_data.path_match_first_set = true;
559         path_match_first_ = path;
560       }
561 
562       path_match_last_ = path;
563     }
564 
565     if (new_match_count != old_match_count)
566       propagate_match_count_change(iter, new_match_count - old_match_count);
567 
568     if (fileinfo != last_selected_ && buffer->is_freeable())
569       Glib::RefPtr<FileBuffer>().swap(fileinfo->buffer); // reduce memory footprint
570   }
571 
572   return false;
573 }
574 
replace_matches_at_path_iter(const Gtk::TreeModel::Path & path,const Gtk::TreeModel::iterator & iter,ReplaceMatchesData & replace_data)575 bool FileTree::replace_matches_at_path_iter(const Gtk::TreeModel::Path& path,
576                                             const Gtk::TreeModel::iterator& iter,
577                                             ReplaceMatchesData& replace_data)
578 {
579   if (signal_pulse()) // emit
580     return true;
581 
582   const FileInfoPtr fileinfo = get_fileinfo_from_iter(iter);
583 
584   if (fileinfo && fileinfo->buffer)
585   {
586     const Glib::RefPtr<FileBuffer> buffer = fileinfo->buffer;
587 
588     const int match_count = buffer->get_match_count();
589 
590     if (match_count > 0)
591     {
592       path_match_first_ = path;
593 
594       if (fileinfo != last_selected_)
595         replace_data.row_reference.reset(new TreeRowRef(treestore_, path));
596       else
597         replace_data.row_reference = last_selected_rowref_;
598 
599       const bool was_modified = buffer->get_modified();
600 
601       {
602         // Redirect the buffer's signal_undo_stack_push() in order to create
603         // a single user action object for all replacements in all buffers.
604         // Note that the caller must block conn_undo_stack_push_ to avoid
605         // double notification.
606         Util::ScopedConnection conn1 (buffer->signal_undo_stack_push
607                                         .connect(replace_data.slot_undo_stack_push));
608         Util::ScopedConnection conn2 (buffer->signal_pulse.connect(signal_pulse.make_slot()));
609 
610         buffer->replace_all_matches(replace_data.substitution);
611       }
612 
613       const bool is_modified = buffer->get_modified();
614 
615       if (was_modified != is_modified)
616         propagate_modified_change(iter, is_modified);
617 
618       propagate_match_count_change(iter, buffer->get_match_count() - match_count);
619     }
620   }
621 
622   return false;
623 }
624 
expand_and_select(const Gtk::TreeModel::Path & path)625 void FileTree::expand_and_select(const Gtk::TreeModel::Path& path)
626 {
627   expand_to_path(path);
628   set_cursor(path);
629 }
630 
on_treestore_rows_reordered(const Gtk::TreeModel::Path & path,const Gtk::TreeModel::iterator & iter,int *)631 void FileTree::on_treestore_rows_reordered(const Gtk::TreeModel::Path& path,
632                                            const Gtk::TreeModel::iterator& iter, int*)
633 {
634   if (sum_matches_ > 0)
635   {
636     const FileTreeColumns& columns = FileTreeColumns::instance();
637     bool bounds_changed = false;
638 
639     if (path.is_ancestor(path_match_first_))
640     {
641       Gtk::TreeModel::iterator first = iter->children().begin();
642 
643       while (first->children() && (*first)[columns.matchcount] > 0)
644         first = first->children().begin();
645 
646       if ((*first)[columns.matchcount] == 0)
647       {
648         const bool found_first = next_match_file(first);
649         g_return_if_fail(found_first);
650       }
651 
652       path_match_first_ = first;
653       bounds_changed = true;
654     }
655 
656     if (path.is_ancestor(path_match_last_))
657     {
658       Gtk::TreeModel::iterator last = iter->children()[iter->children().size() - 1];
659 
660       while (last->children() && (*last)[columns.matchcount] > 0)
661         last = last->children()[last->children().size() - 1];
662 
663       if ((*last)[columns.matchcount] == 0)
664       {
665         const bool found_last = prev_match_file(last);
666         g_return_if_fail(found_last);
667       }
668 
669       path_match_last_ = last;
670       bounds_changed = true;
671     }
672 
673     if (bounds_changed)
674       signal_bound_state_changed(); // emit
675   }
676 }
677 
on_selection_changed()678 void FileTree::on_selection_changed()
679 {
680   last_selected_rowref_.reset();
681 
682   conn_match_count_     .disconnect();
683   conn_modified_changed_.disconnect();
684   conn_undo_stack_push_ .disconnect();
685 
686   FileInfoPtr fileinfo;
687   int file_index = 0;
688 
689   if (const Gtk::TreeModel::iterator iter = get_selection()->get_selected())
690   {
691     const FileInfoBasePtr base = (*iter)[FileTreeColumns::instance().fileinfo];
692 
693     fileinfo   = shared_polymorphic_cast<FileInfo>(base);
694     file_index = calculate_file_index(iter) + 1;
695 
696     if (!fileinfo->buffer)
697       load_file_with_fallback(iter, fileinfo);
698 
699     if (!fileinfo->load_failed)
700     {
701       conn_match_count_ = fileinfo->buffer->signal_match_count_changed.
702           connect(sigc::mem_fun(*this, &FileTree::on_buffer_match_count_changed));
703 
704       conn_modified_changed_ = fileinfo->buffer->signal_modified_changed().
705           connect(sigc::mem_fun(*this, &FileTree::on_buffer_modified_changed));
706 
707       conn_undo_stack_push_ = fileinfo->buffer->signal_undo_stack_push.
708           connect(sigc::mem_fun(*this, &FileTree::on_buffer_undo_stack_push));
709     }
710 
711     last_selected_rowref_.reset(new TreeRowRef(treestore_, Gtk::TreeModel::Path(iter)));
712   }
713 
714   if (last_selected_ && last_selected_ != fileinfo &&
715       last_selected_->buffer && last_selected_->buffer->is_freeable())
716   {
717     Glib::RefPtr<FileBuffer>().swap(last_selected_->buffer); // reduce memory footprint
718   }
719 
720   last_selected_ = fileinfo;
721 
722   signal_switch_buffer(fileinfo, file_index); // emit
723   signal_bound_state_changed(); // emit
724 }
725 
on_buffer_match_count_changed()726 void FileTree::on_buffer_match_count_changed()
727 {
728   const FileTreeColumns& columns = FileTreeColumns::instance();
729 
730   // There has to be a selection since we receive signal_match_count_changed()
731   // from the currently selected FileBuffer.
732   Gtk::TreeModel::iterator iter = get_selection()->get_selected();
733   g_return_if_fail(iter);
734 
735   const FileInfoBasePtr base = (*iter)[columns.fileinfo];
736   const FileInfoPtr fileinfo = shared_polymorphic_cast<FileInfo>(base);
737 
738   g_return_if_fail(fileinfo->buffer);
739 
740   const int match_count     = fileinfo->buffer->get_match_count();
741   const int old_match_count = (*iter)[columns.matchcount];
742 
743   if (match_count == old_match_count)
744     return; // spurious emission -- do nothing
745 
746   const long old_sum_matches = sum_matches_;
747   propagate_match_count_change(iter, match_count - old_match_count);
748 
749   if (old_sum_matches == 0)
750   {
751     path_match_first_ = iter;
752     path_match_last_  = path_match_first_;
753   }
754   else if ((sum_matches_ > 0) && (old_match_count == 0 || match_count == 0))
755   {
756     // OK, this nightmarish-looking code below is all about adjusting the
757     // [path_match_first_,path_match_last_] range.  Adjustment is necessary
758     // if either a) match_count was previously 0 and iter is not already
759     // in the range, or b) match_count dropped to 0 and iter is a boundary
760     // of the range.
761     //
762     // Preconditions we definitely know at this point:
763     // 1) old_sum_matches > 0
764     // 2) sum_matches != old_sum_matches && sum_matches > 0
765     // 3) old_match_count == 0 || match_count == 0
766     // 4) old_match_count != match_count
767 
768     g_assert(old_sum_matches > 0);
769     g_assert(old_match_count != match_count);
770 
771     // The range should've been set up already since old_sum_matches > 0.
772     g_return_if_fail(path_match_first_ <= path_match_last_);
773 
774     Gtk::TreeModel::Path path (iter);
775 
776     if (old_match_count == 0)
777     {
778       g_assert(match_count > 0);
779       g_return_if_fail(path != path_match_first_ && path != path_match_last_);
780 
781       // Expand the range if necessary.
782       if (path < path_match_first_)
783         path_match_first_ = path;
784       else if (path > path_match_last_)
785         path_match_last_ = path;
786     }
787     else
788     {
789       g_assert(match_count == 0);
790       g_return_if_fail(path >= path_match_first_ && path <= path_match_last_);
791 
792       if (path == path_match_first_)
793       {
794         // Find the new start boundary of the range.
795         const bool found_next = next_match_file(iter);
796         g_return_if_fail(found_next);
797 
798         path_match_first_ = iter;
799       }
800       else if (path == path_match_last_)
801       {
802         // Find the new end boundary of the range.
803         const bool found_prev = prev_match_file(iter);
804         g_return_if_fail(found_prev);
805 
806         path_match_last_ = iter;
807       }
808     }
809 
810     signal_bound_state_changed(); // emit
811   }
812 }
813 
on_buffer_modified_changed()814 void FileTree::on_buffer_modified_changed()
815 {
816   const Gtk::TreeModel::iterator selected = get_selection()->get_selected();
817   g_return_if_fail(selected);
818 
819   const FileInfoBasePtr base = (*selected)[FileTreeColumns::instance().fileinfo];
820   const FileInfoPtr fileinfo = shared_polymorphic_cast<FileInfo>(base);
821 
822   g_return_if_fail(fileinfo == last_selected_);
823   g_return_if_fail(fileinfo->buffer);
824 
825   propagate_modified_change(selected, fileinfo->buffer->get_modified());
826 }
827 
on_buffer_undo_stack_push(UndoActionPtr undo_action)828 void FileTree::on_buffer_undo_stack_push(UndoActionPtr undo_action)
829 {
830   g_return_if_fail(last_selected_rowref_);
831 
832   const UndoActionPtr action_shell (new BufferActionShell(*this, last_selected_rowref_, undo_action));
833 
834   signal_undo_stack_push(action_shell); // emit
835 }
836 
calculate_file_index(const Gtk::TreeModel::iterator & pos)837 int FileTree::calculate_file_index(const Gtk::TreeModel::iterator& pos)
838 {
839   int index = 0;
840 
841   Gtk::TreeModel::iterator iter = pos->parent();
842 
843   if (iter) // calculate the parent's index first if there is one
844     index = calculate_file_index(iter); // recurse
845 
846   const FileTreeColumns& columns = FileTreeColumns::instance();
847 
848   for (iter = iter->children().begin(); iter != pos; ++iter)
849   {
850     const FileInfoBasePtr base = (*iter)[columns.fileinfo];
851     g_return_val_if_fail(base, index);
852 
853     if (const DirInfoPtr dirinfo = shared_dynamic_cast<DirInfo>(base))
854       index += dirinfo->file_count; // count whole directory in one step
855     else
856       ++index; // single file
857   }
858 
859   return index;
860 }
861 
propagate_match_count_change(const Gtk::TreeModel::iterator & pos,int difference)862 void FileTree::propagate_match_count_change(const Gtk::TreeModel::iterator& pos, int difference)
863 {
864   const FileTreeColumns& columns = FileTreeColumns::instance();
865 
866   for (Gtk::TreeModel::iterator iter = pos; iter; iter = iter->parent())
867   {
868     const int match_count = (*iter)[columns.matchcount];
869     (*iter)[columns.matchcount] = match_count + difference;
870   }
871 
872   sum_matches_ += difference;
873 
874   signal_match_count_changed(); // emit
875 }
876 
propagate_modified_change(const Gtk::TreeModel::iterator & pos,bool modified)877 void FileTree::propagate_modified_change(const Gtk::TreeModel::iterator& pos, bool modified)
878 {
879   const int difference = (modified) ? 1 : -1;
880   const FileTreeColumns& columns = FileTreeColumns::instance();
881 
882   Gtk::TreeModel::Path path (pos);
883   treestore_->row_changed(path, pos);
884 
885   for (Gtk::TreeModel::iterator iter = pos->parent(); iter && path.up(); iter = iter->parent())
886   {
887     const FileInfoBasePtr base = (*iter)[columns.fileinfo];
888     const DirInfoPtr dirinfo = shared_polymorphic_cast<DirInfo>(base);
889 
890     dirinfo->modified_count += difference;
891 
892     // Update the view only if the count flipped from 0 to 1 or vice versa.
893     if (dirinfo->modified_count == int(modified))
894       treestore_->row_changed(path, iter);
895   }
896 
897   toplevel_.modified_count += difference;
898 
899   signal_modified_count_changed(); // emit
900 }
901 
load_file_with_fallback(const Gtk::TreeModel::iterator & iter,const FileInfoPtr & fileinfo)902 void FileTree::load_file_with_fallback(const Gtk::TreeModel::iterator& iter,
903                                        const FileInfoPtr& fileinfo)
904 {
905   g_return_if_fail(!fileinfo->buffer);
906 
907   const bool old_load_failed = fileinfo->load_failed;
908 
909   try
910   {
911     load_file(fileinfo, fallback_encoding_);
912   }
913   catch (const Glib::Error& error)
914   {
915     fileinfo->buffer = FileBuffer::create_with_error_message(
916         render_icon_pixbuf(Gtk::Stock::DIALOG_ERROR, Gtk::ICON_SIZE_DIALOG), error.what());
917   }
918   catch (const ErrorBinaryFile&)
919   {
920     const Glib::ustring filename = (*iter)[FileTreeColumns::instance().filename];
921 
922     fileinfo->buffer = FileBuffer::create_with_error_message(
923         render_icon_pixbuf(Gtk::Stock::DIALOG_ERROR, Gtk::ICON_SIZE_DIALOG),
924         Util::compose(_("\342\200\234%1\342\200\235 seems to be a binary file."), filename));
925   }
926 
927   if (old_load_failed != fileinfo->load_failed)
928   {
929     // Trigger signal_row_changed() because the value of fileinfo->load_failed
930     // changed, which means we have to change icon and color of the row.
931     treestore_->row_changed(Gtk::TreeModel::Path(iter), iter);
932   }
933 }
934 
on_conf_value_changed(const Glib::ustring & key)935 void FileTree::on_conf_value_changed(const Glib::ustring& key)
936 {
937   if (key == conf_key_fallback_encoding)
938     fallback_encoding_ = Settings::instance()->get_string(key);
939 }
940 
941 } // namespace Regexxer
942