From 1a7443fd3caf986583c4c5e1e3ad2b0f40ae9cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 03:02:04 +0100 Subject: [PATCH 1/3] Switch to a more powerful and accurate stats counter The new stats counter is able to count characters, words, sentences, and reading time. It does so more accurately than before, by leveraging Pandoc's plain format, and a few simple regular expressions that besides accuracy, also improve support for Asian languages. It's all done on a background thread to avoid hogging the UI. --- uberwriter/stats_counter.py | 70 +++++++++++++++++++++++++++++++++++++ uberwriter/window.py | 49 +++++++++++--------------- 2 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 uberwriter/stats_counter.py diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py new file mode 100644 index 0000000..0f8fbd8 --- /dev/null +++ b/uberwriter/stats_counter.py @@ -0,0 +1,70 @@ +import math +import re +from queue import Queue +from threading import Thread + +from gi.repository import GLib + +from uberwriter import helpers + + +class StatsCounter: + """Counts characters, words, sentences and reading time using a background thread.""" + + # Regexp that matches any character, except for newlines and subsequent spaces. + CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))") + + # Regexp that matches Asian letters, general symbols and hieroglyphs, + # as well as sequences of word characters optionally containing non-word characters in-between. + WORDS = re.compile(r"[\u3040-\uffff]|(?:\w+\S?\w*)+", re.UNICODE) + + # Regexp that matches sentence-ending punctuation characters, ie. full stop, question mark, + # exclamation mark, paragraph, and variants. + SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+") + + def __init__(self): + super().__init__() + + self.queue = Queue() + worker = Thread(target=self.__do_count_stats, name="stats-counter") + worker.daemon = True + worker.start() + + def count_stats(self, text, callback): + """Count stats for text, calling callback with a result when done. + + The callback argument contains the result, in the form: + + (characters, words, sentences, (hours, minutes, seconds))""" + + self.queue.put((text, callback)) + + def stop(self): + """Stops the background worker. StatsCounter shouldn't be used after this.""" + + self.queue.put((None, None)) + + def __do_count_stats(self): + while True: + while True: + (text, callback) = self.queue.get() + if text is None and callback is None: + return + if self.queue.empty(): + break + + text = helpers.pandoc_convert(text, to="plain") + + character_count = len(re.findall(self.CHARACTERS, text)) + + word_count = len(re.findall(self.WORDS, text)) + + sentence_count = len(re.findall(self.SENTENCES, text)) + + dec_, int_ = math.modf(word_count / 200) + hours = int(int_ / 60) + minutes = int(int_ % 60) + seconds = round(dec_ * 0.6) + reading_time = (hours, minutes, seconds) + + GLib.idle_add(callback, (character_count, word_count, sentence_count, reading_time)) diff --git a/uberwriter/window.py b/uberwriter/window.py index 7abdca8..95c4fe1 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -26,6 +26,7 @@ from gettext import gettext as _ import gi from uberwriter.export_dialog import Export +from uberwriter.stats_counter import StatsCounter from uberwriter.text_view import TextView gi.require_version('Gtk', '3.0') @@ -66,8 +67,6 @@ class Window(Gtk.ApplicationWindow): 'close-window': (GObject.SIGNAL_ACTION, None, ()) } - WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE) - def __init__(self, app): """Set up the main window""" @@ -83,6 +82,8 @@ class Window(Gtk.ApplicationWindow): self.set_default_size(900, 500) + self.connect('delete-event', self.on_destroy) + # Preferences self.settings = Settings.new() @@ -130,6 +131,9 @@ class Window(Gtk.ApplicationWindow): self.scrolled_window.add(self.text_view) self.editor_viewport = self.builder.get_object('editor_viewport') + # Stats counter + self.stats_counter = StatsCounter() + # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None @@ -190,25 +194,13 @@ class Window(Gtk.ApplicationWindow): # Redraw contents of window self.queue_draw() - def update_line_and_char_count(self): - """it... it updates line and characters count + def update_stats_counts(self, stats): + """Updates line and character counts. """ - if self.status_bar_visible is False: - return - text = self.text_view.get_text() - self.char_count.set_text(str(len(text))) - words = re.split(self.WORDCOUNT, text) - length = len(words) - # Last word a "space" - if not words[-1]: - length = length - 1 - # First word a "space" (happens in focus mode...) - if not words[0]: - length = length - 1 - if length == -1: - length = 0 - self.word_count.set_text(str(length)) + (characters, words, sentences, (hours, minutes, seconds)) = stats + self.char_count.set_text(str(characters)) + self.word_count.set_text(str(words)) def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and @@ -221,7 +213,9 @@ class Window(Gtk.ApplicationWindow): self.set_headerbar_title("* " + title) self.buffer_modified_for_status_bar = True - self.update_line_and_char_count() + + if self.status_bar_visible: + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides @@ -703,7 +697,7 @@ class Window(Gtk.ApplicationWindow): self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True @@ -716,7 +710,7 @@ class Window(Gtk.ApplicationWindow): self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the @@ -764,12 +758,6 @@ class Window(Gtk.ApplicationWindow): self.destroy() return - def on_destroy(self, _widget, _data=None): - """Called when the TexteditorWindow is closed. - """ - # Clean up code for saving application state should be added here. - Gtk.main_quit() - def set_headerbar_title(self, title): """set the desired headerbar title """ @@ -801,3 +789,8 @@ class Window(Gtk.ApplicationWindow): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up" + + def on_destroy(self, _widget, _data=None): + """Called when the Window is closing. + """ + self.stats_counter.stop() From c19f57f64b0dd6e7d5f7f167da62998cf4cb5ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 06:24:43 +0100 Subject: [PATCH 2/3] Add statistics for sentences and read time, allow user to toggle default Fixes #63 --- data/de.wolfvollprecht.UberWriter.gschema.xml | 14 +++ data/media/css/_gtk_base.css | 41 ++----- data/ui/Window.ui | 91 ++-------------- uberwriter/application.py | 18 +++ uberwriter/stats_counter.py | 18 ++- uberwriter/stats_handler.py | 103 ++++++++++++++++++ uberwriter/window.py | 68 ++++-------- 7 files changed, 180 insertions(+), 173 deletions(-) create mode 100644 uberwriter/stats_handler.py diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 7a3b93b..6d2b13a 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -1,6 +1,13 @@ + + + + + + + @@ -54,6 +61,13 @@ Open file paths of the current session + + "words" + Default statistic + + Which statistic is shown on the main window. + + diff --git a/data/media/css/_gtk_base.css b/data/media/css/_gtk_base.css index 84261ab..b4fe25b 100644 --- a/data/media/css/_gtk_base.css +++ b/data/media/css/_gtk_base.css @@ -86,15 +86,10 @@ } -.status-bar-box label { - color: #666; -} - -.status-bar-box button { - /* finding reset */ +.stats-counter { + color: alpha(@foreground_color, 0.6); background-color: @background_color; text-shadow: inherit; - /*icon-shadow: inherit;*/ box-shadow: initial; background-clip: initial; background-origin: initial; @@ -106,37 +101,15 @@ border-image-repeat: initial; border-image-slice: initial; border-image-width: initial; - border-style: none; - -button-images: true; - border-radius: 2px; - color: #666; - padding: 3px 5px; + padding: 0px 16px; transition: 100ms ease-in; } -.status-bar-box button:hover, -.status-bar-box button:checked { - transition: 0s ease-in; - color: @background_color; - background-color: #666; -} - -.status-bar-box button:hover label, -.status-bar-box button:checked label { - color: @background_color; -} - -.status-bar-box button:active { - color: #EEE; - background-color: #EEE; - background-image: none; - box-shadow: 0 0 2px rgba(0,0,0,0.4) -} - -.status-bar-box separator { - border-color: #999; - border-right: none; +.stats-counter:hover, +.stats-counter:checked { + color: @foreground_color; + background-color: lighter(@background_color); } #PreviewMenuItem image { diff --git a/data/ui/Window.ui b/data/ui/Window.ui index 66571fe..bca48f4 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -75,97 +75,20 @@ False True - + True False crossfade 750 True - + + 0 Words True - False - - - True - False - end - True - Words: - - - 3 - 0 - - - - - True - False - end - 0 - right - 4 - 1 - - - 4 - 0 - - - - - True - False - end - 10 - 10 - 10 - 10 - vertical - - - 5 - 0 - - - - - True - False - end - Characters: - - - 6 - 0 - - - - - True - False - end - 11 - 11 - 0 - 6 - 1 - - - 7 - 0 - - - - - - - - - - - + True + True + Show Statistics + end diff --git a/uberwriter/application.py b/uberwriter/application.py index 39038c0..0ad632f 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -40,6 +40,8 @@ class Application(Gtk.Application): self.settings.connect("changed", self.on_settings_changed) + # Header bar + action = Gio.SimpleAction.new("new", None) action.connect("activate", self.on_new) self.add_action(action) @@ -60,6 +62,8 @@ class Application(Gtk.Application): action.connect("activate", self.on_search) self.add_action(action) + # App Menu + action = Gio.SimpleAction.new_stateful( "focus_mode", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_focus_mode) @@ -112,6 +116,14 @@ class Application(Gtk.Application): action.connect("activate", self.on_quit) self.add_action(action) + # Stats Menu + + stat_default = self.settings.get_string("stat-default") + action = Gio.SimpleAction.new_stateful( + "stat_default", GLib.VariantType.new('s'), GLib.Variant.new_string(stat_default)) + action.connect("activate", self.on_stat_default) + self.add_action(action) + # Shortcuts # TODO: be aware that a couple of shortcuts are defined in _gtk_base.css @@ -166,6 +178,8 @@ class Application(Gtk.Application): self.window.toggle_gradient_overlay(settings.get_value(key)) elif key == "input-format": self.window.reload_preview() + elif key == "stat-default": + self.window.update_default_stat() def on_new(self, _action, _value): self.window.new_document() @@ -232,6 +246,10 @@ class Application(Gtk.Application): def on_quit(self, _action, _param): self.quit() + def on_stat_default(self, action, value): + action.set_state(value) + self.settings.set_string("stat-default", value.get_string()) + # ~ if __name__ == "__main__": # ~ app = Application() # ~ app.run(sys.argv) diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py index 0f8fbd8..b753143 100644 --- a/uberwriter/stats_counter.py +++ b/uberwriter/stats_counter.py @@ -9,7 +9,7 @@ from uberwriter import helpers class StatsCounter: - """Counts characters, words, sentences and reading time using a background thread.""" + """Counts characters, words, sentences and read time using a background thread.""" # Regexp that matches any character, except for newlines and subsequent spaces. CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))") @@ -26,11 +26,11 @@ class StatsCounter: super().__init__() self.queue = Queue() - worker = Thread(target=self.__do_count_stats, name="stats-counter") + worker = Thread(target=self.__do_count, name="stats-counter") worker.daemon = True worker.start() - def count_stats(self, text, callback): + def count(self, text, callback): """Count stats for text, calling callback with a result when done. The callback argument contains the result, in the form: @@ -44,7 +44,7 @@ class StatsCounter: self.queue.put((None, None)) - def __do_count_stats(self): + def __do_count(self): while True: while True: (text, callback) = self.queue.get() @@ -61,10 +61,8 @@ class StatsCounter: sentence_count = len(re.findall(self.SENTENCES, text)) - dec_, int_ = math.modf(word_count / 200) - hours = int(int_ / 60) - minutes = int(int_ % 60) - seconds = round(dec_ * 0.6) - reading_time = (hours, minutes, seconds) + read_m, read_s = divmod(word_count / 200 * 60, 60) + read_h, read_m = divmod(read_m, 60) + read_time = (int(read_h), int(read_m), int(read_s)) - GLib.idle_add(callback, (character_count, word_count, sentence_count, reading_time)) + GLib.idle_add(callback, (character_count, word_count, sentence_count, read_time)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py new file mode 100644 index 0000000..ec9f40a --- /dev/null +++ b/uberwriter/stats_handler.py @@ -0,0 +1,103 @@ +import math +import re +from gettext import gettext as _ +from queue import Queue +from threading import Thread + +from gi.repository import GLib, Gio, Gtk + +from uberwriter import helpers +from uberwriter.helpers import get_builder +from uberwriter.settings import Settings +from uberwriter.stats_counter import StatsCounter + + +class StatsHandler: + """Shows a default statistic on the stats button, and allows the user to toggle which one.""" + + def __init__(self, stats_button, text_view): + super().__init__() + + self.stats_button = stats_button + self.stats_button.connect("clicked", self.on_stats_button_clicked) + self.stats_button.connect("destroy", self.on_destroy) + + self.text_view = text_view + self.text_view.get_buffer().connect("changed", self.on_text_changed) + + self.popover = None + + self.characters = 0 + self.words = 0 + self.sentences = 0 + self.read_time = (0, 0, 0) + + self.settings = Settings.new() + self.default_stat = self.settings.get_enum("stat-default") + + self.stats_counter = StatsCounter() + + self.update_default_stat() + + def on_stats_button_clicked(self, _button): + self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False) + + menu = Gio.Menu() + characters_menu_item = Gio.MenuItem.new(self.get_text_for_stat(0), None) + characters_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("characters")) + menu.append_item(characters_menu_item) + words_menu_item = Gio.MenuItem.new(self.get_text_for_stat(1), None) + words_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("words")) + menu.append_item(words_menu_item) + sentences_menu_item = Gio.MenuItem.new(self.get_text_for_stat(2), None) + sentences_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("sentences")) + menu.append_item(sentences_menu_item) + read_time_menu_item = Gio.MenuItem.new(self.get_text_for_stat(3), None) + read_time_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("read_time")) + menu.append_item(read_time_menu_item) + self.popover = Gtk.Popover.new_from_model(self.stats_button, menu) + self.popover.connect('closed', self.on_popover_closed) + self.popover.popup() + + def on_popover_closed(self, _popover): + self.stats_button.unset_state_flags(Gtk.StateFlags.CHECKED) + + self.popover = None + self.text_view.grab_focus() + + def on_text_changed(self, buf): + self.stats_counter.count( + buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False), + self.update_stats) + + def get_text_for_stat(self, stat): + if stat == 0: + return _("{:n} Characters".format(self.characters)) + elif stat == 1: + return _("{:n} Words".format(self.words)) + elif stat == 2: + return _("{:n} Sentences".format(self.sentences)) + elif stat == 3: + return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time)) + + def update_stats(self, stats): + (characters, words, sentences, read_time) = stats + self.characters = characters + self.words = words + self.sentences = sentences + self.read_time = read_time + self.update_default_stat(False) + + def update_default_stat(self, close_popover=True): + stat = self.settings.get_enum("stat-default") + text = self.get_text_for_stat(stat) + self.stats_button.set_label(text) + if close_popover and self.popover: + self.popover.popdown() + + def on_destroy(self, _widget): + self.stats_counter.stop() diff --git a/uberwriter/window.py b/uberwriter/window.py index 95c4fe1..ce7bf25 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -18,7 +18,6 @@ import codecs import locale import logging import os -import re import urllib import webbrowser from gettext import gettext as _ @@ -26,7 +25,7 @@ from gettext import gettext as _ import gi from uberwriter.export_dialog import Export -from uberwriter.stats_counter import StatsCounter +from uberwriter.stats_handler import StatsHandler from uberwriter.text_view import TextView gi.require_version('Gtk', '3.0') @@ -82,8 +81,6 @@ class Window(Gtk.ApplicationWindow): self.set_default_size(900, 500) - self.connect('delete-event', self.on_destroy) - # Preferences self.settings = Settings.new() @@ -95,17 +92,6 @@ class Window(Gtk.ApplicationWindow): self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) - self.word_count = self.builder.get_object('word_count') - self.char_count = self.builder.get_object('char_count') - - # Setup status bar hide after 3 seconds - self.status_bar = self.builder.get_object('status_bar_box') - self.statusbar_revealer = self.builder.get_object('status_bar_revealer') - self.status_bar.get_style_context().add_class('status-bar-box') - self.status_bar_visible = True - self.was_motion = True - self.buffer_modified_for_status_bar = False - self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) @@ -118,11 +104,10 @@ class Window(Gtk.ApplicationWindow): self.text_view = TextView() self.text_view.props.halign = Gtk.Align.CENTER self.text_view.connect('focus-out-event', self.focus_out) + self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() - self.text_view.get_buffer().connect('changed', self.on_text_changed) - # Setup preview webview self.preview_webview = None @@ -132,7 +117,15 @@ class Window(Gtk.ApplicationWindow): self.editor_viewport = self.builder.get_object('editor_viewport') # Stats counter - self.stats_counter = StatsCounter() + self.stats_counter_revealer = self.builder.get_object('stats_counter_revealer') + self.stats_button = self.builder.get_object('stats_counter') + self.stats_button.get_style_context().add_class('stats-counter') + self.stats_handler = StatsHandler(self.stats_button, self.text_view) + + # Setup header/stats bar hide after 3 seconds + self.top_bottom_bars_visible = True + self.was_motion = True + self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it @@ -194,14 +187,6 @@ class Window(Gtk.ApplicationWindow): # Redraw contents of window self.queue_draw() - def update_stats_counts(self, stats): - """Updates line and character counts. - """ - - (characters, words, sentences, (hours, minutes, seconds)) = stats - self.char_count.set_text(str(characters)) - self.word_count.set_text(str(words)) - def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that @@ -214,9 +199,6 @@ class Window(Gtk.ApplicationWindow): self.buffer_modified_for_status_bar = True - if self.status_bar_visible: - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) - def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border @@ -485,6 +467,9 @@ class Window(Gtk.ApplicationWindow): self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) + def update_default_stat(self): + self.stats_handler.update_default_stat() + def menu_toggle_sidebar(self, _widget=None): """WIP """ @@ -666,13 +651,13 @@ class Window(Gtk.ApplicationWindow): """ if (self.was_motion is False - and self.status_bar_visible + and self.top_bottom_bars_visible and self.buffer_modified_for_status_bar and self.text_view.props.has_focus): # pylint: disable=no-member # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) - self.statusbar_revealer.set_reveal_child(False) + self.stats_counter_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) - self.status_bar_visible = False + self.top_bottom_bars_visible = False self.buffer_modified_for_status_bar = False self.was_motion = False @@ -691,26 +676,24 @@ class Window(Gtk.ApplicationWindow): return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + if self.top_bottom_bars_visible is False: + self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 - self.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + if self.top_bottom_bars_visible is False: + self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 - self.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the @@ -789,8 +772,3 @@ class Window(Gtk.ApplicationWindow): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up" - - def on_destroy(self, _widget, _data=None): - """Called when the Window is closing. - """ - self.stats_counter.stop() From dccc645430f081360e8f3a19b43d2b5ad0ea3991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 11:20:47 +0100 Subject: [PATCH 3/3] Add paragraphs stat --- data/de.wolfvollprecht.UberWriter.gschema.xml | 5 ++-- uberwriter/stats_counter.py | 9 +++++- uberwriter/stats_handler.py | 29 ++++++++----------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 6d2b13a..e7d4bfc 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -1,14 +1,15 @@ + - + + - diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py index b753143..fa4b103 100644 --- a/uberwriter/stats_counter.py +++ b/uberwriter/stats_counter.py @@ -22,6 +22,9 @@ class StatsCounter: # exclamation mark, paragraph, and variants. SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+") + # Regexp that matches paragraphs, ie. anything separated by newlines. + PARAGRAPHS = re.compile(r".+\n?") + def __init__(self): super().__init__() @@ -61,8 +64,12 @@ class StatsCounter: sentence_count = len(re.findall(self.SENTENCES, text)) + paragraph_count = len(re.findall(self.PARAGRAPHS, text)) + read_m, read_s = divmod(word_count / 200 * 60, 60) read_h, read_m = divmod(read_m, 60) read_time = (int(read_h), int(read_m), int(read_s)) - GLib.idle_add(callback, (character_count, word_count, sentence_count, read_time)) + GLib.idle_add( + callback, + (character_count, word_count, sentence_count, paragraph_count, read_time)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py index ec9f40a..8fce782 100644 --- a/uberwriter/stats_handler.py +++ b/uberwriter/stats_handler.py @@ -30,6 +30,7 @@ class StatsHandler: self.characters = 0 self.words = 0 self.sentences = 0 + self.paragraphs = 0 self.read_time = (0, 0, 0) self.settings = Settings.new() @@ -43,22 +44,11 @@ class StatsHandler: self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False) menu = Gio.Menu() - characters_menu_item = Gio.MenuItem.new(self.get_text_for_stat(0), None) - characters_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("characters")) - menu.append_item(characters_menu_item) - words_menu_item = Gio.MenuItem.new(self.get_text_for_stat(1), None) - words_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("words")) - menu.append_item(words_menu_item) - sentences_menu_item = Gio.MenuItem.new(self.get_text_for_stat(2), None) - sentences_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("sentences")) - menu.append_item(sentences_menu_item) - read_time_menu_item = Gio.MenuItem.new(self.get_text_for_stat(3), None) - read_time_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("read_time")) - menu.append_item(read_time_menu_item) + stats = self.settings.props.settings_schema.get_key("stat-default").get_range()[1] + for i, stat in enumerate(stats): + menu_item = Gio.MenuItem.new(self.get_text_for_stat(i), None) + menu_item.set_action_and_target_value("app.stat_default", GLib.Variant.new_string(stat)) + menu.append_item(menu_item) self.popover = Gtk.Popover.new_from_model(self.stats_button, menu) self.popover.connect('closed', self.on_popover_closed) self.popover.popup() @@ -82,13 +72,18 @@ class StatsHandler: elif stat == 2: return _("{:n} Sentences".format(self.sentences)) elif stat == 3: + return _("{:n} Paragraphs".format(self.paragraphs)) + elif stat == 4: return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time)) + else: + raise ValueError("Unknown stat {}".format(stat)) def update_stats(self, stats): - (characters, words, sentences, read_time) = stats + (characters, words, sentences, paragraphs, read_time) = stats self.characters = characters self.words = words self.sentences = sentences + self.paragraphs = paragraphs self.read_time = read_time self.update_default_stat(False)