forked from Mirrors/apostrophe
Merge branch 'master'
commit
7e34e9cc62
|
@ -16,6 +16,7 @@ flatpak/*
|
||||||
data/ui/shortcut_handlers
|
data/ui/shortcut_handlers
|
||||||
*.ui~
|
*.ui~
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.glade~
|
*.glade~
|
||||||
dist/uberwriter-2.0b0-py3.7.egg
|
dist/uberwriter-2.0b0-py3.7.egg
|
||||||
builddir/*
|
builddir/*
|
||||||
|
|
|
@ -10,6 +10,13 @@
|
||||||
<value nick='read_time' value='4' />
|
<value nick='read_time' value='4' />
|
||||||
</enum>
|
</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">
|
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
|
||||||
|
|
||||||
<key name='dark-mode-auto' type='b'>
|
<key name='dark-mode-auto' type='b'>
|
||||||
|
@ -41,6 +48,13 @@
|
||||||
It can cause performance problems to some users.
|
It can cause performance problems to some users.
|
||||||
</description>
|
</description>
|
||||||
</key>
|
</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'>
|
<key name='input-format' type='s'>
|
||||||
<default>"markdown"</default>
|
<default>"markdown"</default>
|
||||||
<summary>Input format</summary>
|
<summary>Input format</summary>
|
||||||
|
@ -69,6 +83,20 @@
|
||||||
Which statistic is shown on the main window.
|
Which statistic is shown on the main window.
|
||||||
</description>
|
</description>
|
||||||
</key>
|
</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>
|
</schema>
|
||||||
|
|
||||||
|
|
|
@ -22,25 +22,11 @@
|
||||||
/* Main window and text colors */
|
/* Main window and text colors */
|
||||||
|
|
||||||
.uberwriter-window {
|
.uberwriter-window {
|
||||||
/*border-radius: 7px 7px 3px 3px;*/
|
|
||||||
background: @theme_base_color;
|
background: @theme_base_color;
|
||||||
color: @theme_fg_color;
|
color: @theme_fg_color;
|
||||||
caret-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 {
|
#titlebar-revealer {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -55,10 +41,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.uberwriter-editor {
|
.uberwriter-editor {
|
||||||
|
-gtk-key-bindings: editor-bindings;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
text-decoration-color: @error_color;
|
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 {
|
.uberwriter-editor text {
|
||||||
|
@ -84,9 +92,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-counter {
|
.inline-button {
|
||||||
color: alpha(@theme_fg_color, 0.6);
|
color: alpha(@theme_fg_color, 0.6);
|
||||||
background-color: @theme_base_color;
|
|
||||||
text-shadow: inherit;
|
text-shadow: inherit;
|
||||||
box-shadow: initial;
|
box-shadow: initial;
|
||||||
background-clip: initial;
|
background-clip: initial;
|
||||||
|
@ -104,8 +111,8 @@
|
||||||
transition: 100ms ease-in;
|
transition: 100ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-counter:hover,
|
.inline-button:hover,
|
||||||
.stats-counter:checked {
|
.inline-button:checked {
|
||||||
color: @theme_fg_color;
|
color: @theme_fg_color;
|
||||||
background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
|
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 {
|
:root {
|
||||||
--text-color: #eeeeec;
|
--text-color: #eeeeec;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import url("web/web__base.css");
|
@import url("base.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--text-color: #3b3e45;
|
--text-color: #3b3e45;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import url("web/web__base.css");
|
@import url("base.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--text-color: #d3dae3;
|
--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 {
|
html {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
@ -57,9 +57,9 @@ body {
|
||||||
font-family: "Fira Sans", fira-sans, sans-serif, color-emoji;
|
font-family: "Fira Sans", fira-sans, sans-serif, color-emoji;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-width: 978px;
|
max-width: 980px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 2em;
|
padding: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import url("web/web__base.css");
|
@import url("base.css");
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import url("web/web__base.css");
|
@import url("base.css");
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
@ -138,11 +138,11 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="format_label">
|
<object class="GtkLabel" id="sync_scroll_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">start</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>
|
<property name="justify">right</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
@ -151,17 +151,29 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="input_format_help_button">
|
<object class="GtkSwitch" id="sync_scroll_switch">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">False</property>
|
<property name="halign">end</property>
|
||||||
<property name="image">help</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="top_attach">4</property>
|
<property name="top_attach">4</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</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>
|
<child>
|
||||||
<object class="GtkComboBox" id="input_format_combobox">
|
<object class="GtkComboBox" id="input_format_combobox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -172,9 +184,24 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="top_attach">4</property>
|
<property name="top_attach">5</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</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>
|
<child>
|
||||||
<placeholder/>
|
<placeholder/>
|
||||||
</child>
|
</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>
|
<interface>
|
||||||
<requires lib="gtk+" version="3.20"/>
|
<requires lib="gtk+" version="3.20"/>
|
||||||
<!-- interface-local-resource-path ../media -->
|
<!-- interface-local-resource-path ../media -->
|
||||||
<object class="GtkAdjustment" id="adjustment1">
|
<object class="GtkImage" id="edit-find-replace">
|
||||||
<property name="upper">100</property>
|
<property name="visible">True</property>
|
||||||
<property name="step_increment">1</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="page_increment">10</property>
|
<property name="icon_name">edit-find-replace-symbolic</property>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkImage" id="amunt">
|
<object class="GtkImage" id="go-up">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="icon_name">go-up-symbolic</property>
|
<property name="icon_name">go-up-symbolic</property>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkImage" id="avall">
|
<object class="GtkImage" id="go_down">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="icon_name">go-down-symbolic</property>
|
<property name="icon_name">go-down-symbolic</property>
|
||||||
</object>
|
</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="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="stock">gtk-spell-check</property>
|
<property name="stock">gtk-spell-check</property>
|
||||||
</object>
|
</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">
|
<object class="GtkOverlay" id="FullscreenOverlay">
|
||||||
<property name="name">FullscreenOverlay</property>
|
<property name="name">FullscreenOverlay</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -43,10 +38,10 @@
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkPaned" id="main_pained">
|
<object class="GtkPaned" id="main_paned">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="app_paintable">True</property>
|
<property name="app_paintable">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">False</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="sidebar_box">
|
<object class="GtkBox" id="sidebar_box">
|
||||||
<property name="width_request">200</property>
|
<property name="width_request">200</property>
|
||||||
|
@ -70,65 +65,75 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkGrid" id="grid2">
|
<object class="GtkBox" id="content">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="homogeneous">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkRevealer" id="stats_counter_revealer">
|
<object class="GtkBox" id="editor">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="transition_type">crossfade</property>
|
<property name="orientation">vertical</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>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
|
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
|
||||||
<property name="height_request">500</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="vexpand">True</property>
|
<property name="vexpand">True</property>
|
||||||
<property name="vadjustment">adjustment1</property>
|
|
||||||
<child>
|
<child>
|
||||||
<placeholder/>
|
<placeholder/>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</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>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">0</property>
|
<property name="expand">False</property>
|
||||||
<property name="top_attach">0</property>
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="resize">False</property>
|
<property name="resize">True</property>
|
||||||
<property name="shrink">False</property>
|
<property name="shrink">False</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
@ -191,7 +196,7 @@
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="tooltip_text" translatable="yes">Previous Match</property>
|
<property name="tooltip_text" translatable="yes">Previous Match</property>
|
||||||
<property name="image">amunt</property>
|
<property name="image">go-up</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
@ -205,7 +210,7 @@
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="tooltip_text" translatable="yes">Next Match</property>
|
<property name="tooltip_text" translatable="yes">Next Match</property>
|
||||||
<property name="image">avall</property>
|
<property name="image">go_down</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
@ -264,7 +269,7 @@
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="tooltip_text" translatable="yes">Open Replace</property>
|
<property name="tooltip_text" translatable="yes">Open Replace</property>
|
||||||
<property name="image">reemplaza</property>
|
<property name="image">edit-find-replace</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
@ -345,7 +350,7 @@
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="image">ortografia1</property>
|
<property name="image">spell-check</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
regex
|
regex
|
||||||
enchant
|
pypandoc==1.4
|
||||||
pypandoc==1.4
|
|
||||||
pyenchant
|
|
||||||
pygtkspellcheck
|
|
|
@ -19,7 +19,7 @@ import gi
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
from uberwriter import window
|
from uberwriter import main_window
|
||||||
from uberwriter import application
|
from uberwriter import application
|
||||||
from uberwriter.helpers import set_up_logging
|
from uberwriter.helpers import set_up_logging
|
||||||
from uberwriter.config import get_version
|
from uberwriter.config import get_version
|
||||||
|
|
|
@ -15,10 +15,12 @@ from gettext import gettext as _
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
|
from uberwriter.main_window import MainWindow
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
||||||
from gi.repository import GLib, Gio, Gtk, GdkPixbuf
|
from gi.repository import GLib, Gio, Gtk, GdkPixbuf
|
||||||
|
|
||||||
from uberwriter import window
|
from uberwriter import main_window
|
||||||
from uberwriter.settings import Settings
|
from uberwriter.settings import Settings
|
||||||
from uberwriter.helpers import set_up_logging
|
from uberwriter.helpers import set_up_logging
|
||||||
from uberwriter.preferences_dialog import PreferencesDialog
|
from uberwriter.preferences_dialog import PreferencesDialog
|
||||||
|
@ -120,10 +122,18 @@ class Application(Gtk.Application):
|
||||||
|
|
||||||
stat_default = self.settings.get_string("stat-default")
|
stat_default = self.settings.get_string("stat-default")
|
||||||
action = Gio.SimpleAction.new_stateful(
|
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)
|
action.connect("activate", self.on_stat_default)
|
||||||
self.add_action(action)
|
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
|
# Shortcuts
|
||||||
|
|
||||||
# TODO: be aware that a couple of shortcuts are defined in base.css
|
# 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
|
# Windows are associated with the application
|
||||||
# when the last one is closed the application shuts down
|
# when the last one is closed the application shuts down
|
||||||
# self.window = Window(application=self, title="UberWriter")
|
# self.window = Window(application=self, title="UberWriter")
|
||||||
self.window = window.Window(self)
|
self.window = MainWindow(self)
|
||||||
if self.args:
|
if self.args:
|
||||||
self.window.load_file(self.args[0])
|
self.window.load_file(self.args[0])
|
||||||
|
|
||||||
|
@ -178,8 +188,12 @@ class Application(Gtk.Application):
|
||||||
self.window.toggle_gradient_overlay(settings.get_value(key))
|
self.window.toggle_gradient_overlay(settings.get_value(key))
|
||||||
elif key == "input-format":
|
elif key == "input-format":
|
||||||
self.window.reload_preview()
|
self.window.reload_preview()
|
||||||
|
elif key == "sync-scroll":
|
||||||
|
self.window.reload_preview(reshow=True)
|
||||||
elif key == "stat-default":
|
elif key == "stat-default":
|
||||||
self.window.update_default_stat()
|
self.window.update_default_stat()
|
||||||
|
elif key == "preview-mode":
|
||||||
|
self.window.update_preview_mode()
|
||||||
|
|
||||||
def on_new(self, _action, _value):
|
def on_new(self, _action, _value):
|
||||||
self.window.new_document()
|
self.window.new_document()
|
||||||
|
@ -250,6 +264,10 @@ class Application(Gtk.Application):
|
||||||
action.set_state(value)
|
action.set_state(value)
|
||||||
self.settings.set_string("stat-default", value.get_string())
|
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__":
|
# ~ if __name__ == "__main__":
|
||||||
# ~ app = Application()
|
# ~ app = Application()
|
||||||
# ~ app.run(sys.argv)
|
# ~ 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.add(self.hb_revealer)
|
||||||
self.hb_container.show()
|
self.hb_container.show()
|
||||||
|
|
||||||
self.btns = buttons(app)
|
self.btns = main_buttons(app)
|
||||||
pack_buttons(self.hb, self.btns)
|
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()
|
self.hb.show_all()
|
||||||
|
|
||||||
|
@ -70,14 +94,14 @@ class FullscreenHeaderbar:
|
||||||
self.hb.show()
|
self.hb.show()
|
||||||
self.events.hide()
|
self.events.hide()
|
||||||
|
|
||||||
self.btns = buttons(app)
|
self.btns = main_buttons(app)
|
||||||
|
|
||||||
fs_btn_exit = Gtk.Button().new_from_icon_name("view-restore-symbolic",
|
fs_btn_exit = Gtk.Button().new_from_icon_name("view-restore-symbolic",
|
||||||
Gtk.IconSize.BUTTON)
|
Gtk.IconSize.BUTTON)
|
||||||
fs_btn_exit.set_tooltip_text(_("Exit Fullscreen"))
|
fs_btn_exit.set_tooltip_text(_("Exit Fullscreen"))
|
||||||
fs_btn_exit.set_action_name("app.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()
|
self.hb.show_all()
|
||||||
|
|
||||||
|
@ -101,7 +125,8 @@ class FullscreenHeaderbar:
|
||||||
else:
|
else:
|
||||||
self.revealer.set_reveal_child(False)
|
self.revealer.set_reveal_child(False)
|
||||||
|
|
||||||
def buttons(app):
|
|
||||||
|
def main_buttons(app):
|
||||||
"""constructor for the headerbar buttons
|
"""constructor for the headerbar buttons
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -154,7 +179,7 @@ def buttons(app):
|
||||||
return btn
|
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
|
"""Pack the given buttons in the given headerbar
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
|
@ -55,7 +55,7 @@ def get_builder(builder_file_name):
|
||||||
def path_to_file(path):
|
def path_to_file(path):
|
||||||
"""Return a file path (file:///) for the given path"""
|
"""Return a file path (file:///) for the given path"""
|
||||||
|
|
||||||
return "file:///" + path
|
return "file://" + path
|
||||||
|
|
||||||
|
|
||||||
def get_media_file(media_file_path):
|
def get_media_file(media_file_path):
|
||||||
|
|
|
@ -28,9 +28,11 @@ from urllib.parse import unquote
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
|
from uberwriter.text_view_markup_handler import MarkupHandler
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
|
from gi.repository import Gtk, Gdk, GdkPixbuf
|
||||||
from uberwriter import latex_to_PNG, text_view_markup_handler
|
from uberwriter import latex_to_PNG
|
||||||
from uberwriter.settings import Settings
|
from uberwriter.settings import Settings
|
||||||
|
|
||||||
from uberwriter.fix_table import FixTable
|
from uberwriter.fix_table import FixTable
|
||||||
|
@ -360,8 +362,8 @@ class InlinePreview:
|
||||||
|
|
||||||
text = self.text_buffer.get_text(start_iter, end_iter, False)
|
text = self.text_buffer.get_text(start_iter, end_iter, False)
|
||||||
|
|
||||||
math = text_view_markup_handler.regex["MATH"]
|
math = MarkupHandler.regex["MATH"]
|
||||||
link = text_view_markup_handler.regex["LINK"]
|
link = MarkupHandler.regex["LINK"]
|
||||||
|
|
||||||
footnote = re.compile(r'\[\^([^\s]+?)\]')
|
footnote = re.compile(r'\[\^([^\s]+?)\]')
|
||||||
image = re.compile(r"!\[(.*?)\]\((.+?)\)")
|
image = re.compile(r"!\[(.*?)\]\((.+?)\)")
|
||||||
|
@ -455,8 +457,7 @@ class InlinePreview:
|
||||||
path = path[7:]
|
path = path[7:]
|
||||||
elif not path.startswith("/"):
|
elif not path.startswith("/"):
|
||||||
# then the path is relative
|
# then the path is relative
|
||||||
base_path = self.settings.get_value(
|
base_path = self.settings.get_string("open-file-path")
|
||||||
"open-file-path").get_string()
|
|
||||||
path = base_path + "/" + path
|
path = base_path + "/" + path
|
||||||
|
|
||||||
LOGGER.info(path)
|
LOGGER.info(path)
|
||||||
|
|
|
@ -19,19 +19,18 @@ import locale
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import urllib
|
import urllib
|
||||||
import webbrowser
|
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
from uberwriter.export_dialog import Export
|
from uberwriter.export_dialog import Export
|
||||||
|
from uberwriter.preview_handler import PreviewHandler
|
||||||
from uberwriter.stats_handler import StatsHandler
|
from uberwriter.stats_handler import StatsHandler
|
||||||
|
from uberwriter.styled_window import StyledWindow
|
||||||
from uberwriter.text_view import TextView
|
from uberwriter.text_view import TextView
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
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 Gtk, Gdk, GObject, GLib, Gio
|
||||||
from gi.repository import WebKit2 as WebKit
|
|
||||||
|
|
||||||
import cairo
|
import cairo
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ LOGGER = logging.getLogger('uberwriter')
|
||||||
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
|
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
|
||||||
|
|
||||||
|
|
||||||
class Window(Gtk.ApplicationWindow):
|
class MainWindow(StyledWindow):
|
||||||
__gsignals__ = {
|
__gsignals__ = {
|
||||||
'save-file': (GObject.SIGNAL_ACTION, None, ()),
|
'save-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||||
'open-file': (GObject.SIGNAL_ACTION, None, ()),
|
'open-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||||
|
@ -68,17 +67,17 @@ class Window(Gtk.ApplicationWindow):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
"""Set up the main window"""
|
"""Set up the main window"""
|
||||||
|
|
||||||
Gtk.ApplicationWindow.__init__(self,
|
super().__init__(application=Gio.Application.get_default(), title="Uberwriter")
|
||||||
application=Gio.Application.get_default(),
|
|
||||||
title="Uberwriter")
|
self.get_style_context().add_class('uberwriter-window')
|
||||||
|
|
||||||
# Set UI
|
# Set UI
|
||||||
self.builder = get_builder('Window')
|
builder = get_builder('Window')
|
||||||
root = self.builder.get_object("FullscreenOverlay")
|
root = builder.get_object("FullscreenOverlay")
|
||||||
root.connect('style-updated', self.apply_current_theme)
|
self.connect("delete-event", self.on_delete_called)
|
||||||
self.add(root)
|
self.add(root)
|
||||||
|
|
||||||
self.set_default_size(900, 500)
|
self.set_default_size(1000, 600)
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
self.settings = Settings.new()
|
self.settings = Settings.new()
|
||||||
|
@ -86,7 +85,7 @@ class Window(Gtk.ApplicationWindow):
|
||||||
# Headerbars
|
# Headerbars
|
||||||
self.headerbar = headerbars.MainHeaderbar(app)
|
self.headerbar = headerbars.MainHeaderbar(app)
|
||||||
self.set_titlebar(self.headerbar.hb_container)
|
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.title_end = " – UberWriter"
|
||||||
self.set_headerbar_title("New File" + self.title_end)
|
self.set_headerbar_title("New File" + self.title_end)
|
||||||
|
@ -99,28 +98,26 @@ class Window(Gtk.ApplicationWindow):
|
||||||
self.accel_group = Gtk.AccelGroup()
|
self.accel_group = Gtk.AccelGroup()
|
||||||
self.add_accel_group(self.accel_group)
|
self.add_accel_group(self.accel_group)
|
||||||
|
|
||||||
|
self.scrolled_window = builder.get_object('editor_scrolledwindow')
|
||||||
|
|
||||||
# Setup text editor
|
# Setup text editor
|
||||||
self.text_view = TextView()
|
self.text_view = TextView(self.settings.get_int("characters-per-line"))
|
||||||
self.text_view.props.halign = Gtk.Align.CENTER
|
|
||||||
self.text_view.connect('focus-out-event', self.focus_out)
|
self.text_view.connect('focus-out-event', self.focus_out)
|
||||||
self.text_view.get_buffer().connect('changed', self.on_text_changed)
|
self.text_view.get_buffer().connect('changed', self.on_text_changed)
|
||||||
self.text_view.show()
|
self.text_view.show()
|
||||||
self.text_view.grab_focus()
|
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.scrolled_window.add(self.text_view)
|
||||||
self.editor_viewport = self.builder.get_object('editor_viewport')
|
|
||||||
|
|
||||||
# Stats counter
|
# Setup stats counter
|
||||||
self.stats_counter_revealer = self.builder.get_object('stats_counter_revealer')
|
self.stats_revealer = builder.get_object('editor_stats_revealer')
|
||||||
self.stats_button = self.builder.get_object('stats_counter')
|
self.stats_button = builder.get_object('editor_stats_button')
|
||||||
self.stats_button.get_style_context().add_class('stats-counter')
|
|
||||||
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
|
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
|
# Setup header/stats bar hide after 3 seconds
|
||||||
self.top_bottom_bars_visible = True
|
self.top_bottom_bars_visible = True
|
||||||
self.was_motion = True
|
self.was_motion = True
|
||||||
|
@ -142,8 +139,8 @@ class Window(Gtk.ApplicationWindow):
|
||||||
###
|
###
|
||||||
# Sidebar initialization test
|
# Sidebar initialization test
|
||||||
###
|
###
|
||||||
self.paned_window = self.builder.get_object("main_pained")
|
self.paned_window = builder.get_object("main_paned")
|
||||||
self.sidebar_box = self.builder.get_object("sidebar_box")
|
self.sidebar_box = builder.get_object("sidebar_box")
|
||||||
self.sidebar = Sidebar(self)
|
self.sidebar = Sidebar(self)
|
||||||
self.sidebar_box.hide()
|
self.sidebar_box.hide()
|
||||||
|
|
||||||
|
@ -151,40 +148,7 @@ class Window(Gtk.ApplicationWindow):
|
||||||
# Search and replace initialization
|
# Search and replace initialization
|
||||||
# Same interface as Sidebar ;)
|
# Same interface as Sidebar ;)
|
||||||
###
|
###
|
||||||
self.searchreplace = SearchAndReplace(self, self.text_view)
|
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
def on_text_changed(self, *_args):
|
def on_text_changed(self, *_args):
|
||||||
"""called when the text changes, sets the self.did_change to true and
|
"""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():
|
if state.get_boolean():
|
||||||
self.fullscreen()
|
self.fullscreen()
|
||||||
self.fs_headerbar.events.show()
|
self.fs_headerbar.events.show()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.unfullscreen()
|
self.unfullscreen()
|
||||||
self.fs_headerbar.events.hide()
|
self.fs_headerbar.events.hide()
|
||||||
|
|
||||||
self.text_view.grab_focus()
|
self.text_view.grab_focus()
|
||||||
|
|
||||||
def set_focus_mode(self, state):
|
def set_focus_mode(self, state):
|
||||||
"""toggle focusmode
|
"""toggle focusmode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
focus_mode = state.get_boolean()
|
self.text_view.set_focus_mode(state.get_boolean())
|
||||||
self.text_view.set_focus_mode(focus_mode)
|
|
||||||
self.text_view.grab_focus()
|
self.text_view.grab_focus()
|
||||||
|
|
||||||
def set_hemingway_mode(self, state):
|
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.set_hemingway_mode(state.get_boolean())
|
||||||
self.text_view.grab_focus()
|
self.text_view.grab_focus()
|
||||||
|
|
||||||
def window_resize(self, window, event=None):
|
def toggle_preview(self, state):
|
||||||
"""set paddings dependant of the window size
|
"""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.
|
if state.get_boolean():
|
||||||
# On Wayland (bug?), sub-windows such as the recents popover will also trigger this.
|
self.text_view.grab_focus()
|
||||||
if event and event.window != window.get_window():
|
self.preview_handler.show()
|
||||||
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")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
font_size = 18
|
self.preview_handler.hide()
|
||||||
self.get_style_context().remove_class("small")
|
self.text_view.grab_focus()
|
||||||
self.get_style_context().add_class("large")
|
|
||||||
|
|
||||||
font_width = int(font_size * 1/1.6) # Ratio specific to Fira Mono
|
return True
|
||||||
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
|
|
||||||
|
|
||||||
# TODO: refactorizable
|
# TODO: refactorizable
|
||||||
def save_document(self, _widget=None, _data=None):
|
def save_document(self, _widget=None, _data=None):
|
||||||
|
@ -467,6 +402,9 @@ class Window(Gtk.ApplicationWindow):
|
||||||
def update_default_stat(self):
|
def update_default_stat(self):
|
||||||
self.stats_handler.update_default_stat()
|
self.stats_handler.update_default_stat()
|
||||||
|
|
||||||
|
def update_preview_mode(self):
|
||||||
|
self.preview_handler.update_preview_mode()
|
||||||
|
|
||||||
def menu_toggle_sidebar(self, _widget=None):
|
def menu_toggle_sidebar(self, _widget=None):
|
||||||
"""WIP
|
"""WIP
|
||||||
"""
|
"""
|
||||||
|
@ -479,9 +417,7 @@ class Window(Gtk.ApplicationWindow):
|
||||||
status {gtk bool} -- Desired status of the spellchecking
|
status {gtk bool} -- Desired status of the spellchecking
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.text_view.gspell_view\
|
self.text_view.set_spellcheck(state.get_boolean())
|
||||||
.set_inline_spell_checking(state.get_boolean()
|
|
||||||
and not self.text_view.focus_mode)
|
|
||||||
|
|
||||||
def toggle_gradient_overlay(self, state):
|
def toggle_gradient_overlay(self, state):
|
||||||
"""Toggle the gradient overlay
|
"""Toggle the gradient overlay
|
||||||
|
@ -495,58 +431,8 @@ class Window(Gtk.ApplicationWindow):
|
||||||
elif self.overlay_id:
|
elif self.overlay_id:
|
||||||
self.scrolled_window.disconnect(self.overlay_id)
|
self.scrolled_window.disconnect(self.overlay_id)
|
||||||
|
|
||||||
def toggle_preview(self, state):
|
def reload_preview(self, reshow=False):
|
||||||
"""Toggle the preview mode
|
self.preview_handler.reload(reshow=reshow)
|
||||||
|
|
||||||
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 load_file(self, filename=None):
|
def load_file(self, filename=None):
|
||||||
"""Open File from command line or open / open recent etc."""
|
"""Open File from command line or open / open recent etc."""
|
||||||
|
@ -617,15 +503,10 @@ class Window(Gtk.ApplicationWindow):
|
||||||
True -- Gtk things
|
True -- Gtk things
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if (self.was_motion is False
|
if (not self.was_motion
|
||||||
and self.top_bottom_bars_visible
|
|
||||||
and self.buffer_modified_for_status_bar
|
and self.buffer_modified_for_status_bar
|
||||||
and self.text_view.props.has_focus): # pylint: disable=no-member
|
and self.text_view.props.has_focus):
|
||||||
# self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True)
|
self.reveal_top_bottom_bars(False)
|
||||||
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
|
|
||||||
|
|
||||||
self.was_motion = False
|
self.was_motion = False
|
||||||
return True
|
return True
|
||||||
|
@ -643,24 +524,22 @@ class Window(Gtk.ApplicationWindow):
|
||||||
return
|
return
|
||||||
if now - self.timestamp_last_mouse_motion > 100:
|
if now - self.timestamp_last_mouse_motion > 100:
|
||||||
# react on motion by fading in headerbar and statusbar
|
# react on motion by fading in headerbar and statusbar
|
||||||
if self.top_bottom_bars_visible is False:
|
self.reveal_top_bottom_bars(True)
|
||||||
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.was_motion = True
|
self.was_motion = True
|
||||||
|
|
||||||
def focus_out(self, _widget, _data=None):
|
def focus_out(self, _widget, _data=None):
|
||||||
"""events called when the window losses focus
|
"""events called when the window losses focus
|
||||||
"""
|
"""
|
||||||
if self.top_bottom_bars_visible is False:
|
self.reveal_top_bottom_bars(True)
|
||||||
self.stats_counter_revealer.set_reveal_child(True)
|
|
||||||
self.headerbar.hb_revealer.set_reveal_child(True)
|
def reveal_top_bottom_bars(self, reveal):
|
||||||
self.headerbar.hb.props.opacity = 1
|
if self.top_bottom_bars_visible != reveal:
|
||||||
self.top_bottom_bars_visible = True
|
self.headerbar.hb_revealer.set_reveal_child(reveal)
|
||||||
self.buffer_modified_for_status_bar = False
|
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):
|
def draw_gradient(self, _widget, cr):
|
||||||
"""draw fading gradient over the top and the bottom of the
|
"""draw fading gradient over the top and the bottom of the
|
||||||
|
@ -726,18 +605,4 @@ class Window(Gtk.ApplicationWindow):
|
||||||
else:
|
else:
|
||||||
self.filename = None
|
self.filename = None
|
||||||
base_path = "/"
|
base_path = "/"
|
||||||
self.settings.set_value("open-file-path", GLib.Variant("s", base_path))
|
self.settings.set_string("open-file-path", 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"
|
|
|
@ -82,8 +82,12 @@ class PreferencesDialog:
|
||||||
self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay"))
|
self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay"))
|
||||||
self.gradient_overlay_switch.connect("state-set", self.on_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_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
|
input_format_active = 0
|
||||||
for i, fmt in enumerate(self.formats):
|
for i, fmt in enumerate(self.formats):
|
||||||
input_format_store.append([i, fmt["name"]])
|
input_format_store.append([i, fmt["name"]])
|
||||||
|
@ -107,28 +111,32 @@ class PreferencesDialog:
|
||||||
preferences_window.show()
|
preferences_window.show()
|
||||||
|
|
||||||
def on_dark_mode_auto(self, _, state):
|
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():
|
if state and self.dark_mode_switch.get_active():
|
||||||
self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False))
|
self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_dark_mode(self, _, state):
|
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():
|
if state and self.dark_mode_auto_switch.get_active():
|
||||||
self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False))
|
self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_spellcheck(self, _, state):
|
def on_spellcheck(self, _, state):
|
||||||
self.settings.set_value("spellcheck", GLib.Variant.new_boolean(state))
|
self.settings.set_boolean("spellcheck", state)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_gradient_overlay(self, _, state):
|
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
|
return False
|
||||||
|
|
||||||
def on_input_format(self, combobox):
|
def on_input_format(self, combobox):
|
||||||
fmt = self.formats[combobox.get_active()]
|
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, _):
|
def on_input_format_help(self, _):
|
||||||
fmt = self.formats[self.input_format_combobox.get_active()]
|
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
|
uberwriter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parentwindow, textview):
|
def __init__(self, parentwindow, textview, builder):
|
||||||
self.parentwindow = parentwindow
|
self.parentwindow = parentwindow
|
||||||
self.textview = textview
|
self.textview = textview
|
||||||
self.textbuffer = textview.get_buffer()
|
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.box.set_reveal_child(False)
|
||||||
self.searchbar = parentwindow.builder.get_object("searchbar")
|
self.searchbar = builder.get_object("searchbar")
|
||||||
self.searchentry = parentwindow.builder.get_object("searchentrybox")
|
self.searchentry = builder.get_object("searchentrybox")
|
||||||
self.searchentry.connect('changed', self.search)
|
self.searchentry.connect('changed', self.search)
|
||||||
self.searchentry.connect('activate', self.scrolltonext)
|
self.searchentry.connect('activate', self.scrolltonext)
|
||||||
self.searchentry.connect('key-press-event', self.key_pressed)
|
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.open_replace_button.connect("toggled", self.toggle_replace)
|
||||||
|
|
||||||
self.nextbutton = parentwindow.builder.get_object("next_result")
|
self.nextbutton = builder.get_object("next_result")
|
||||||
self.prevbutton = parentwindow.builder.get_object("previous_result")
|
self.prevbutton = builder.get_object("previous_result")
|
||||||
self.regexbutton = parentwindow.builder.get_object("regex")
|
self.regexbutton = builder.get_object("regex")
|
||||||
self.casesensitivebutton = parentwindow.builder.get_object("case_sensitive")
|
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.replacebox.set_reveal_child(False)
|
||||||
self.replace_one_button = parentwindow.builder.get_object("replace_one")
|
self.replace_one_button = builder.get_object("replace_one")
|
||||||
self.replace_all_button = parentwindow.builder.get_object("replace_all")
|
self.replace_all_button = builder.get_object("replace_all")
|
||||||
self.replaceentry = parentwindow.builder.get_object("replaceentrybox")
|
self.replaceentry = builder.get_object("replaceentrybox")
|
||||||
|
|
||||||
self.replace_all_button.connect('clicked', self.replace_all)
|
self.replace_all_button.connect('clicked', self.replace_all)
|
||||||
self.replace_one_button.connect('clicked', self.replace_clicked)
|
self.replace_one_button.connect('clicked', self.replace_clicked)
|
||||||
|
@ -96,7 +96,8 @@ class SearchAndReplace:
|
||||||
"""
|
"""
|
||||||
show search box
|
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.searchbar.set_search_mode(True)
|
||||||
self.box.set_reveal_child(True)
|
self.box.set_reveal_child(True)
|
||||||
self.searchentry.grab_focus()
|
self.searchentry.grab_focus()
|
||||||
|
|
|
@ -11,8 +11,11 @@ from uberwriter import helpers
|
||||||
class StatsCounter:
|
class StatsCounter:
|
||||||
"""Counts characters, words, sentences and read time using a background thread."""
|
"""Counts characters, words, sentences and read time using a background thread."""
|
||||||
|
|
||||||
# Regexp that matches any character, except for newlines and subsequent spaces.
|
# Regexp that matches characters, with the following exceptions:
|
||||||
CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))")
|
# * Newlines
|
||||||
|
# * Sequential spaces
|
||||||
|
# * Sequential dashes
|
||||||
|
CHARACTERS = re.compile(r"[^\s-]|(?:[^\S\n](?!\s)|-(?![-\n]))")
|
||||||
|
|
||||||
# Regexp that matches Asian letters, general symbols and hieroglyphs,
|
# Regexp that matches Asian letters, general symbols and hieroglyphs,
|
||||||
# as well as sequences of word characters optionally containing non-word characters in-between.
|
# 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 gettext import gettext as _
|
||||||
from queue import Queue
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from gi.repository import GLib, Gio, Gtk
|
from gi.repository import GLib, Gio, Gtk
|
||||||
|
|
||||||
from uberwriter import helpers
|
|
||||||
from uberwriter.helpers import get_builder
|
|
||||||
from uberwriter.settings import Settings
|
from uberwriter.settings import Settings
|
||||||
from uberwriter.stats_counter import StatsCounter
|
from uberwriter.stats_counter import StatsCounter
|
||||||
|
|
||||||
|
@ -15,6 +9,13 @@ from uberwriter.stats_counter import StatsCounter
|
||||||
class StatsHandler:
|
class StatsHandler:
|
||||||
"""Shows a default statistic on the stats button, and allows the user to toggle which one."""
|
"""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):
|
def __init__(self, stats_button, text_view):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
@ -34,7 +35,6 @@ class StatsHandler:
|
||||||
self.read_time = (0, 0, 0)
|
self.read_time = (0, 0, 0)
|
||||||
|
|
||||||
self.settings = Settings.new()
|
self.settings = Settings.new()
|
||||||
self.default_stat = self.settings.get_enum("stat-default")
|
|
||||||
|
|
||||||
self.stats_counter = StatsCounter()
|
self.stats_counter = StatsCounter()
|
||||||
|
|
||||||
|
@ -65,16 +65,16 @@ class StatsHandler:
|
||||||
self.update_stats)
|
self.update_stats)
|
||||||
|
|
||||||
def get_text_for_stat(self, stat):
|
def get_text_for_stat(self, stat):
|
||||||
if stat == 0:
|
if stat == self.CHARACTERS:
|
||||||
return _("{:n} Characters".format(self.characters))
|
return _("{:n} Characters").format(self.characters)
|
||||||
elif stat == 1:
|
elif stat == self.WORDS:
|
||||||
return _("{:n} Words".format(self.words))
|
return _("{:n} Words").format(self.words)
|
||||||
elif stat == 2:
|
elif stat == self.SENTENCES:
|
||||||
return _("{:n} Sentences".format(self.sentences))
|
return _("{:n} Sentences").format(self.sentences)
|
||||||
elif stat == 3:
|
elif stat == self.PARAGRAPHS:
|
||||||
return _("{:n} Paragraphs".format(self.paragraphs))
|
return _("{:n} Paragraphs").format(self.paragraphs)
|
||||||
elif stat == 4:
|
elif stat == self.READ_TIME:
|
||||||
return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time))
|
return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown stat {}".format(stat))
|
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
|
import gi
|
||||||
|
from gi.repository.GObject import SignalMatchType
|
||||||
|
|
||||||
from uberwriter.inline_preview import InlinePreview
|
from uberwriter.inline_preview import InlinePreview
|
||||||
from uberwriter.text_view_format_inserter import FormatInserter
|
from uberwriter.text_view_format_inserter import FormatInserter
|
||||||
from uberwriter.text_view_markup_handler import MarkupHandler
|
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_undo_redo_handler import UndoRedoHandler
|
||||||
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
|
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('Gtk', '3.0')
|
||||||
gi.require_version('Gspell', '1')
|
gi.require_version('Gspell', '1')
|
||||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
|
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
LOGGER = logging.getLogger('uberwriter')
|
LOGGER = logging.getLogger('uberwriter')
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,10 +38,13 @@ class TextView(Gtk.TextView):
|
||||||
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
|
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
|
||||||
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
|
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
|
||||||
'undo': (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__()
|
super().__init__()
|
||||||
|
|
||||||
# Appearance
|
# Appearance
|
||||||
|
@ -49,11 +54,18 @@ class TextView(Gtk.TextView):
|
||||||
self.set_pixels_inside_wrap(8)
|
self.set_pixels_inside_wrap(8)
|
||||||
self.get_style_context().add_class('uberwriter-editor')
|
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
|
# General behavior
|
||||||
self.get_buffer().connect('changed', self.on_text_changed)
|
|
||||||
self.connect('size-allocate', self.on_size_allocate)
|
self.connect('size-allocate', self.on_size_allocate)
|
||||||
|
self.get_buffer().connect('changed', self.on_text_changed)
|
||||||
|
|
||||||
# Spell checking
|
# Spell checking
|
||||||
|
self.spellcheck = True
|
||||||
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
|
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
|
||||||
self.gspell_view.basic_setup()
|
self.gspell_view.basic_setup()
|
||||||
|
|
||||||
|
@ -85,6 +97,7 @@ class TextView(Gtk.TextView):
|
||||||
|
|
||||||
# Scrolling
|
# Scrolling
|
||||||
self.scroller = None
|
self.scroller = None
|
||||||
|
self.connect('parent-set', self.on_parent_set)
|
||||||
self.get_buffer().connect('mark-set', self.on_mark_set)
|
self.get_buffer().connect('mark-set', self.on_mark_set)
|
||||||
|
|
||||||
# Focus mode
|
# Focus mode
|
||||||
|
@ -95,6 +108,12 @@ class TextView(Gtk.TextView):
|
||||||
self.hemingway_mode = False
|
self.hemingway_mode = False
|
||||||
self.connect('key-press-event', self.on_key_press_event)
|
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):
|
def get_text(self):
|
||||||
text_buffer = self.get_buffer()
|
text_buffer = self.get_buffer()
|
||||||
start_iter = text_buffer.get_start_iter()
|
start_iter = text_buffer.get_start_iter()
|
||||||
|
@ -105,13 +124,59 @@ class TextView(Gtk.TextView):
|
||||||
text_buffer = self.get_buffer()
|
text_buffer = self.get_buffer()
|
||||||
text_buffer.set_text(text)
|
text_buffer.set_text(text)
|
||||||
|
|
||||||
def on_text_changed(self, *_):
|
def get_scroll_scale(self):
|
||||||
self.markup.apply()
|
return self.scroller.get_scroll_scale() if self.scroller else 0
|
||||||
GLib.idle_add(self.scroll_to)
|
|
||||||
|
def set_scroll_scale(self, scale):
|
||||||
|
if self.scroller:
|
||||||
|
self.scroller.set_scroll_scale(scale)
|
||||||
|
|
||||||
def on_size_allocate(self, *_):
|
def on_size_allocate(self, *_):
|
||||||
|
self.update_horizontal_margin()
|
||||||
self.update_vertical_margin()
|
self.update_vertical_margin()
|
||||||
self.markup.update_margins_indents()
|
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):
|
def set_focus_mode(self, focus_mode):
|
||||||
"""Toggle focus mode.
|
"""Toggle focus mode.
|
||||||
|
@ -120,10 +185,33 @@ class TextView(Gtk.TextView):
|
||||||
and the surrounding text is greyed out."""
|
and the surrounding text is greyed out."""
|
||||||
|
|
||||||
self.focus_mode = focus_mode
|
self.focus_mode = focus_mode
|
||||||
self.gspell_view.set_inline_spell_checking(not focus_mode)
|
|
||||||
self.update_vertical_margin()
|
self.update_vertical_margin()
|
||||||
self.markup.apply()
|
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):
|
def update_vertical_margin(self):
|
||||||
if self.focus_mode:
|
if self.focus_mode:
|
||||||
|
@ -134,11 +222,6 @@ class TextView(Gtk.TextView):
|
||||||
self.props.top_margin = 80
|
self.props.top_margin = 80
|
||||||
self.props.bottom_margin = 64
|
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):
|
def set_hemingway_mode(self, hemingway_mode):
|
||||||
"""Toggle hemingway mode.
|
"""Toggle hemingway mode.
|
||||||
|
|
||||||
|
@ -158,48 +241,34 @@ class TextView(Gtk.TextView):
|
||||||
self.get_buffer().set_text('')
|
self.get_buffer().set_text('')
|
||||||
self.undo_redo.clear()
|
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.
|
"""Scrolls if needed to ensure mark is visible.
|
||||||
|
|
||||||
If mark is unspecified, the cursor is used."""
|
If mark is unspecified, the cursor is used."""
|
||||||
|
|
||||||
margin = 32
|
if self.scroller is None:
|
||||||
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:
|
|
||||||
return
|
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()
|
def get_min_width(self, font_size=None):
|
||||||
if mark:
|
"""Returns the minimum width of this text view."""
|
||||||
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)
|
|
||||||
|
|
||||||
pos_y = mark_rect.y + mark_rect.height + self.props.top_margin
|
if font_size is None:
|
||||||
pos = pos_y - va.props.value
|
font_size = self.font_sizes[-1]
|
||||||
target_pos = None
|
return (self.line_chars + self.get_pad_chars(font_size) + 1) \
|
||||||
if self.focus_mode:
|
* self.get_char_width(font_size) - 1
|
||||||
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 self.scroller and self.scroller.is_started:
|
def get_pad_chars(self, font_size):
|
||||||
self.scroller.end()
|
"""Returns the amount of character padding for font_size.
|
||||||
if target_pos:
|
|
||||||
self.scroller = Scroller(scrolled_window, va.props.value, target_pos)
|
|
||||||
self.scroller.start()
|
|
||||||
|
|
||||||
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
|
Markup can use up to 7 in normal conditions."""
|
||||||
if mark.get_name() == 'insert':
|
|
||||||
self.markup.apply()
|
return 8 * (1 + font_size - self.font_sizes[-1])
|
||||||
if self.focus_mode:
|
|
||||||
self.scroll_to(mark)
|
@staticmethod
|
||||||
elif mark.get_name() == 'gtk_drag_target':
|
def get_char_width(font_size):
|
||||||
self.scroll_to(mark)
|
"""Returns the font width for a given size. Note: specific to Fira Mono!"""
|
||||||
return True
|
|
||||||
|
return font_size * 1 / 1.6
|
||||||
|
|
|
@ -37,13 +37,13 @@ class MarkupHandler:
|
||||||
"BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"),
|
"BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"),
|
||||||
"STRIKETHROUGH": re.compile(r"~~.+?~~"),
|
"STRIKETHROUGH": re.compile(r"~~.+?~~"),
|
||||||
"LINK": 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),
|
"LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE),
|
||||||
"NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE),
|
"NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE),
|
||||||
"NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[.)]) .+", re.MULTILINE),
|
"NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[.)]) .+", re.MULTILINE),
|
||||||
"BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:>|(?:> )+).+", re.MULTILINE),
|
"BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:>|(?:> )+).+", re.MULTILINE),
|
||||||
"HEADER": re.compile(r"^[ ]{0,3}(#{1,6}) [^\n]+", 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),
|
"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),
|
"TABLE": re.compile(r"^[\-+]{5,}\n(.+?)\n[\-+]{5,}\n", re.DOTALL),
|
||||||
"MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"),
|
"MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"),
|
||||||
|
@ -79,10 +79,10 @@ class MarkupHandler:
|
||||||
strikethrough=False,
|
strikethrough=False,
|
||||||
justification=Gtk.Justification.LEFT)
|
justification=Gtk.Justification.LEFT)
|
||||||
|
|
||||||
self.table = buffer.create_tag('table')
|
self.table = buffer.create_tag('table',
|
||||||
self.table.set_property('wrap-mode', Gtk.WrapMode.NONE)
|
wrap_mode=Gtk.WrapMode.NONE,
|
||||||
self.table.set_property('pixels-above-lines', 0)
|
pixels_above_lines=0,
|
||||||
self.table.set_property('pixels-below-lines', 0)
|
pixels_below_lines=0)
|
||||||
|
|
||||||
self.mathtext = buffer.create_tag('mathtext')
|
self.mathtext = buffer.create_tag('mathtext')
|
||||||
|
|
||||||
|
@ -94,6 +94,8 @@ class MarkupHandler:
|
||||||
# Margin and indents
|
# Margin and indents
|
||||||
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc
|
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc
|
||||||
self.margins_indents = {}
|
self.margins_indents = {}
|
||||||
|
self.baseline_margin = 0
|
||||||
|
self.char_width = 0
|
||||||
self.update_margins_indents()
|
self.update_margins_indents()
|
||||||
|
|
||||||
# Style
|
# Style
|
||||||
|
@ -272,11 +274,10 @@ class MarkupHandler:
|
||||||
def get_margin_indent_tag(self, margin_level, indent_level):
|
def get_margin_indent_tag(self, margin_level, indent_level):
|
||||||
level = (margin_level, indent_level)
|
level = (margin_level, indent_level)
|
||||||
if level not in self.margins_indents:
|
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)
|
margin, indent = self.get_margin_indent(margin_level, indent_level)
|
||||||
tag.set_property("left-margin", margin)
|
tag = self.text_buffer.create_tag(
|
||||||
tag.set_property("indent", indent)
|
"margin_indent_{}_{}".format(margin_level, indent_level),
|
||||||
|
left_margin=margin, indent=indent)
|
||||||
self.margins_indents[level] = tag
|
self.margins_indents[level] = tag
|
||||||
return tag
|
return tag
|
||||||
else:
|
else:
|
||||||
|
@ -284,7 +285,7 @@ class MarkupHandler:
|
||||||
|
|
||||||
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
|
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
|
||||||
if baseline_margin is 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:
|
if char_width is None:
|
||||||
char_width = helpers.get_char_width(self.text_view)
|
char_width = helpers.get_char_width(self.text_view)
|
||||||
margin = max(baseline_margin + char_width * margin_level, 0)
|
margin = max(baseline_margin + char_width * margin_level, 0)
|
||||||
|
@ -292,16 +293,21 @@ class MarkupHandler:
|
||||||
return margin, indent
|
return margin, indent
|
||||||
|
|
||||||
def update_margins_indents(self):
|
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)
|
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 = Pango.TabArray.new(1, True)
|
||||||
tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
|
tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
|
||||||
self.text_view.set_tabs(tab_array)
|
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():
|
for level, tag in self.margins_indents.items():
|
||||||
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
|
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
|
||||||
tag.set_property("left-margin", margin)
|
tag.set_properties(left_margin=margin, indent=indent)
|
||||||
tag.set_property("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
|
@classmethod
|
||||||
def get_current_changed(cls):
|
def get_current_changed(cls):
|
||||||
theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name')
|
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_auto = cls.settings.get_boolean('dark-mode-auto')
|
||||||
dark_mode = cls.settings.get_value('dark-mode').get_boolean()
|
dark_mode = cls.settings.get_boolean('dark-mode')
|
||||||
current_theme = cls.get_for_name(theme_name)
|
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:
|
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)
|
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