Commit dca845d8 authored by Michael Gratton's avatar Michael Gratton 🤞

Reimplement in-conversation find.

* src/client/application/geary-controller.vala (GearyController): Remove
  ACTION_FIND_NEXT_IN_CONVERSATION and
  ACTION_FIND_PREVIOUS_IN_CONVERSATION arctions and callbacks since they
  will be taken care of by the search entry & search bar buttons, and
  remove from accelerators.ui. Add ACTION_TOGGLE_FIND action to handle
  toggling find bar in the same way as the search bar.

* src/client/components/main-toolbar.vala (MainToolbar): Add new button
  and infrastrcuture for toggling the find bar.

* src/client/conversation-viewer/conversation-viewer.vala
  (ConversationViewer): Convert ::conversation_page to be grid, add new
  ::conversation_scroller property for the scrollbar, update call
  sites. Add props for accessing find widgets, remove old find methods
  and add callbacks for handling find start, change, etc.

* src/client/conversation-viewer/conversation-email.vala,
  src/client/conversation-viewer/conversation-message.vala: Add methods
  for accessing selected text for find.

* src/client/conversation-viewer/conversation-listbox.vala
  (ConversationListBox::highlight_search_terms): Updated to return a flag
  specifiying whether any search results were found, and to
  expand/collapse messsages depending on whether they have any.

* src/client/conversation-viewer/conversation-message.vala
  (ConversationMessage::highlight_search_terms): Keep track of how many
  results were found, and return that.

* ui/conversation-viewer.ui: Convert conversation_page to be a grid, add
  a search bar and search widgets to it, and move conversation
  ScrolledWindow to it.
parent 1874be94
......@@ -35,8 +35,6 @@ public class GearyController : Geary.BaseObject {
public const string ACTION_EMPTY_TRASH = "GearyEmptyTrash";
public const string ACTION_UNDO = "GearyUndo";
public const string ACTION_FIND_IN_CONVERSATION = "GearyFindInConversation";
public const string ACTION_FIND_NEXT_IN_CONVERSATION = "GearyFindNextInConversation";
public const string ACTION_FIND_PREVIOUS_IN_CONVERSATION = "GearyFindPreviousInConversation";
public const string ACTION_ZOOM_IN = "GearyZoomIn";
public const string ACTION_ZOOM_OUT = "GearyZoomOut";
public const string ACTION_ZOOM_NORMAL = "GearyZoomNormal";
......@@ -51,7 +49,8 @@ public class GearyController : Geary.BaseObject {
public const string ACTION_SEARCH = "GearySearch";
public const string ACTION_CONVERSATION_LIST = "GearyConversationList";
public const string ACTION_TOGGLE_SEARCH = "GearyToggleSearch";
public const string ACTION_TOGGLE_FIND = "GearyToggleFind";
public const string PROP_CURRENT_CONVERSATION ="current-conversations";
public const int MIN_CONVERSATION_COUNT = 50;
......@@ -474,15 +473,7 @@ public class GearyController : Geary.BaseObject {
null, on_find_in_conversation_action };
entries += find_in_conversation;
add_accelerator("slash", ACTION_FIND_IN_CONVERSATION);
Gtk.ActionEntry find_next_in_conversation = { ACTION_FIND_NEXT_IN_CONVERSATION, null, null,
"<Ctrl>G", null, on_find_next_in_conversation_action };
entries += find_next_in_conversation;
Gtk.ActionEntry find_previous_in_conversation = { ACTION_FIND_PREVIOUS_IN_CONVERSATION,
null, null, "<Shift><Ctrl>G", null, on_find_previous_in_conversation_action };
entries += find_previous_in_conversation;
Gtk.ActionEntry archive_message = { ACTION_ARCHIVE_MESSAGE, ARCHIVE_MESSAGE_ICON_NAME,
ARCHIVE_MESSAGE_LABEL, "A", null, on_archive_message };
archive_message.tooltip = ARCHIVE_MESSAGE_TOOLTIP_SINGLE;
......@@ -546,6 +537,11 @@ public class GearyController : Geary.BaseObject {
_("Toggle search bar"), null };
entries += toggle_search;
// No callback is connected, since we bind the toggle button to the find bar visibility
Gtk.ActionEntry toggle_find = { ACTION_TOGGLE_FIND, null, null, null,
_("Toggle find bar"), null };
entries += toggle_find;
return entries;
}
......@@ -1298,6 +1294,9 @@ public class GearyController : Geary.BaseObject {
private void on_folder_selected(Geary.Folder? folder) {
debug("Folder %s selected", folder != null ? folder.to_string() : "(null)");
this.main_window.conversation_viewer.show_loading();
GearyApplication.instance.get_action(
ACTION_FIND_IN_CONVERSATION
).set_sensitive(false);
enable_message_buttons(false);
// If the folder is being unset, clear the message list and exit here.
......@@ -1507,6 +1506,9 @@ public class GearyController : Geary.BaseObject {
private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
this.selected_conversations = selected;
GearyApplication.instance.get_action(
ACTION_FIND_IN_CONVERSATION
).set_sensitive(false);
ConversationViewer viewer = this.main_window.conversation_viewer;
if (this.current_folder != null && !viewer.is_composer_visible) {
switch(selected.size) {
......@@ -1527,6 +1529,9 @@ public class GearyController : Geary.BaseObject {
try {
viewer.load_conversation.end(ret);
enable_message_buttons(!is_search);
GearyApplication.instance.get_action(
ACTION_FIND_IN_CONVERSATION
).set_sensitive(true);
} catch (Error err) {
debug("Unable to load conversation: %s",
err.message);
......@@ -2272,11 +2277,14 @@ public class GearyController : Geary.BaseObject {
if (widget.state == ComposerWidget.ComposerState.NEW ||
widget.state == ComposerWidget.ComposerState.PANED) {
main_window.conversation_viewer.do_compose(widget);
GearyApplication.instance.get_action(
ACTION_FIND_IN_CONVERSATION
).set_sensitive(false);
} else {
ComposerEmbed embed = new ComposerEmbed(
referred,
widget,
main_window.conversation_viewer.conversation_page
main_window.conversation_viewer.conversation_scroller
);
if (conversation_view != null) {
conversation_view.add_embedded_composer(embed);
......@@ -2414,19 +2422,11 @@ public class GearyController : Geary.BaseObject {
private void on_forward_message_action() {
create_reply_forward_widget(ComposerWidget.ComposeType.FORWARD, null);
}
private void on_find_in_conversation_action() {
main_window.conversation_viewer.show_find_bar();
}
private void on_find_next_in_conversation_action() {
main_window.conversation_viewer.find(true);
this.main_window.conversation_viewer.conversation_find_bar.set_search_mode(true);
}
private void on_find_previous_in_conversation_action() {
main_window.conversation_viewer.find(false);
}
private void on_archive_message() {
archive_or_delete_selection_async.begin(true, false, cancellable_folder,
on_archive_or_delete_selection_finished);
......
......@@ -14,6 +14,7 @@ public class MainToolbar : Gtk.Box {
public bool show_close_button_left { get; private set; default = true; }
public bool show_close_button_right { get; private set; default = true; }
public bool search_open { get; set; default = false; }
public bool find_open { get; set; default = false; }
public int left_pane_width { get; set; }
private PillHeaderbar folder_header;
......@@ -106,17 +107,27 @@ public class MainToolbar : Gtk.Box {
insert.add(conversation_header.create_popover_button("folder-symbolic", move_folder_menu,
GearyController.ACTION_MOVE_MENU));
conversation_header.add_start(conversation_header.create_pill_buttons(insert));
// Archive, undo, find
insert.clear();
insert.add(archive_button = conversation_header.create_toolbar_button(null, GearyController.ACTION_ARCHIVE_MESSAGE, true));
insert.add(trash_delete_button = conversation_header.create_toolbar_button(null, GearyController.ACTION_TRASH_MESSAGE, false));
Gtk.Box archive_trash_delete = conversation_header.create_pill_buttons(insert);
insert.clear();
insert.add(conversation_header.create_toolbar_button(null, GearyController.ACTION_UNDO,
false));
Gtk.Box undo = conversation_header.create_pill_buttons(insert);
Gtk.Button find_button = folder_header.create_toggle_button(
"preferences-system-search-symbolic", GearyController.ACTION_TOGGLE_FIND);
this.bind_property("find-open", find_button, "active",
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
insert.clear();
insert.add(find_button);
Gtk.Box find = conversation_header.create_pill_buttons(insert);
conversation_header.add_end(find);
conversation_header.add_end(undo);
conversation_header.add_end(archive_trash_delete);
......
......@@ -90,6 +90,8 @@ public class MainWindow : Gtk.ApplicationWindow {
main_toolbar = new MainToolbar();
main_toolbar.bind_property("search-open", search_bar, "search-mode-enabled",
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
main_toolbar.bind_property("find-open", conversation_viewer.conversation_find_bar, "search-mode-enabled",
BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
main_toolbar.show_close_button = true;
set_titlebar(main_toolbar);
......
......@@ -550,6 +550,15 @@ public class ConversationEmail : Gtk.Box {
: null;
}
/**
* Returns user-selected body text from a message, if any.
*/
public string? get_selection_for_find() {
return (this.body_selection_message != null)
? this.body_selection_message.get_selection_for_find()
: null;
}
/**
* Attach an embedded composer to this email view.
*/
......
......@@ -456,8 +456,10 @@ public class ConversationListBox : Gtk.ListBox {
/**
* Applies search term highlighting to all email views.
*
* Returns true if any were found, else returns false.
*/
public void highlight_search_terms(Gee.Set<string>? search_matches) {
public bool highlight_search_terms(Gee.Set<string> search_matches) {
// Webkit's highlighting is ... weird. In order to actually
// see all the highlighting you're applying, it seems
// necessary to start with the shortest string and work up.
......@@ -467,10 +469,24 @@ public class ConversationListBox : Gtk.ListBox {
ordered_matches.add_all(search_matches);
ordered_matches.sort((a, b) => a.length - b.length);
message_view_iterator().foreach((msg_view) => {
msg_view.highlight_search_terms(search_matches);
return true;
bool any_found = false;
this.foreach((child) => {
EmailRow row = (EmailRow) child;
bool email_found = false;
row.view.message_view_iterator().foreach((msg_view) => {
if (msg_view.highlight_search_terms(search_matches) > 0) {
email_found = true;
}
return true;
});
if (email_found) {
row.expand();
any_found = true;
} else {
row.collapse();
}
});
return any_found;
}
/**
......
......@@ -465,8 +465,10 @@ public class ConversationMessage : Gtk.Grid {
/**
* Highlights user search terms in the message view.
&
* Returns the number of matching search terms.
*/
public void highlight_search_terms(Gee.Set<string> search_matches) {
public uint highlight_search_terms(Gee.Set<string> search_matches) {
// XXX Need to highlight subject, sender and recipient matches too
// Remove existing highlights.
......@@ -481,11 +483,13 @@ public class ConversationMessage : Gtk.Grid {
ordered_matches.add_all(search_matches);
ordered_matches.sort((a, b) => a.length - b.length);
uint found = 0;
foreach(string match in ordered_matches) {
web_view.mark_text_matches(match, false, 0);
found += web_view.mark_text_matches(match, false, 0);
}
web_view.set_highlight_text_matches(true);
return found;
}
/**
......@@ -538,6 +542,28 @@ public class ConversationMessage : Gtk.Grid {
return quote;
}
/**
* Returns the current selection as a string, suitable for find.
*/
internal string? get_selection_for_find() {
string? value = null;
WebKit.DOM.Document document = web_view.get_dom_document();
WebKit.DOM.DOMWindow window = document.get_default_view();
WebKit.DOM.DOMSelection selection = window.get_selection();
if (selection.get_range_count() > 0) {
try {
WebKit.DOM.Range range = selection.get_range_at(0);
value = range.get_text().strip();
if (value.length <= 0)
value = null;
} catch (Error e) {
warning("Could not get selected text from web view: %s", e.message);
}
}
return value;
}
private SimpleAction add_action(string name, bool enabled, VariantType? type = null) {
SimpleAction action = new SimpleAction(name, type);
action.set_enabled(enabled);
......
......@@ -53,7 +53,7 @@ public class ConversationViewer : Gtk.Stack {
[GtkChild]
private Gtk.Grid no_conversations_page;
[GtkChild]
internal Gtk.ScrolledWindow conversation_page;
private Gtk.Grid conversation_page;
[GtkChild]
private Gtk.Grid multiple_conversations_page;
[GtkChild]
......@@ -63,7 +63,20 @@ public class ConversationViewer : Gtk.Stack {
[GtkChild]
private Gtk.Grid composer_page;
private ConversationFindBar conversation_find_bar;
[GtkChild]
internal Gtk.ScrolledWindow conversation_scroller;
[GtkChild]
internal Gtk.SearchBar conversation_find_bar;
[GtkChild]
internal Gtk.SearchEntry conversation_find_entry;
[GtkChild]
private Gtk.Button conversation_find_next;
[GtkChild]
private Gtk.Button conversation_find_prev;
// State machine setup for search/find modes.
private Geary.State.MachineDescriptor search_machine_desc = new Geary.State.MachineDescriptor(
......@@ -131,6 +144,12 @@ public class ConversationViewer : Gtk.Stack {
fsm = new Geary.State.Machine(search_machine_desc, mappings, null);
fsm.set_logging(false);
this.conversation_find_bar.notify["search-mode-enabled"].connect(
on_find_search_started
);
// XXX Do this in Glade when possible.
this.conversation_find_bar.connect_entry(this.conversation_find_entry);
//conversation_find_bar = new ConversationFindBar(web_view);
//conversation_find_bar.no_show_all = true;
//conversation_find_bar.close.connect(() => { fsm.issue(SearchEvent.CLOSE_FIND_BAR); });
......@@ -158,25 +177,7 @@ public class ConversationViewer : Gtk.Stack {
}
});
this.composer_page.add(box);
set_visible_child(composer_page);
}
/**
* Shows the in-conversation search UI.
*/
public void show_find_bar() {
fsm.issue(SearchEvent.OPEN_FIND_BAR);
conversation_find_bar.focus_entry();
}
/**
* Displays the next/previous match for an in-conversation search.
*/
public void find(bool forward) {
if (!conversation_find_bar.visible)
show_find_bar();
conversation_find_bar.find(forward);
set_visible_child(this.composer_page);
}
/**
......@@ -243,7 +244,7 @@ public class ConversationViewer : Gtk.Stack {
new Geary.App.EmailStore(account),
account.information,
location.special_folder_type == Geary.SpecialFolderType.DRAFTS,
conversation_page.get_vadjustment()
conversation_scroller.get_vadjustment()
);
// Need to fire this signal early so the the controller
......@@ -283,12 +284,12 @@ public class ConversationViewer : Gtk.Stack {
viewport.show();
viewport.add(list);
this.conversation_page.add(viewport);
this.conversation_scroller.add(viewport);
}
// Remove any existing conversation list, cancelling its loading
private void remove_current_list() {
Gtk.Widget? scrolled_child = this.conversation_page.get_child();
Gtk.Widget? scrolled_child = this.conversation_scroller.get_child();
if (scrolled_child != null) {
scrolled_child.destroy();
}
......@@ -314,10 +315,10 @@ public class ConversationViewer : Gtk.Stack {
// Find bar opened.
private uint on_open_find_bar(uint state, uint event, void *user, Object? object) {
if (!conversation_find_bar.visible)
conversation_find_bar.show();
//if (!conversation_find_bar.visible)
// conversation_find_bar.show();
conversation_find_bar.focus_entry();
//conversation_find_bar.focus_entry();
//web_view.allow_collapsing(false);
return SearchState.FIND;
......@@ -336,4 +337,62 @@ public class ConversationViewer : Gtk.Stack {
// }
}
private void on_find_search_started(Object obj, ParamSpec param) {
if (this.conversation_find_bar.get_search_mode()) {
if (this.current_list != null) {
ConversationEmail? email_view =
this.current_list.get_selection_view();
if (email_view != null) {
string text = email_view.get_selection_for_find();
if (text != null) {
this.conversation_find_entry.set_text(text);
this.conversation_find_entry.select_region(0, -1);
}
}
}
}
}
[GtkCallback]
private void on_find_search_changed(Gtk.SearchEntry entry) {
string search = entry.get_text().strip();
bool have_matches = false;
if (this.current_list != null) {
if (search.length > 0) {
// Have a search string
Gee.Set<string> search_matches = new Gee.HashSet<string>();
search_matches.add(search);
have_matches =
this.current_list.highlight_search_terms(search_matches);
} else {
// Have no search string
// if (location is Geary.SearchFolder) {
// // Re-display the search results
// yield this.current_list.load_search_terms(
// (Geary.SearchFolder) location
// );
// } else {
this.current_list.unmark_search_terms();
// }
}
}
this.conversation_find_next.set_sensitive(have_matches);
this.conversation_find_prev.set_sensitive(have_matches);
}
[GtkCallback]
private void on_find_next(Gtk.Widget entry) {
if (this.current_list != null) {
//this.current_list.show_prev_search_term();
}
}
[GtkCallback]
private void on_find_prev(Gtk.Widget entry) {
if (this.current_list != null) {
//this.current_list.show_next_search_term();
}
}
}
......@@ -4,8 +4,6 @@
<accelerator action="GearyCopyMenuButton" />
<accelerator action="GearyDeleteMessage" />
<accelerator action="GearyFindInConversation" />
<accelerator action="GearyFindNextInConversation" />
<accelerator action="GearyFindPreviousInConversation" />
<accelerator action="GearyForwardMessage" />
<accelerator action="GearyMoveMenuButton" />
<accelerator action="GearyNewMessage" />
......
......@@ -32,13 +32,105 @@
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="conversation_page">
<object class="GtkGrid" id="conversation_page">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
<object class="GtkSearchBar" id="conversation_find_bar">
<property name="visible">True</property>
<property name="app_paintable">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkSearchEntry" id="conversation_find_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">Find in conversation</property>
<signal name="next-match" handler="on_find_next" swapped="no"/>
<signal name="previous-match" handler="on_find_prev" swapped="no"/>
<signal name="search-changed" handler="on_find_search_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="conversation_find_prev">
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Find the previous occurrence of the search string.</property>
<signal name="clicked" handler="on_find_prev" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-up-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="conversation_find_next">
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Find the next occurrence of the search string.</property>
<signal name="activate" handler="on_find_next" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-down-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="conversation_scroller">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment