1 #include "selection_dialog.hpp"
2 #include <algorithm>
3 
ListViewText(bool use_markup)4 SelectionDialogBase::ListViewText::ListViewText(bool use_markup) : Gtk::TreeView(), use_markup(use_markup) {
5   list_store = Gtk::ListStore::create(column_record);
6   set_model(list_store);
7   append_column("", cell_renderer);
8   if(use_markup)
9     get_column(0)->add_attribute(cell_renderer.property_markup(), column_record.text);
10   else
11     get_column(0)->add_attribute(cell_renderer.property_text(), column_record.text);
12 
13   get_selection()->set_mode(Gtk::SelectionMode::SELECTION_BROWSE);
14   set_enable_search(true);
15   set_headers_visible(false);
16   set_hscroll_policy(Gtk::ScrollablePolicy::SCROLL_NATURAL);
17   set_activate_on_single_click(true);
18   set_hover_selection(false);
19 }
20 
append(const std::string & value)21 void SelectionDialogBase::ListViewText::append(const std::string &value) {
22   auto new_row = list_store->append();
23   new_row->set_value(column_record.text, value);
24   new_row->set_value(column_record.index, size++);
25 }
26 
erase_rows()27 void SelectionDialogBase::ListViewText::erase_rows() {
28   list_store->clear();
29   size = 0;
30 }
31 
clear()32 void SelectionDialogBase::ListViewText::clear() {
33   unset_model();
34   list_store.reset();
35   size = 0;
36 }
37 
SelectionDialogBase(Source::BaseView * view_,const boost::optional<Gtk::TextIter> & start_iter,bool show_search_entry_,bool use_markup)38 SelectionDialogBase::SelectionDialogBase(Source::BaseView *view_, const boost::optional<Gtk::TextIter> &start_iter, bool show_search_entry_, bool use_markup)
39     : start_mark(start_iter ? Source::Mark(*start_iter) : Source::Mark()), view(view_), window(Gtk::WindowType::WINDOW_POPUP), vbox(Gtk::Orientation::ORIENTATION_VERTICAL), list_view_text(use_markup), show_search_entry(show_search_entry_) {
40   auto g_application = g_application_get_default();
41   auto gio_application = Glib::wrap(g_application, true);
42   auto application = Glib::RefPtr<Gtk::Application>::cast_static(gio_application);
43   window.set_transient_for(*application->get_active_window());
44 
45   window.set_type_hint(Gdk::WindowTypeHint::WINDOW_TYPE_HINT_COMBO);
46 
47   window.get_style_context()->add_class("juci_selection_dialog");
48 
49   search_entry.signal_changed().connect(
50       [this] {
51         if(on_search_entry_changed)
52           on_search_entry_changed(search_entry.get_text());
53       },
54       false);
55 
56   list_view_text.set_search_entry(search_entry);
57 
58   window.set_default_size(0, 0);
59   window.property_decorated() = false;
60   window.set_skip_taskbar_hint(true);
61 
62   scrolled_window.set_policy(Gtk::PolicyType::POLICY_AUTOMATIC, Gtk::PolicyType::POLICY_AUTOMATIC);
63 
64   scrolled_window.add(list_view_text);
65   if(show_search_entry)
66     vbox.pack_start(search_entry, false, false);
67   vbox.pack_start(scrolled_window, true, true);
68   window.add(vbox);
69 
70   list_view_text.signal_realize().connect([this]() {
71     auto g_application = g_application_get_default();
72     auto gio_application = Glib::wrap(g_application, true);
73     auto application = Glib::RefPtr<Gtk::Application>::cast_static(gio_application);
74     auto application_window = application->get_active_window();
75 
76     // Calculate window width and height
77     int row_width = 0, padding_height = 0, window_height = 0;
78     Gdk::Rectangle rect;
79     auto children = list_view_text.get_model()->children();
80     size_t c = 0;
81     for(auto it = children.begin(); it != children.end() && c < 10; ++it) {
82       list_view_text.get_cell_area(list_view_text.get_model()->get_path(it), *(list_view_text.get_column(0)), rect);
83       if(c == 0) {
84         row_width = rect.get_width() + rect.get_x() * 2;
85         padding_height = rect.get_y() * 2;
86       }
87       window_height += rect.get_height() + padding_height;
88       ++c;
89     }
90 
91     if(view && row_width > view->get_width() * 2 / 3)
92       row_width = view->get_width() * 2 / 3;
93     else if(row_width > application_window->get_width() / 2)
94       row_width = application_window->get_width() / 2;
95 
96     if(show_search_entry)
97       window_height += search_entry.get_height();
98     int window_width = row_width + 1;
99     window.resize(window_width, window_height);
100 
101     auto move_window_to_center = [this, application_window, window_width, window_height] {
102       int root_x, root_y;
103       application_window->get_position(root_x, root_y);
104       root_x += application_window->get_width() / 2 - window_width / 2;
105       root_y += application_window->get_height() / 2 - window_height / 2;
106       window.move(root_x, root_y);
107     };
108 
109     if(view) {
110       Gdk::Rectangle visible_rect;
111       view->get_visible_rect(visible_rect);
112       int visible_window_x, visible_window_max_y;
113       view->buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, visible_rect.get_x(), visible_rect.get_y() + visible_rect.get_height(), visible_window_x, visible_window_max_y);
114 
115       Gdk::Rectangle iter_rect;
116       view->get_iter_location(start_mark->get_iter(), iter_rect);
117       int buffer_x = iter_rect.get_x();
118       int buffer_y = iter_rect.get_y() + iter_rect.get_height();
119       int window_x, window_y;
120       view->buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, buffer_x, buffer_y, window_x, window_y);
121 
122       if(window_y < 0 || window_y > visible_window_max_y) // Move dialog to center if it is above or below visible parts of view
123         move_window_to_center();
124       else {
125         window_x = std::max(window_x, visible_window_x); // Adjust right if dialog is left of view
126 
127         int root_x, root_y;
128         view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->get_root_coords(window_x, window_y, root_x, root_y);
129 
130         // Adjust left if dialog is right of screen
131         auto screen_width = Gdk::Screen::get_default()->get_width();
132         root_x = root_x + window_width > screen_width ? screen_width - window_width : root_x;
133 
134         window.move(root_x, root_y + 1); //TODO: replace 1 with some margin
135       }
136     }
137     else
138       move_window_to_center();
139   });
140 
141   list_view_text.signal_cursor_changed().connect([this] {
142     cursor_changed();
143   });
144 }
145 
cursor_changed()146 void SelectionDialogBase::cursor_changed() {
147   if(!is_visible())
148     return;
149   auto it = list_view_text.get_selection()->get_selected();
150   boost::optional<unsigned int> index;
151   if(it)
152     index = it->get_value(list_view_text.column_record.index);
153   if(last_index == index)
154     return;
155   if(on_change) {
156     std::string text;
157     if(it)
158       text = it->get_value(list_view_text.column_record.text);
159     on_change(index, text);
160   }
161   last_index = index;
162 }
add_row(const std::string & row)163 void SelectionDialogBase::add_row(const std::string &row) {
164   list_view_text.append(row);
165 }
166 
erase_rows()167 void SelectionDialogBase::erase_rows() {
168   list_view_text.erase_rows();
169 }
170 
show()171 void SelectionDialogBase::show() {
172   window.show_all();
173   if(view)
174     view->grab_focus();
175 
176   if(list_view_text.get_model()->children().size() > 0) {
177     if(!list_view_text.get_selection()->get_selected()) {
178       list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
179       cursor_changed();
180     }
181     else if(list_view_text.get_model()->children().begin() != list_view_text.get_selection()->get_selected()) {
182       Glib::signal_idle().connect([this] {
183         if((this == SelectionDialog::get().get() || this == CompletionDialog::get().get()) && is_visible())
184           list_view_text.scroll_to_row(list_view_text.get_model()->get_path(list_view_text.get_selection()->get_selected()), 0.5);
185         return false;
186       });
187     }
188   }
189   if(on_show)
190     on_show();
191 }
192 
set_cursor_at_last_row()193 void SelectionDialogBase::set_cursor_at_last_row() {
194   auto children = list_view_text.get_model()->children();
195   if(children.size() > 0) {
196     list_view_text.set_cursor(list_view_text.get_model()->get_path(children[children.size() - 1]));
197     cursor_changed();
198   }
199 }
200 
hide()201 void SelectionDialogBase::hide() {
202   if(!is_visible())
203     return;
204   window.hide();
205   if(on_hide)
206     on_hide();
207   list_view_text.clear();
208   last_index.reset();
209 }
210 
211 std::unique_ptr<SelectionDialog> SelectionDialog::instance;
212 
SelectionDialog(Source::BaseView * view,const boost::optional<Gtk::TextIter> & start_iter,bool show_search_entry,bool use_markup)213 SelectionDialog::SelectionDialog(Source::BaseView *view, const boost::optional<Gtk::TextIter> &start_iter, bool show_search_entry, bool use_markup)
214     : SelectionDialogBase(view, start_iter, show_search_entry, use_markup) {
215   auto search_text = std::make_shared<std::string>();
216   auto filter_model = Gtk::TreeModelFilter::create(list_view_text.get_model());
217 
218   filter_model->set_visible_func([this, search_text](const Gtk::TreeModel::const_iterator &iter) {
219     std::string row_lc;
220     iter->get_value(0, row_lc);
221     auto search_text_lc = *search_text;
222     std::transform(row_lc.begin(), row_lc.end(), row_lc.begin(), ::tolower);
223     std::transform(search_text_lc.begin(), search_text_lc.end(), search_text_lc.begin(), ::tolower);
224     if(list_view_text.use_markup) {
225       size_t pos = 0;
226       while((pos = row_lc.find('<', pos)) != std::string::npos) {
227         auto pos2 = row_lc.find('>', pos + 1);
228         row_lc.erase(pos, pos2 - pos + 1);
229       }
230       search_text_lc = Glib::Markup::escape_text(search_text_lc);
231     }
232     if(row_lc.find(search_text_lc) != std::string::npos)
233       return true;
234     return false;
235   });
236 
237   list_view_text.set_model(filter_model);
238 
239   list_view_text.set_search_equal_func([](const Glib::RefPtr<Gtk::TreeModel> &model, int column, const Glib::ustring &key, const Gtk::TreeModel::iterator &iter) {
240     return false;
241   });
242 
243   search_entry.signal_changed().connect([this, search_text, filter_model]() {
244     *search_text = search_entry.get_text();
245     filter_model->refilter();
246     list_view_text.set_search_entry(search_entry); //TODO:Report the need of this to GTK's git (bug)
247     if(search_text->empty()) {
248       if(list_view_text.get_model()->children().size() > 0)
249         list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
250     }
251   });
252 
253   auto activate = [this]() {
254     auto it = list_view_text.get_selection()->get_selected();
255     if(on_select && it)
256       on_select(it->get_value(list_view_text.column_record.index), it->get_value(list_view_text.column_record.text), true);
257     hide();
258   };
259   search_entry.signal_activate().connect([activate]() {
260     activate();
261   });
262   list_view_text.signal_row_activated().connect([activate](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *) {
263     activate();
264   });
265 }
266 
on_key_press(GdkEventKey * event)267 bool SelectionDialog::on_key_press(GdkEventKey *event) {
268   if((event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down) && list_view_text.get_model()->children().size() > 0) {
269     auto it = list_view_text.get_selection()->get_selected();
270     if(it) {
271       it++;
272       if(it)
273         list_view_text.set_cursor(list_view_text.get_model()->get_path(it));
274       else
275         list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
276     }
277     return true;
278   }
279   else if((event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up) && list_view_text.get_model()->children().size() > 0) {
280     auto it = list_view_text.get_selection()->get_selected();
281     if(it) {
282       it--;
283       if(it)
284         list_view_text.set_cursor(list_view_text.get_model()->get_path(it));
285       else {
286         auto last_it = list_view_text.get_model()->children().end();
287         last_it--;
288         if(last_it)
289           list_view_text.set_cursor(list_view_text.get_model()->get_path(last_it));
290       }
291     }
292     return true;
293   }
294   else if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter || event->keyval == GDK_KEY_ISO_Left_Tab || event->keyval == GDK_KEY_Tab) {
295     auto it = list_view_text.get_selection()->get_selected();
296     if(it) {
297       auto column = list_view_text.get_column(0);
298       list_view_text.row_activated(list_view_text.get_model()->get_path(it), *column);
299     }
300     else
301       hide();
302     return true;
303   }
304   else if(event->keyval == GDK_KEY_Escape) {
305     hide();
306     return true;
307   }
308   else if(event->keyval == GDK_KEY_Left || event->keyval == GDK_KEY_KP_Left || event->keyval == GDK_KEY_Right || event->keyval == GDK_KEY_KP_Right) {
309     hide();
310     return false;
311   }
312   else if(show_search_entry) {
313 #ifdef __APPLE__ //OS X bug most likely: Gtk::Entry will not work if window is of type POPUP
314     if(event->is_modifier)
315       return true;
316     else if(event->keyval == GDK_KEY_BackSpace) {
317       int start_pos, end_pos;
318       if(search_entry.get_selection_bounds(start_pos, end_pos)) {
319         search_entry.delete_selection();
320         return true;
321       }
322       auto length = search_entry.get_text_length();
323       if(length > 0)
324         search_entry.delete_text(length - 1, length);
325       return true;
326     }
327     else if(event->keyval == GDK_KEY_v && event->state & GDK_META_MASK) {
328       search_entry.paste_clipboard();
329       return true;
330     }
331     else if(event->keyval == GDK_KEY_c && event->state & GDK_META_MASK) {
332       search_entry.copy_clipboard();
333       return true;
334     }
335     else if(event->keyval == GDK_KEY_x && event->state & GDK_META_MASK) {
336       search_entry.cut_clipboard();
337       return true;
338     }
339     else if(event->keyval == GDK_KEY_a && event->state & GDK_META_MASK) {
340       search_entry.select_region(0, -1);
341       return true;
342     }
343     else {
344       search_entry.on_key_press_event(event);
345       return true;
346     }
347 #else
348     search_entry.on_key_press_event(event);
349     return true;
350 #endif
351   }
352   hide();
353   return false;
354 }
355 
356 std::unique_ptr<CompletionDialog> CompletionDialog::instance;
357 
CompletionDialog(Source::BaseView * view,const Gtk::TextIter & start_iter)358 CompletionDialog::CompletionDialog(Source::BaseView *view, const Gtk::TextIter &start_iter) : SelectionDialogBase(view, start_iter, false, false) {
359   show_offset = view->get_buffer()->get_insert()->get_iter().get_offset();
360 
361   auto search_text = std::make_shared<std::string>();
362   auto filter_model = Gtk::TreeModelFilter::create(list_view_text.get_model());
363   if(show_offset == start_mark->get_iter().get_offset()) {
364     filter_model->set_visible_func([search_text](const Gtk::TreeModel::const_iterator &iter) {
365       std::string row_lc;
366       iter->get_value(0, row_lc);
367       auto search_text_lc = *search_text;
368       std::transform(row_lc.begin(), row_lc.end(), row_lc.begin(), ::tolower);
369       std::transform(search_text_lc.begin(), search_text_lc.end(), search_text_lc.begin(), ::tolower);
370       if(row_lc.find(search_text_lc) != std::string::npos)
371         return true;
372       return false;
373     });
374   }
375   else {
376     filter_model->set_visible_func([search_text](const Gtk::TreeModel::const_iterator &iter) {
377       std::string row;
378       iter->get_value(0, row);
379       if(row.find(*search_text) == 0)
380         return true;
381       return false;
382     });
383   }
384   list_view_text.set_model(filter_model);
385   search_entry.signal_changed().connect([this, search_text, filter_model]() {
386     *search_text = search_entry.get_text();
387     filter_model->refilter();
388     list_view_text.set_search_entry(search_entry); //TODO:Report the need of this to GTK's git (bug)
389   });
390 
391   list_view_text.signal_row_activated().connect([this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *) {
392     select();
393   });
394 
395   auto text = view->get_buffer()->get_text(start_mark->get_iter(), view->get_buffer()->get_insert()->get_iter());
396   if(text.size() > 0) {
397     search_entry.set_text(text);
398     list_view_text.set_search_entry(search_entry);
399   }
400 }
401 
select(bool hide_window)402 void CompletionDialog::select(bool hide_window) {
403   row_in_entry = true;
404 
405   auto it = list_view_text.get_selection()->get_selected();
406   if(on_select && it)
407     on_select(it->get_value(list_view_text.column_record.index), it->get_value(list_view_text.column_record.text), hide_window);
408   if(hide_window)
409     hide();
410 }
411 
on_key_release(GdkEventKey * event)412 bool CompletionDialog::on_key_release(GdkEventKey *event) {
413   if(event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down || event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up ||
414      (event->keyval >= GDK_KEY_Shift_L && event->keyval <= GDK_KEY_Hyper_R))
415     return false;
416 
417   if(show_offset > view->get_buffer()->get_insert()->get_iter().get_offset())
418     hide();
419   else {
420     auto text = view->get_buffer()->get_text(start_mark->get_iter(), view->get_buffer()->get_insert()->get_iter());
421     search_entry.set_text(text);
422     list_view_text.set_search_entry(search_entry);
423     if(text == "") {
424       if(list_view_text.get_model()->children().size() > 0)
425         list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
426     }
427     cursor_changed();
428   }
429   return false;
430 }
431 
on_key_press(GdkEventKey * event)432 bool CompletionDialog::on_key_press(GdkEventKey *event) {
433   if(view->is_token_char(gdk_keyval_to_unicode(event->keyval)) || event->keyval == GDK_KEY_BackSpace) {
434     if(row_in_entry) {
435       view->get_buffer()->erase(start_mark->get_iter(), view->get_buffer()->get_insert()->get_iter());
436       row_in_entry = false;
437       if(event->keyval == GDK_KEY_BackSpace)
438         return true;
439     }
440     return false;
441   }
442   if(event->keyval == GDK_KEY_Shift_L || event->keyval == GDK_KEY_Shift_R || event->keyval == GDK_KEY_Alt_L || event->keyval == GDK_KEY_Alt_R ||
443      event->keyval == GDK_KEY_Control_L || event->keyval == GDK_KEY_Control_R || event->keyval == GDK_KEY_Meta_L || event->keyval == GDK_KEY_Meta_R)
444     return false;
445   if((event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down) && list_view_text.get_model()->children().size() > 0) {
446     auto it = list_view_text.get_selection()->get_selected();
447     if(it) {
448       it++;
449       if(it) {
450         list_view_text.set_cursor(list_view_text.get_model()->get_path(it));
451         cursor_changed();
452       }
453       else {
454         list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
455         cursor_changed();
456       }
457     }
458     else
459       list_view_text.set_cursor(list_view_text.get_model()->get_path(list_view_text.get_model()->children().begin()));
460     select(false);
461     return true;
462   }
463   if((event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up) && list_view_text.get_model()->children().size() > 0) {
464     auto it = list_view_text.get_selection()->get_selected();
465     if(it) {
466       it--;
467       if(it) {
468         list_view_text.set_cursor(list_view_text.get_model()->get_path(it));
469         cursor_changed();
470       }
471       else {
472         auto last_it = list_view_text.get_model()->children().end();
473         last_it--;
474         if(last_it) {
475           list_view_text.set_cursor(list_view_text.get_model()->get_path(last_it));
476           cursor_changed();
477         }
478       }
479     }
480     else {
481       auto last_it = list_view_text.get_model()->children().end();
482       last_it--;
483       if(last_it)
484         list_view_text.set_cursor(list_view_text.get_model()->get_path(last_it));
485     }
486     select(false);
487     return true;
488   }
489   if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter || event->keyval == GDK_KEY_ISO_Left_Tab || event->keyval == GDK_KEY_Tab) {
490     select();
491     return true;
492   }
493   hide();
494   if(event->keyval == GDK_KEY_Escape)
495     return true;
496   return false;
497 }
498