Add statistics for sentences and read time, allow user to toggle default

Fixes #63
github/fork/yochananmarqos/patch-1
Gonçalo Silva 2019-04-19 06:24:43 +01:00
parent 1a7443fd3c
commit c19f57f64b
7 changed files with 180 additions and 173 deletions

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<enum id='de.wolfvollprecht.UberWriter.Stat'>
<value nick='characters' value='0' />
<value nick='words' value='1' />
<value nick='sentences' value='2' />
<value nick='read_time' value='3' />
</enum>
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
@ -54,6 +61,13 @@
Open file paths of the current session
</description>
</key>
<key name='stat-default' enum='de.wolfvollprecht.UberWriter.Stat'>
<default>"words"</default>
<summary>Default statistic</summary>
<description>
Which statistic is shown on the main window.
</description>
</key>
</schema>

View File

@ -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 {

View File

@ -75,97 +75,20 @@
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkRevealer" id="status_bar_revealer">
<object class="GtkRevealer" id="stats_counter_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="transition_duration">750</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkGrid" id="status_bar_box">
<object class="GtkButton" id="stats_counter">
<property name="label" translatable="yes">0 Words</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Words:</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="word_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label">0</property>
<property name="justify">right</property>
<property name="width_chars">4</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Characters:</property>
</object>
<packing>
<property name="left_attach">6</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="char_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_right">11</property>
<property name="margin_end">11</property>
<property name="label">0</property>
<property name="width_chars">6</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">7</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Show Statistics</property>
<property name="halign">end</property>
</object>
</child>
</object>

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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()