Skip to content

Commit f2d00f2

Browse files
authored
Merge pull request #146 from goncalossilva/ft.stats
This PR improves statistics counting, as well as adds support for more of them, fixing #63. It does so in a iA Writer inspired way, meaning: * The stats bar displays one stat, word count by default. * The stats bar now contains a button that [displays all stats and allows toggling between them](https://cl.ly/a0ce3fad3d72/gnome-shell-screenshot-QRNG0Z.png). * The default stat is saved between sessions. * All the stats are objective and deterministic. For instance, I contemplated adding GhostWriter's "Pages" estimation, but it's outright broken in my testing, and I also think we should be strict about what too include. Too many things will make it less useful. Regardless, it's trivial to extend. * Calculations are done on a worker thread, to prevent hogging the UI and allowing future extensibility without much consideration.
2 parents f466171 + dccc645 commit f2d00f2

File tree

7 files changed

+245
-172
lines changed

7 files changed

+245
-172
lines changed

data/de.wolfvollprecht.UberWriter.gschema.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
<schemalist>
44

5+
<enum id='de.wolfvollprecht.UberWriter.Stat'>
6+
<value nick='characters' value='0' />
7+
<value nick='words' value='1' />
8+
<value nick='sentences' value='2' />
9+
<value nick='paragraphs' value='3' />
10+
<value nick='read_time' value='4' />
11+
</enum>
12+
513
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
614

715
<key name='dark-mode-auto' type='b'>
@@ -54,6 +62,13 @@
5462
Open file paths of the current session
5563
</description>
5664
</key>
65+
<key name='stat-default' enum='de.wolfvollprecht.UberWriter.Stat'>
66+
<default>"words"</default>
67+
<summary>Default statistic</summary>
68+
<description>
69+
Which statistic is shown on the main window.
70+
</description>
71+
</key>
5772

5873
</schema>
5974

data/media/css/_gtk_base.css

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,10 @@
8686
}
8787

8888

89-
.status-bar-box label {
90-
color: #666;
91-
}
92-
93-
.status-bar-box button {
94-
/* finding reset */
89+
.stats-counter {
90+
color: alpha(@foreground_color, 0.6);
9591
background-color: @background_color;
9692
text-shadow: inherit;
97-
/*icon-shadow: inherit;*/
9893
box-shadow: initial;
9994
background-clip: initial;
10095
background-origin: initial;
@@ -106,37 +101,15 @@
106101
border-image-repeat: initial;
107102
border-image-slice: initial;
108103
border-image-width: initial;
109-
110104
border-style: none;
111-
-button-images: true;
112-
border-radius: 2px;
113-
color: #666;
114-
padding: 3px 5px;
105+
padding: 0px 16px;
115106
transition: 100ms ease-in;
116107
}
117108

118-
.status-bar-box button:hover,
119-
.status-bar-box button:checked {
120-
transition: 0s ease-in;
121-
color: @background_color;
122-
background-color: #666;
123-
}
124-
125-
.status-bar-box button:hover label,
126-
.status-bar-box button:checked label {
127-
color: @background_color;
128-
}
129-
130-
.status-bar-box button:active {
131-
color: #EEE;
132-
background-color: #EEE;
133-
background-image: none;
134-
box-shadow: 0 0 2px rgba(0,0,0,0.4)
135-
}
136-
137-
.status-bar-box separator {
138-
border-color: #999;
139-
border-right: none;
109+
.stats-counter:hover,
110+
.stats-counter:checked {
111+
color: @foreground_color;
112+
background-color: lighter(@background_color);
140113
}
141114

142115
#PreviewMenuItem image {

data/ui/Window.ui

Lines changed: 7 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -75,97 +75,20 @@
7575
<property name="can_focus">False</property>
7676
<property name="hexpand">True</property>
7777
<child>
78-
<object class="GtkRevealer" id="status_bar_revealer">
78+
<object class="GtkRevealer" id="stats_counter_revealer">
7979
<property name="visible">True</property>
8080
<property name="can_focus">False</property>
8181
<property name="transition_type">crossfade</property>
8282
<property name="transition_duration">750</property>
8383
<property name="reveal_child">True</property>
8484
<child>
85-
<object class="GtkGrid" id="status_bar_box">
85+
<object class="GtkButton" id="stats_counter">
86+
<property name="label" translatable="yes">0 Words</property>
8687
<property name="visible">True</property>
87-
<property name="can_focus">False</property>
88-
<child>
89-
<object class="GtkLabel" id="label2">
90-
<property name="visible">True</property>
91-
<property name="can_focus">False</property>
92-
<property name="halign">end</property>
93-
<property name="hexpand">True</property>
94-
<property name="label" translatable="yes">Words:</property>
95-
</object>
96-
<packing>
97-
<property name="left_attach">3</property>
98-
<property name="top_attach">0</property>
99-
</packing>
100-
</child>
101-
<child>
102-
<object class="GtkLabel" id="word_count">
103-
<property name="visible">True</property>
104-
<property name="can_focus">False</property>
105-
<property name="halign">end</property>
106-
<property name="label">0</property>
107-
<property name="justify">right</property>
108-
<property name="width_chars">4</property>
109-
<property name="xalign">1</property>
110-
</object>
111-
<packing>
112-
<property name="left_attach">4</property>
113-
<property name="top_attach">0</property>
114-
</packing>
115-
</child>
116-
<child>
117-
<object class="GtkSeparator" id="separator1">
118-
<property name="visible">True</property>
119-
<property name="can_focus">False</property>
120-
<property name="halign">end</property>
121-
<property name="margin_left">10</property>
122-
<property name="margin_right">10</property>
123-
<property name="margin_start">10</property>
124-
<property name="margin_end">10</property>
125-
<property name="orientation">vertical</property>
126-
</object>
127-
<packing>
128-
<property name="left_attach">5</property>
129-
<property name="top_attach">0</property>
130-
</packing>
131-
</child>
132-
<child>
133-
<object class="GtkLabel" id="label1">
134-
<property name="visible">True</property>
135-
<property name="can_focus">False</property>
136-
<property name="halign">end</property>
137-
<property name="label" translatable="yes">Characters:</property>
138-
</object>
139-
<packing>
140-
<property name="left_attach">6</property>
141-
<property name="top_attach">0</property>
142-
</packing>
143-
</child>
144-
<child>
145-
<object class="GtkLabel" id="char_count">
146-
<property name="visible">True</property>
147-
<property name="can_focus">False</property>
148-
<property name="halign">end</property>
149-
<property name="margin_right">11</property>
150-
<property name="margin_end">11</property>
151-
<property name="label">0</property>
152-
<property name="width_chars">6</property>
153-
<property name="xalign">1</property>
154-
</object>
155-
<packing>
156-
<property name="left_attach">7</property>
157-
<property name="top_attach">0</property>
158-
</packing>
159-
</child>
160-
<child>
161-
<placeholder/>
162-
</child>
163-
<child>
164-
<placeholder/>
165-
</child>
166-
<child>
167-
<placeholder/>
168-
</child>
88+
<property name="can_focus">True</property>
89+
<property name="receives_default">True</property>
90+
<property name="tooltip_text" translatable="yes">Show Statistics</property>
91+
<property name="halign">end</property>
16992
</object>
17093
</child>
17194
</object>

uberwriter/application.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def do_startup(self, *args, **kwargs):
4040

4141
self.settings.connect("changed", self.on_settings_changed)
4242

43+
# Header bar
44+
4345
action = Gio.SimpleAction.new("new", None)
4446
action.connect("activate", self.on_new)
4547
self.add_action(action)
@@ -60,6 +62,8 @@ def do_startup(self, *args, **kwargs):
6062
action.connect("activate", self.on_search)
6163
self.add_action(action)
6264

65+
# App Menu
66+
6367
action = Gio.SimpleAction.new_stateful(
6468
"focus_mode", None, GLib.Variant.new_boolean(False))
6569
action.connect("change-state", self.on_focus_mode)
@@ -112,6 +116,14 @@ def do_startup(self, *args, **kwargs):
112116
action.connect("activate", self.on_quit)
113117
self.add_action(action)
114118

119+
# Stats Menu
120+
121+
stat_default = self.settings.get_string("stat-default")
122+
action = Gio.SimpleAction.new_stateful(
123+
"stat_default", GLib.VariantType.new('s'), GLib.Variant.new_string(stat_default))
124+
action.connect("activate", self.on_stat_default)
125+
self.add_action(action)
126+
115127
# Shortcuts
116128

117129
# TODO: be aware that a couple of shortcuts are defined in _gtk_base.css
@@ -166,6 +178,8 @@ def on_settings_changed(self, settings, key):
166178
self.window.toggle_gradient_overlay(settings.get_value(key))
167179
elif key == "input-format":
168180
self.window.reload_preview()
181+
elif key == "stat-default":
182+
self.window.update_default_stat()
169183

170184
def on_new(self, _action, _value):
171185
self.window.new_document()
@@ -232,6 +246,10 @@ def on_about(self, _action, _param):
232246
def on_quit(self, _action, _param):
233247
self.quit()
234248

249+
def on_stat_default(self, action, value):
250+
action.set_state(value)
251+
self.settings.set_string("stat-default", value.get_string())
252+
235253
# ~ if __name__ == "__main__":
236254
# ~ app = Application()
237255
# ~ app.run(sys.argv)

uberwriter/stats_counter.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import math
2+
import re
3+
from queue import Queue
4+
from threading import Thread
5+
6+
from gi.repository import GLib
7+
8+
from uberwriter import helpers
9+
10+
11+
class StatsCounter:
12+
"""Counts characters, words, sentences and read time using a background thread."""
13+
14+
# Regexp that matches any character, except for newlines and subsequent spaces.
15+
CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))")
16+
17+
# Regexp that matches Asian letters, general symbols and hieroglyphs,
18+
# as well as sequences of word characters optionally containing non-word characters in-between.
19+
WORDS = re.compile(r"[\u3040-\uffff]|(?:\w+\S?\w*)+", re.UNICODE)
20+
21+
# Regexp that matches sentence-ending punctuation characters, ie. full stop, question mark,
22+
# exclamation mark, paragraph, and variants.
23+
SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+")
24+
25+
# Regexp that matches paragraphs, ie. anything separated by newlines.
26+
PARAGRAPHS = re.compile(r".+\n?")
27+
28+
def __init__(self):
29+
super().__init__()
30+
31+
self.queue = Queue()
32+
worker = Thread(target=self.__do_count, name="stats-counter")
33+
worker.daemon = True
34+
worker.start()
35+
36+
def count(self, text, callback):
37+
"""Count stats for text, calling callback with a result when done.
38+
39+
The callback argument contains the result, in the form:
40+
41+
(characters, words, sentences, (hours, minutes, seconds))"""
42+
43+
self.queue.put((text, callback))
44+
45+
def stop(self):
46+
"""Stops the background worker. StatsCounter shouldn't be used after this."""
47+
48+
self.queue.put((None, None))
49+
50+
def __do_count(self):
51+
while True:
52+
while True:
53+
(text, callback) = self.queue.get()
54+
if text is None and callback is None:
55+
return
56+
if self.queue.empty():
57+
break
58+
59+
text = helpers.pandoc_convert(text, to="plain")
60+
61+
character_count = len(re.findall(self.CHARACTERS, text))
62+
63+
word_count = len(re.findall(self.WORDS, text))
64+
65+
sentence_count = len(re.findall(self.SENTENCES, text))
66+
67+
paragraph_count = len(re.findall(self.PARAGRAPHS, text))
68+
69+
read_m, read_s = divmod(word_count / 200 * 60, 60)
70+
read_h, read_m = divmod(read_m, 60)
71+
read_time = (int(read_h), int(read_m), int(read_s))
72+
73+
GLib.idle_add(
74+
callback,
75+
(character_count, word_count, sentence_count, paragraph_count, read_time))

0 commit comments

Comments
 (0)