diff --git a/plugins/colorize/color_converter_mixin.py b/plugins/colorize/color_converter_mixin.py new file mode 100644 index 0000000..4e33240 --- /dev/null +++ b/plugins/colorize/color_converter_mixin.py @@ -0,0 +1,63 @@ +# Python imports +import colorsys + +# Lib imports + +# Application imports + + +class ColorConverterMixin: + def get_color_text(self, buffer, start, end): + text = buffer.get_text(start, end, include_hidden_chars = False) + + try: + if "hsl" in text: + text = self.hsl_to_rgb(text) + + if "hsv" in text: + text = self.hsv_to_rgb(text) + except Exception as e: + ... + + return text + + def hsl_to_rgb(self, text): + _h, _s , _l = text.replace("hsl", "") \ + .replace("deg", "") \ + .replace("(", "") \ + .replace(")", "") \ + .replace("%", "") \ + .replace(" ", "") \ + .split(",") + + h = None + s = None + l = None + + h, s , l = int(_h) / 360, float(_s) / 100, float(_l) / 100 + + rgb = tuple(round(i * 255) for i in colorsys.hls_to_rgb(h, l, s)) + rgb_sub = ','.join(map(str, rgb)) + + return f"rgb({rgb_sub})" + + + def hsv_to_rgb(self, text): + _h, _s , _v = text.replace("hsv", "") \ + .replace("deg", "") \ + .replace("(", "") \ + .replace(")", "") \ + .replace("%", "") \ + .replace(" ", "") \ + .split(",") + + h = None + s = None + v = None + + h, s , v = int(_h) / 360, float(_s) / 100, float(_v) / 100 + + rgb = tuple(round(i * 255) for i in colorsys.hsv_to_rgb(h,s,v)) + rgb_sub = ','.join(map(str, rgb)) + + return f"rgb({rgb_sub})" diff --git a/plugins/colorize/plugin.py b/plugins/colorize/plugin.py index 652100b..3e45455 100644 --- a/plugins/colorize/plugin.py +++ b/plugins/colorize/plugin.py @@ -1,9 +1,4 @@ # Python imports -import os -import threading -import subprocess -import time -import colorsys # Lib imports import gi @@ -14,26 +9,11 @@ from gi.repository import Gdk # Application imports from plugins.plugin_base import PluginBase +from .color_converter_mixin import ColorConverterMixin - -# NOTE: Threads WILL NOT die with parent's destruction. -def threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start() - return wrapper - -# NOTE: Threads WILL die with parent's destruction. -def daemon_threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() - return wrapper - - - - -class Plugin(PluginBase): +class Plugin(ColorConverterMixin, PluginBase): def __init__(self): super().__init__() @@ -57,7 +37,7 @@ class Plugin(PluginBase): self._active_src_view = source_view def _buffer_changed_first_load(self, buffer): - self._handle(buffer) + self._do_colorize(buffer) def _buffer_changed(self, buffer): @@ -76,10 +56,10 @@ class Plugin(PluginBase): buffer.remove_tag(tag, start, end) tag_table.remove(tag) - self._handle(buffer, start, end) + self._do_colorize(buffer, start, end) - def _handle(self, buffer = None, start_itr = None, end_itr = None): + def _do_colorize(self, buffer = None, start_itr = None, end_itr = None): # rgb(a), hsl, hsv results = self.finalize_non_hex_matches( self.collect_preliminary_results(buffer, start_itr, end_itr) ) self.process_results(buffer, results) @@ -171,20 +151,6 @@ class Plugin(PluginBase): tag = self.get_colorized_tag(buffer, text, color) buffer.apply_tag(tag, start, end) - def get_color_text(self, buffer, start, end): - text = buffer.get_text(start, end, include_hidden_chars = False) - - try: - if "hsl" in text: - text = self.hsl_to_rgb(text) - - if "hsv" in text: - text = self.hsv_to_rgb(text) - except Exception as e: - ... - - return text - def get_colorized_tag(self, buffer, tag, color: Gdk.RGBA): tag_table = buffer.get_tag_table() colorize_tag = f"{self.tag_stub_name}_{tag}" @@ -193,44 +159,3 @@ class Plugin(PluginBase): search_tag = buffer.create_tag(colorize_tag, background_rgba = color) return search_tag - - def hsl_to_rgb(self, text): - _h, _s , _l = text.replace("hsl", "") \ - .replace("deg", "") \ - .replace("(", "") \ - .replace(")", "") \ - .replace("%", "") \ - .replace(" ", "") \ - .split(",") - - h = None - s = None - l = None - - h, s , l = int(_h) / 360, float(_s) / 100, float(_l) / 100 - - rgb = tuple(round(i * 255) for i in colorsys.hls_to_rgb(h, l, s)) - rgb_sub = ','.join(map(str, rgb)) - - return f"rgb({rgb_sub})" - - - def hsv_to_rgb(self, text): - _h, _s , _v = text.replace("hsv", "") \ - .replace("deg", "") \ - .replace("(", "") \ - .replace(")", "") \ - .replace("%", "") \ - .replace(" ", "") \ - .split(",") - - h = None - s = None - v = None - - h, s , v = int(_h) / 360, float(_s) / 100, float(_v) / 100 - - rgb = tuple(round(i * 255) for i in colorsys.hsv_to_rgb(h,s,v)) - rgb_sub = ','.join(map(str, rgb)) - - return f"rgb({rgb_sub})" diff --git a/plugins/search_replace/plugin.py b/plugins/search_replace/plugin.py index 2ab6971..1dc49c5 100644 --- a/plugins/search_replace/plugin.py +++ b/plugins/search_replace/plugin.py @@ -1,19 +1,22 @@ # Python imports import os import re +import threading # Lib imports import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk +from gi.repository import GLib # Application imports from plugins.plugin_base import PluginBase +from .styling_mixin import StylingMixin +from .replace_mixin import ReplaceMixin - -class Plugin(PluginBase): +class Plugin(StylingMixin, ReplaceMixin, PluginBase): def __init__(self): super().__init__() @@ -34,6 +37,9 @@ class Plugin(PluginBase): self.search_only_in_selection = False self.use_whole_word_search = False + self.timer = None + self.search_time = 0.35 + self.find_text = "" self.search_tag = "search_tag" self.highlight_color = "#FBF719" self.text_color = "#000000" @@ -91,39 +97,6 @@ class Plugin(PluginBase): self._search_replace_dialog.popdown() self._find_entry.set_text("") - def tggle_regex(self, widget): - self.use_regex = not widget.get_active() - self._set_find_options_lbl() - self.search_for_string(self._find_entry) - - def tggle_case_sensitive(self, widget): - self.use_case_sensitive = widget.get_active() - self._set_find_options_lbl() - self.search_for_string(self._find_entry) - - def tggle_selection_only_scan(self, widget): - self.search_only_in_selection = widget.get_active() - self._set_find_options_lbl() - self.search_for_string(self._find_entry) - - def tggle_whole_word_search(self, widget): - self.use_whole_word_search = widget.get_active() - self._set_find_options_lbl() - self.search_for_string(self._find_entry) - - def _set_find_options_lbl(self): - # Finding with Options: Case Insensitive - # Finding with Options: Regex, Case Sensitive, Within Current Selection, Whole Word - # Finding with Options: Regex, Case Inensitive, Within Current Selection, Whole Word - # f"Finding with Options: {regex}, {case}, {selection}, {word}" - ... - - def _update_status_lbl(self, total_count: int = 0, query: str = None): - if not query: return - - count = total_count if total_count > 0 else "No" - plural = "s" if total_count > 1 else "" - self._find_status_lbl.set_label(f"{count} results{plural} found for '{query}'") def get_search_tag(self, buffer): tag_table = buffer.get_tag_table() @@ -134,14 +107,44 @@ class Plugin(PluginBase): buffer.remove_tag_by_name(self.search_tag, buffer.get_start_iter(), buffer.get_end_iter()) return search_tag + + def cancel_timer(self): + if self.timer: + self.timer.cancel() + GLib.idle_remove_by_data(None) + + def delay_search_glib(self): + GLib.idle_add(self._do_highlight) + + def delay_search(self): + wait_time = self.search_time / len(self.find_text) + wait_time = max(wait_time, 0.05) + + self.timer = threading.Timer(wait_time, self.delay_search_glib) + self.timer.daemon = True + self.timer.start() + + def search_for_string(self, widget): - query = widget.get_text() + self.cancel_timer() + + self.find_text = widget.get_text() + if len(self.find_text) > 0 and len(self.find_text) < 5: + self.delay_search() + else: + self._do_highlight(self.find_text) + + + def _do_highlight(self, query = None): + query = self.find_text if not query else query buffer = self._active_src_view.get_buffer() # Also clears tag from buffer so if no query we're clean in ui search_tag = self.get_search_tag(buffer) + self.update_style(1) if not query: self._find_status_lbl.set_label(f"Find in current buffer") + self.update_style(0) return start_itr = buffer.get_start_iter() @@ -178,20 +181,9 @@ class Plugin(PluginBase): for start, end in _results: text = self._buffer.get_slice(start, end, include_hidden_chars = False) if self.use_whole_word_search: - end.forward_char() - start.backward_char() - - match = self.alpha_num_under.match( start.get_char() ) - if not match is None: + if not self.is_whole_word(start, end): continue - match = self.alpha_num_under.match( end.get_char() ) - if not match is None: - continue - - end.backward_char() - start.forward_char() - results.append([start, end]) return results @@ -215,9 +207,3 @@ class Plugin(PluginBase): def find_all(self, widget): ... - - def replace(self, widget): - ... - - def replace_all(self, widget): - ... \ No newline at end of file diff --git a/plugins/search_replace/replace_mixin.py b/plugins/search_replace/replace_mixin.py new file mode 100644 index 0000000..e129e57 --- /dev/null +++ b/plugins/search_replace/replace_mixin.py @@ -0,0 +1,94 @@ +# Python imports + +# Lib imports + +# Application imports + + + +class ReplaceMixin: + def replace(self, widget): + replace_text = self._replace_entry.get_text() + if self.find_text and replace_text: + self._buffer.begin_user_action() + + iter = self._buffer.get_start_iter() + search_tag = self._tag_table.lookup(self.search_tag) + + iter.forward_to_tag_toggle(replace_text) + self._do_replace(iter, find_text) + self._active_src_view.scroll_to_mark( self._buffer.get_insert(), 0.0, True, 0.0, 0.0 ) + + self._buffer.end_user_action() + + def replace_all(self, widget): + replace_text = self._replace_entry.get_text() + if self.find_text and replace_text: + self._buffer.begin_user_action() + + mark = self._buffer.get_insert() + iter = self._buffer.get_start_iter() + search_tag = self._tag_table.lookup(self.search_tag) + + while iter.forward_to_tag_toggle(search_tag): + self._do_replace(iter, replace_text) + iter = self._buffer.get_start_iter() + + self._buffer.end_user_action() + + + def _do_replace(self, iter, text): + start, end = self.get_start_end(iter) + self.replace_in_buffer(start, end, text) + + def replace_in_buffer(self, start, end, text): + pos_mark = self._buffer.create_mark("find-replace", end, True) + self._buffer.delete(start, end) + replace_iter = self._buffer.get_iter_at_mark(pos_mark) + self._buffer.insert(replace_iter, text) + + def get_start_end(self, iter): + start = iter.copy() + end = None + + while True: + iter.forward_char() + tags = iter.get_tags() + valid = False + for tag in tags: + if tag.props.name and self.search_tag in tag.props.name: + valid = True + break + + if valid: + continue + + end = iter.copy() + break + + return start, end + + # NOTE: Below, lovingly taken from Hamad Al Marri's Gamma text editor. + # Link: https://gitlab.com/hamadmarri/gamma-text-editor + def is_whole_word(self, match_start, match_end): + is_prev_a_char = True + is_next_a_char = True + + prev_iter = match_start.copy() + next_iter = match_end.copy() + if not prev_iter.backward_char(): + is_prev_a_char = False + else: + c = prev_iter.get_char() + is_prev_a_char = (c.isalpha() or c.isdigit()) + + if not next_iter: + is_next_a_char = False + else: + c = next_iter.get_char() + is_next_a_char = (c.isalpha() or c.isdigit()) + + is_word = (not is_prev_a_char and not is_next_a_char) + + # Note: Both must be false to be a word... + return is_word diff --git a/plugins/search_replace/search_replace.glade b/plugins/search_replace/search_replace.glade index 2904d59..b8aa4ab 100644 --- a/plugins/search_replace/search_replace.glade +++ b/plugins/search_replace/search_replace.glade @@ -179,40 +179,6 @@ True False True - - - True - True - 5 - 10 - 5 - 5 - Find in current buffer - - - - - 0 - 0 - 8 - - - - - True - True - 5 - 10 - 5 - 5 - Replace in current buffer - - - 0 - 1 - 8 - - Replace All @@ -287,6 +253,37 @@ 0 + + + True + True + edit-find-symbolic + False + False + Find in current buffer + + + + 0 + 0 + 8 + + + + + True + True + edit-find-symbolic + False + False + Replace in current buffer + + + 0 + 1 + 8 + + False diff --git a/plugins/search_replace/styling_mixin.py b/plugins/search_replace/styling_mixin.py new file mode 100644 index 0000000..1baf8a5 --- /dev/null +++ b/plugins/search_replace/styling_mixin.py @@ -0,0 +1,66 @@ +# Python imports + +# Lib imports + +# Application imports + + + +class StylingMixin: + def tggle_regex(self, widget): + self.use_regex = not widget.get_active() + self._set_find_options_lbl() + self.search_for_string(self._find_entry) + + def tggle_case_sensitive(self, widget): + self.use_case_sensitive = widget.get_active() + self._set_find_options_lbl() + self.search_for_string(self._find_entry) + + def tggle_selection_only_scan(self, widget): + self.search_only_in_selection = widget.get_active() + self._set_find_options_lbl() + self.search_for_string(self._find_entry) + + def tggle_whole_word_search(self, widget): + self.use_whole_word_search = widget.get_active() + self._set_find_options_lbl() + self.search_for_string(self._find_entry) + + def _set_find_options_lbl(self): + find_options = "Finding with Options: " + + if self.use_regex: + find_options += "Regex" + + find_options += ", " if self.use_regex else "" + find_options += "Case Sensitive" if self.use_case_sensitive else "Case Inensitive" + + if self.search_only_in_selection: + find_options += ", Within Current Selection" + + if self.use_whole_word_search: + find_options += ", Whole Word" + + self._find_options_lbl.set_label(find_options) + + def update_style(self, state): + self._find_entry.get_style_context().remove_class("searching") + self._find_entry.get_style_context().remove_class("search_success") + self._find_entry.get_style_context().remove_class("search_fail") + + if state == 0: + self._find_entry.get_style_context().add_class("searching") + elif state == 1: + self._find_entry.get_style_context().add_class("search_success") + elif state == 2: + self._find_entry.get_style_context().add_class("search_fail") + + def _update_status_lbl(self, total_count: int = 0, query: str = None): + if not query: return + + count = total_count if total_count > 0 else "No" + plural = "s" if total_count > 1 else "" + + if total_count == 0: self.update_style(2) + self._find_status_lbl.set_label(f"{count} results{plural} found for '{query}'") diff --git a/user_config/usr/share/newton/stylesheet.css b/user_config/usr/share/newton/stylesheet.css index 6b94ea9..8b0b3a8 100644 --- a/user_config/usr/share/newton/stylesheet.css +++ b/user_config/usr/share/newton/stylesheet.css @@ -103,6 +103,25 @@ popover { +.searching, +.search_success, +.search_fail { + border-style: solid; +} + +.searching { + border-color: rgba(0, 225, 225, 0.64); +} +.search_success { + background: rgba(136, 204, 39, 0.12); + border-color: rgba(136, 204, 39, 1); +} +.search_fail { + background: rgba(170, 18, 18, 0.12); + border-color: rgba(200, 18, 18, 1); +} + + .error_txt { color: rgb(170, 18, 18); } .warning_txt { color: rgb(255, 168, 0); } .success_txt { color: rgb(136, 204, 39); } @@ -227,4 +246,4 @@ popover { .mini-view > text { background: rgba(39, 43, 52, 0.64); -} \ No newline at end of file +}