Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# 2.22 (2021-04-25)
* Add a "Give Feedback" button (#551, Rahul Jha).
* Test code on macOS (#552, Rahul Jha).
* Support searching for multiple words (#558, Rahul Jha).

# 2.21 (2020-12-07)
* Update MathJax to version 3 (#515, @dgcampea).
Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ please check back with me briefly about its status.
- [ ] Update GTK stack for Windows: use MinGW and Python >= 3.6.
- [ ] Use separate file for storing CSS to allow users to override styles more easily.
- [ ] Make default CSS prettier (see private email exchange).
- [ ] Allow searching for days that contain **multiple** words or tags.
- [ ] Check that non-ASCII image filenames work (https://bugs.launchpad.net/bugs/1739701).
- [ ] Search and replace (useful for renaming tags and other names).
Show "replace" text field after search text has been entered.
- [ ] Add simple way to show all entries: allow searching for whitespace (i.e., don't strip whitespace from search string).
- [ ] Copy files and pictures into data subdirectory (#163, #469).
- [ ] Require minimum width for calendar panel to avoid hiding it by accident.

Expand Down
70 changes: 41 additions & 29 deletions rednotebook/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import datetime
import re
import sys


TEXT_RESULT_LENGTH = 42
Expand Down Expand Up @@ -178,14 +179,14 @@ def get_words(self, with_special_chars=False):
def get_number_of_words(self):
return len(self.get_words(with_special_chars=True))

def search(self, text, tags):
def search(self, words, tags):
"""
This method is only called for days that have all given tags.
Search in date first, then in the text, then in the tags.
Uses case-insensitive search.
"""
results = []
if not text:
if not words:
# Only add text result once for all tags.
add_text_to_results = False
for day_tag, entries in self.get_category_content_pairs().items():
Expand All @@ -200,41 +201,52 @@ def search(self, text, tags):
add_text_to_results = True
if add_text_to_results:
results.append(get_text_with_dots(self.text, 0, TEXT_RESULT_LENGTH))
elif text in str(self):
# Date contains searched text.
results.append(get_text_with_dots(self.text, 0, TEXT_RESULT_LENGTH))
else:
text_result = self.search_in_text(text)
non_date_words = [word for word in words if word not in str(self)]
words_contain_date = len(non_date_words) != len(words)
if words_contain_date:
results.append(get_text_with_dots(self.text, 0, TEXT_RESULT_LENGTH))
# If all the words matched agains the date, return.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"matched agains" -> "match"

if not non_date_words:
return str(self), results

text_result = self.search_in_text(non_date_words)
if text_result:
results.append(text_result)
results.extend(self.search_in_categories(text))
results.extend(self.search_in_categories(non_date_words))
return str(self), results

def search_in_text(self, search_text):
occurence = self.text.upper().find(search_text.upper())

# Check if search_text is in text
if occurence < 0:
return None

found_text = self.text[occurence : occurence + len(search_text)]
result_text = get_text_with_dots(
self.text, occurence, occurence + len(search_text), found_text
def search_in_text(self, words):
"""
If all words are in the text, return a suitable text substring.
Otherwise, return None.
"""
match_word, smallest_index = None, sys.maxsize
for word in words:
index = self.text.lower().find(word.lower())
if index < 0:
return
if index < smallest_index:
match_word, smallest_index = word, index

found_text = self.text[smallest_index : smallest_index + len(match_word)]
return get_text_with_dots(
self.text, smallest_index, smallest_index + len(match_word), found_text
)
return result_text

def search_in_categories(self, text):
def search_in_categories(self, words):
results = []
for category, content in self.get_category_content_pairs().items():
if content:
if text.upper() in category.upper():
results.extend(content)
else:
results.extend(
entry for entry in content if text.upper() in entry.upper()
)
elif text.upper() in category.upper():
results.append(category)
for word in words:
for category, content in self.get_category_content_pairs().items():
if content:
if word.upper() in category.upper():
results.extend(content)
else:
results.extend(
entry for entry in content if word.upper() in entry.upper()
)
elif word.upper() in category.upper():
results.append(category)
return results

def __str__(self):
Expand Down
4 changes: 3 additions & 1 deletion rednotebook/gui/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def highlight(self, search_text):
# Tell the webview which text to highlight after the html is loaded
self.search_text = search_text
self.get_find_controller().search(
self.search_text, WebKit2.FindOptions.CASE_INSENSITIVE, MAX_HITS
self.search_text,
WebKit2.FindOptions.CASE_INSENSITIVE | WebKit2.FindOptions.WRAP_AROUND,
MAX_HITS,
)

def on_load_changed(self, webview, event):
Expand Down
10 changes: 5 additions & 5 deletions rednotebook/gui/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, day_text_view):

self._connect_undo_signals()

self.search_text = ""
self.search_words = []

# spell checker
self._spell_checker = None
Expand Down Expand Up @@ -121,8 +121,8 @@ def replace_selection_and_highlight(self, p1, p2, p3):
end.backward_chars(len(p3))
self.day_text_buffer.select_range(start, end)

def highlight(self, text):
self.search_text = text
def highlight(self, words):
self.search_words = words
buf = self.day_text_buffer

# Clear previous highlighting
Expand All @@ -131,8 +131,8 @@ def highlight(self, text):
buf.remove_tag_by_name("highlighter", start, end)

# Highlight matches
if text:
for match_start, match_end in self.iter_search_matches(text):
for word in words:
for match_start, match_end in self.iter_search_matches(word):
buf.apply_tag_by_name("highlighter", match_start, match_end)

search_flags = (
Expand Down
35 changes: 29 additions & 6 deletions rednotebook/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,9 +724,20 @@ def set_date(self, new_month, new_date, day):
def get_day_text(self):
return self.day_text_field.get_text()

def highlight_text(self, search_text):
self.html_editor.highlight(search_text)
self.day_text_field.highlight(search_text)
def highlight_text(self, search_words):
self.day_text_field.highlight(search_words)

# The HTML view can only highlight one match, so we search for a match ourselves
# and highlight the last match, since it's what the user is currently typing.
day_text = self.day_text_field.get_text()

def get_last_match():
for word in reversed(search_words):
if word.lower() in day_text.lower():
return word
return ""

self.html_editor.highlight(get_last_match())

def show_message(self, title, msg, msg_type):
if msg_type == Gtk.MessageType.ERROR:
Expand Down Expand Up @@ -814,17 +825,29 @@ def _get_buffer(self, key, text):
def _get_buffer_for_day(self, day):
return self._get_buffer(day.date, day.text)

def scroll_to_non_date_text(self, words):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method name.

"""
Find the first non-date word in words, and pass it on to
`Editor.scroll_to_text`.
"""
for word in words:
# If word matches date, it probably is not present in the text.
if word in str(self.day):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the pass by inverting the if statement: if word not in str(self.day)

pass
else:
super().scroll_to_text(word)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the super() now that the child method is renamed?


def show_day(self, new_day):
# Show new day
self.day = new_day
buf = self._get_buffer_for_day(new_day)
self.replace_buffer(buf)
self.day_text_view.grab_focus()

if self.search_text:
if self.search_words:
# If a search is currently made, scroll to the text and return.
GObject.idle_add(self.scroll_to_text, self.search_text)
GObject.idle_add(self.highlight, self.search_text)
GObject.idle_add(self.scroll_to_non_date_text, self.search_words)
GObject.idle_add(self.highlight, self.search_words)
return

def show_template(self, title, text):
Expand Down
27 changes: 11 additions & 16 deletions rednotebook/gui/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from xml.sax.saxutils import escape

from gi.repository import GObject, Gtk
from gi.repository import Gtk

from rednotebook.gui.customwidgets import CustomComboBoxEntry, CustomListView
from rednotebook.util import dates
Expand Down Expand Up @@ -52,26 +52,21 @@ def on_entry_activated(self, entry):
self.search(search_text)

def search(self, search_text):
tags = []
queries = []
tags, words = [], []
for part in search_text.split():
if part.startswith("#"):
tags.append(part.lstrip("#").lower())
else:
queries.append(part)
words.append(part)

search_text = " ".join(queries)

# Highlight all occurences in the current day's text
self.main_window.highlight_text(search_text)
# Highlight all occurrences in the current day's text.
self.main_window.highlight_text(words)

# Scroll to query.
if search_text:
GObject.idle_add(
self.main_window.day_text_field.scroll_to_text, search_text
)
if words:
self.main_window.day_text_field.scroll_to_non_date_text(tags + words)

self.main_window.search_tree_view.update_data(search_text, tags)
self.main_window.search_tree_view.update_data(words, tags)

# Without the following, showing the search results sometimes lets the
# search entry lose focus and search phrases are added to a day's text.
Expand All @@ -89,18 +84,18 @@ def __init__(self, main_window, always_show_results):

self.connect("cursor_changed", self.on_cursor_changed)

def update_data(self, search_text, tags):
def update_data(self, words, tags):
self.tree_store.clear()

if not self.always_show_results and not tags and not search_text:
if not self.always_show_results and not tags and not words:
self.main_window.cloud.show()
self.main_window.search_scroll.hide()
return

self.main_window.cloud.hide()
self.main_window.search_scroll.show()

for date_string, entries in self.journal.search(search_text, tags):
for date_string, entries in self.journal.search(words, tags):
for entry in entries:
entry = escape(entry)
entry = entry.replace("STARTBOLD", "<b>").replace("ENDBOLD", "</b>")
Expand Down
4 changes: 2 additions & 2 deletions rednotebook/journal.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,10 @@ def get_entries(self, category):
entries |= set(day.get_entries(category))
return sorted(entries)

def search(self, text, tags):
def search(self, words, tags):
results = []
for day in reversed(self.get_days_with_tags(tags)):
results.append(day.search(text, tags))
results.append(day.search(words, tags))
return results

def get_days_with_tags(self, tags):
Expand Down