apostrophe/uberwriter/main_window.py

607 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
# BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
# END LICENSE
import codecs
import locale
import logging
import os
import urllib
from gettext import gettext as _
import gi
from uberwriter.export_dialog import Export
from uberwriter.preview_handler import PreviewHandler
from uberwriter.stats_handler import StatsHandler
from uberwriter.styled_window import StyledWindow
from uberwriter.text_view import TextView
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
import cairo
from uberwriter import helpers
from uberwriter.theme import Theme
from uberwriter.helpers import get_builder
from uberwriter.sidebar import Sidebar
from uberwriter.search_and_replace import SearchAndReplace
from uberwriter.settings import Settings
from . import headerbars
# Some Globals
# TODO move them somewhere for better
# accesibility from other files
LOGGER = logging.getLogger('uberwriter')
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
class MainWindow(StyledWindow):
__gsignals__ = {
'save-file': (GObject.SIGNAL_ACTION, None, ()),
'open-file': (GObject.SIGNAL_ACTION, None, ()),
'save-file-as': (GObject.SIGNAL_ACTION, None, ()),
'new-file': (GObject.SIGNAL_ACTION, None, ()),
'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()),
'toggle-preview': (GObject.SIGNAL_ACTION, None, ()),
'close-window': (GObject.SIGNAL_ACTION, None, ())
}
def __init__(self, app):
"""Set up the main window"""
super().__init__(application=Gio.Application.get_default(), title="Uberwriter")
self.get_style_context().add_class('uberwriter-window')
# Set UI
builder = get_builder('Window')
root = builder.get_object("FullscreenOverlay")
self.connect("delete-event", self.on_delete_called)
self.add(root)
self.set_default_size(1000, 600)
# Preferences
self.settings = Settings.new()
# Headerbars
self.headerbar = headerbars.MainHeaderbar(app)
self.set_titlebar(self.headerbar.hb_container)
self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app)
self.title_end = " UberWriter"
self.set_headerbar_title("New File" + self.title_end)
self.timestamp_last_mouse_motion = 0
if self.settings.get_value("poll-motion"):
self.connect("motion-notify-event", self.on_motion_notify)
GObject.timeout_add(3000, self.poll_for_motion)
self.accel_group = Gtk.AccelGroup()
self.add_accel_group(self.accel_group)
self.scrolled_window = builder.get_object('editor_scrolledwindow')
# Setup text editor
self.text_view = TextView(self.settings.get_int("characters-per-line"))
self.text_view.connect('focus-out-event', self.focus_out)
self.text_view.get_buffer().connect('changed', self.on_text_changed)
self.text_view.show()
self.text_view.grab_focus()
self.scrolled_window.add(self.text_view)
# Setup stats counter
self.stats_revealer = builder.get_object('editor_stats_revealer')
self.stats_button = builder.get_object('editor_stats_button')
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
# Setup preview
content = builder.get_object('content')
editor = builder.get_object('editor')
self.preview_handler = PreviewHandler(self, content, editor, self.text_view)
# Setup header/stats bar hide after 3 seconds
self.top_bottom_bars_visible = True
self.was_motion = True
self.buffer_modified_for_status_bar = False
# some people seems to have performance problems with the overlay.
# Let them disable it
self.overlay_id = None
self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay"))
# Init file name with None
self.set_filename()
# Setting up spellcheck
self.auto_correct = None
self.toggle_spellcheck(self.settings.get_value("spellcheck"))
self.did_change = False
###
# Sidebar initialization test
###
self.paned_window = builder.get_object("main_paned")
self.sidebar_box = builder.get_object("sidebar_box")
self.sidebar = Sidebar(self)
self.sidebar_box.hide()
###
# Search and replace initialization
# Same interface as Sidebar ;)
###
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
def on_text_changed(self, *_args):
"""called when the text changes, sets the self.did_change to true and
updates the title and the counters to reflect that
"""
if self.did_change is False:
self.did_change = True
title = self.get_title()
self.set_headerbar_title("* " + title)
self.buffer_modified_for_status_bar = True
def set_fullscreen(self, state):
"""Puts the application in fullscreen mode and show/hides
the poller for motion in the top border
Arguments:
state {almost bool} -- The desired fullscreen state of the window
"""
if state.get_boolean():
self.fullscreen()
self.fs_headerbar.events.show()
else:
self.unfullscreen()
self.fs_headerbar.events.hide()
self.text_view.grab_focus()
def set_focus_mode(self, state):
"""toggle focusmode
"""
self.text_view.set_focus_mode(state.get_boolean())
self.text_view.grab_focus()
def set_hemingway_mode(self, state):
"""toggle hemingwaymode
"""
self.text_view.set_hemingway_mode(state.get_boolean())
self.text_view.grab_focus()
def toggle_preview(self, state):
"""Toggle the preview mode
Arguments:
state {gtk bool} -- Desired state of the preview mode (enabled/disabled)
"""
if state.get_boolean():
self.text_view.grab_focus()
self.preview_handler.show()
else:
self.preview_handler.hide()
self.text_view.grab_focus()
return True
# TODO: refactorizable
def save_document(self, _widget=None, _data=None):
"""provide to the user a filechooser and save the document
where he wants. Call set_headbar_title after that
"""
if self.filename:
LOGGER.info("saving")
filename = self.filename
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.text_view.get_text())
file_to_save.close()
if self.did_change:
self.did_change = False
title = self.get_title()
self.set_headerbar_title(title[2:])
return Gtk.ResponseType.OK
filefilter = Gtk.FileFilter.new()
filefilter.add_mime_type('text/x-markdown')
filefilter.add_mime_type('text/plain')
filefilter.set_name('Markdown (.md)')
filechooser = Gtk.FileChooserDialog(
_("Save your File"),
self,
Gtk.FileChooserAction.SAVE,
("_Cancel", Gtk.ResponseType.CANCEL,
"_Save", Gtk.ResponseType.OK)
)
filechooser.set_do_overwrite_confirmation(True)
filechooser.add_filter(filefilter)
response = filechooser.run()
if response == Gtk.ResponseType.OK:
filename = filechooser.get_filename()
if filename[-3:] != ".md":
filename = filename + ".md"
try:
self.recent_manager.add_item("file:/ " + filename)
except:
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
self.set_headerbar_title(
os.path.basename(filename) + self.title_end)
self.did_change = False
filechooser.destroy()
return response
filechooser.destroy()
return Gtk.ResponseType.CANCEL
def save_document_as(self, _widget=None, _data=None):
"""provide to the user a filechooser and save the document
where he wants. Call set_headbar_title after that
"""
filechooser = Gtk.FileChooserDialog(
"Save your File",
self,
Gtk.FileChooserAction.SAVE,
("_Cancel", Gtk.ResponseType.CANCEL,
"_Save", Gtk.ResponseType.OK)
)
filechooser.set_do_overwrite_confirmation(True)
if self.filename:
filechooser.set_filename(self.filename)
response = filechooser.run()
if response == Gtk.ResponseType.OK:
filename = filechooser.get_filename()
if filename[-3:] != ".md":
filename = filename + ".md"
try:
self.recent_manager.remove_item("file:/" + filename)
self.recent_manager.add_item("file:/ " + filename)
except:
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
self.set_headerbar_title(
os.path.basename(filename) + self.title_end)
try:
self.recent_manager.add_item(filename)
except:
pass
filechooser.destroy()
self.did_change = False
else:
filechooser.destroy()
return Gtk.ResponseType.CANCEL
def copy_html_to_clipboard(self, _widget=None, _date=None):
"""Copies only html without headers etc. to Clipboard
"""
output = helpers.pandoc_convert(self.text_view.get_text())
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(output, -1)
clipboard.store()
def open_document(self, _widget=None):
"""open the desired file
"""
if self.check_change() == Gtk.ResponseType.CANCEL:
return
markdown_filter = Gtk.FileFilter.new()
markdown_filter.add_mime_type('text/markdown')
markdown_filter.add_mime_type('text/x-markdown')
markdown_filter.set_name(_('Markdown Files'))
plaintext_filter = Gtk.FileFilter.new()
plaintext_filter.add_mime_type('text/plain')
plaintext_filter.set_name(_('Plain Text Files'))
filechooser = Gtk.FileChooserDialog(
_("Open a .md file"),
self,
Gtk.FileChooserAction.OPEN,
("_Cancel", Gtk.ResponseType.CANCEL,
"_Open", Gtk.ResponseType.OK)
)
filechooser.add_filter(markdown_filter)
filechooser.add_filter(plaintext_filter)
response = filechooser.run()
if response == Gtk.ResponseType.OK:
filename = filechooser.get_filename()
self.load_file(filename)
filechooser.destroy()
elif response == Gtk.ResponseType.CANCEL:
filechooser.destroy()
def check_change(self):
"""Show dialog to prevent loss of unsaved changes
"""
if self.did_change and self.text_view.get_text():
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.NONE,
_("You have not saved your changes.")
)
dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO)
dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("Save now"), Gtk.ResponseType.YES)
# dialog.set_default_size(200, 60)
dialog.set_default_response(Gtk.ResponseType.YES)
response = dialog.run()
if response == Gtk.ResponseType.YES:
if self.save_document() == Gtk.ResponseType.CANCEL:
dialog.destroy()
return self.check_change()
dialog.destroy()
return response
if response == Gtk.ResponseType.NO:
dialog.destroy()
return response
dialog.destroy()
return Gtk.ResponseType.CANCEL
def new_document(self, _widget=None):
"""create new document
"""
if self.check_change() == Gtk.ResponseType.CANCEL:
return
self.text_view.clear()
self.did_change = False
self.set_filename()
self.set_headerbar_title(_("New File") + self.title_end)
def update_default_stat(self):
self.stats_handler.update_default_stat()
def update_preview_mode(self):
self.preview_handler.update_preview_mode()
def menu_toggle_sidebar(self, _widget=None):
"""WIP
"""
self.sidebar.toggle_sidebar()
def toggle_spellcheck(self, state):
"""Enable/disable the autospellchecking
Arguments:
status {gtk bool} -- Desired status of the spellchecking
"""
self.text_view.set_spellcheck(state.get_boolean())
def toggle_gradient_overlay(self, state):
"""Toggle the gradient overlay
Arguments:
state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled)
"""
if state.get_boolean():
self.overlay_id = self.scrolled_window.connect_after("draw", self.draw_gradient)
elif self.overlay_id:
self.scrolled_window.disconnect(self.overlay_id)
def reload_preview(self, reshow=False):
self.preview_handler.reload(reshow=reshow)
def load_file(self, filename=None):
"""Open File from command line or open / open recent etc."""
if self.check_change() == Gtk.ResponseType.CANCEL:
return
if filename:
if filename.startswith('file://'):
filename = filename[7:]
filename = urllib.parse.unquote_plus(filename)
self.text_view.clear()
try:
if os.path.exists(filename):
current_file = codecs.open(filename, encoding="utf-8", mode='r')
self.text_view.set_text(current_file.read())
current_file.close()
self.set_headerbar_title(os.path.basename(filename) + self.title_end)
self.set_filename(filename)
except Exception:
LOGGER.warning("Error Reading File: %r" % Exception)
self.did_change = False
else:
LOGGER.warning("No File arg")
def open_uberwriter_markdown(self, _widget=None, _data=None):
"""open a markdown mini tutorial
"""
if self.check_change() == Gtk.ResponseType.CANCEL:
return
self.load_file(helpers.get_media_file('uberwriter_markdown.md'))
def open_search_and_replace(self):
"""toggle the search box
"""
self.searchreplace.toggle_search()
def open_advanced_export(self, _widget=None, _data=None):
"""open the export and advanced export dialog
"""
self.export = Export(self.filename)
self.export.dialog.set_transient_for(self)
response = self.export.dialog.run()
if response == 1:
self.export.export(bytes(self.text_view.get_text(), "utf-8"))
self.export.dialog.destroy()
def open_recent(self, _widget, data=None):
"""open the given recent document
"""
print("open")
if data:
if self.check_change() == Gtk.ResponseType.CANCEL:
return
self.load_file(data)
def poll_for_motion(self):
"""check if the user has moved the cursor to show the headerbar
Returns:
True -- Gtk things
"""
if (not self.was_motion
and self.buffer_modified_for_status_bar
and self.text_view.props.has_focus):
self.reveal_top_bottom_bars(False)
self.was_motion = False
return True
def on_motion_notify(self, _widget, event, _data=None):
"""check the motion of the mouse to fade in the headerbar
"""
now = event.get_time()
if now - self.timestamp_last_mouse_motion > 150:
# filter out accidental motions
self.timestamp_last_mouse_motion = now
return
if now - self.timestamp_last_mouse_motion < 100:
# filter out accidental motion
return
if now - self.timestamp_last_mouse_motion > 100:
# react on motion by fading in headerbar and statusbar
self.reveal_top_bottom_bars(True)
self.was_motion = True
def focus_out(self, _widget, _data=None):
"""events called when the window losses focus
"""
self.reveal_top_bottom_bars(True)
def reveal_top_bottom_bars(self, reveal):
if self.top_bottom_bars_visible != reveal:
self.headerbar.hb_revealer.set_reveal_child(reveal)
self.stats_revealer.set_reveal_child(reveal)
for revealer in self.preview_handler.get_top_bottom_bar_revealers():
revealer.set_reveal_child(reveal)
self.top_bottom_bars_visible = reveal
self.buffer_modified_for_status_bar = reveal
def draw_gradient(self, _widget, cr):
"""draw fading gradient over the top and the bottom of the
TextWindow
"""
bg_color = self.get_style_context().get_background_color(Gtk.StateFlags.ACTIVE)
lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member
lg_top.add_color_stop_rgba(
0, bg_color.red, bg_color.green, bg_color.blue, 1)
lg_top.add_color_stop_rgba(
1, bg_color.red, bg_color.green, bg_color.blue, 0)
width = self.scrolled_window.get_allocation().width
height = self.scrolled_window.get_allocation().height
cr.rectangle(0, 0, width, 32)
cr.set_source(lg_top)
cr.fill()
cr.rectangle(0, height - 32, width, height)
lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member
lg_btm.add_color_stop_rgba(
1, bg_color.red, bg_color.green, bg_color.blue, 1)
lg_btm.add_color_stop_rgba(
0, bg_color.red, bg_color.green, bg_color.blue, 0)
cr.set_source(lg_btm)
cr.fill()
def on_delete_called(self, _widget, _data=None):
"""Called when the TexteditorWindow is closed.
"""
LOGGER.info('delete called')
if self.check_change() == Gtk.ResponseType.CANCEL:
return True
return False
def on_mnu_close_activate(self, _widget, _data=None):
"""Signal handler for closing the Window.
Overriden from parent Window Class
"""
if self.on_delete_called(self): # Really destroy?
return
self.destroy()
return
def set_headerbar_title(self, title):
"""set the desired headerbar title
"""
self.headerbar.hb.props.title = title
self.fs_headerbar.hb.props.title = title
self.set_title(title)
def set_filename(self, filename=None):
"""set filename
"""
if filename:
self.filename = filename
base_path = os.path.dirname(self.filename)
else:
self.filename = None
base_path = "/"
self.settings.set_string("open-file-path", base_path)