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
-
-
-
- 0
- 0
- 8
-
-
-
-
-
- 0
- 1
- 8
-
-
+
+
+
+ 0
+ 0
+ 8
+
+
+
+
+
+ 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
+}