forked from Mirrors/apostrophe
Add statistics for sentences and read time, allow user to toggle default
Fixes #63github/fork/yochananmarqos/patch-1
parent
1a7443fd3c
commit
c19f57f64b
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue