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