forked from Mirrors/apostrophe
Merge branch 'master'
commit
7e34e9cc62
|
@ -16,6 +16,7 @@ flatpak/*
|
|||
data/ui/shortcut_handlers
|
||||
*.ui~
|
||||
.vscode/
|
||||
.idea/
|
||||
*.glade~
|
||||
dist/uberwriter-2.0b0-py3.7.egg
|
||||
builddir/*
|
||||
|
|
|
@ -10,6 +10,13 @@
|
|||
<value nick='read_time' value='4' />
|
||||
</enum>
|
||||
|
||||
<enum id='de.wolfvollprecht.UberWriter.PreviewMode'>
|
||||
<value nick='full-width' value='0' />
|
||||
<value nick='half-width' value='1' />
|
||||
<value nick='half-height' value='2' />
|
||||
<value nick='windowed' value='3' />
|
||||
</enum>
|
||||
|
||||
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
|
||||
|
||||
<key name='dark-mode-auto' type='b'>
|
||||
|
@ -41,6 +48,13 @@
|
|||
It can cause performance problems to some users.
|
||||
</description>
|
||||
</key>
|
||||
<key name='sync-scroll' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Synchronize editor/preview scrolling</summary>
|
||||
<description>
|
||||
Keep the editor and preview scroll positions in sync.
|
||||
</description>
|
||||
</key>
|
||||
<key name='input-format' type='s'>
|
||||
<default>"markdown"</default>
|
||||
<summary>Input format</summary>
|
||||
|
@ -69,6 +83,20 @@
|
|||
Which statistic is shown on the main window.
|
||||
</description>
|
||||
</key>
|
||||
<key name='characters-per-line' type='i'>
|
||||
<default>66</default>
|
||||
<summary>Characters per line</summary>
|
||||
<description>
|
||||
Maximum number of characters per line within the editor.
|
||||
</description>
|
||||
</key>
|
||||
<key name='preview-mode' enum='de.wolfvollprecht.UberWriter.PreviewMode'>
|
||||
<default>"full-width"</default>
|
||||
<summary>Preview mode</summary>
|
||||
<description>
|
||||
How to display the preview.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</schema>
|
||||
|
||||
|
|
|
@ -22,25 +22,11 @@
|
|||
/* Main window and text colors */
|
||||
|
||||
.uberwriter-window {
|
||||
/*border-radius: 7px 7px 3px 3px;*/
|
||||
background: @theme_base_color;
|
||||
color: @theme_fg_color;
|
||||
caret-color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.uberwriter-window .uberwriter-editor {
|
||||
font-family: 'Fira Mono', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uberwriter-window.small .uberwriter-editor {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uberwriter-window.large .uberwriter-editor {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#titlebar-revealer {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -55,10 +41,32 @@
|
|||
}
|
||||
|
||||
.uberwriter-editor {
|
||||
-gtk-key-bindings: editor-bindings;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
text-decoration-color: @error_color;
|
||||
-gtk-key-bindings: editor-bindings;
|
||||
font-family: 'Fira Mono', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uberwriter-editor.size14 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uberwriter-editor.size15 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.uberwriter-editor.size16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uberwriter-editor.size17 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.uberwriter-editor.size18 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.uberwriter-editor text {
|
||||
|
@ -84,9 +92,8 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.stats-counter {
|
||||
.inline-button {
|
||||
color: alpha(@theme_fg_color, 0.6);
|
||||
background-color: @theme_base_color;
|
||||
text-shadow: inherit;
|
||||
box-shadow: initial;
|
||||
background-clip: initial;
|
||||
|
@ -104,8 +111,8 @@
|
|||
transition: 100ms ease-in;
|
||||
}
|
||||
|
||||
.stats-counter:hover,
|
||||
.stats-counter:checked {
|
||||
.inline-button:hover,
|
||||
.inline-button:checked {
|
||||
color: @theme_fg_color;
|
||||
background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url("web/web__base.css");
|
||||
@import url("base.css");
|
||||
|
||||
:root {
|
||||
--text-color: #eeeeec;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url("web/web__base.css");
|
||||
@import url("base.css");
|
||||
|
||||
:root {
|
||||
--text-color: #3b3e45;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url("web/web__base.css");
|
||||
@import url("base.css");
|
||||
|
||||
:root {
|
||||
--text-color: #d3dae3;
|
||||
|
|
|
@ -1 +1 @@
|
|||
@import url("web/web_arc.css");
|
||||
@import url("arc.css");
|
||||
|
|
|
@ -45,7 +45,7 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
@media screen and (min-width: 1280px) {
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
@ -57,9 +57,9 @@ body {
|
|||
font-family: "Fira Sans", fira-sans, sans-serif, color-emoji;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
max-width: 978px;
|
||||
max-width: 980px;
|
||||
margin: auto;
|
||||
padding: 2em;
|
||||
padding: 4em;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url("web/web__base.css");
|
||||
@import url("base.css");
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url("web/web__base.css");
|
||||
@import url("base.css");
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -138,11 +138,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="format_label">
|
||||
<object class="GtkLabel" id="sync_scroll_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Input format</property>
|
||||
<property name="label" translatable="yes">Synchronize editor/preview scrolling</property>
|
||||
<property name="justify">right</property>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -151,17 +151,29 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_format_help_button">
|
||||
<object class="GtkSwitch" id="sync_scroll_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="image">help</property>
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="input_format_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Input format</property>
|
||||
<property name="justify">right</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="input_format_combobox">
|
||||
<property name="visible">True</property>
|
||||
|
@ -172,9 +184,24 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">4</property>
|
||||
<property name="top_attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_format_help_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="image">help</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkImage" id="pan-down">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">pan-down-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="preview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="preview_mode_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="GtkButton" id="preview_mode_button">
|
||||
<property name="label" translatable="yes">Full-Width</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Switch Preview Mode</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="image">pan-down</property>
|
||||
<property name="image_position">right</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<style>
|
||||
<class name="inline-button"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
|
@ -3,37 +3,32 @@
|
|||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<!-- interface-local-resource-path ../media -->
|
||||
<object class="GtkAdjustment" id="adjustment1">
|
||||
<property name="upper">100</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">10</property>
|
||||
<object class="GtkImage" id="edit-find-replace">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="amunt">
|
||||
<object class="GtkImage" id="go-up">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">go-up-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="avall">
|
||||
<object class="GtkImage" id="go_down">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">go-down-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="ortografia1">
|
||||
<object class="GtkImage" id="pan-down">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">pan-down-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="spell-check">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-spell-check</property>
|
||||
</object>
|
||||
<object class="GtkRecentFilter" id="recentfilter1">
|
||||
<mime-types>
|
||||
<mime-type>text/plain</mime-type>
|
||||
<mime-type>text/x-markdown</mime-type>
|
||||
</mime-types>
|
||||
</object>
|
||||
<object class="GtkImage" id="reemplaza">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkOverlay" id="FullscreenOverlay">
|
||||
<property name="name">FullscreenOverlay</property>
|
||||
<property name="visible">True</property>
|
||||
|
@ -43,10 +38,10 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_pained">
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="app_paintable">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="sidebar_box">
|
||||
<property name="width_request">200</property>
|
||||
|
@ -70,65 +65,75 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="grid2">
|
||||
<object class="GtkBox" id="content">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="stats_counter_revealer">
|
||||
<object class="GtkBox" id="editor">
|
||||
<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="GtkButton" id="stats_counter">
|
||||
<property name="label" translatable="yes">0 Words</property>
|
||||
<property name="visible">True</property>
|
||||
<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>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkViewport" id="editor_viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hscroll_policy">natural</property>
|
||||
<property name="vscroll_policy">natural</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
|
||||
<property name="height_request">500</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="vadjustment">adjustment1</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="editor_stats_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="GtkButton" id="editor_stats_button">
|
||||
<property name="label" translatable="yes">0 Words</property>
|
||||
<property name="visible">True</property>
|
||||
<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>
|
||||
<property name="image">pan-down</property>
|
||||
<property name="image_position">right</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<style>
|
||||
<class name="inline-button"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">False</property>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
@ -191,7 +196,7 @@
|
|||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous Match</property>
|
||||
<property name="image">amunt</property>
|
||||
<property name="image">go-up</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
@ -205,7 +210,7 @@
|
|||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Next Match</property>
|
||||
<property name="image">avall</property>
|
||||
<property name="image">go_down</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
@ -264,7 +269,7 @@
|
|||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Open Replace</property>
|
||||
<property name="image">reemplaza</property>
|
||||
<property name="image">edit-find-replace</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
@ -345,7 +350,7 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="image">ortografia1</property>
|
||||
<property name="image">spell-check</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
|
|
@ -1,5 +1,2 @@
|
|||
regex
|
||||
enchant
|
||||
pypandoc==1.4
|
||||
pyenchant
|
||||
pygtkspellcheck
|
||||
pypandoc==1.4
|
|
@ -19,7 +19,7 @@ import gi
|
|||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from uberwriter import window
|
||||
from uberwriter import main_window
|
||||
from uberwriter import application
|
||||
from uberwriter.helpers import set_up_logging
|
||||
from uberwriter.config import get_version
|
||||
|
|
|
@ -15,10 +15,12 @@ from gettext import gettext as _
|
|||
|
||||
import gi
|
||||
|
||||
from uberwriter.main_window import MainWindow
|
||||
|
||||
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import GLib, Gio, Gtk, GdkPixbuf
|
||||
|
||||
from uberwriter import window
|
||||
from uberwriter import main_window
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.helpers import set_up_logging
|
||||
from uberwriter.preferences_dialog import PreferencesDialog
|
||||
|
@ -120,10 +122,18 @@ class Application(Gtk.Application):
|
|||
|
||||
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))
|
||||
"stat_default", GLib.VariantType.new("s"), GLib.Variant.new_string(stat_default))
|
||||
action.connect("activate", self.on_stat_default)
|
||||
self.add_action(action)
|
||||
|
||||
# Preview Menu
|
||||
|
||||
preview_mode = self.settings.get_string("preview-mode")
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"preview_mode", GLib.VariantType.new("s"), GLib.Variant.new_string(preview_mode))
|
||||
action.connect("activate", self.on_preview_mode)
|
||||
self.add_action(action)
|
||||
|
||||
# Shortcuts
|
||||
|
||||
# TODO: be aware that a couple of shortcuts are defined in base.css
|
||||
|
@ -147,7 +157,7 @@ class Application(Gtk.Application):
|
|||
# Windows are associated with the application
|
||||
# when the last one is closed the application shuts down
|
||||
# self.window = Window(application=self, title="UberWriter")
|
||||
self.window = window.Window(self)
|
||||
self.window = MainWindow(self)
|
||||
if self.args:
|
||||
self.window.load_file(self.args[0])
|
||||
|
||||
|
@ -178,8 +188,12 @@ class Application(Gtk.Application):
|
|||
self.window.toggle_gradient_overlay(settings.get_value(key))
|
||||
elif key == "input-format":
|
||||
self.window.reload_preview()
|
||||
elif key == "sync-scroll":
|
||||
self.window.reload_preview(reshow=True)
|
||||
elif key == "stat-default":
|
||||
self.window.update_default_stat()
|
||||
elif key == "preview-mode":
|
||||
self.window.update_preview_mode()
|
||||
|
||||
def on_new(self, _action, _value):
|
||||
self.window.new_document()
|
||||
|
@ -250,6 +264,10 @@ class Application(Gtk.Application):
|
|||
action.set_state(value)
|
||||
self.settings.set_string("stat-default", value.get_string())
|
||||
|
||||
def on_preview_mode(self, action, value):
|
||||
action.set_state(value)
|
||||
self.settings.set_string("preview-mode", value.get_string())
|
||||
|
||||
# ~ if __name__ == "__main__":
|
||||
# ~ app = Application()
|
||||
# ~ app.run(sys.argv)
|
||||
|
|
|
@ -48,8 +48,32 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
|
|||
self.hb_container.add(self.hb_revealer)
|
||||
self.hb_container.show()
|
||||
|
||||
self.btns = buttons(app)
|
||||
pack_buttons(self.hb, self.btns)
|
||||
self.btns = main_buttons(app)
|
||||
pack_main_buttons(self.hb, self.btns)
|
||||
|
||||
self.hb.show_all()
|
||||
|
||||
|
||||
class PreviewHeaderbar:
|
||||
"""Sets up the preview headerbar
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.hb = Gtk.HeaderBar().new()
|
||||
self.hb.props.show_close_button = True
|
||||
self.hb.get_style_context().add_class("titlebar")
|
||||
|
||||
self.hb_revealer = Gtk.Revealer(name="titlebar-revealer")
|
||||
self.hb_revealer.add(self.hb)
|
||||
self.hb_revealer.props.transition_duration = 750
|
||||
self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
|
||||
self.hb_revealer.show()
|
||||
self.hb_revealer.set_reveal_child(True)
|
||||
|
||||
self.hb_container = Gtk.Frame(name="titlebar-container")
|
||||
self.hb_container.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
self.hb_container.add(self.hb_revealer)
|
||||
self.hb_container.show()
|
||||
|
||||
self.hb.show_all()
|
||||
|
||||
|
@ -70,14 +94,14 @@ class FullscreenHeaderbar:
|
|||
self.hb.show()
|
||||
self.events.hide()
|
||||
|
||||
self.btns = buttons(app)
|
||||
self.btns = main_buttons(app)
|
||||
|
||||
fs_btn_exit = Gtk.Button().new_from_icon_name("view-restore-symbolic",
|
||||
Gtk.IconSize.BUTTON)
|
||||
fs_btn_exit.set_tooltip_text(_("Exit Fullscreen"))
|
||||
fs_btn_exit.set_action_name("app.fullscreen")
|
||||
|
||||
pack_buttons(self.hb, self.btns, fs_btn_exit)
|
||||
pack_main_buttons(self.hb, self.btns, fs_btn_exit)
|
||||
|
||||
self.hb.show_all()
|
||||
|
||||
|
@ -101,7 +125,8 @@ class FullscreenHeaderbar:
|
|||
else:
|
||||
self.revealer.set_reveal_child(False)
|
||||
|
||||
def buttons(app):
|
||||
|
||||
def main_buttons(app):
|
||||
"""constructor for the headerbar buttons
|
||||
|
||||
Returns:
|
||||
|
@ -154,7 +179,7 @@ def buttons(app):
|
|||
return btn
|
||||
|
||||
|
||||
def pack_buttons(headerbar, btn, btn_exit=None):
|
||||
def pack_main_buttons(headerbar, btn, btn_exit=None):
|
||||
"""Pack the given buttons in the given headerbar
|
||||
|
||||
Arguments:
|
||||
|
|
|
@ -55,7 +55,7 @@ def get_builder(builder_file_name):
|
|||
def path_to_file(path):
|
||||
"""Return a file path (file:///) for the given path"""
|
||||
|
||||
return "file:///" + path
|
||||
return "file://" + path
|
||||
|
||||
|
||||
def get_media_file(media_file_path):
|
||||
|
|
|
@ -28,9 +28,11 @@ from urllib.parse import unquote
|
|||
|
||||
import gi
|
||||
|
||||
from uberwriter.text_view_markup_handler import MarkupHandler
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
|
||||
from uberwriter import latex_to_PNG, text_view_markup_handler
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf
|
||||
from uberwriter import latex_to_PNG
|
||||
from uberwriter.settings import Settings
|
||||
|
||||
from uberwriter.fix_table import FixTable
|
||||
|
@ -360,8 +362,8 @@ class InlinePreview:
|
|||
|
||||
text = self.text_buffer.get_text(start_iter, end_iter, False)
|
||||
|
||||
math = text_view_markup_handler.regex["MATH"]
|
||||
link = text_view_markup_handler.regex["LINK"]
|
||||
math = MarkupHandler.regex["MATH"]
|
||||
link = MarkupHandler.regex["LINK"]
|
||||
|
||||
footnote = re.compile(r'\[\^([^\s]+?)\]')
|
||||
image = re.compile(r"!\[(.*?)\]\((.+?)\)")
|
||||
|
@ -455,8 +457,7 @@ class InlinePreview:
|
|||
path = path[7:]
|
||||
elif not path.startswith("/"):
|
||||
# then the path is relative
|
||||
base_path = self.settings.get_value(
|
||||
"open-file-path").get_string()
|
||||
base_path = self.settings.get_string("open-file-path")
|
||||
path = base_path + "/" + path
|
||||
|
||||
LOGGER.info(path)
|
||||
|
|
|
@ -19,19 +19,18 @@ import locale
|
|||
import logging
|
||||
import os
|
||||
import urllib
|
||||
import webbrowser
|
||||
from gettext import gettext as _
|
||||
|
||||
import gi
|
||||
|
||||
from uberwriter.export_dialog import Export
|
||||
from uberwriter.preview_handler import PreviewHandler
|
||||
from uberwriter.stats_handler import StatsHandler
|
||||
from uberwriter.styled_window import StyledWindow
|
||||
from uberwriter.text_view import TextView
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('WebKit2', '4.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
|
||||
from gi.repository import WebKit2 as WebKit
|
||||
|
||||
import cairo
|
||||
|
||||
|
@ -54,7 +53,7 @@ LOGGER = logging.getLogger('uberwriter')
|
|||
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
|
||||
|
||||
|
||||
class Window(Gtk.ApplicationWindow):
|
||||
class MainWindow(StyledWindow):
|
||||
__gsignals__ = {
|
||||
'save-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'open-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||
|
@ -68,17 +67,17 @@ class Window(Gtk.ApplicationWindow):
|
|||
def __init__(self, app):
|
||||
"""Set up the main window"""
|
||||
|
||||
Gtk.ApplicationWindow.__init__(self,
|
||||
application=Gio.Application.get_default(),
|
||||
title="Uberwriter")
|
||||
super().__init__(application=Gio.Application.get_default(), title="Uberwriter")
|
||||
|
||||
self.get_style_context().add_class('uberwriter-window')
|
||||
|
||||
# Set UI
|
||||
self.builder = get_builder('Window')
|
||||
root = self.builder.get_object("FullscreenOverlay")
|
||||
root.connect('style-updated', self.apply_current_theme)
|
||||
builder = get_builder('Window')
|
||||
root = builder.get_object("FullscreenOverlay")
|
||||
self.connect("delete-event", self.on_delete_called)
|
||||
self.add(root)
|
||||
|
||||
self.set_default_size(900, 500)
|
||||
self.set_default_size(1000, 600)
|
||||
|
||||
# Preferences
|
||||
self.settings = Settings.new()
|
||||
|
@ -86,7 +85,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
# Headerbars
|
||||
self.headerbar = headerbars.MainHeaderbar(app)
|
||||
self.set_titlebar(self.headerbar.hb_container)
|
||||
self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app)
|
||||
self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app)
|
||||
|
||||
self.title_end = " – UberWriter"
|
||||
self.set_headerbar_title("New File" + self.title_end)
|
||||
|
@ -99,28 +98,26 @@ class Window(Gtk.ApplicationWindow):
|
|||
self.accel_group = Gtk.AccelGroup()
|
||||
self.add_accel_group(self.accel_group)
|
||||
|
||||
self.scrolled_window = builder.get_object('editor_scrolledwindow')
|
||||
|
||||
# Setup text editor
|
||||
self.text_view = TextView()
|
||||
self.text_view.props.halign = Gtk.Align.CENTER
|
||||
self.text_view = TextView(self.settings.get_int("characters-per-line"))
|
||||
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()
|
||||
|
||||
# Setup preview webview
|
||||
self.preview_webview = None
|
||||
|
||||
self.scrolled_window = self.builder.get_object('editor_scrolledwindow')
|
||||
self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window')
|
||||
self.scrolled_window.add(self.text_view)
|
||||
self.editor_viewport = self.builder.get_object('editor_viewport')
|
||||
|
||||
# Stats counter
|
||||
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')
|
||||
# Setup stats counter
|
||||
self.stats_revealer = builder.get_object('editor_stats_revealer')
|
||||
self.stats_button = builder.get_object('editor_stats_button')
|
||||
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
|
||||
|
||||
# Setup preview
|
||||
content = builder.get_object('content')
|
||||
editor = builder.get_object('editor')
|
||||
self.preview_handler = PreviewHandler(self, content, editor, self.text_view)
|
||||
|
||||
# Setup header/stats bar hide after 3 seconds
|
||||
self.top_bottom_bars_visible = True
|
||||
self.was_motion = True
|
||||
|
@ -142,8 +139,8 @@ class Window(Gtk.ApplicationWindow):
|
|||
###
|
||||
# Sidebar initialization test
|
||||
###
|
||||
self.paned_window = self.builder.get_object("main_pained")
|
||||
self.sidebar_box = self.builder.get_object("sidebar_box")
|
||||
self.paned_window = builder.get_object("main_paned")
|
||||
self.sidebar_box = builder.get_object("sidebar_box")
|
||||
self.sidebar = Sidebar(self)
|
||||
self.sidebar_box.hide()
|
||||
|
||||
|
@ -151,40 +148,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
# Search and replace initialization
|
||||
# Same interface as Sidebar ;)
|
||||
###
|
||||
self.searchreplace = SearchAndReplace(self, self.text_view)
|
||||
|
||||
# Window resize
|
||||
self.window_resize(self)
|
||||
self.connect("configure-event", self.window_resize)
|
||||
self.connect("delete-event", self.on_delete_called)
|
||||
|
||||
# Set current theme
|
||||
self.apply_current_theme()
|
||||
self.get_style_context().add_class('uberwriter-window')
|
||||
|
||||
def apply_current_theme(self, *_):
|
||||
"""Adjusts the window, CSD and preview for the current theme.
|
||||
"""
|
||||
# Get current theme
|
||||
theme, changed = Theme.get_current_changed()
|
||||
if changed:
|
||||
# Set theme variant (dark/light)
|
||||
Gtk.Settings.get_default().set_property(
|
||||
"gtk-application-prefer-dark-theme",
|
||||
GLib.Variant("b", theme.is_dark))
|
||||
|
||||
# Set theme css
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(helpers.get_css_path("gtk/base.css"))
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
self.get_screen(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
# Reload preview if it exists
|
||||
self.reload_preview()
|
||||
|
||||
# Redraw contents of window
|
||||
self.queue_draw()
|
||||
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
|
||||
|
||||
def on_text_changed(self, *_args):
|
||||
"""called when the text changes, sets the self.did_change to true and
|
||||
|
@ -209,19 +173,16 @@ class Window(Gtk.ApplicationWindow):
|
|||
if state.get_boolean():
|
||||
self.fullscreen()
|
||||
self.fs_headerbar.events.show()
|
||||
|
||||
else:
|
||||
self.unfullscreen()
|
||||
self.fs_headerbar.events.hide()
|
||||
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def set_focus_mode(self, state):
|
||||
"""toggle focusmode
|
||||
"""
|
||||
|
||||
focus_mode = state.get_boolean()
|
||||
self.text_view.set_focus_mode(focus_mode)
|
||||
self.text_view.set_focus_mode(state.get_boolean())
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def set_hemingway_mode(self, state):
|
||||
|
@ -231,47 +192,21 @@ class Window(Gtk.ApplicationWindow):
|
|||
self.text_view.set_hemingway_mode(state.get_boolean())
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def window_resize(self, window, event=None):
|
||||
"""set paddings dependant of the window size
|
||||
def toggle_preview(self, state):
|
||||
"""Toggle the preview mode
|
||||
|
||||
Arguments:
|
||||
state {gtk bool} -- Desired state of the preview mode (enabled/disabled)
|
||||
"""
|
||||
|
||||
# Ensure the window receiving the event is the one we care about, ie. the main window.
|
||||
# On Wayland (bug?), sub-windows such as the recents popover will also trigger this.
|
||||
if event and event.window != window.get_window():
|
||||
return
|
||||
|
||||
# Adjust text editor width depending on window width, so that:
|
||||
# - The number of characters per line is adequate (http://webtypography.net/2.1.2)
|
||||
# - The number of characters stays constant while resizing the window / font
|
||||
# - There is enough text margin for MarkupBuffer to apply indents / negative margins
|
||||
#
|
||||
# TODO: Avoid hard-coding. Font size is clearer than unclear dimensions, but not ideal.
|
||||
w_width = event.width if event else window.get_allocation().width
|
||||
if w_width < 900:
|
||||
font_size = 14
|
||||
self.get_style_context().add_class("small")
|
||||
self.get_style_context().remove_class("large")
|
||||
|
||||
elif w_width < 1280:
|
||||
font_size = 16
|
||||
self.get_style_context().remove_class("small")
|
||||
self.get_style_context().remove_class("large")
|
||||
|
||||
if state.get_boolean():
|
||||
self.text_view.grab_focus()
|
||||
self.preview_handler.show()
|
||||
else:
|
||||
font_size = 18
|
||||
self.get_style_context().remove_class("small")
|
||||
self.get_style_context().add_class("large")
|
||||
self.preview_handler.hide()
|
||||
self.text_view.grab_focus()
|
||||
|
||||
font_width = int(font_size * 1/1.6) # Ratio specific to Fira Mono
|
||||
width = 67 * font_width - 1 # 66 characters
|
||||
horizontal_margin = 8 * font_width # 8 characters
|
||||
width_request = width + horizontal_margin * 2
|
||||
|
||||
if self.text_view.props.width_request != width_request:
|
||||
self.text_view.props.width_request = width_request
|
||||
self.text_view.set_left_margin(horizontal_margin)
|
||||
self.text_view.set_right_margin(horizontal_margin)
|
||||
self.scrolled_window.props.width_request = width_request
|
||||
return True
|
||||
|
||||
# TODO: refactorizable
|
||||
def save_document(self, _widget=None, _data=None):
|
||||
|
@ -467,6 +402,9 @@ class Window(Gtk.ApplicationWindow):
|
|||
def update_default_stat(self):
|
||||
self.stats_handler.update_default_stat()
|
||||
|
||||
def update_preview_mode(self):
|
||||
self.preview_handler.update_preview_mode()
|
||||
|
||||
def menu_toggle_sidebar(self, _widget=None):
|
||||
"""WIP
|
||||
"""
|
||||
|
@ -479,9 +417,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
status {gtk bool} -- Desired status of the spellchecking
|
||||
"""
|
||||
|
||||
self.text_view.gspell_view\
|
||||
.set_inline_spell_checking(state.get_boolean()
|
||||
and not self.text_view.focus_mode)
|
||||
self.text_view.set_spellcheck(state.get_boolean())
|
||||
|
||||
def toggle_gradient_overlay(self, state):
|
||||
"""Toggle the gradient overlay
|
||||
|
@ -495,58 +431,8 @@ class Window(Gtk.ApplicationWindow):
|
|||
elif self.overlay_id:
|
||||
self.scrolled_window.disconnect(self.overlay_id)
|
||||
|
||||
def toggle_preview(self, state):
|
||||
"""Toggle the preview mode
|
||||
|
||||
Arguments:
|
||||
state {gtk bool} -- Desired state of the preview mode (enabled/disabled)
|
||||
"""
|
||||
|
||||
if state.get_boolean():
|
||||
self.show_preview()
|
||||
else:
|
||||
self.show_text_editor()
|
||||
|
||||
return True
|
||||
|
||||
def show_text_editor(self):
|
||||
self.scrolled_window.remove(self.scrolled_window.get_child())
|
||||
self.scrolled_window.add(self.text_view)
|
||||
self.text_view.show()
|
||||
self.preview_webview.destroy()
|
||||
self.preview_webview = None
|
||||
self.queue_draw()
|
||||
|
||||
def show_preview(self, loaded=False):
|
||||
if loaded:
|
||||
self.scrolled_window.remove(self.scrolled_window.get_child())
|
||||
self.scrolled_window.add(self.preview_webview)
|
||||
self.preview_webview.show()
|
||||
self.queue_draw()
|
||||
else:
|
||||
args = ['--standalone',
|
||||
'--mathjax',
|
||||
'--css=' + Theme.get_current().web_css_path,
|
||||
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
|
||||
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
|
||||
output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args)
|
||||
|
||||
if self.preview_webview is None:
|
||||
self.preview_webview = WebKit.WebView()
|
||||
self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.preview_webview.connect("load-changed", self.on_preview_load_change)
|
||||
|
||||
# This saying that all links will be opened in default browser, \
|
||||
# but local files are opened in appropriate apps:
|
||||
self.preview_webview.connect("decide-policy", self.on_click_link)
|
||||
|
||||
self.preview_webview.load_html(output, 'file://localhost/')
|
||||
|
||||
def reload_preview(self):
|
||||
if self.preview_webview:
|
||||
self.show_preview()
|
||||
def reload_preview(self, reshow=False):
|
||||
self.preview_handler.reload(reshow=reshow)
|
||||
|
||||
def load_file(self, filename=None):
|
||||
"""Open File from command line or open / open recent etc."""
|
||||
|
@ -617,15 +503,10 @@ class Window(Gtk.ApplicationWindow):
|
|||
True -- Gtk things
|
||||
"""
|
||||
|
||||
if (self.was_motion is False
|
||||
and self.top_bottom_bars_visible
|
||||
if (not self.was_motion
|
||||
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.stats_counter_revealer.set_reveal_child(False)
|
||||
self.headerbar.hb_revealer.set_reveal_child(False)
|
||||
self.top_bottom_bars_visible = False
|
||||
self.buffer_modified_for_status_bar = False
|
||||
and self.text_view.props.has_focus):
|
||||
self.reveal_top_bottom_bars(False)
|
||||
|
||||
self.was_motion = False
|
||||
return True
|
||||
|
@ -643,24 +524,22 @@ class Window(Gtk.ApplicationWindow):
|
|||
return
|
||||
if now - self.timestamp_last_mouse_motion > 100:
|
||||
# react on motion by fading in headerbar and statusbar
|
||||
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.top_bottom_bars_visible = True
|
||||
self.buffer_modified_for_status_bar = False
|
||||
# self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True)
|
||||
self.reveal_top_bottom_bars(True)
|
||||
self.was_motion = True
|
||||
|
||||
def focus_out(self, _widget, _data=None):
|
||||
"""events called when the window losses focus
|
||||
"""
|
||||
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.top_bottom_bars_visible = True
|
||||
self.buffer_modified_for_status_bar = False
|
||||
self.reveal_top_bottom_bars(True)
|
||||
|
||||
def reveal_top_bottom_bars(self, reveal):
|
||||
if self.top_bottom_bars_visible != reveal:
|
||||
self.headerbar.hb_revealer.set_reveal_child(reveal)
|
||||
self.stats_revealer.set_reveal_child(reveal)
|
||||
for revealer in self.preview_handler.get_top_bottom_bar_revealers():
|
||||
revealer.set_reveal_child(reveal)
|
||||
self.top_bottom_bars_visible = reveal
|
||||
self.buffer_modified_for_status_bar = reveal
|
||||
|
||||
def draw_gradient(self, _widget, cr):
|
||||
"""draw fading gradient over the top and the bottom of the
|
||||
|
@ -726,18 +605,4 @@ class Window(Gtk.ApplicationWindow):
|
|||
else:
|
||||
self.filename = None
|
||||
base_path = "/"
|
||||
self.settings.set_value("open-file-path", GLib.Variant("s", base_path))
|
||||
|
||||
def on_preview_load_change(self, webview, event):
|
||||
"""swaps text editor with preview once the load is complete
|
||||
"""
|
||||
if event == WebKit.LoadEvent.FINISHED:
|
||||
self.show_preview(loaded=True)
|
||||
|
||||
def on_click_link(self, web_view, decision, _decision_type):
|
||||
"""provide ability for self.webview to open links in default browser
|
||||
"""
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
webbrowser.open(web_view.get_uri())
|
||||
decision.ignore()
|
||||
return True # Don't let the event "bubble up"
|
||||
self.settings.set_string("open-file-path", base_path)
|
|
@ -82,8 +82,12 @@ class PreferencesDialog:
|
|||
self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay"))
|
||||
self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay)
|
||||
|
||||
self.sync_scroll_switch = self.builder.get_object("sync_scroll_switch")
|
||||
self.sync_scroll_switch.set_active(self.settings.get_value("sync-scroll"))
|
||||
self.sync_scroll_switch.connect("state-set", self.on_sync_scroll)
|
||||
|
||||
input_format_store = Gtk.ListStore(int, str)
|
||||
input_format = self.settings.get_value("input-format").get_string()
|
||||
input_format = self.settings.get_string("input-format")
|
||||
input_format_active = 0
|
||||
for i, fmt in enumerate(self.formats):
|
||||
input_format_store.append([i, fmt["name"]])
|
||||
|
@ -107,28 +111,32 @@ class PreferencesDialog:
|
|||
preferences_window.show()
|
||||
|
||||
def on_dark_mode_auto(self, _, state):
|
||||
self.settings.set_value("dark-mode-auto", GLib.Variant.new_boolean(state))
|
||||
self.settings.set_boolean("dark-mode-auto", state)
|
||||
if state and self.dark_mode_switch.get_active():
|
||||
self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False))
|
||||
return False
|
||||
|
||||
def on_dark_mode(self, _, state):
|
||||
self.settings.set_value("dark-mode", GLib.Variant.new_boolean(state))
|
||||
self.settings.set_boolean("dark-mode", state)
|
||||
if state and self.dark_mode_auto_switch.get_active():
|
||||
self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False))
|
||||
return False
|
||||
|
||||
def on_spellcheck(self, _, state):
|
||||
self.settings.set_value("spellcheck", GLib.Variant.new_boolean(state))
|
||||
self.settings.set_boolean("spellcheck", state)
|
||||
return False
|
||||
|
||||
def on_gradient_overlay(self, _, state):
|
||||
self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(state))
|
||||
self.settings.set_boolean("gradient-overlay", state)
|
||||
return False
|
||||
|
||||
def on_sync_scroll(self, _, state):
|
||||
self.settings.set_boolean("sync-scroll", state)
|
||||
return False
|
||||
|
||||
def on_input_format(self, combobox):
|
||||
fmt = self.formats[combobox.get_active()]
|
||||
self.settings.set_value("input-format", GLib.Variant.new_string(fmt["format"]))
|
||||
self.settings.set_string("input-format", fmt["format"])
|
||||
|
||||
def on_input_format_help(self, _):
|
||||
fmt = self.formats[self.input_format_combobox.get_active()]
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from uberwriter import helpers
|
||||
from uberwriter.theme import Theme
|
||||
|
||||
|
||||
class PreviewConverter:
|
||||
"""Converts markdown to html using a background thread."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.queue = Queue()
|
||||
worker = Thread(target=self.__do_convert, name="preview-converter")
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
|
||||
def convert(self, text, callback, *user_data):
|
||||
"""Converts text to html, calling callback when done.
|
||||
|
||||
The callback argument contains the result."""
|
||||
|
||||
self.queue.put((text, callback, user_data))
|
||||
|
||||
def stop(self):
|
||||
"""Stops the background worker. PreviewConverter shouldn't be used after this."""
|
||||
|
||||
self.queue.put((None, None))
|
||||
|
||||
def __do_convert(self):
|
||||
while True:
|
||||
while True:
|
||||
(text, callback, user_data) = self.queue.get()
|
||||
if text is None and callback is None:
|
||||
return
|
||||
if self.queue.empty():
|
||||
break
|
||||
|
||||
args = ['--standalone',
|
||||
'--mathjax',
|
||||
'--css=' + Theme.get_current().web_css_path,
|
||||
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
|
||||
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
|
||||
text = helpers.pandoc_convert(text, to="html5", args=args)
|
||||
|
||||
GLib.idle_add(callback, text, *user_data)
|
|
@ -0,0 +1,166 @@
|
|||
import math
|
||||
import webbrowser
|
||||
from enum import auto, IntEnum
|
||||
|
||||
import gi
|
||||
|
||||
from uberwriter.helpers import get_builder
|
||||
from uberwriter.preview_renderer import PreviewRenderer
|
||||
from uberwriter.settings import Settings
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2, GLib
|
||||
|
||||
from uberwriter.preview_converter import PreviewConverter
|
||||
from uberwriter.web_view import WebView
|
||||
|
||||
|
||||
class Step(IntEnum):
|
||||
CONVERT_HTML = auto()
|
||||
LOAD_WEBVIEW = auto()
|
||||
RENDER = auto()
|
||||
|
||||
|
||||
class PreviewHandler:
|
||||
"""Handles showing/hiding the preview, and allows the user to toggle between modes.
|
||||
|
||||
The rendering itself is handled by `PreviewRendered`. This class handles conversion/loading and
|
||||
connects it all together (including synchronization, ie. text changes, scroll)."""
|
||||
|
||||
def __init__(self, window, content, editor, text_view):
|
||||
self.text_view = text_view
|
||||
|
||||
self.web_view = None
|
||||
self.web_view_pending_html = None
|
||||
|
||||
builder = get_builder("Preview")
|
||||
preview = builder.get_object("preview")
|
||||
mode_button = builder.get_object("preview_mode_button")
|
||||
self.mode_revealer = builder.get_object("preview_mode_revealer")
|
||||
|
||||
self.preview_converter = PreviewConverter()
|
||||
self.preview_renderer = PreviewRenderer(
|
||||
window, content, editor, text_view, preview, self.mode_revealer, mode_button)
|
||||
|
||||
window.connect("style-updated", self.reload)
|
||||
|
||||
self.text_changed_handler_id = None
|
||||
|
||||
self.settings = Settings.new()
|
||||
self.web_scroll_handler_id = None
|
||||
self.text_scroll_handler_id = None
|
||||
|
||||
self.loading = False
|
||||
self.shown = False
|
||||
|
||||
def show(self):
|
||||
self.__show()
|
||||
|
||||
def __show(self, html=None, step=Step.CONVERT_HTML):
|
||||
if step == Step.CONVERT_HTML:
|
||||
# First step: convert text to HTML.
|
||||
buf = self.text_view.get_buffer()
|
||||
self.preview_converter.convert(
|
||||
buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False),
|
||||
self.__show, Step.LOAD_WEBVIEW)
|
||||
|
||||
elif step == Step.LOAD_WEBVIEW:
|
||||
# Second step: load HTML.
|
||||
self.loading = True
|
||||
|
||||
if not self.web_view:
|
||||
self.web_view = WebView()
|
||||
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.web_view.connect("load-changed", self.on_load_changed)
|
||||
|
||||
# All links will be opened in default browser, but local files are opened in apps.
|
||||
self.web_view.connect("decide-policy", self.on_click_link)
|
||||
|
||||
if self.web_view.is_loading():
|
||||
self.web_view_pending_html = html
|
||||
else:
|
||||
self.web_view.load_html(html, "file://localhost/")
|
||||
|
||||
elif step == Step.RENDER:
|
||||
# Last step: show the preview. This is a one-time step.
|
||||
if self.shown:
|
||||
return
|
||||
self.shown = True
|
||||
|
||||
self.text_changed_handler_id = \
|
||||
self.text_view.get_buffer().connect("changed", self.__show)
|
||||
|
||||
GLib.idle_add(self.web_view.set_scroll_scale, self.text_view.get_scroll_scale())
|
||||
|
||||
self.preview_renderer.show(self.web_view)
|
||||
|
||||
if self.settings.get_boolean("sync-scroll"):
|
||||
self.web_scroll_handler_id = \
|
||||
self.web_view.connect("scroll-scale-changed", self.on_web_view_scrolled)
|
||||
self.text_scroll_handler_id = \
|
||||
self.text_view.connect("scroll-scale-changed", self.on_text_view_scrolled)
|
||||
|
||||
def reload(self, *_widget, reshow=False):
|
||||
if self.shown:
|
||||
if reshow:
|
||||
self.hide()
|
||||
self.show()
|
||||
|
||||
def hide(self):
|
||||
if self.shown:
|
||||
self.shown = False
|
||||
|
||||
self.text_view.get_buffer().disconnect(self.text_changed_handler_id)
|
||||
|
||||
GLib.idle_add(self.text_view.set_scroll_scale, self.web_view.get_scroll_scale())
|
||||
|
||||
self.preview_renderer.hide(self.web_view)
|
||||
|
||||
if self.text_scroll_handler_id:
|
||||
self.text_view.disconnect(self.text_scroll_handler_id)
|
||||
self.text_scroll_handler_id = None
|
||||
if self.web_scroll_handler_id:
|
||||
self.web_view.disconnect(self.web_scroll_handler_id)
|
||||
self.web_scroll_handler_id = None
|
||||
|
||||
if self.loading:
|
||||
self.loading = False
|
||||
|
||||
self.web_view.destroy()
|
||||
self.web_view = None
|
||||
|
||||
def update_preview_mode(self):
|
||||
self.preview_renderer.update_mode(self.web_view)
|
||||
|
||||
def get_top_bottom_bar_revealers(self):
|
||||
if self.shown and not self.preview_renderer.window:
|
||||
return [self.mode_revealer]
|
||||
else:
|
||||
return []
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
if event == WebKit2.LoadEvent.FINISHED:
|
||||
self.loading = False
|
||||
if self.web_view_pending_html:
|
||||
self.__show(html=self.web_view_pending_html, step=Step.LOAD_WEBVIEW)
|
||||
self.web_view_pending_html = None
|
||||
else:
|
||||
self.__show(step=Step.RENDER)
|
||||
|
||||
def on_text_view_scrolled(self, _text_view, scale):
|
||||
if self.shown and not math.isclose(scale, self.web_view.get_scroll_scale(), rel_tol=1e-5):
|
||||
self.web_view.set_scroll_scale(scale)
|
||||
|
||||
def on_web_view_scrolled(self, _web_view, scale):
|
||||
if self.shown and self.text_view.get_mapped() and \
|
||||
not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5):
|
||||
self.text_view.set_scroll_scale(scale)
|
||||
|
||||
@staticmethod
|
||||
def on_click_link(web_view, decision, _decision_type):
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
webbrowser.open(web_view.get_uri())
|
||||
decision.ignore()
|
||||
return True
|
|
@ -0,0 +1,174 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from gi.repository import Gtk, Gio, GLib
|
||||
|
||||
from uberwriter import headerbars
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.styled_window import StyledWindow
|
||||
|
||||
|
||||
class PreviewRenderer:
|
||||
"""Renders the preview according to the user selected mode."""
|
||||
|
||||
# Must match the order/index defined in gschema.xml
|
||||
FULL_WIDTH = 0
|
||||
HALF_WIDTH = 1
|
||||
HALF_HEIGHT = 2
|
||||
WINDOWED = 3
|
||||
|
||||
def __init__(
|
||||
self, main_window, content, editor, text_view, preview, mode_revealer, mode_button):
|
||||
self.main_window = main_window
|
||||
self.main_window.connect("delete-event", self.on_window_closed)
|
||||
self.content = content
|
||||
self.editor = editor
|
||||
self.text_view = text_view
|
||||
self.preview = preview
|
||||
self.mode_revealer = mode_revealer
|
||||
self.mode_button = mode_button
|
||||
self.mode_button.connect("clicked", self.show_mode_popover)
|
||||
|
||||
self.settings = Settings.new()
|
||||
self.popover = None
|
||||
self.window = None
|
||||
self.headerbar = None
|
||||
|
||||
self.mode = self.settings.get_enum("preview-mode")
|
||||
self.update_mode()
|
||||
|
||||
def show(self, web_view):
|
||||
"""Show the preview, depending on the currently selected mode."""
|
||||
|
||||
# Windowed preview: create a window and show the preview in it.
|
||||
if self.mode == self.WINDOWED:
|
||||
# Create transient window of the main window.
|
||||
self.window = StyledWindow(application=self.main_window.get_application())
|
||||
self.window.connect("delete-event", self.on_window_closed)
|
||||
|
||||
# Create a custom header bar and move the mode button there.
|
||||
headerbar = headerbars.PreviewHeaderbar()
|
||||
self.headerbar = headerbar.hb
|
||||
self.headerbar.set_title(_("Preview"))
|
||||
self.mode_button.get_style_context().remove_class("inline-button")
|
||||
self.mode_revealer.remove(self.mode_button)
|
||||
self.headerbar.pack_end(self.mode_button)
|
||||
self.window.set_titlebar(headerbar.hb_container)
|
||||
|
||||
# Position it next to the main window.
|
||||
width, height = self.main_window.get_size()
|
||||
self.window.resize(width, height)
|
||||
x, y = self.main_window.get_position()
|
||||
if x is not None and y is not None:
|
||||
self.main_window.move(x, y)
|
||||
self.window.move(x + width + 16, y)
|
||||
|
||||
# Add webview and show.
|
||||
self.window.add(web_view)
|
||||
self.window.show()
|
||||
|
||||
else:
|
||||
self.preview.pack_start(web_view, True, True, 0)
|
||||
self.content.add(self.preview)
|
||||
|
||||
# Full-width preview: swap editor with preview.
|
||||
if self.mode == self.FULL_WIDTH:
|
||||
self.content.remove(self.editor)
|
||||
|
||||
# Half-width preview: set horizontal orientation and add the preview.
|
||||
# Ask for a minimum width that respects the editor's minimum requirements.
|
||||
elif self.mode == self.HALF_WIDTH:
|
||||
self.content.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.content.set_size_request(self.text_view.get_min_width() * 2, -1)
|
||||
|
||||
# Half-height preview: set vertical orientation and add the preview.
|
||||
# Ask for a minimum height that provides a comfortable experience.
|
||||
elif self.mode == self.HALF_HEIGHT:
|
||||
self.content.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.content.set_size_request(-1, 768)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(self.mode))
|
||||
|
||||
web_view.show()
|
||||
|
||||
def hide(self, web_view):
|
||||
"""Hide the preview, depending on the currently selected mode."""
|
||||
|
||||
# Windowed preview: remove preview and destroy window.
|
||||
if self.mode == self.WINDOWED:
|
||||
self.main_window.present()
|
||||
self.headerbar.remove(self.mode_button)
|
||||
self.mode_button.get_style_context().add_class("inline-button")
|
||||
self.mode_revealer.add(self.mode_button)
|
||||
self.headerbar = None
|
||||
self.window.remove(web_view)
|
||||
self.window.destroy()
|
||||
self.window = None
|
||||
|
||||
else:
|
||||
self.preview.remove(web_view)
|
||||
self.content.remove(self.preview)
|
||||
|
||||
# Full-width preview: swap preview with editor.
|
||||
if self.mode == self.FULL_WIDTH:
|
||||
self.content.add(self.editor)
|
||||
|
||||
# Half-width/height previews: remove preview and reset size requirements.
|
||||
elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT:
|
||||
self.content.set_size_request(-1, -1)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(self.mode))
|
||||
|
||||
def update_mode(self, web_view=None):
|
||||
"""Update preview mode, adjusting the mode button and the preview itself."""
|
||||
|
||||
mode = self.settings.get_enum("preview-mode")
|
||||
if web_view and mode != self.mode:
|
||||
self.hide(web_view)
|
||||
self.mode = mode
|
||||
self.show(web_view)
|
||||
else:
|
||||
self.mode = mode
|
||||
if self.mode_button:
|
||||
text = self.get_text_for_preview_mode(self.mode)
|
||||
self.mode_button.set_label(text)
|
||||
if self.popover:
|
||||
self.popover.popdown()
|
||||
|
||||
def show_mode_popover(self, button):
|
||||
"""Show preview mode popover."""
|
||||
|
||||
self.mode_button.set_state_flags(Gtk.StateFlags.CHECKED, False)
|
||||
|
||||
menu = Gio.Menu()
|
||||
modes = self.settings.props.settings_schema.get_key("preview-mode").get_range()[1]
|
||||
for i, mode in enumerate(modes):
|
||||
menu_item = Gio.MenuItem.new(self.get_text_for_preview_mode(i), None)
|
||||
menu_item.set_action_and_target_value("app.preview_mode", GLib.Variant.new_string(mode))
|
||||
menu.append_item(menu_item)
|
||||
self.popover = Gtk.Popover.new_from_model(button, menu)
|
||||
self.popover.connect('closed', self.on_popover_closed)
|
||||
self.popover.popup()
|
||||
|
||||
def on_popover_closed(self, _popover):
|
||||
self.mode_button.unset_state_flags(Gtk.StateFlags.CHECKED)
|
||||
|
||||
self.popover = None
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def on_window_closed(self, window, _event):
|
||||
preview_action = window.get_application().lookup_action("preview")
|
||||
preview_action.change_state(GLib.Variant.new_boolean(False))
|
||||
|
||||
def get_text_for_preview_mode(self, mode):
|
||||
if mode == self.FULL_WIDTH:
|
||||
return _("Full-Width")
|
||||
elif mode == self.HALF_WIDTH:
|
||||
return _("Half-Width")
|
||||
elif mode == self.HALF_HEIGHT:
|
||||
return _("Half-Height")
|
||||
elif mode == self.WINDOWED:
|
||||
return _("Windowed")
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(mode))
|
|
@ -1,48 +0,0 @@
|
|||
class Scroller:
|
||||
def __init__(self, scrolled_window, source_pos, target_pos):
|
||||
super().__init__()
|
||||
|
||||
self.scrolled_window = scrolled_window
|
||||
self.source_pos = source_pos
|
||||
self.target_pos = target_pos
|
||||
self.duration = max(200, (target_pos - source_pos) / 50) * 1000
|
||||
|
||||
self.is_started = False
|
||||
self.is_setup = False
|
||||
self.start_time = 0
|
||||
self.end_time = 0
|
||||
self.tick_callback_id = 0
|
||||
|
||||
def start(self):
|
||||
self.is_started = True
|
||||
self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick)
|
||||
|
||||
def end(self):
|
||||
self.scrolled_window.remove_tick_callback(self.tick_callback_id)
|
||||
self.is_started = False
|
||||
|
||||
def setup(self, time):
|
||||
self.start_time = time
|
||||
self.end_time = time + self.duration
|
||||
self.is_setup = True
|
||||
|
||||
def on_tick(self, widget, frame_clock):
|
||||
def ease_out_cubic(value):
|
||||
return pow(value - 1, 3) + 1
|
||||
|
||||
now = frame_clock.get_frame_time()
|
||||
if not self.is_setup:
|
||||
self.setup(now)
|
||||
|
||||
if now < self.end_time:
|
||||
time = float(now - self.start_time) / float(self.end_time - self.start_time)
|
||||
else:
|
||||
time = 1
|
||||
self.end()
|
||||
|
||||
time = ease_out_cubic(time)
|
||||
pos = self.source_pos + (time * (self.target_pos - self.source_pos))
|
||||
widget.get_vadjustment().props.value = pos
|
||||
return True
|
||||
|
||||
|
|
@ -33,32 +33,32 @@ class SearchAndReplace:
|
|||
uberwriter
|
||||
"""
|
||||
|
||||
def __init__(self, parentwindow, textview):
|
||||
def __init__(self, parentwindow, textview, builder):
|
||||
self.parentwindow = parentwindow
|
||||
self.textview = textview
|
||||
self.textbuffer = textview.get_buffer()
|
||||
|
||||
self.box = parentwindow.builder.get_object("searchbar_placeholder")
|
||||
self.box = builder.get_object("searchbar_placeholder")
|
||||
self.box.set_reveal_child(False)
|
||||
self.searchbar = parentwindow.builder.get_object("searchbar")
|
||||
self.searchentry = parentwindow.builder.get_object("searchentrybox")
|
||||
self.searchbar = builder.get_object("searchbar")
|
||||
self.searchentry = builder.get_object("searchentrybox")
|
||||
self.searchentry.connect('changed', self.search)
|
||||
self.searchentry.connect('activate', self.scrolltonext)
|
||||
self.searchentry.connect('key-press-event', self.key_pressed)
|
||||
|
||||
self.open_replace_button = parentwindow.builder.get_object("replace")
|
||||
self.open_replace_button = builder.get_object("replace")
|
||||
self.open_replace_button.connect("toggled", self.toggle_replace)
|
||||
|
||||
self.nextbutton = parentwindow.builder.get_object("next_result")
|
||||
self.prevbutton = parentwindow.builder.get_object("previous_result")
|
||||
self.regexbutton = parentwindow.builder.get_object("regex")
|
||||
self.casesensitivebutton = parentwindow.builder.get_object("case_sensitive")
|
||||
self.nextbutton = builder.get_object("next_result")
|
||||
self.prevbutton = builder.get_object("previous_result")
|
||||
self.regexbutton = builder.get_object("regex")
|
||||
self.casesensitivebutton = builder.get_object("case_sensitive")
|
||||
|
||||
self.replacebox = parentwindow.builder.get_object("replace_placeholder")
|
||||
self.replacebox = builder.get_object("replace_placeholder")
|
||||
self.replacebox.set_reveal_child(False)
|
||||
self.replace_one_button = parentwindow.builder.get_object("replace_one")
|
||||
self.replace_all_button = parentwindow.builder.get_object("replace_all")
|
||||
self.replaceentry = parentwindow.builder.get_object("replaceentrybox")
|
||||
self.replace_one_button = builder.get_object("replace_one")
|
||||
self.replace_all_button = builder.get_object("replace_all")
|
||||
self.replaceentry = builder.get_object("replaceentrybox")
|
||||
|
||||
self.replace_all_button.connect('clicked', self.replace_all)
|
||||
self.replace_one_button.connect('clicked', self.replace_clicked)
|
||||
|
@ -96,7 +96,8 @@ class SearchAndReplace:
|
|||
"""
|
||||
show search box
|
||||
"""
|
||||
if self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False:
|
||||
if self.textview.get_mapped() and (
|
||||
self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False):
|
||||
self.searchbar.set_search_mode(True)
|
||||
self.box.set_reveal_child(True)
|
||||
self.searchentry.grab_focus()
|
||||
|
|
|
@ -11,8 +11,11 @@ from uberwriter import helpers
|
|||
class StatsCounter:
|
||||
"""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))")
|
||||
# Regexp that matches characters, with the following exceptions:
|
||||
# * Newlines
|
||||
# * Sequential spaces
|
||||
# * Sequential dashes
|
||||
CHARACTERS = re.compile(r"[^\s-]|(?:[^\S\n](?!\s)|-(?![-\n]))")
|
||||
|
||||
# Regexp that matches Asian letters, general symbols and hieroglyphs,
|
||||
# as well as sequences of word characters optionally containing non-word characters in-between.
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
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
|
||||
|
||||
|
@ -15,6 +9,13 @@ from uberwriter.stats_counter import StatsCounter
|
|||
class StatsHandler:
|
||||
"""Shows a default statistic on the stats button, and allows the user to toggle which one."""
|
||||
|
||||
# Must match the order/index defined in gschema.xml
|
||||
CHARACTERS = 0
|
||||
WORDS = 1
|
||||
SENTENCES = 2
|
||||
PARAGRAPHS = 3
|
||||
READ_TIME = 4
|
||||
|
||||
def __init__(self, stats_button, text_view):
|
||||
super().__init__()
|
||||
|
||||
|
@ -34,7 +35,6 @@ class StatsHandler:
|
|||
self.read_time = (0, 0, 0)
|
||||
|
||||
self.settings = Settings.new()
|
||||
self.default_stat = self.settings.get_enum("stat-default")
|
||||
|
||||
self.stats_counter = StatsCounter()
|
||||
|
||||
|
@ -65,16 +65,16 @@ class StatsHandler:
|
|||
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 _("{:n} Paragraphs".format(self.paragraphs))
|
||||
elif stat == 4:
|
||||
return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time))
|
||||
if stat == self.CHARACTERS:
|
||||
return _("{:n} Characters").format(self.characters)
|
||||
elif stat == self.WORDS:
|
||||
return _("{:n} Words").format(self.words)
|
||||
elif stat == self.SENTENCES:
|
||||
return _("{:n} Sentences").format(self.sentences)
|
||||
elif stat == self.PARAGRAPHS:
|
||||
return _("{:n} Paragraphs").format(self.paragraphs)
|
||||
elif stat == self.READ_TIME:
|
||||
return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time)
|
||||
else:
|
||||
raise ValueError("Unknown stat {}".format(stat))
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import gi
|
||||
|
||||
from uberwriter import helpers
|
||||
from uberwriter.theme import Theme
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
|
||||
class StyledWindow(Gtk.ApplicationWindow):
|
||||
"""A window that will redraw itself upon theme changes."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.connect("style-updated", self.apply_current_theme)
|
||||
self.apply_current_theme()
|
||||
|
||||
def apply_current_theme(self, *_):
|
||||
"""Adjusts the window, CSD and preview for the current theme."""
|
||||
# Get current theme
|
||||
theme, changed = Theme.get_current_changed()
|
||||
if changed:
|
||||
# Set theme variant (dark/light)
|
||||
Gtk.Settings.get_default().set_property(
|
||||
"gtk-application-prefer-dark-theme",
|
||||
GLib.Variant("b", theme.is_dark))
|
||||
|
||||
# Set theme css
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(helpers.get_css_path("gtk/base.css"))
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
self.get_screen(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
# Redraw contents of window
|
||||
self.queue_draw()
|
|
@ -1,17 +1,19 @@
|
|||
import gi
|
||||
from gi.repository.GObject import SignalMatchType
|
||||
|
||||
from uberwriter.inline_preview import InlinePreview
|
||||
from uberwriter.text_view_format_inserter import FormatInserter
|
||||
from uberwriter.text_view_markup_handler import MarkupHandler
|
||||
from uberwriter.text_view_scroller import TextViewScroller
|
||||
from uberwriter.text_view_undo_redo_handler import UndoRedoHandler
|
||||
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
|
||||
from uberwriter.scroller import Scroller
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gspell', '1')
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('uberwriter')
|
||||
|
||||
|
||||
|
@ -36,10 +38,13 @@ class TextView(Gtk.TextView):
|
|||
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'undo': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'redo': (GObject.SignalFlags.ACTION, None, ())
|
||||
'redo': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'scroll-scale-changed': (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
font_sizes = [18, 17, 16, 15, 14] # Must match CSS selectors in gtk/base.css
|
||||
|
||||
def __init__(self, line_chars):
|
||||
super().__init__()
|
||||
|
||||
# Appearance
|
||||
|
@ -49,11 +54,18 @@ class TextView(Gtk.TextView):
|
|||
self.set_pixels_inside_wrap(8)
|
||||
self.get_style_context().add_class('uberwriter-editor')
|
||||
|
||||
# Text sizing
|
||||
self.props.halign = Gtk.Align.FILL
|
||||
self.line_chars = line_chars
|
||||
self.font_size = 16
|
||||
self.get_style_context().add_class('size16')
|
||||
|
||||
# General behavior
|
||||
self.get_buffer().connect('changed', self.on_text_changed)
|
||||
self.connect('size-allocate', self.on_size_allocate)
|
||||
self.get_buffer().connect('changed', self.on_text_changed)
|
||||
|
||||
# Spell checking
|
||||
self.spellcheck = True
|
||||
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
|
||||
self.gspell_view.basic_setup()
|
||||
|
||||
|
@ -85,6 +97,7 @@ class TextView(Gtk.TextView):
|
|||
|
||||
# Scrolling
|
||||
self.scroller = None
|
||||
self.connect('parent-set', self.on_parent_set)
|
||||
self.get_buffer().connect('mark-set', self.on_mark_set)
|
||||
|
||||
# Focus mode
|
||||
|
@ -95,6 +108,12 @@ class TextView(Gtk.TextView):
|
|||
self.hemingway_mode = False
|
||||
self.connect('key-press-event', self.on_key_press_event)
|
||||
|
||||
# While resizing the TextView, there is unwanted scroll upwards if a top margin is present.
|
||||
# When a size allocation is detected, this variable will hold the scroll to re-set until the
|
||||
# UI is idle again.
|
||||
# TODO: Find a better way to handle unwanted scroll.
|
||||
self.frozen_scroll_scale = None
|
||||
|
||||
def get_text(self):
|
||||
text_buffer = self.get_buffer()
|
||||
start_iter = text_buffer.get_start_iter()
|
||||
|
@ -105,13 +124,59 @@ class TextView(Gtk.TextView):
|
|||
text_buffer = self.get_buffer()
|
||||
text_buffer.set_text(text)
|
||||
|
||||
def on_text_changed(self, *_):
|
||||
self.markup.apply()
|
||||
GLib.idle_add(self.scroll_to)
|
||||
def get_scroll_scale(self):
|
||||
return self.scroller.get_scroll_scale() if self.scroller else 0
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
if self.scroller:
|
||||
self.scroller.set_scroll_scale(scale)
|
||||
|
||||
def on_size_allocate(self, *_):
|
||||
self.update_horizontal_margin()
|
||||
self.update_vertical_margin()
|
||||
self.markup.update_margins_indents()
|
||||
self.queue_draw()
|
||||
|
||||
# TODO: Find a better way to handle unwanted scroll on resize.
|
||||
self.frozen_scroll_scale = self.get_scroll_scale()
|
||||
GLib.idle_add(self.unfreeze_scroll_scale)
|
||||
|
||||
def on_text_changed(self, *_):
|
||||
self.markup.apply()
|
||||
self.smooth_scroll_to()
|
||||
|
||||
def on_parent_set(self, *_):
|
||||
parent = self.get_parent()
|
||||
if parent:
|
||||
parent.set_size_request(self.get_min_width(), 500)
|
||||
self.scroller = TextViewScroller(self, parent)
|
||||
parent.get_vadjustment().connect("changed", self.on_scroll_scale_changed)
|
||||
parent.get_vadjustment().connect("value-changed", self.on_scroll_scale_changed)
|
||||
else:
|
||||
self.scroller = None
|
||||
|
||||
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
|
||||
if mark.get_name() == 'insert':
|
||||
self.markup.apply()
|
||||
self.smooth_scroll_to(mark)
|
||||
elif mark.get_name() == 'gtk_drag_target':
|
||||
self.smooth_scroll_to(mark)
|
||||
return True
|
||||
|
||||
def on_button_release_event(self, _widget, _event):
|
||||
if self.focus_mode:
|
||||
self.markup.apply()
|
||||
return False
|
||||
|
||||
def on_scroll_scale_changed(self, *_):
|
||||
if self.frozen_scroll_scale is not None:
|
||||
self.set_scroll_scale(self.frozen_scroll_scale)
|
||||
else:
|
||||
self.emit("scroll-scale-changed", self.get_scroll_scale())
|
||||
|
||||
def unfreeze_scroll_scale(self):
|
||||
self.frozen_scroll_scale = None
|
||||
self.queue_draw()
|
||||
|
||||
def set_focus_mode(self, focus_mode):
|
||||
"""Toggle focus mode.
|
||||
|
@ -120,10 +185,33 @@ class TextView(Gtk.TextView):
|
|||
and the surrounding text is greyed out."""
|
||||
|
||||
self.focus_mode = focus_mode
|
||||
self.gspell_view.set_inline_spell_checking(not focus_mode)
|
||||
self.update_vertical_margin()
|
||||
self.markup.apply()
|
||||
self.scroll_to()
|
||||
self.smooth_scroll_to()
|
||||
self.set_spellcheck(self.spellcheck)
|
||||
|
||||
def set_spellcheck(self, spellcheck):
|
||||
self.spellcheck = spellcheck
|
||||
self.gspell_view.set_inline_spell_checking(self.spellcheck and not self.focus_mode)
|
||||
|
||||
def update_horizontal_margin(self):
|
||||
width = self.get_allocation().width
|
||||
|
||||
# Ensure the appropriate font size is being used
|
||||
for font_size in self.font_sizes:
|
||||
if width >= self.get_min_width(font_size) or font_size == self.font_sizes[-1]:
|
||||
if font_size != self.font_size:
|
||||
self.font_size = font_size
|
||||
for fs in self.font_sizes:
|
||||
self.get_style_context().remove_class("size{}".format(fs))
|
||||
self.get_style_context().add_class("size{}".format(font_size))
|
||||
break
|
||||
|
||||
# Apply margin with the remaining space to allow for markup
|
||||
line_width = (self.line_chars + 1) * int(self.get_char_width(self.font_size)) - 1
|
||||
horizontal_margin = (width - line_width) / 2
|
||||
self.props.left_margin = horizontal_margin
|
||||
self.props.right_margin = horizontal_margin
|
||||
|
||||
def update_vertical_margin(self):
|
||||
if self.focus_mode:
|
||||
|
@ -134,11 +222,6 @@ class TextView(Gtk.TextView):
|
|||
self.props.top_margin = 80
|
||||
self.props.bottom_margin = 64
|
||||
|
||||
def on_button_release_event(self, _widget, _event):
|
||||
if self.focus_mode:
|
||||
self.markup.apply()
|
||||
return False
|
||||
|
||||
def set_hemingway_mode(self, hemingway_mode):
|
||||
"""Toggle hemingway mode.
|
||||
|
||||
|
@ -158,48 +241,34 @@ class TextView(Gtk.TextView):
|
|||
self.get_buffer().set_text('')
|
||||
self.undo_redo.clear()
|
||||
|
||||
def scroll_to(self, mark=None):
|
||||
def smooth_scroll_to(self, mark=None):
|
||||
"""Scrolls if needed to ensure mark is visible.
|
||||
|
||||
If mark is unspecified, the cursor is used."""
|
||||
|
||||
margin = 32
|
||||
scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__)
|
||||
if not scrolled_window:
|
||||
return
|
||||
va = scrolled_window.get_vadjustment()
|
||||
if va.props.page_size < margin * 2:
|
||||
if self.scroller is None:
|
||||
return
|
||||
if mark is None:
|
||||
mark = self.get_buffer().get_insert()
|
||||
GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode)
|
||||
|
||||
text_buffer = self.get_buffer()
|
||||
if mark:
|
||||
mark_iter = text_buffer.get_iter_at_mark(mark)
|
||||
else:
|
||||
mark_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
|
||||
mark_rect = self.get_iter_location(mark_iter)
|
||||
def get_min_width(self, font_size=None):
|
||||
"""Returns the minimum width of this text view."""
|
||||
|
||||
pos_y = mark_rect.y + mark_rect.height + self.props.top_margin
|
||||
pos = pos_y - va.props.value
|
||||
target_pos = None
|
||||
if self.focus_mode:
|
||||
if pos != (va.props.page_size * 0.5):
|
||||
target_pos = pos_y - (va.props.page_size * 0.5)
|
||||
elif pos > va.props.page_size - margin:
|
||||
target_pos = pos_y - va.props.page_size + margin
|
||||
elif pos < margin:
|
||||
target_pos = pos_y - margin
|
||||
if font_size is None:
|
||||
font_size = self.font_sizes[-1]
|
||||
return (self.line_chars + self.get_pad_chars(font_size) + 1) \
|
||||
* self.get_char_width(font_size) - 1
|
||||
|
||||
if self.scroller and self.scroller.is_started:
|
||||
self.scroller.end()
|
||||
if target_pos:
|
||||
self.scroller = Scroller(scrolled_window, va.props.value, target_pos)
|
||||
self.scroller.start()
|
||||
def get_pad_chars(self, font_size):
|
||||
"""Returns the amount of character padding for font_size.
|
||||
|
||||
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
|
||||
if mark.get_name() == 'insert':
|
||||
self.markup.apply()
|
||||
if self.focus_mode:
|
||||
self.scroll_to(mark)
|
||||
elif mark.get_name() == 'gtk_drag_target':
|
||||
self.scroll_to(mark)
|
||||
return True
|
||||
Markup can use up to 7 in normal conditions."""
|
||||
|
||||
return 8 * (1 + font_size - self.font_sizes[-1])
|
||||
|
||||
@staticmethod
|
||||
def get_char_width(font_size):
|
||||
"""Returns the font width for a given size. Note: specific to Fira Mono!"""
|
||||
|
||||
return font_size * 1 / 1.6
|
||||
|
|
|
@ -37,13 +37,13 @@ class MarkupHandler:
|
|||
"BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"),
|
||||
"STRIKETHROUGH": re.compile(r"~~.+?~~"),
|
||||
"LINK": re.compile(r"(\[).*(\]\(.+?\))"),
|
||||
"HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n", re.MULTILINE),
|
||||
"HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n\n", re.MULTILINE),
|
||||
"LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE),
|
||||
"NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE),
|
||||
"NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[.)]) .+", re.MULTILINE),
|
||||
"BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:>|(?:> )+).+", re.MULTILINE),
|
||||
"HEADER": re.compile(r"^[ ]{0,3}(#{1,6}) [^\n]+", re.MULTILINE),
|
||||
"HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE),
|
||||
"HEADER_UNDER": re.compile(r"^\n[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE),
|
||||
"CODE": re.compile(r"(?:^|\n)[ ]{0,3}(([`~]{3}).+?[ ]{0,3}\2)(?:\n|$)", re.DOTALL),
|
||||
"TABLE": re.compile(r"^[\-+]{5,}\n(.+?)\n[\-+]{5,}\n", re.DOTALL),
|
||||
"MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"),
|
||||
|
@ -79,10 +79,10 @@ class MarkupHandler:
|
|||
strikethrough=False,
|
||||
justification=Gtk.Justification.LEFT)
|
||||
|
||||
self.table = buffer.create_tag('table')
|
||||
self.table.set_property('wrap-mode', Gtk.WrapMode.NONE)
|
||||
self.table.set_property('pixels-above-lines', 0)
|
||||
self.table.set_property('pixels-below-lines', 0)
|
||||
self.table = buffer.create_tag('table',
|
||||
wrap_mode=Gtk.WrapMode.NONE,
|
||||
pixels_above_lines=0,
|
||||
pixels_below_lines=0)
|
||||
|
||||
self.mathtext = buffer.create_tag('mathtext')
|
||||
|
||||
|
@ -94,6 +94,8 @@ class MarkupHandler:
|
|||
# Margin and indents
|
||||
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc
|
||||
self.margins_indents = {}
|
||||
self.baseline_margin = 0
|
||||
self.char_width = 0
|
||||
self.update_margins_indents()
|
||||
|
||||
# Style
|
||||
|
@ -272,11 +274,10 @@ class MarkupHandler:
|
|||
def get_margin_indent_tag(self, margin_level, indent_level):
|
||||
level = (margin_level, indent_level)
|
||||
if level not in self.margins_indents:
|
||||
tag = self.text_buffer.create_tag(
|
||||
"margin_indent_" + str(margin_level) + "_" + str(indent_level))
|
||||
margin, indent = self.get_margin_indent(margin_level, indent_level)
|
||||
tag.set_property("left-margin", margin)
|
||||
tag.set_property("indent", indent)
|
||||
tag = self.text_buffer.create_tag(
|
||||
"margin_indent_{}_{}".format(margin_level, indent_level),
|
||||
left_margin=margin, indent=indent)
|
||||
self.margins_indents[level] = tag
|
||||
return tag
|
||||
else:
|
||||
|
@ -284,7 +285,7 @@ class MarkupHandler:
|
|||
|
||||
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
|
||||
if baseline_margin is None:
|
||||
baseline_margin = self.text_view.get_left_margin()
|
||||
baseline_margin = self.text_view.props.left_margin
|
||||
if char_width is None:
|
||||
char_width = helpers.get_char_width(self.text_view)
|
||||
margin = max(baseline_margin + char_width * margin_level, 0)
|
||||
|
@ -292,16 +293,21 @@ class MarkupHandler:
|
|||
return margin, indent
|
||||
|
||||
def update_margins_indents(self):
|
||||
baseline_margin = self.text_view.get_left_margin()
|
||||
baseline_margin = self.text_view.props.left_margin
|
||||
char_width = helpers.get_char_width(self.text_view)
|
||||
|
||||
# Adjust tab size, as character width can change
|
||||
# Bail out if neither the baseline margin nor character width change
|
||||
if baseline_margin == self.baseline_margin and char_width == self.char_width:
|
||||
return
|
||||
self.baseline_margin = baseline_margin
|
||||
self.char_width = char_width
|
||||
|
||||
# Adjust tab size
|
||||
tab_array = Pango.TabArray.new(1, True)
|
||||
tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
|
||||
self.text_view.set_tabs(tab_array)
|
||||
|
||||
# Adjust margins and indents, as character width can change
|
||||
# Adjust margins and indents
|
||||
for level, tag in self.margins_indents.items():
|
||||
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
|
||||
tag.set_property("left-margin", margin)
|
||||
tag.set_property("indent", indent)
|
||||
tag.set_properties(left_margin=margin, indent=indent)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
class TextViewScroller:
|
||||
def __init__(self, text_view, scrolled_window):
|
||||
super().__init__()
|
||||
|
||||
self.text_view = text_view
|
||||
self.scrolled_window = scrolled_window
|
||||
self.smooth_scroller = None
|
||||
|
||||
def get_scroll_scale(self):
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
if vap.upper > vap.page_size:
|
||||
return vap.value / (vap.upper - vap.page_size)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
vap.value = (vap.upper - vap.page_size) * scale
|
||||
|
||||
def scroll_to_mark(self, mark, center):
|
||||
"""Scrolls until mark is visible, if needed."""
|
||||
|
||||
target_pos = self.get_target_pos_for_mark(mark, center)
|
||||
if target_pos:
|
||||
self.scrolled_window.get_vadjustment().set_value(target_pos)
|
||||
|
||||
def smooth_scroll_to_mark(self, mark, center):
|
||||
"""Smoothly scrolls until mark is visible, if needed."""
|
||||
|
||||
if self.smooth_scroller and self.smooth_scroller.is_started:
|
||||
self.smooth_scroller.end()
|
||||
|
||||
target_pos = self.get_target_pos_for_mark(mark, center)
|
||||
if target_pos:
|
||||
source_pos = self.scrolled_window.get_vadjustment().props.value
|
||||
self.smooth_scroller = SmoothScroller(self.scrolled_window, source_pos, target_pos)
|
||||
self.smooth_scroller.start()
|
||||
|
||||
def get_target_pos_for_mark(self, mark, center):
|
||||
margin = 32
|
||||
|
||||
mark_iter = self.text_view.get_buffer().get_iter_at_mark(mark)
|
||||
mark_rect = self.text_view.get_iter_location(mark_iter)
|
||||
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
|
||||
pos_y = mark_rect.y + mark_rect.height + self.text_view.props.top_margin
|
||||
pos_viewport_y = pos_y - vap.value
|
||||
target_pos = None
|
||||
if center:
|
||||
if pos_viewport_y != vap.page_size / 2:
|
||||
target_pos = pos_y - (vap.page_size / 2)
|
||||
elif pos_viewport_y > vap.page_size - margin:
|
||||
target_pos = pos_y - vap.page_size + margin
|
||||
elif pos_viewport_y < margin:
|
||||
target_pos = pos_y - margin - mark_rect.height
|
||||
|
||||
return target_pos
|
||||
|
||||
|
||||
class SmoothScroller:
|
||||
def __init__(self, scrolled_window, source_pos, target_pos):
|
||||
super().__init__()
|
||||
|
||||
self.scrolled_window = scrolled_window
|
||||
self.source_pos = source_pos
|
||||
self.target_pos = target_pos
|
||||
self.duration = max(100, (target_pos - source_pos) / 50) * 1000
|
||||
|
||||
self.is_started = False
|
||||
self.is_setup = False
|
||||
self.start_time = 0
|
||||
self.end_time = 0
|
||||
self.tick_callback_id = 0
|
||||
|
||||
def start(self):
|
||||
self.is_started = True
|
||||
self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick)
|
||||
|
||||
def end(self):
|
||||
self.scrolled_window.remove_tick_callback(self.tick_callback_id)
|
||||
self.is_started = False
|
||||
|
||||
def setup(self, time):
|
||||
self.start_time = time
|
||||
self.end_time = time + self.duration
|
||||
self.is_setup = True
|
||||
|
||||
def on_tick(self, widget, frame_clock):
|
||||
def ease_out_cubic(value):
|
||||
return pow(value - 1, 3) + 1
|
||||
|
||||
now = frame_clock.get_frame_time()
|
||||
if not self.is_setup:
|
||||
self.setup(now)
|
||||
|
||||
if now < self.end_time:
|
||||
time = float(now - self.start_time) / float(self.end_time - self.start_time)
|
||||
else:
|
||||
time = 1
|
||||
self.end()
|
||||
|
||||
time = ease_out_cubic(time)
|
||||
pos = self.source_pos + (time * (self.target_pos - self.source_pos))
|
||||
widget.get_vadjustment().props.value = pos
|
||||
return True
|
|
@ -31,8 +31,8 @@ class Theme:
|
|||
@classmethod
|
||||
def get_current_changed(cls):
|
||||
theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name')
|
||||
dark_mode_auto = cls.settings.get_value('dark-mode-auto').get_boolean()
|
||||
dark_mode = cls.settings.get_value('dark-mode').get_boolean()
|
||||
dark_mode_auto = cls.settings.get_boolean('dark-mode-auto')
|
||||
dark_mode = cls.settings.get_boolean('dark-mode')
|
||||
current_theme = cls.get_for_name(theme_name)
|
||||
if not dark_mode_auto and dark_mode != current_theme.is_dark and current_theme.inverse_name:
|
||||
current_theme = cls.get_for_name(current_theme.inverse_name, current_theme.name)
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import gi
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2, GLib, GObject
|
||||
|
||||
|
||||
class WebView(WebKit2.WebView):
|
||||
"""A WebView that provides read/write access to scroll.
|
||||
|
||||
It does so using JavaScript, by continuously monitoring it while loaded.
|
||||
The alternative is using a WebExtension and C-bindings (see reference), but that is more
|
||||
complicated implementation-wise, as well as build-wise until we start building with Meson.
|
||||
|
||||
Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
|
||||
"""
|
||||
|
||||
GET_SCROLL_SCALE_JS = """
|
||||
e = document.documentElement;
|
||||
e.scrollHeight > e.clientHeight ? e.scrollTop / (e.scrollHeight - e.clientHeight) : -1;
|
||||
"""
|
||||
|
||||
SET_SCROLL_SCALE_JS = """
|
||||
scale = {:.16f};
|
||||
e = document.documentElement;
|
||||
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
|
||||
"""
|
||||
|
||||
__gsignals__ = {
|
||||
"scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.connect("load-changed", self.on_load_changed)
|
||||
self.connect("load-failed", self.on_load_failed)
|
||||
self.connect("size-allocate", self.on_size_allocate)
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
self.scroll_scale = 0.0
|
||||
self.pending_scroll_scale = None
|
||||
|
||||
self.state_loaded = False
|
||||
self.state_load_failed = False
|
||||
self.state_discard_result = False
|
||||
self.state_waiting = False
|
||||
|
||||
self.timeout_id = None
|
||||
|
||||
def get_scroll_scale(self):
|
||||
return self.scroll_scale
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
self.pending_scroll_scale = scale
|
||||
self.state_loop()
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
|
||||
self.state_load_failed = False
|
||||
self.state_discard_result = event == WebKit2.LoadEvent.STARTED and self.state_waiting
|
||||
self.pending_scroll_scale = self.scroll_scale
|
||||
self.state_loop()
|
||||
|
||||
def on_load_failed(self, _web_view, _event):
|
||||
self.state_loaded = False
|
||||
self.state_load_failed = True
|
||||
self.state_loop()
|
||||
|
||||
def on_size_allocate(self, *_):
|
||||
self.set_scroll_scale(self.scroll_scale)
|
||||
|
||||
def on_destroy(self, _widget):
|
||||
self.state_loaded = False
|
||||
self.state_loop()
|
||||
|
||||
def read_scroll_scale(self):
|
||||
self.state_waiting = True
|
||||
self.run_javascript(
|
||||
self.GET_SCROLL_SCALE_JS, None, self.finish_read_scroll_scale)
|
||||
|
||||
def write_scroll_scale(self, scroll_scale):
|
||||
self.run_javascript(
|
||||
self.SET_SCROLL_SCALE_JS.format(scroll_scale), None, None)
|
||||
|
||||
def finish_read_scroll_scale(self, _web_view, result):
|
||||
self.state_waiting = False
|
||||
if not self.state_discard_result:
|
||||
result = self.run_javascript_finish(result)
|
||||
self.state_loop(result.get_js_value().to_double())
|
||||
else:
|
||||
self.state_discard_result = False
|
||||
self.state_loop()
|
||||
|
||||
def state_loop(self, scroll_scale=None, delay=16): # 16ms ~ 60hz
|
||||
# Remove any pending callbacks
|
||||
if self.timeout_id:
|
||||
GLib.source_remove(self.timeout_id)
|
||||
self.timeout_id = None
|
||||
|
||||
# Set scroll scale if specified, and the state is not dirty
|
||||
if scroll_scale not in (None, -1, self.scroll_scale):
|
||||
self.scroll_scale = scroll_scale
|
||||
self.emit("scroll-scale-changed", self.scroll_scale)
|
||||
|
||||
# Handle the current state
|
||||
if not self.state_loaded or self.state_load_failed or self.state_waiting:
|
||||
return
|
||||
if self.pending_scroll_scale:
|
||||
self.write_scroll_scale(self.pending_scroll_scale)
|
||||
self.pending_scroll_scale = None
|
||||
self.read_scroll_scale()
|
||||
elif delay > 0:
|
||||
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
|
||||
else:
|
||||
self.read_scroll_scale()
|
Loading…
Reference in New Issue