#!usr/bin/python3 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2020 <Jamie McGowan> <jamiemcgowan.dev@gmail.com> # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. ### END LICENSE import gi gi.require_version('Gtk', '3.0') gi.require_version('GtkSource', '3.0') gi.require_version('WebKit2', '4.0') from bs4 import BeautifulSoup from gi.repository import Gdk, Gtk, GtkSource, Pango, WebKit2 from locale import gettext as _ from urllib.request import urlopen import markdown import os import pdfkit import re, subprocess, datetime, os, webbrowser, _thread, sys, locale import tempfile import traceback import styles import unicodedata import warnings from findBar import FindBar # Check if gtkspellcheck is installed try: from gtkspellcheck import SpellChecker spellcheck_enabled = True except: print("*Spellchecking not enabled.\n*To enable spellchecking install pygtkspellcheck\n*https://pypi.python.org/pypi/pygtkspellcheck/") spellcheck_enabled = False import logging logger = logging.getLogger('remarkable') # Ignore warnings re. scroll handler (temp. fix) && starting GTK warning warnings.filterwarnings("ignore", ".*has no handler with id.*") from remarkable_lib import Window, remarkableconfig from remarkable.AboutRemarkableDialog import AboutRemarkableDialog app_version = 1.9 # Remarkable app version class RemarkableWindow(Window): __gtype_name__ = "RemarkableWindow" def finish_initializing(self, builder): # pylint: disable=E1002 """Set up the main window""" super(RemarkableWindow, self).finish_initializing(builder) self.AboutDialog = AboutRemarkableDialog self.settings = Gtk.Settings.get_default() self.is_fullscreen = False self.zoom_steps = 0.1 self.editor_position = 0 self.homeDir = os.environ['HOME'] self.path = os.path.join(self.homeDir, ".remarkable/") self.settings_path = os.path.join(self.path, "remarkable.settings") self.media_path = remarkableconfig.get_data_path() + os.path.sep + "media" + os.path.sep self.name = "Untitled" # Title of the current file, set to 'Untitled' as default self.default_html_start = '<!doctype HTML><html><head><meta charset="utf-8"><title>Made with Remarkable!</title><link rel="stylesheet" href="' + self.media_path + 'highlightjs.default.min.css">' self.default_html_start += "<style type='text/css'>" + styles.get() + "</style>" self.default_html_start += "</head><body id='MathPreviewF'>" self.default_html_end = '<script src="' + self.media_path + 'highlight.min.js"></script><script>hljs.initHighlightingOnLoad();</script><script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script><script type="text/javascript">MathJax.Hub.Config({"showProcessingMessages" : false,"messageStyle" : "none","tex2jax": { inlineMath: [ [ "$", "$" ] ] }});</script></body></html>' self.remarkable_settings = {} self.default_extensions = ['markdown.extensions.extra','markdown.extensions.toc', 'markdown.extensions.smarty', 'markdown.extensions.urlize', 'markdown.extensions.Highlighting', 'markdown.extensions.Strikethrough', 'markdown.extensions.markdown_checklist', 'markdown.extensions.superscript', 'markdown.extensions.subscript', 'markdown.extensions.mathjax'] self.safe_extensions = ['markdown.extensions.extra'] self.pdf_error_warning = False self.window = self.builder.get_object("remarkable_window") self.window.connect("delete-event", self.window_delete_event) self.window.connect("destroy", self.quit_requested) self.text_buffer = GtkSource.Buffer() self.text_view = GtkSource.View.new_with_buffer(self.text_buffer) self.text_view.set_show_line_numbers(True) self.text_view.set_auto_indent(True) # Force the SourceView to use a SourceBuffer and not a TextBuffer self.lang_manager = GtkSource.LanguageManager() self.text_buffer.set_language(self.lang_manager.get_language('markdown')) self.text_buffer.set_highlight_matching_brackets(True) self.undo_manager = self.text_buffer.get_undo_manager() self.undo_manager.connect("can-undo-changed", self.can_undo_changed) self.undo_manager.connect("can-redo-changed", self.can_redo_changed) self.text_buffer.connect("changed", self.on_text_view_changed) self.text_view.set_buffer(self.text_buffer) self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) self.text_view.connect('key-press-event', self.cursor_ctrl_arrow_rtl_fix) self.live_preview = WebKit2.WebView() self.scrolledwindow_text_view = Gtk.ScrolledWindow() self.scrolledwindow_text_view.add(self.text_view) self.scrolledwindow_live_preview = Gtk.ScrolledWindow() self.scrolledwindow_live_preview.add(self.live_preview) self.paned = self.builder.get_object("paned") self.paned.set_position(self.window.get_size()[0]/2) self.paned.pack1(self.scrolledwindow_text_view) self.paned.pack2(self.scrolledwindow_live_preview) self.toolbar = self.builder.get_object("toolbar") self.toolbutton_undo = self.builder.get_object("toolbutton_undo") self.toolbutton_undo.set_sensitive(False) self.toolbutton_redo = self.builder.get_object("toolbutton_redo") self.toolbutton_redo.set_sensitive(False) self.statusbar = self.builder.get_object("statusbar") self.context_id = self.statusbar.get_context_id("main status bar") self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.update_status_bar(self) self.update_live_preview(self) text = "" self.wrap_box = self.builder.get_object("wrap_box") self.find_entry = self.builder.get_object("find_entry") self.replace_entry = self.builder.get_object("replace_entry") match_case = self.builder.get_object("match_case") whole_word = self.builder.get_object("whole_word") regex = self.builder.get_object("regex") findbar = self.builder.get_object('findbar') self.findbar = FindBar(findbar, self.wrap_box, self.find_entry, self.replace_entry, match_case, whole_word, regex) self.findbar.set_text_view(self.text_view) # Check if filename has been specified in terminal command if len(sys.argv) > 1: self.name = sys.argv[1] title = self.name.split("/")[-1] self.window.set_title("Remarkable: " + title) try: with open(sys.argv[1], 'r') as temp: text = temp.read() self.text_buffer.set_text(text) self.text_buffer.set_modified(False) except: print(self.name + " does not exist, creating it") self.update_status_bar(self) self.update_live_preview(self) # Check if an updated version of application exists [removed this functionality] # _thread.start_new_thread(self.check_for_updates, ()) self.text_view.grab_focus() if spellcheck_enabled: try: self.spellchecker = SpellChecker(self.text_view, locale.getdefaultlocale()[0]) # Enabling spell checking except: pass # Spell checking not enabled self.tv_scrolled = self.scrolledwindow_text_view.get_vadjustment().connect("value-changed", self.scrollPreviewTo) self.lp_scrolled_fix = self.scrolledwindow_live_preview.get_vadjustment().connect("value-changed", self.scrollPreviewToFix) self.scrolledwindow_live_preview.get_vadjustment().set_lower(1) self.temp_file_list = [] def on_find_next_button_clicked(self, widget): self.findbar.on_find_next_button_clicked(widget) def on_find_previous_button_clicked(self, widget): self.findbar.on_find_previous_button_clicked(widget) def on_find_entry_changed(self, entry): self.findbar.on_find_entry_changed(entry) def on_replace_button_clicked(self, widget): self.findbar.on_replace_button_clicked(widget) def on_replace_all_button_clicked(self, widget): self.findbar.on_replace_all_button_clicked(widget) def can_redo_changed(self, widget): if self.text_buffer.can_redo(): self.builder.get_object("menuitem_redo").set_sensitive(True) self.builder.get_object("toolbutton_redo").set_sensitive(True) else: self.builder.get_object("menuitem_redo").set_sensitive(False) self.builder.get_object("toolbutton_redo").set_sensitive(False) def can_undo_changed(self, widget): if self.text_buffer.can_undo(): self.builder.get_object("menuitem_undo").set_sensitive(True) self.builder.get_object("toolbutton_undo").set_sensitive(True) else: self.builder.get_object("menuitem_undo").set_sensitive(False) self.builder.get_object("toolbutton_undo").set_sensitive(False) def check_settings(self): if not os.path.exists(self.path): os.makedirs(self.path) if not os.path.isfile(self.settings_path): self.remarkable_settings = {} self.remarkable_settings['css'] = '' self.remarkable_settings['font'] = "Sans 10" self.remarkable_settings['line-numbers'] = True self.remarkable_settings['live-preview'] = True self.remarkable_settings['nightmode'] = False self.remarkable_settings['statusbar'] = True self.remarkable_settings['style'] = "github" self.remarkable_settings['toolbar'] = True self.remarkable_settings['vertical'] = False self.remarkable_settings['word-wrap'] = True self.remarkable_settings['zoom-level'] = 1 self.remarkable_settings['rtl'] = False settings_file = open(self.settings_path, 'w') settings_file.write(str(self.remarkable_settings)) settings_file.close() else: settings_file = open(self.settings_path) self.remarkable_settings = eval(settings_file.read()) settings_file.close() self.load_settings() self.wrap_box.set_visible(False) def write_settings(self): settings_file = open(self.settings_path, 'w') settings_file.write(str(self.remarkable_settings)) settings_file.close() def load_settings(self): self.custom_css = self.remarkable_settings['css'] # Load the custom css (don't auto enable) if self.remarkable_settings['nightmode']: # Enable night/dark mode on startup self.builder.get_object("menuitem_night_mode").set_active(True) self.on_menuitem_night_mode_activate(self) if self.remarkable_settings['word-wrap'] == False: # Disable word wrap on startup self.builder.get_object("menuitem_word_wrap").set_active(False) self.on_menuitem_word_wrap_activate(self) if self.remarkable_settings['live-preview'] == False: # Disable Live Preview on startup self.builder.get_object("menuitem_live_preview").set_active(False) if self.remarkable_settings['toolbar'] == False: # Hide the toolbar on startup self.on_menuitem_toolbar_activate(self) if self.remarkable_settings['statusbar'] == False: # Hide the statusbar on startup self.on_menuitem_statusbar_activate(self) # New settings, create them with default if they don't exist if "line-numbers" not in self.remarkable_settings: self.remarkable_settings['line-numbers'] = True if self.remarkable_settings['line-numbers'] == False: # Hide line numbers on startup self.builder.get_object("menuitem_line_numbers").set_active(False) if "vertical" not in self.remarkable_settings: self.remarkable_settings['vertical'] = False if self.remarkable_settings['vertical'] == True: # Switch to vertical layout self.builder.get_object("menuitem_vertical_layout").set_active(True) if 'zoom-level' in self.remarkable_settings: self.live_preview.set_zoom_level(self.remarkable_settings['zoom-level']) if 'rtl' in self.remarkable_settings and self.remarkable_settings['rtl']: self.builder.get_object("menuitem_rtl").set_active(True) # Try to load the previously chosen font, may fail as font may not exist, ect. try: self.font = self.remarkable_settings['font'] self.text_view.override_font(Pango.FontDescription(self.font)) except: pass # Loading font failed --> leave at default font # Try to load the previously chosen style. May fail if so, ignore try: self.style = self.remarkable_settings['style'] if self.style == "dark": styles.set(styles.dark) elif self.style == "foghorn": styles.set(styles.foghorn) elif self.style == "github": styles.set(styles.github) elif self.style == "handwriting_css": styles.set(styles.handwriting_css) elif self.style == "markdown": styles.set(styles.markdown) elif self.style == "metro_vibes": styles.set(styles.metro_vibes) elif self.style == "metro_vibes_dark": styles.set(styles.metro_vibes_dark) elif self.style == "modern_css": styles.set(styles.modern_css) elif self.style == "screen": styles.set(styles.screen) elif self.style == "solarized_dark": styles.set(styles.solarized_dark) elif self.style == "solarized_light": styles.set(styles.solarized_light) elif self.style == "custom": styles.set(styles.custom_css) else: print("Style key error") self.update_style(self) self.update_live_preview(self) except: print("Couldn't choose previously selected style") def scrollPreviewToFix(self, widget): self.scrolledwindow_live_preview.get_vadjustment().disconnect(self.lp_scrolled_fix) value = self.scrolledwindow_live_preview.get_vadjustment().get_value() if value == 0: # Fix self.scrollPreviewTo(self) else: pass # Something better? def scrollPreviewTo(self, widget): self.scrolledwindow_live_preview.get_vadjustment().disconnect(self.lp_scrolled_fix) value = self.scrolledwindow_text_view.get_vadjustment().get_value() upper_edit = self.scrolledwindow_text_view.get_vadjustment().get_upper() preview_upper = self.scrolledwindow_live_preview.get_vadjustment().get_upper() if value >= upper_edit - self.scrolledwindow_text_view.get_vadjustment().get_page_size(): self.scrolledwindow_live_preview.get_vadjustment().set_value(preview_upper - self.scrolledwindow_live_preview.get_vadjustment().get_page_size()) else: self.scrolledwindow_live_preview.get_vadjustment().set_value(value / upper_edit * preview_upper) self.lp_scrolled_fix = self.scrolledwindow_live_preview.get_vadjustment().connect("value-changed", self.scrollPreviewToFix) def on_menuitem_numbered_list_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() start_line = start.get_line() end_line = end.get_line() i = 1 while (start_line <= end_line): temp_iter = self.text_buffer.get_iter_at_line(start_line) self.text_buffer.insert(temp_iter, str(i) + ". ") start_line += 1 i += 1 else: temp_iter = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) line_number = temp_iter.get_line() start_iter = self.text_buffer.get_iter_at_line(line_number) self.text_buffer.insert(start_iter, "1. ") def on_menuitem_new_activate(self, widget): self.new(self) def on_toolbutton_new_clicked(self, widget): self.new(self) """ Launches a new instance of Remarkable """ def new(self, widget): subprocess.Popen(sys.argv[0]) def on_menuitem_open_activate(self, widget): self.open(self) def on_toolbutton_open_clicked(self, widget): self.open(self) """ Opens a file for editing / viewing """ def open(self, widget): start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) self.window.set_sensitive(False) chooser = Gtk.FileChooserDialog(title="Open File", action=Gtk.FileChooserAction.OPEN, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) self.set_file_chooser_path(chooser) response = chooser.run() if response == Gtk.ResponseType.OK: # The user has selected a file selected_file = chooser.get_filename() if len(text) == 0 and not self.text_buffer.get_modified(): # Current file is empty. Load contents of selected file into this view self.text_buffer.begin_not_undoable_action() file = open(selected_file, 'r') text = file.read() file.close() self.name = chooser.get_filename() self.text_buffer.set_text(text) title = chooser.get_filename().split("/")[-1] self.window.set_title("Remarkable: " + title) self.text_buffer.set_modified(False) self.text_buffer.end_not_undoable_action() else: # A file is already open. Load the selected file in a new Remarkable process subprocess.Popen([sys.argv[0], selected_file]) elif response == Gtk.ResponseType.CANCEL: # The user has clicked cancel pass chooser.destroy() self.window.set_sensitive(True) def check_for_save(self, widget): reply = False if self.text_buffer.get_modified(): message = "Do you want to save the changes you have made?" dialog = Gtk.MessageDialog(self.window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, message) dialog.set_title("Save?") dialog.set_default_response(Gtk.ResponseType.YES) if dialog.run() == Gtk.ResponseType.NO: reply = False else: reply = True dialog.destroy() return reply def on_menuitem_save_activate(self, widget): self.save(self) def on_toolbutton_save_clicked(self, widget): self.save(self) def save(self, widget): if self.name != "Untitled": file = open(self.name, 'w') text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) file.write(text) file.close() title = self.name.split("/")[-1] self.text_buffer.set_modified(False) self.window.set_title("Remarkable: " + title) return True else: return self.save_as(self) def on_menuitem_save_as_activate(self, widget, crap = ""): self.save_as(self) def save_as(self, widget): self.window.set_sensitive(False) chooser = Gtk.FileChooserDialog(title=None, action=Gtk.FileChooserAction.SAVE, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) self.set_file_chooser_path(chooser) chooser.set_do_overwrite_confirmation(True) title = self.name.split("/")[-1] chooser.set_title("Save As: " + title) response = chooser.run() saved = True if response == Gtk.ResponseType.OK: file = open(chooser.get_filename(), 'w') self.name = chooser.get_filename() text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) file.write(text) file.close() self.text_buffer.set_modified(False) title = self.name.split("/")[-1] self.window.set_title("Remarkable: " + title) else: saved = False # User cancelled saving after choosing to save. Need to cancel quit operation now chooser.destroy() self.window.set_sensitive(True) return saved def on_menuitem_rtl_toggled(self, widget): self.rtl(widget.get_active()) self.remarkable_settings['rtl'] = widget.get_active() self.write_settings() def rtl(self, enabled): # whatever the swap choice was, it needs to be flipped now self.on_menuitem_swap_activate(None) styles.rtl(enabled) self.update_style(self) self.update_live_preview(self) def on_menuitem_export_html_activate(self, widget): self.window.set_sensitive(False) start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions =self.safe_extensions) except: html_middle = markdown.markdown(text) html = self.default_html_start + html_middle + self.default_html_end self.save_html(html) def on_menuitem_export_html_plain_activate(self, widget): # This can be re-factored. A lot of duplicated code. Migrate some functions to external .py files self.window.set_sensitive(False) start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions =self.safe_extensions) except: html_middle = markdown.markdown(text) html = html_middle self.save_html(html) def save_html(self, data): html = data chooser = Gtk.FileChooserDialog("Export HTML", None, Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self.set_file_chooser_path(chooser) html_filter = Gtk.FileFilter() html_filter.set_name("HTML Files") html_filter.add_pattern("*.html") html_filter.add_pattern("*.htm") chooser.set_do_overwrite_confirmation(True) chooser.add_filter(html_filter) response = chooser.run() if response == Gtk.ResponseType.OK: file_name = chooser.get_filename() if not file_name.endswith(".html"): file_name += ".html" file = open(file_name, 'w') soup = BeautifulSoup(html, "lxml") file.write(soup.prettify()) file.close() elif response == Gtk.ResponseType.CANCEL: pass chooser.destroy() self.window.set_sensitive(True) def on_menuitem_export_pdf_activate(self, widget): self.window.set_sensitive(False) start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) dirname = os.path.dirname(self.name) text = re.sub(r'(\!\[.*?\]\()([^/][^:]*?\))', lambda m, dirname=dirname: m.group(1) + os.path.join(dirname, m.group(2)), text) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, self.safe_extensions) except: html_middle = markdown.markdown(text) html = self.default_html_start + html_middle + self.default_html_end self.save_pdf(html) def on_menuitem_export_pdf_plain_activate(self, widget): self.window.set_sensitive(False) start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, self.safe_extensions) except: html_middle = markdown.markdown(text) html = html_middle self.save_pdf(html) def save_pdf(self, html): chooser = Gtk.FileChooserDialog("Export PDF", None, Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self.set_file_chooser_path(chooser) pdf_filter = Gtk.FileFilter() pdf_filter.add_pattern("*.pdf") pdf_filter.set_name("PDF Files") chooser.add_filter(pdf_filter) chooser.set_do_overwrite_confirmation(True) response = chooser.run() if response == Gtk.ResponseType.OK: file_name = chooser.get_filename() if not file_name.endswith(".pdf"): file_name += ".pdf" try: pdfkit.from_string(html, file_name, options= {'quiet': '', 'page-size': 'Letter', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in', 'margin-left': '0.75in', 'encoding': "UTF-8", 'javascript-delay' : '550', 'no-outline': None}) except: try: # Failed so try with no options pdfkit.from_string(html, file_name) except: # Pdf Export failed, show warning message if not self.pdf_error_warning: self.pdf_error_warning = True print("\nRemarkable Error:\tPDF Export Failed!!") pdf_fail_dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CANCEL, "PDF EXPORT FAILED") pdf_fail_dialog.format_secondary_text( "File export to PDF was unsuccessful.") pdf_fail_dialog.run() pdf_fail_dialog.destroy() elif response == Gtk.ResponseType.CANCEL: pass chooser.destroy() self.window.set_sensitive(True) def on_menuitem_quit_activate(self, widget): self.clean_up() self.window_delete_event(self) def window_delete_event(self, widget, callback=None): start, end = self.text_buffer.get_bounds() text = self.text_buffer.get_text(start, end, False) safe_to_quit = True # Keep track if user cancelled save operation if len(text) > 0: if self.check_for_save(None): safe_to_quit = self.save(self) if safe_to_quit: self.quit_requested(None) else: pass def quit_requested(self, widget, callback_data=None): self.clean_up() # Second time, just to be safe Gtk.main_quit() def on_menuitem_undo_activate(self, widget): self.undo(self) def on_toolbutton_undo_clicked(self, widget): self.undo(self) def undo(self, widget): if self.text_buffer.can_undo(): self.text_buffer.undo() def on_menuitem_redo_activate(self, widget): self.redo(self) def on_toolbutton_redo_clicked(self, widget): self.redo(self) def zoom_in(self): self.live_preview.set_zoom_level((1+self.zoom_steps)*self.live_preview.get_zoom_level()) self.remarkable_settings['zoom-level'] = self.live_preview.get_zoom_level() self.write_settings() self.scrollPreviewToFix(self) def zoom_out(self): self.live_preview.set_zoom_level((1-self.zoom_steps)*self.live_preview.get_zoom_level()) self.remarkable_settings['zoom-level'] = self.live_preview.get_zoom_level() self.write_settings() self.scrollPreviewToFix(self) def on_toolbutton_zoom_in_clicked(self, widget): self.zoom_in() def on_toolbutton_zoom_out_clicked(self, widget): self.zoom_out() def redo(self, widget): if self.text_buffer.can_redo(): self.text_buffer.redo() def on_menuitem_find_activate(self, widget): self.findbar.show() def on_menuitem_cut_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) self.clipboard.set_text(text, -1) self.text_buffer.delete(start, end) def on_menuitem_copy_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) self.clipboard.set_text(text, -1) else: self.live_preview.can_execute_editing_command(WebKit2.EDITING_COMMAND_COPY, None, self.execute_copy_command, None) def execute_copy_command(self, source, result, user_data): if self.live_preview.can_execute_editing_command_finish(result): self.live_preview.execute_editing_command(WebKit2.EDITING_COMMAND_COPY) def on_menuitem_paste_activate(self, widget): text = self.clipboard.wait_for_text() image = self.clipboard.wait_for_image() if text != None: if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(text) elif image != None: image_rel_path = 'imgs' if self.name == 'Untitled': # File not yet saved (i.e. we do not have path for the file) self.save(widget) assert self.name != 'Untitled' image_dir = os.path.join(os.path.dirname(self.name), image_rel_path) image_fname = datetime.datetime.now().strftime('%Y%m%d-%H%M%S.png') image_path = os.path.join(image_dir, image_fname) text = '' % (image_rel_path, image_fname) if not os.path.exists(image_dir): os.makedirs(image_dir) image.savev(image_path, 'png', [], []) if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(text) def on_menuitem_lower_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) text = text.lower() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(text) def on_menuitem_title_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) text = text.title() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(text) def on_menuitem_upper_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) text = text.upper() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(text) def on_menuitem_join_lines_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() self.text_buffer.join_lines(start, end) def on_menuitem_sort_lines_activate(self, widget): if self.text_buffer.get_has_selection(): # Sort the selected lines start, end = self.text_buffer.get_selection_bounds() self.text_buffer.sort_lines(start, end, GtkSource.SortFlags.CASE_SENSITIVE, 0) else: # No selection active, sort all lines start, end = self.text_buffer.get_bounds() self.text_buffer.sort_lines(start, end, GtkSource.SortFlags.CASE_SENSITIVE, 0) def on_menuitem_sort_lines_reverse_activate(self, widget): if self.text_buffer.get_has_selection(): # Sort the selected lines in reverse start, end = self.text_buffer.get_selection_bounds() self.text_buffer.sort_lines(start, end, GtkSource.SortFlags.REVERSE_ORDER, 0) else: # No selection active, sort all lines in reverse start, end = self.text_buffer.get_bounds() self.text_buffer.sort_lines(start, end, GtkSource.SortFlags.REVERSE_ORDER, 0) # Copy all text from the editor pane and format it as HTML in the clipboard def on_menuitem_copy_all_activate(self, widget): text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) try: text = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions = self.safe_extensions) except: html_middle = markdown.markdown(text) self.clipboard.set_text(text, -1) # Copy selected text from the editor pane and format as HTML in the clipboard def on_menuitem_copy_selection_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) try: text = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions =self.safe_extensions) except: html_middle = markdown.markdown(text) self.clipboard.set_text(text, -1) def on_menuitem_vertical_layout_activate(self, widget): if self.builder.get_object("menuitem_vertical_layout").get_active(): # Switch to vertical layout and need to reset position self.paned.set_orientation(Gtk.Orientation.VERTICAL) self.paned.set_orientation(Gtk.Orientation.HORIZONTAL) self.paned.set_orientation(Gtk.Orientation.VERTICAL) self.paned.set_position(self.paned.get_allocation().height/2) self.remarkable_settings['vertical'] = True else: self.paned.set_orientation(Gtk.Orientation.HORIZONTAL) self.paned.set_position(self.paned.get_allocation().width/2) self.remarkable_settings['vertical'] = False self.write_settings() def on_menuitem_word_wrap_activate(self, widget): if self.builder.get_object("menuitem_word_wrap").get_active(): self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) self.remarkable_settings['word-wrap'] = True else: self.text_view.set_wrap_mode(Gtk.WrapMode.NONE) self.remarkable_settings['word-wrap'] = False self.write_settings() def on_menuitem_line_numbers_activate(self, widget): if self.builder.get_object("menuitem_line_numbers").get_active(): self.text_view.set_show_line_numbers(True) self.remarkable_settings['line-numbers'] = True else: self.text_view.set_show_line_numbers(False) self.remarkable_settings['line-numbers'] = False self.write_settings() def on_menuitem_live_preview_activate(self, widget): self.toggle_live_preview(self) def toggle_live_preview(self, widget): if self.live_preview.get_visible(): # Hide the live preview self.paned.remove(self.scrolledwindow_live_preview) self.live_preview.set_visible(False) self.builder.get_object("menuitem_swap").set_sensitive(False) self.builder.get_object("menuitem_swap").set_tooltip_text("Enable Live Preview First") self.builder.get_object("toolbar1").set_visible(False) self.remarkable_settings['live-preview'] = False else: # Show the live preview if self.editor_position == 0: self.paned.add(self.scrolledwindow_live_preview) else: self.paned.remove(self.scrolledwindow_text_view) self.paned.add(self.scrolledwindow_live_preview) self.paned.add(self.scrolledwindow_text_view) self.live_preview.set_visible(True) self.remarkable_settings['live-preview'] = True self.builder.get_object("menuitem_swap").set_sensitive(True) self.builder.get_object("menuitem_swap").set_tooltip_text("") self.builder.get_object("toolbar1").set_visible(True) self.update_live_preview(self) self.write_settings() def on_menuitem_swap_activate(self, widget): if self.live_preview.get_visible(): self.paned.remove(self.scrolledwindow_live_preview) self.paned.remove(self.scrolledwindow_text_view) if self.editor_position == 0: self.paned.add(self.scrolledwindow_live_preview) self.paned.add(self.scrolledwindow_text_view) self.editor_position = 1 else: self.paned.add(self.scrolledwindow_text_view) self.paned.add(self.scrolledwindow_live_preview) self.editor_position = 0 else: pass # Do nothing as live preview is not visible def on_menuitem_zoom_in_activate(self, widget): self.zoom_in() def on_menuitem_zoom_out_activate(self, widget): self.zoom_out() def on_menuitem_editor_font_activate(self, widget): self.font_chooser = Gtk.FontSelectionDialog() self.font_chooser.set_preview_text("Remarkable is the best markdown editor for Linux") try: self.font_chooser.set_font_name(self.font) except: pass # Font not initialized, do nothing, continue self.font_chooser.connect("destroy", self.font_dialog_destroyed) self.font_ok_button = self.font_chooser.get_ok_button() self.font_ok_button.connect("clicked", self.font_dialog_ok) self.font_cancel_button = self.font_chooser.get_cancel_button() self.font_cancel_button.connect("clicked", self.font_dialog_cancel) self.font_chooser.show() def font_dialog_destroyed(self, widget): self.font_chooser.destroy() def font_dialog_cancel(self, widget): self.font_chooser.destroy() def font_dialog_ok(self, widget): self.font = self.font_chooser.get_font_name() self.remarkable_settings['font'] = self.font # Save prefs self.write_settings() self.text_view.override_font(Pango.FontDescription(self.font)) # Now adjust the size using TextTag self.font_dialog_destroyed(self) def on_menuitem_statusbar_activate(self, widget): if self.statusbar.get_visible(): self.statusbar.set_visible(False) self.builder.get_object("menuitem_statusbar").set_label("Show Statusbar") self.remarkable_settings['statusbar'] = False else: self.statusbar.set_visible(True) self.update_status_bar(self) self.builder.get_object("menuitem_statusbar").set_label("Hide Statusbar") self.remarkable_settings['statusbar'] = True self.write_settings() def on_menuitem_toolbar_activate(self, widget): if self.toolbar.get_visible(): self.toolbar.set_visible(False) self.builder.get_object("menuitem_toolbar").set_label("Show Toolbar") self.builder.get_object("toolbar1").set_visible(False) self.remarkable_settings['toolbar'] = False else: self.toolbar.set_visible(True) self.builder.get_object("menuitem_toolbar").set_label("Hide Toolbar") self.builder.get_object("toolbar1").set_visible(True) self.remarkable_settings['toolbar'] = True self.write_settings() def on_menuitem_preview_browser_activate(self, widget): # Create a temporary HTML file tf = tempfile.NamedTemporaryFile(delete = False) self.temp_file_list.append(tf) tf_name = tf.name text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) dirname = os.path.dirname(self.name) text = re.sub(r'(\!\[.*?\]\()([^/][^:]*?\))', lambda m, dirname=dirname: m.group(1) + os.path.join(dirname, m.group(2)), text) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions =self.safe_extensions) except: html_middle = markdown.markdown(text) html = self.default_html_start + html_middle + self.default_html_end tf.write(html.encode()) tf.flush() # Load the temporary HTML file in the user's default browser webbrowser.open_new_tab(tf_name) def on_menuitem_night_mode_activate(self, widget): if self.builder.get_object("menuitem_night_mode").get_active(): self.settings.set_property("gtk-application-prefer-dark-theme", True) self.remarkable_settings['nightmode'] = True else: self.settings.set_property("gtk-application-prefer-dark-theme", False) self.remarkable_settings['nightmode'] = False self.write_settings() def on_menuitem_fullscreen_activate(self, widget): if self.is_fullscreen: self.window.unfullscreen() self.is_fullscreen = False self.builder.get_object("menuitem_fullscreen").set_label("Fullscreen") else: self.window.fullscreen() self.is_fullscreen = True self.builder.get_object("menuitem_fullscreen").set_label("Exit fullscreen") def on_menuitem_bold_activate(self, widget): self.bold(self) def on_toolbutton_bold_clicked(self, widget): self.bold(self) def bold(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add **** and place cursor in middle self.text_buffer.insert_at_cursor("****") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(2) self.text_buffer.place_cursor(loc) else: # Turn selection bold selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "**") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "**") def on_menuitem_italic_activate(self, widget): self.italic(self) def on_toolbutton_italic_clicked(self, widget): self.italic(self) def italic(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add ** and place cursor in middle self.text_buffer.insert_at_cursor("**") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(1) self.text_buffer.place_cursor(loc) else: # Turn selection italic selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "*") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "*") def on_menuitem_strikethrough_activate(self, widget): self.strikethrough(self) def on_toolbutton_strikethrough_clicked(self, widget): self.strikethrough(self) def strikethrough(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add **** and place cursor in middle self.text_buffer.insert_at_cursor("~~~~") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(2) self.text_buffer.place_cursor(loc) else: # Strikethrough selection selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "~~") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "~~") def on_menuitem_highlight_activate(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add ==== and place cursor in middle self.text_buffer.insert_at_cursor("====") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(2) self.text_buffer.place_cursor(loc) else: # Highlight the selected text selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "==") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "==") def on_menuitem_superscript_activate(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add ^^ and place cursor in middle self.text_buffer.insert_at_cursor("^^") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(1) self.text_buffer.place_cursor(loc) else: # Convert selection to superscript selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "^") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "^") def on_menuitem_subscript_activate(self, widget): if not self.text_buffer.get_has_selection(): # Nothing has been selected, add ~~ and place cursor in middle self.text_buffer.insert_at_cursor("~~") loc = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) loc.backward_chars(1) self.text_buffer.place_cursor(loc) else: # Convert selection to subscript selection_bounds = self.text_buffer.get_selection_bounds() mark1 = self.text_buffer.create_mark(None, selection_bounds[0], False) mark2 = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark1), "~") self.text_buffer.insert(self.text_buffer.get_iter_at_mark(mark2), "~") def on_menuitem_block_quote_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() start_line = start.get_line() end_line = end.get_line() while start_line <= end_line: temp_iter = self.text_buffer.get_iter_at_line(start_line) self.text_buffer.insert(temp_iter, ">") start_line += 1 else: temp_iter = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) line_number = temp_iter.get_line() start_iter = self.text_buffer.get_iter_at_line(line_number) self.text_buffer.insert(start_iter, ">") def on_menuitem_code_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() start_line = start.get_line() end_line = end.get_line() while (start_line <= end_line): temp_iter = self.text_buffer.get_iter_at_line(start_line) self.text_buffer.insert(temp_iter, "\t") start_line += 1 else: temp_iter = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) line_number = temp_iter.get_line() start_iter = self.text_buffer.get_iter_at_line(line_number) self.text_buffer.insert(start_iter, "\t") def on_menuitem_bullet_list_activate(self, widget): if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() start_line = start.get_line() end_line = end.get_line() while (start_line <= end_line): temp_iter = self.text_buffer.get_iter_at_line(start_line) self.text_buffer.insert(temp_iter, "- ") start_line += 1 else: temp_iter = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) line_number = temp_iter.get_line() start_iter = self.text_buffer.get_iter_at_line(line_number) self.text_buffer.insert(start_iter, "- ") def add_heading(self, heading_size): # Get iters for start and end of line at cursor temp_iter = self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()) line_number = temp_iter.get_line() start_iter = self.text_buffer.get_iter_at_line(line_number) end_iter = self.text_buffer.get_iter_at_line(line_number) end_iter.forward_to_line_end() # Get the text on the current line and check if there is already a heading text = self.text_buffer.get_text(start_iter, end_iter, True) if len(text) == 0: # This line is empty, add the #'s text = ("#") * heading_size + " " elif text.lstrip()[0] == "#": # This line is already a heading. Remove #'s and replace with new #'s # Issue, this uses lstrip() to remove whitespace, which user may wish to preserve text_without_heading = "".join(re.split("^#+", text)).lstrip() text = ("#" * heading_size) + " " + text_without_heading else: # This line doesn't already have a heading, simple prepend #'s text = ("#" * heading_size) + " " + text # Replace text with new heading character(s) self.text_buffer.delete(start_iter, end_iter) self.text_buffer.insert(start_iter, text) def on_menuitem_heading_1_activate(self, widget): self.add_heading(1) def on_menuitem_heading_2_activate(self, widget): self.add_heading(2) def on_menuitem_heading_3_activate(self, widget): self.add_heading(3) def on_menuitem_heading_4_activate(self, widget): self.add_heading(4) def on_menuitem_horizonatal_rule_activate(self, widget): if not self.text_buffer.get_has_selection(): self.text_buffer.insert_at_cursor("\n***\n") else: # Turn selection bold selection_bounds = self.text_buffer.get_selection_bounds() markR = self.text_buffer.create_mark(None, selection_bounds[1], False) self.text_buffer.insert(self.text_buffer.get_iter_at_mark(markR), "\n***\n") def on_menuitem_timestamp_activate(self, widget): self.insert_timestamp(self) def on_toolbutton_timestamp_clicked(self, widget): self.insert_timestamp(self) def insert_timestamp(self, widget): text = datetime.datetime.now().strftime("%A, %d. %B %Y %I:%M%p") + " " self.text_buffer.insert_at_cursor(text + "\n") self.text_view.grab_focus() def on_menuitem_image_activate(self, widget): self.insert_image(self) def on_toolbutton_image_clicked(self, widget): self.insert_image(self) def on_menuitem_table_activate(self, widget): self.insert_table(self) def insert_table(self, widget): self.insert_window_table = Gtk.Window() self.insert_window_table.set_title("Insert Table") self.insert_window_table.set_resizable(True) self.insert_window_table.set_border_width(6) self.insert_window_table.set_default_size(300, 250) self.insert_window_table.set_position(Gtk.WindowPosition.CENTER) vbox = Gtk.VBox() label_n_rows = Gtk.Label("Number of Rows:") self.entry_n_rows = Gtk.Entry() label_n_columns = Gtk.Label("Number of Columns") self.entry_n_columns = Gtk.Entry() vbox.pack_start(label_n_rows, self, False, False) vbox.pack_start(self.entry_n_rows, self, False, False) vbox.pack_start(label_n_columns, self, False, False) vbox.pack_start(self.entry_n_columns, self, False, False) button = Gtk.Button("Insert Table") vbox.pack_end(button, self, False, False) self.insert_window_table.add(vbox) self.insert_window_table.show_all() button.connect("clicked", self.insert_table_cmd, self.insert_window_table) def insert_table_cmd(self, widget, window): # if self.entry_url_i.get_text(): n_rows = self.entry_n_rows.get_text() n_columns = self.entry_n_columns.get_text() if n_rows and n_columns: try: n_rows = int(n_rows) except: return try: n_columns = int(n_columns) except: return if n_rows > 0 and n_columns > 0: table_str = "" line = ("| " * n_columns) + "|" header_line = ("|--" * n_columns) + "|" table_str = line + "\n" + header_line + "\n" if n_rows > 1: n_rows -= 1 while n_rows > 0: table_str += line + "\n" n_rows -= 1 self.text_buffer.insert_at_cursor(table_str) self.insert_window_table.hide() def insert_image(self, widget): self.insert_window_image = Gtk.Window() self.insert_window_image.set_title("Insert Image") self.insert_window_image.set_resizable(True) self.insert_window_image.set_border_width(6) self.insert_window_image.set_default_size(300, 250) self.insert_window_image.set_position(Gtk.WindowPosition.CENTER) vbox = Gtk.VBox() label_alt_text = Gtk.Label("Alt Text:") self.entry_alt_text_i = Gtk.Entry() label_title = Gtk.Label("Title:") self.entry_title_i = Gtk.Entry() label_url = Gtk.Label("Path/Url:") self.entry_url_i = Gtk.Entry() vbox.pack_start(label_alt_text, self, False, False) vbox.pack_start(self.entry_alt_text_i, self, False, False) vbox.pack_start(label_title, self, False, False) vbox.pack_start(self.entry_title_i, self, False, False) vbox.pack_start(label_url, self, False, False) self.hbox_url = Gtk.HBox() self.hbox_url.pack_start(self.entry_url_i, self, True, False) self.path_file_button = Gtk.FileChooserButton(title= "Select an image") self.path_file_button.connect("file-set", self.file_chooser_button_clicked) self.hbox_url.pack_start(self.path_file_button, self, False, False) vbox.pack_start(self.hbox_url, self, False, False) button = Gtk.Button("Insert Image") vbox.pack_end(button, self, False, False) self.insert_window_image.add(vbox) self.insert_window_image.show_all() button.connect("clicked", self.insert_image_cmd, self.insert_window_image) def file_chooser_button_clicked(self, widget): filePath = widget.get_filename() self.entry_url_i.set_text(filePath) def insert_image_cmd(self, widget, window): if self.entry_url_i.get_text(): if self.entry_title_i.get_text(): if self.entry_alt_text_i.get_text(): # Fill alt_text with a space ( > 1 char required) link = " link += ' "' + self.entry_title_i.get_text() + '")' self.text_buffer.insert_at_cursor(link) else: link = " link += ' "' + self.entry_title_i.get_text() + '")' self.text_buffer.insert_at_cursor(link) else: link = " + ") " self.text_buffer.insert_at_cursor(link) else: pass self.insert_window_image.hide() def on_menuitem_link_activate(self, widget): self.insert_link(self) def on_toolbutton_link_clicked(self, widget): self.insert_link(self) def insert_link(self, widget): self.insert_window_link = Gtk.Window() self.insert_window_link.set_title("Insert Link") self.insert_window_link.set_resizable(True) self.insert_window_link.set_border_width(6) self.insert_window_link.set_default_size(350, 250) self.insert_window_link.set_position(Gtk.WindowPosition.CENTER) vbox = Gtk.VBox() label_alt_text = Gtk.Label("Alt Text:") self.entry_alt_text = Gtk.Entry() label_url = Gtk.Label("Url:") self.entry_url = Gtk.Entry() vbox.pack_start(label_alt_text, self, False, False) vbox.pack_start(self.entry_alt_text, self, False, False) vbox.pack_start(label_url, self, False, False) vbox.pack_start(self.entry_url, self, False, False) button = Gtk.Button("Insert Link") vbox.pack_end(button, self, False, False) # Use highligted text as the default "alt text" if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() text = self.text_buffer.get_text(start, end, True) self.entry_alt_text.set_text(text) self.insert_window_link.add(vbox) self.insert_window_link.show_all() button.connect("clicked", self.insert_link_cmd, self.insert_window_link) def insert_link_cmd(self, widget, window): if self.entry_url.get_text(): link = "[" + self.entry_alt_text.get_text() + "](" + self.entry_url.get_text() + ") " # Delete highlighted text before inserting the link if self.text_buffer.get_has_selection(): start, end = self.text_buffer.get_selection_bounds() self.text_buffer.delete(start, end) self.text_buffer.insert_at_cursor(link) else: pass self.insert_window_link.hide() # Styles def update_style(self, widget): self.default_html_start = '<!doctype HTML><html><head><meta charset="utf-8"><title>Made with Remarkable!</title><link rel="stylesheet" href="' + self.media_path + 'highlightjs.default.min.css">' self.default_html_start += "<style type='text/css'>" + styles.get() + "</style>" self.default_html_start += "</head><body>" def on_menuitem_dark_activate(self, widget): styles.set(styles.dark) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "dark" self.write_settings() def on_menuitem_foghorn_activate(self, widget): styles.set(styles.foghorn) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "foghorn" self.write_settings() def on_menuitem_github_activate(self, widget): styles.set(styles.github) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "github" self.write_settings() def on_menuitem_handwritten_activate(self, widget): styles.set(styles.handwriting_css) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "handwriting_css" self.write_settings() def on_menuitem_markdown_activate(self, widget): styles.set(styles.markdown) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "markdown" self.write_settings() def on_menuitem_metro_vibes_activate(self, widget): styles.set(styles.metro_vibes) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "metro_vibes" self.write_settings() def on_menuitem_metro_vibes_dark_activate(self, widget): styles.set(styles.metro_vibes_dark) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "metro_vibes_dark" self.write_settings() def on_menuitem_modern_activate(self, widget): styles.set(styles.modern_css) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "modern_css" self.write_settings() def on_menuitem_screen_activate(self, widget): styles.set(styles.screen) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "screen" self.write_settings() def on_menuitem_solarized_dark_activate(self, widget): styles.set(styles.solarized_dark) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "solarized_dark" self.write_settings() def on_menuitem_solarized_light_activate(self, widget): styles.set(styles.solarized_light) self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "solarized_light" self.write_settings() # Custom CSS def on_menuitem_custom_activate(self, widget): self.custom_window = Gtk.Window() self.custom_window.set_default_size(640, 480) self.custom_window.set_position(Gtk.WindowPosition.CENTER) self.custom_window.set_title("Custom CSS") self.custom_vbox = Gtk.VBox() self.custom_scroller = Gtk.ScrolledWindow() self.custom_button = Gtk.Button("Apply") self.custom_vbox.pack_end(self.custom_button, False, False, 0) self.custom_text_view = Gtk.TextView() self.custom_text_buffer = Gtk.TextBuffer() self.custom_text_buffer.set_text(self.custom_css) self.custom_text_view.set_buffer(self.custom_text_buffer) self.custom_scroller.add(self.custom_text_view) self.custom_vbox.pack_start(self.custom_scroller, True, True, 0) self.custom_window.add(self.custom_vbox) self.custom_window.show_all() self.custom_button.connect("clicked", self.apply_custom_css, self.custom_window, self.custom_text_buffer) def apply_custom_css(self, widget, window, tb): start, end = tb.get_bounds() self.custom_css = tb.get_text(start, end, False).replace("'", '"') styles.set(self.custom_css) self.remarkable_settings['css'] = styles.get() window.hide() self.update_style(self) self.update_live_preview(self) self.remarkable_settings['style'] = "custom" self.write_settings() ## End Custom CSS def on_menuitem_github_page_activate(self, widget): webbrowser.open_new_tab("https://github.com/jamiemcg/remarkable") def on_menuitem_reportbug_activate(self, widget): webbrowser.open_new_tab("https://github.com/jamiemcg/remarkable/issues") def on_menuitem_about_activate(self, widget): self.AboutDialog.show(self) def on_menuitem_markdown_tutorial_activate(self, widget): tutorial_path = self.media_path + "MarkdownTutorial.md" subprocess.Popen([sys.argv[0], tutorial_path]) def on_menuitem_homepage_activate(self, widget): webbrowser.open_new_tab("http://remarkableapp.github.io") def on_menuitem_donate_activate(self, widget): webbrowser.open_new_tab("http://remarkableapp.github.io/linux/donate") # Have disabled the check for updates function and also removed this choice from the About menu # def on_menuitem_check_for_updates_activate(self, widget): # _thread.start_new_thread(self.check_for_updates, (True,)) # def check_for_updates(self, show = False): # try: # update_check = urlopen("http://remarkableapp.github.io/latest") # latest_version = float(update_check.readline()) # if app_version < latest_version: # print("There is a new version avaiable") # subprocess.Popen(['notify-send', "Remarkable: A new version of this app is avaiable"]) # update_check = urlopen("http://remarkableapp.github.io/change_log") # md = update_check.read() # html = markdown.markdown(md) # if show: # webbrowser.open_new_tab("http://remarkableapp.github.io") # else: # if show: # subprocess.Popen(['notify-send', "Remarkable: You already have the latest version of this app available"]) # print("You have the latest version of this app available") # except: # print("Warning: Remarkable could not connect to the internet to check for updates") def on_text_view_changed(self, widget): start, end = self.text_buffer.get_bounds() if self.statusbar.get_visible(): self.update_status_bar(self) else: # statusbar not present, don't need to update/count words, etc. pass if self.live_preview.get_visible(): self.update_live_preview(self) self.scrollPreviewTo(self) else: # Live preview not enabled, don't need to update the view pass # Update title to reflect changes if self.text_buffer.get_modified(): title = self.window.get_title() if title[0] != "*": title = "*" + title self.window.set_title(title) """ GtkTextView simply does not seem to handle visual word movements correctly in bi-directional move. This is a hack for going the opposite of logical order in case RTL mode was selected in the editor. It's not perfect, but at least for people who are mostly working in RTL mode the cursor will behave as expected more frequently. The inherent bug here, of course, is that if there is an English paragraph in an RTL document, the cursor will go backwards on that specific paragraph. This might be fixed if we look at the current paragraph and infer how to act based on its type. But I didn't bother for now, as this, admittedly, is a double edge case: 1) RTL users; and 2) Having English paragraphs and caring about Ctrl+Arrows behavior in them to the point where it gets irritating. In short, this is a quick, but useful, hack. """ def cursor_ctrl_arrow_rtl_fix(self, widget, event): if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right]: if event.state & Gdk.ModifierType.CONTROL_MASK: is_rtl = self.remarkable_settings['rtl'] dirs = { Gdk.KEY_Left: is_rtl and 1 or -1, Gdk.KEY_Right: is_rtl and -1 or 1, } widget.emit('move-cursor', Gtk.MovementStep.WORDS, dirs[event.keyval], event.state & Gdk.ModifierType.SHIFT_MASK != 0) return True return False """ Update the text in the status bar. Displays the number of lines, words and characters. This approach is possible inefficient. """ def update_status_bar(self, widget): self.statusbar.pop(self.context_id) lines = self.text_buffer.get_line_count() chars = self.text_buffer.get_char_count() words = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False).split() word_count = 0 word_exceptions = ["#", "##", "###", "####", "#####", "######", "*", "**", "-", "+", "_", "/", "\\", "/", ":", ";", "@", "'", "~", "(", ")", "[", "]", "{", "}", "((", "))", "+-", "-+", "/=", ".", "|", "!", "!!", "!!!", "$", "", "%", "^", "&"] # Exclude these from word count for w in words: if w not in word_exceptions: if not re.match('^[0-9]{1,3}$', w): word_count += 1 self.status_message = "Lines: " + str(lines) + ", " + "Words: " + str(word_count) + ", Characters: " + str(chars) self.statusbar.push(self.context_id, self.status_message) def update_live_preview(self, widet): text = self.text_buffer.get_text(self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False) try: html_middle = markdown.markdown(text, self.default_extensions) except: try: html_middle = markdown.markdown(text, extensions =self.safe_extensions) except: html_middle = markdown.markdown(text) html = self.default_html_start + html_middle + self.default_html_end # Update the display, supporting relative paths to local images self.live_preview.load_html(html, "file://{}".format(os.path.abspath(self.name))) """ This function suppresses the messages from the WebKit (live preview) console """ def _javascript_console_message(self, view, message, line, sourceid): return True """ This function deletes any temporary files that were created during execution """ def clean_up(self): i = len(self.temp_file_list) - 1 while i >= 0: os.remove(self.temp_file_list[0].name) del self.temp_file_list[0] i -= 1 def set_file_chooser_path(self, chooser): chooser.set_current_folder(os.path.dirname(self.name))