Example #1
0
class NorkaWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'NorkaWindow'

    def __init__(self, settings: Gio.Settings, **kwargs):
        super().__init__(**kwargs)

        self.set_default_icon(Pixbuf.new_from_resource_at_scale(
            '/com/github/tenderowl/norka/icons/com.github.tenderowl.norka.svg',
            128, 128, True
        ))
        self.settings = settings
        self._configure_timeout_id = None
        self.preview = None

        self.apply_styling()

        self.current_size = (786, 520)
        self.resize(*self.settings.get_value('window-size'))
        self.connect('configure-event', self.on_configure_event)
        self.connect('destroy', self.on_window_delete_event)

        # Export clients
        self.medium_client = Medium()
        self.writeas_client = Writeas()
        self.uri_to_open = None

        # Make a header
        self.header = Header(self.settings)
        self.set_titlebar(self.header)
        self.header.show()

        # Init screens
        self.welcome_grid = Welcome()
        self.welcome_grid.connect('activated', self.on_welcome_activated)
        self.welcome_grid.connect('document-import', self.on_document_import)

        self.document_grid = DocumentGrid()
        self.document_grid.connect('document-create', self.on_document_create_activated)
        self.document_grid.connect('document-import', self.on_document_import)
        self.document_grid.view.connect('item-activated', self.on_document_item_activated)

        self.editor = Editor()

        self.screens = Gtk.Stack()
        self.screens.set_transition_duration(400)
        self.screens.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)

        self.screens.add_named(self.welcome_grid, 'welcome-grid')
        self.screens.add_named(self.document_grid, 'document-grid')
        self.screens.add_named(self.editor, 'editor-grid')

        self.screens.show_all()

        self.toast = Granite.WidgetsToast()

        self.overlay = Gtk.Overlay()
        self.overlay.add_overlay(self.screens)
        self.overlay.add_overlay(self.toast)
        self.overlay.show_all()

        self.add(self.overlay)

        # Init actions
        self.init_actions()

        # If here's at least one document in storage
        # then show documents grid
        self.check_documents_count()

        # Pull the Settings
        self.toggle_spellcheck(self.settings.get_boolean('spellcheck'))
        self.autosave = self.settings.get_boolean('autosave')
        self.set_autoindent(self.settings.get_boolean('autoindent'))
        self.set_tabs_spaces(self.settings.get_boolean('spaces-instead-of-tabs'))
        self.set_indent_width(self.settings.get_int('indent-width'))
        self.set_style_scheme(self.settings.get_string('stylescheme'))
        self.editor.update_font(self.settings.get_string('font'))

    def apply_styling(self):
        """Apply elementary OS header styling only for elementary OS"""
        if distro.id() == 'elementary':
            Granite.widgets_utils_set_color_primary(self,
                                                    Gdk.RGBA(red=0.29, green=0.50, blue=0.64, alpha=1.0),
                                                    Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
            self.get_style_context().add_class('elementary')


    def init_actions(self) -> None:
        """Initialize app-wide actions.

        """
        action_items = {
            'document': [
                {
                    'name': 'create',
                    'action': self.on_document_create_activated,
                    'accels': ('<Control>n',)
                },
                {
                    'name': 'save',
                    'action': self.on_document_save_activated,
                    'accels': ('<Control>s',)
                },
                {
                    'name': 'close',
                    'action': self.on_document_close_activated,
                    'accels': ('<Control>w',)
                },
                {
                    'name': 'rename',
                    'action': self.on_document_rename,
                    'accels': ('F2',)
                },
                {
                    'name': 'archive',
                    'action': self.on_document_archive_activated,
                    'accels': (None,)
                },
                {
                    'name': 'unarchive',
                    'action': self.on_document_unarchive_activated,
                    'accels': (None,)
                },
                {
                    'name': 'delete',
                    'action': self.on_document_delete_activated,
                    'accels': ('<Shift>Delete',)
                },
                {
                    'name': 'import',
                    'action': self.on_document_import_activated,
                    'accels': ('<Control>o',)
                },
                {
                    'name': 'export',
                    'action': self.on_export_plaintext,
                    'accels': (None,)
                },
                {
                    'name': 'export-markdown',
                    'action': self.on_export_markdown,
                    'accels': ('<Control><Shift>s',)
                },
                {
                    'name': 'export-html',
                    'action': self.on_export_html,
                    'accels': (None,)
                },
                {
                    'name': 'export-medium',
                    'action': self.on_export_medium,
                    'accels': (None,)
                },
                {
                    'name': 'export-writeas',
                    'action': self.on_export_writeas,
                    'accels': (None,)
                },
                {
                    'name': 'preview',
                    'action': self.on_preview,
                    'accels': ('<Control><Shift>p',)
                },
                # {
                #     'name': 'print',
                #     'action': self.on_print,
                #     'accels': ('<Control>p',)
                # },
                # {
                #     'name': 'search',
                #     'action': self.search_activated,
                #     'accels': ('<Control>k',)
                # },
                {
                    'name': 'zoom_in',
                    'action': self.on_zoom_in,
                    'accels': ('<Control>equal', '<Control>plus')
                },
                {
                    'name': 'zoom_out',
                    'action': self.on_zoom_out,
                    'accels': ('<Control>minus',)
                },
                {
                    'name': 'zoom_default',
                    'action': self.on_zoom_default,
                    'accels': ('<Control>0',)
                },
                {
                    'name': 'search_text',
                    'action': self.search_activated,
                    'accels': ('<Control>f',)
                },
                {
                    'name': 'search_text_next',
                    'action': self.on_text_search_forward,
                    'accels': ('<Control>g',)
                },
                {
                    'name': 'search_text_prev',
                    'action': self.on_text_search_backward,
                    'accels': ('<Control><Shift>g',)
                },
                {
                    'name': 'toggle_archived',
                    'action': self.on_toggle_archive,
                    'accels': (None,)
                },
            ]
        }

        for action_group_key, actions in action_items.items():
            action_group = Gio.SimpleActionGroup()

            for item in actions:
                action = Gio.SimpleAction(name=item['name'])
                action.connect('activate', item['action'])
                self.get_application().set_accels_for_action(f'{action_group_key}.{item["name"]}', item["accels"])
                action_group.add_action(action)

            self.insert_action_group(action_group_key, action_group)

    def on_window_delete_event(self, sender: Gtk.Widget = None) -> None:
        """Save opened document before window is closed.

        """
        try:
            if self.autosave:
                self.editor.save_document()
            else:
                print('Ask for action!')

            if not self.is_maximized():
                self.settings.set_value("window-size", GLib.Variant("ai", self.current_size))
                self.settings.set_value("window-position", GLib.Variant("ai", self.current_position))

        except Exception as e:
            Logger.error(e)

    def on_configure_event(self, window, event: Gdk.EventConfigure):
        if self._configure_timeout_id:
            GLib.source_remove(self._configure_timeout_id)

        self.current_size = window.get_size()
        self.current_position = window.get_position()

    def check_documents_count(self) -> None:
        """Check for documents count in storage and switch between screens
        whether there is at least one document or not.

        """
        if storage.count() > 0:
            self.screens.set_visible_child_name('document-grid')

            last_doc_id = self.settings.get_int('last-document-id')
            if last_doc_id and last_doc_id != -1:
                self.screens.set_visible_child_name('editor-grid')
                self.editor.load_document(last_doc_id)
                self.header.toggle_document_mode()
                self.header.update_title(title=self.editor.document.title)
        else:
            self.toggle_welcome(True)

    def toggle_welcome(self, state=True):
        if state:
            self.screens.set_visible_child_name('welcome-grid')
        else:
            self.screens.set_visible_child_name('document-grid')

    def on_document_close_activated(self, sender: Gtk.Widget, event=None) -> None:
        """Save and close opened document.

        """

        # Should work only in editor mode.
        if self.screens.get_visible_child_name() == 'editor-grid':
            self.screens.set_visible_child_name('document-grid')
            self.editor.unload_document(save=self.autosave)
            self.document_grid.reload_items()
            self.header.toggle_document_mode()
            self.header.update_title()
            self.settings.set_int('last-document-id', -1)

    def on_welcome_activated(self, sender: Welcome, index: int) -> None:
        if index == 0:
            self.on_document_create_activated(sender, index)

    def on_document_item_activated(self, sender: Gtk.Widget, path: Gtk.TreePath) -> None:
        """Activate currently selected document in grid and open it in editor.

        :param sender:
        :param path:
        :return:
        """
        model_iter = self.document_grid.model.get_iter(path)
        doc_id = self.document_grid.model.get_value(model_iter, 3)
        Logger.debug('Activated Document.Id %s', doc_id)

        self.document_activate(doc_id)

    def document_activate(self, doc_id):
        editor = self.screens.get_child_by_name('editor-grid')
        editor.load_document(doc_id)
        self.screens.set_visible_child_name('editor-grid')
        self.header.toggle_document_mode()
        self.header.update_title(title=editor.document.title)
        self.settings.set_int('last-document-id', doc_id)

    def on_document_create_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Create new document named 'Nameless' :) and activate it in editor.

        :param sender:
        :param event:
        :return:
        """
        # If document already loaded to editor we need to close it before create new one
        if self.editor.document:
            self.on_document_close_activated(sender, event)

        self.editor.create_document()
        self.screens.set_visible_child_name('editor-grid')
        self.header.toggle_document_mode()
        self.header.update_title(title=self.editor.document.title)

    def on_document_save_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Save opened document to storage.

        :param sender:
        :param event:
        :return:
        """
        self.editor.save_document()

    def on_document_import_activated(self, sender, event):
        dialog = Gtk.FileChooserNative.new(
            _("Import files into Norka"),
            self,
            Gtk.FileChooserAction.OPEN
        )

        filter_markdown = Gtk.FileFilter()
        filter_markdown.set_name(_("Text Files"))
        filter_markdown.add_mime_type("text/plain")
        dialog.add_filter(filter_markdown)
        dialog_result = dialog.run()

        if dialog_result == Gtk.ResponseType.ACCEPT:
            file_path = dialog.get_filename()
            self.import_document(file_path)

        dialog.destroy()

    def on_document_import(self, sender: Gtk.Widget = None, file_path: str = None) -> None:
        if self.import_document(file_path=file_path):
            self.check_documents_count()

    def import_document(self, file_path: str) -> bool:
        """Import files from filesystem.
        Creates new document in storage and fill it with file's contents.

        :param sender:
        :param filepath: path to file to import
        """
        if not os.path.exists(file_path):
            return False

        self.header.show_spinner(True)
        try:
            with open(file_path, 'r') as _file:
                lines = _file.readlines()
                filename = os.path.basename(file_path)[:file_path.rfind('.')]

                _doc = Document(title=filename, content='\r\n'.join(lines))
                _doc_id = storage.add(_doc)

                self.document_grid.reload_items()
            return True
        except Exception as e:
            print(e)
            return False
        finally:
            self.header.show_spinner(False)

    def on_document_rename(self, sender: Gtk.Widget = None, event=None) -> None:
        """Rename currently selected document.
        Show rename dialog and update document's title
        if user puts new one in the entry.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        found, rect = self.document_grid.view.get_cell_rect(self.document_grid.selected_path)
        popover = RenamePopover(self.overlay, doc.title)
        popover.set_pointing_to(rect)
        popover.connect('activate', self.on_document_rename_activated)
        popover.popup()

    def on_document_rename_activated(self, sender: Gtk.Widget, title: str):
        sender.destroy()

        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        if storage.update(doc_id=doc.document_id, data={'title': title}):
            self.document_grid.reload_items()

    def on_document_archive_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Marks document as archived. Recoverable.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document
        if doc:
            if storage.update(doc_id=doc.document_id, data={'archived': True}):
                self.check_documents_count()
                self.document_grid.reload_items()

    def on_document_unarchive_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Unarchive document.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document
        if doc:
            if storage.update(doc_id=doc.document_id, data={'archived': False}):
                self.check_documents_count()
                self.document_grid.reload_items()

    def on_document_delete_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Permanently remove document from storage. Non-recoverable.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document

        prompt = MessageDialog(
            f"Permanently delete “{doc.title}”?",
            "Deleted items are not sent to Archive and not recoverable at all",
            "dialog-warning",
        )

        if doc:
            result = prompt.run()
            prompt.destroy()

            if result == Gtk.ResponseType.APPLY and storage.delete(doc.document_id):
                self.document_grid.reload_items()
                self.check_documents_count()

    def on_export_plaintext(self, sender: Gtk.Widget = None, event=None) -> None:
        """Export document from storage to local files or web-services.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        dialog = ExportFileDialog(
            "Export document to file",
            self,
            Gtk.FileChooserAction.SAVE
        )
        dialog.set_current_name(doc.title)
        export_format = ExportFormat.PlainText
        dialog.set_format(export_format)
        dialog_result = dialog.run()

        if dialog_result == Gtk.ResponseType.ACCEPT:
            self.header.show_spinner(True)
            basename, ext = os.path.splitext(dialog.get_filename())
            if ext not in export_format[1]:
                ext = export_format[1][0][1:]

            GObjectWorker.call(Exporter.export_plaintext,
                               (basename + ext, doc),
                               callback=self.on_export_callback)

        dialog.destroy()

    def on_export_markdown(self, sender: Gtk.Widget = None, event=None) -> None:
        """Export document from storage to local files or web-services.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        dialog = ExportFileDialog(
            "Export document to file",
            self,
            Gtk.FileChooserAction.SAVE
        )
        dialog.set_current_name(doc.title)
        export_format = ExportFormat.Markdown
        dialog.set_format(export_format)
        dialog_result = dialog.run()

        if dialog_result == Gtk.ResponseType.ACCEPT:
            self.header.show_spinner(True)
            basename, ext = os.path.splitext(dialog.get_filename())
            if ext not in export_format[1]:
                ext = export_format[1][0][1:]

            GObjectWorker.call(Exporter.export_markdown,
                               (basename + ext, doc),
                               callback=self.on_export_callback)

        dialog.destroy()

    def on_export_html(self, sender: Gtk.Widget = None, event=None) -> None:
        """Export document from storage to local files or web-services.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        dialog = ExportFileDialog(
            _("Export document to file"),
            self,
            Gtk.FileChooserAction.SAVE
        )
        dialog.set_current_name(doc.title)
        export_format = ExportFormat.Html
        dialog.set_format(export_format)
        dialog_result = dialog.run()

        if dialog_result == Gtk.ResponseType.ACCEPT:
            self.header.show_spinner(True)
            basename, ext = os.path.splitext(dialog.get_filename())
            if ext not in export_format[1]:
                ext = export_format[1][0][1:]

            GObjectWorker.call(Exporter.export_html,
                               (basename + ext, doc),
                               callback=self.on_export_callback)

        dialog.destroy()

    def on_export_callback(self, result):
        self.header.show_spinner(False)
        self.disconnect_toast()
        if result:
            self.toast.set_title(_("Document exported."))
            self.toast.set_default_action(_("Open folder"))
            self.uri_to_open = f"file://{os.path.dirname(result)}"
            self.toast.connect("default-action", self.open_uri)
            self.toast.send_notification()
        else:
            self.toast.set_title(_("Export goes wrong."))
            self.toast.send_notification()

    def on_export_medium(self, sender: Gtk.Widget = None, event=None) -> None:
        """Configure Medium client and export document asynchronously

        :param sender:
        :param event:
        :return:
        """

        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        token = self.settings.get_string("medium-personal-token")
        user_id = self.settings.get_string("medium-user-id")

        if not token or not user_id:
            self.toast.set_title(_("You need to set Medium token in Preferences -> Export"))
            self.toast.set_default_action(_("Configure"))
            self.disconnect_toast()
            self.toast.connect("default-action", self.get_application().on_preferences)
            self.toast.send_notification()

        else:
            self.header.show_spinner(True)
            self.medium_client.set_token(token)
            GObjectWorker.call(self.medium_client.create_post,
                               args=(user_id, doc, PublishStatus.DRAFT),
                               callback=self.on_export_medium_callback)

    def on_export_medium_callback(self, result):

        self.header.show_spinner(False)
        if result:
            self.toast.set_title(_("Document successfully exported!"))
            self.toast.set_default_action(_("View"))
            self.uri_to_open = result["url"]
            self.disconnect_toast()
            self.toast.connect("default-action", self.open_uri)
        else:
            self.toast.set_title(_("Export failed!"))
            self.toast.set_default_action(None)
        self.toast.send_notification()

    def on_export_writeas(self, sender: Gtk.Widget = None, event=None) -> None:
        """Configure Write.as client and export document asynchronously

        :param sender:
        :param event:
        :return:
        """

        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        token = self.settings.get_string("writeas-access-token")

        if not token:
            self.toast.set_title("You have to login to Write.as in Preferences -> Export")
            self.toast.set_default_action("Configure")
            self.disconnect_toast()
            self.toast.connect("default-action", self.get_application().on_preferences)
            self.toast.send_notification()

        else:
            self.header.show_spinner(True)
            self.writeas_client.set_token(access_token=token)
            GObjectWorker.call(self.writeas_client.create_post,
                               args=(doc,),
                               callback=self.on_export_writeas_callback)

    def on_export_writeas_callback(self, result):
        self.header.show_spinner(False)
        if result:
            self.toast.set_title(_("Document successfully exported!"))
            self.toast.set_default_action(_("View"))
            self.disconnect_toast()
            self.uri_to_open = f"https://write.as/{result['id']}"
            self.toast.connect("default-action", self.open_uri)
        else:
            self.toast.set_title(_("Export failed."))
            self.toast.set_default_action(None)
        self.toast.send_notification()

    def search_activated(self, sender, event=None):
        if self.screens.get_visible_child_name() == 'document-grid':
            self.on_document_search_activated(sender, event)
        elif self.screens.get_visible_child_name() == 'editor-grid':
            self.on_text_search_activated(sender, event)
        else:
            pass

    def on_document_search_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Open search dialog to find a documents

        :param sender:
        :param event:
        :return:
        """
        dialog = QuickFindDialog()
        response = dialog.run()

        if response == Gtk.ResponseType.APPLY and dialog.document_id:
            self.document_activate(dialog.document_id)
        dialog.destroy()

    def on_text_search_activated(self, sender: Gtk.Widget = None, event=None) -> None:
        """Open search dialog to find text in a documents
        """
        self.editor.on_search_text_activated(sender, event)

    def on_text_search_forward(self, sender: Gtk.Widget = None, event=None) -> None:
        if self.screens.get_visible_child_name() == 'editor-grid' \
                and self.editor.search_revealer.get_child_revealed():
            self.editor.search_forward(sender=sender, event=event)

    def on_text_search_backward(self, sender: Gtk.Widget = None, event=None) -> None:
        if self.screens.get_visible_child_name() == 'editor-grid' \
                and self.editor.search_revealer.get_child_revealed():
            self.editor.search_backward(sender=sender, event=event)

    def on_zoom_in(self, sender, event) -> None:
        self.zooming(Gdk.ScrollDirection.UP)

    def on_zoom_out(self, sender, event) -> None:
        self.zooming(Gdk.ScrollDirection.DOWN)

    def on_zoom_default(self, sender, event) -> None:
        self.settings.set_int('zoom', 100)
        self.settings.set_string("font", f'{FONT_SIZE_FAMILY} {FONT_SIZE_DEFAULT}')

    def toggle_spellcheck(self, state: bool) -> None:
        self.editor.set_spellcheck(state)

    def set_style_scheme(self, scheme_id: str) -> None:
        self.editor.set_style_scheme(scheme_id)

    def set_autoindent(self, autoindent: bool) -> None:
        self.editor.view.set_auto_indent(autoindent)

    def set_tabs_spaces(self, state: bool) -> None:
        self.editor.view.set_insert_spaces_instead_of_tabs(state)

    def set_indent_width(self, size: int) -> None:
        self.editor.view.set_tab_width(size)

    def zooming(self, direction: Gdk.ScrollDirection) -> None:

        font = self.get_current_font()
        zoom = self.settings.get_int('zoom')

        if direction == Gdk.ScrollDirection.UP:
            zoom += 10
        elif direction == Gdk.ScrollDirection.DOWN:
            zoom -= 10

        font_size = int(FONT_SIZE_DEFAULT * zoom / 100)
        if font_size < FONT_SIZE_MIN or font_size > FONT_SIZE_MAX:
            return

        self.settings.set_string('font', f'{font} {font_size}')
        self.settings.set_int('zoom', zoom)

    def get_current_font(self) -> str:
        font = self.settings.get_string("font")
        return font[:font.rfind(" ")]

    def get_current_font_size(self) -> float:
        font = self.settings.get_string("font")
        return float(font[font.rfind(" ") + 1:])

    def on_toggle_archive(self, action: Gio.SimpleAction, name: str = None):
        show_archived = self.header.archived_button.get_active()
        self.document_grid.show_archived = show_archived
        self.document_grid.reload_items()

        self.toggle_welcome(not show_archived and storage.count(with_archived=show_archived) == 0)

    def open_uri(self, event):
        if self.uri_to_open:
            Gtk.show_uri_on_window(None, self.uri_to_open, Gdk.CURRENT_TIME)
            self.uri_to_open = None

    def disconnect_toast(self):
        """Disconnect toast action. Weird way, need ti rewrite it"""
        try:
            self.toast.disconnect_by_func(self.get_application().on_preferences)
        except:
            pass

        try:
            self.toast.disconnect_by_func(self.open_uri)
        except:
            pass

    def on_preview(self, sender, event):
        doc = self.document_grid.selected_document or self.editor.document

        if not self.preview:
            # create preview window
            text = doc.content if doc else None
            self.preview = Preview(parent=self, text=text)
            # connect signal handlers
            # self.editor.scrolled.get_vscrollbar().connect('value-changed', self.scroll_preview)
            self.editor.buffer.connect('changed', self.preview.buffer_changed)
            self.editor.connect('document-load', self.preview.show_preview)
            self.editor.connect('document-close', self.preview.show_empty_page)
            self.preview.connect('destroy', self.on_preview_close)
            self.preview.show_all()
        else:
            self.preview.present()

        if doc:
            self.preview.show_preview(self)

    def scroll_preview(self, range: Gtk.Range):
        adjustment = range.get_adjustment()
        percent = adjustment.get_value() / adjustment.get_upper()
        print(f'Scrolled to: {percent * 100}% / {adjustment.get_lower()} / {adjustment.get_upper()}')
        self.preview.scroll_to(percent)

    def on_preview_close(self, sender):
        self.preview = None
Example #2
0
class NorkaWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'NorkaWindow'

    def __init__(self, settings: Gio.Settings, **kwargs):
        super().__init__(**kwargs)

        self.set_default_icon(
            Pixbuf.new_from_resource_at_scale(
                '/com/github/tenderowl/norka/icons/com.github.tenderowl.norka.svg',
                128, 128, True))
        self.settings = settings
        self._configure_timeout_id = None

        self.current_size = (786, 520)
        self.resize(*self.settings.get_value('window-size'))
        self.connect('configure-event', self.on_configure_event)
        self.connect('destroy', self.on_window_delete_event)

        # Init actions
        self.init_actions()

        # Make a header
        self.header = Header(self.settings)
        self.set_titlebar(self.header)
        self.header.show()

        # Init screens
        self.welcome_grid = Welcome()
        self.welcome_grid.connect('activated', self.on_welcome_activated)

        self.document_grid = DocumentGrid()
        self.document_grid.connect('document-create',
                                   self.on_document_create_activated)
        self.document_grid.view.connect('item-activated',
                                        self.on_document_item_activated)

        self.editor = Editor()

        self.screens = Gtk.Stack()
        self.screens.set_transition_duration(400)
        self.screens.set_transition_type(
            Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)

        self.screens.add_named(self.welcome_grid, 'welcome-grid')
        self.screens.add_named(self.document_grid, 'document-grid')
        self.screens.add_named(self.editor, 'editor-grid')

        self.screens.show_all()

        self.add(self.screens)

        # If here's at least one document in storage
        # then show documents grid
        self.check_documents_count()

        # Pull the Settings
        self.toggle_spellcheck(self.settings.get_boolean('spellcheck'))
        self.autosave = self.settings.get_boolean('autosave')
        self.set_autoindent(self.settings.get_boolean('autoindent'))
        self.set_tabs_spaces(
            self.settings.get_boolean('spaces-instead-of-tabs'))
        self.set_indent_width(self.settings.get_int('indent-width'))
        self.set_style_scheme(self.settings.get_string('stylescheme'))
        self.editor.update_font(self.settings.get_string('font'))

    def init_actions(self) -> None:
        """Initialize app-wide actions.

        """
        action_items = {
            'document': [
                {
                    'name': 'create',
                    'action': self.on_document_create_activated,
                    'accels': ('<Control>n', )
                },
                {
                    'name': 'save',
                    'action': self.on_document_save_activated,
                    'accels': ('<Control>s', )
                },
                {
                    'name': 'close',
                    'action': self.on_document_close_activated,
                    'accels': ('<Control>w', )
                },
                {
                    'name': 'rename',
                    'action': self.on_document_rename_activated,
                    'accels': ('F2', )
                },
                {
                    'name': 'archive',
                    'action': self.on_document_archive_activated,
                    'accels': ('Delete', )
                },
                {
                    'name': 'delete',
                    'action': self.on_document_delete_activated,
                    'accels': ('<Shift>Delete', )
                },
                {
                    'name': 'export',
                    'action': self.on_document_export_activated,
                    'accels': ('<Control><Shift>s', )
                },
                {
                    'name': 'search',
                    'action': self.on_document_search_activated,
                    'accels': ('<Control>k', )
                },
                {
                    'name': 'zoom_in',
                    'action': self.on_zoom_in,
                    'accels': ('<Control>equal', '<Control>plus')
                },
                {
                    'name': 'zoom_out',
                    'action': self.on_zoom_out,
                    'accels': ('<Control>minus', )
                },
                {
                    'name': 'zoom_default',
                    'action': self.on_zoom_default,
                    'accels': ('<Control>0', )
                },
            ]
        }

        for action_group_key, actions in action_items.items():
            action_group = Gio.SimpleActionGroup()

            for item in actions:
                action = Gio.SimpleAction(name=item['name'])
                action.connect('activate', item['action'])
                self.get_application().set_accels_for_action(
                    f'{action_group_key}.{item["name"]}', item["accels"])
                action_group.add_action(action)

            self.insert_action_group(action_group_key, action_group)

    def on_window_delete_event(self, sender: Gtk.Widget = None) -> None:
        """Save opened document before window is closed.

        """
        try:
            if self.autosave:
                self.editor.save_document()
            else:
                print('Ask for action!')

            if not self.is_maximized():
                self.settings.set_value("window-size",
                                        GLib.Variant("ai", self.current_size))
                self.settings.set_value(
                    "window-position", GLib.Variant("ai",
                                                    self.current_position))

        except Exception as e:
            Logger.error(e)

    def on_configure_event(self, window, event: Gdk.EventConfigure):
        if self._configure_timeout_id:
            GLib.source_remove(self._configure_timeout_id)

        self.current_size = window.get_size()
        self.current_position = window.get_position()

    def check_documents_count(self) -> None:
        """Check for documents count in storage and switch between screens
        whether there is at least one document or not.

        """
        if storage.count() > 0:
            self.screens.set_visible_child_name('document-grid')

            last_doc_id = self.settings.get_int('last-document-id')
            if last_doc_id and last_doc_id != -1:
                self.screens.set_visible_child_name('editor-grid')
                self.editor.load_document(last_doc_id)
                self.header.toggle_document_mode()
                self.header.update_title(title=self.editor.document.title)
        else:
            self.screens.set_visible_child_name('welcome-grid')

    def on_document_close_activated(self,
                                    sender: Gtk.Widget,
                                    event=None) -> None:
        """Save and close opened document.

        """
        self.screens.set_visible_child_name('document-grid')
        self.editor.unload_document(save=self.autosave)
        self.document_grid.reload_items()
        self.header.toggle_document_mode()
        self.header.update_title()
        self.settings.set_int('last-document-id', -1)

    def on_welcome_activated(self, sender: Welcome, index: int) -> None:
        if index == 0:
            self.on_document_create_activated(sender, index)

    def on_document_item_activated(self, sender: Gtk.Widget,
                                   path: Gtk.TreePath) -> None:
        """Activate currently selected document in grid and open it in editor.

        :param sender:
        :param path:
        :return:
        """
        model_iter = self.document_grid.model.get_iter(path)
        doc_id = self.document_grid.model.get_value(model_iter, 3)
        Logger.debug('Activated Document.Id %s', doc_id)

        editor = self.screens.get_child_by_name('editor-grid')
        editor.load_document(doc_id)
        self.screens.set_visible_child_name('editor-grid')

        self.header.toggle_document_mode()
        self.header.update_title(
            title=self.document_grid.model.get_value(model_iter, 1))
        self.settings.set_int('last-document-id', doc_id)

    def on_document_create_activated(self,
                                     sender: Gtk.Widget = None,
                                     event=None) -> None:
        """Create new document named 'Nameless' :) and activate it in editor.

        :param sender:
        :param event:
        :return:
        """
        self.editor.create_document()
        self.screens.set_visible_child_name('editor-grid')
        self.header.toggle_document_mode()
        self.header.update_title(title=self.editor.document.title)

    def on_document_save_activated(self,
                                   sender: Gtk.Widget = None,
                                   event=None) -> None:
        """Save opened document to storage.

        :param sender:
        :param event:
        :return:
        """
        self.editor.save_document()

    def on_document_rename_activated(self,
                                     sender: Gtk.Widget = None,
                                     event=None) -> None:
        """Rename currently selected document.
        Show rename dialog and update document's title
        if user puts new one in the entry.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document
        if doc:
            popover = RenameDialog(doc.title)
            response = popover.run()
            try:
                if response == Gtk.ResponseType.APPLY:
                    new_title = popover.entry.get_text()

                    if storage.update(doc_id=doc._id,
                                      data={'title': new_title}):
                        self.document_grid.reload_items()
            except Exception as e:
                Logger.debug(e)
            finally:
                popover.destroy()

    def on_document_archive_activated(self,
                                      sender: Gtk.Widget = None,
                                      event=None) -> None:
        """Marks document as archived. Recoverable.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document
        if doc:
            if storage.update(doc_id=doc._id, data={'archived': True}):
                self.check_documents_count()
                self.document_grid.reload_items()

    def on_document_delete_activated(self,
                                     sender: Gtk.Widget = None,
                                     event=None) -> None:
        """Permanently remove document from storage. Non-recoverable.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document

        prompt = MessageDialog(
            f"Permanently delete “{doc.title}”?",
            "Deleted items are not sent to Archive and not recoverable at all",
            "dialog-warning",
        )

        if doc:
            result = prompt.run()
            prompt.destroy()

            if result == Gtk.ResponseType.APPLY and storage.update(
                    doc_id=doc._id, data={'archived': True}):
                self.document_grid.reload_items()
                self.check_documents_count()

    def on_document_export_activated(self,
                                     sender: Gtk.Widget = None,
                                     event=None) -> None:
        """Export document from storage to local files or web-services.

        :param sender:
        :param event:
        :return:
        """
        doc = self.document_grid.selected_document or self.editor.document
        if not doc:
            return

        dialog = Gtk.FileChooserDialog(
            "Export document to file",
            self,
            Gtk.FileChooserAction.SAVE,
            (
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.CANCEL,
                Gtk.STOCK_SAVE,
                Gtk.ResponseType.ACCEPT,
            ),
            # use_header_bar=1
        )
        dialog.set_current_name(doc.title)

        filter_markdown = Gtk.FileFilter()
        filter_markdown.set_name("Markdown")
        filter_markdown.add_pattern("*.md")
        filter_markdown.add_pattern("*.MD")
        filter_markdown.add_pattern("*.markdown")
        dialog.add_filter(filter_markdown)
        dialog.set_do_overwrite_confirmation(True)

        extensions = (
            '.md',
            '.markdown',
        )

        if dialog.run() == Gtk.ResponseType.ACCEPT:
            file_name = dialog.get_filename()
            ex_ok = False

            for extension in extensions:
                if file_name.lower().endswith(extension):
                    ex_ok = True
                    break
            if not ex_ok and extensions:
                file_name += extensions[0]

            with open(file_name, "w+", encoding="utf-8") as output:
                data = self.editor.get_text()
                output.write(data)
        dialog.destroy()

    def on_document_search_activated(self,
                                     sender: Gtk.Widget = None,
                                     event=None) -> None:
        """Open search dialog.

        :param sender:
        :param event:
        :return:
        """
        # dialog = SearchDialog()
        # dialog.run()
        # dialog.destroy()
        pass

    def on_zoom_in(self, sender, event) -> None:
        self.zooming(Gdk.ScrollDirection.UP)

    def on_zoom_out(self, sender, event) -> None:
        self.zooming(Gdk.ScrollDirection.DOWN)

    def on_zoom_default(self, sender, event) -> None:
        self.settings.set_int('zoom', 100)
        self.settings.set_string("font",
                                 f'{FONT_SIZE_FAMILY} {FONT_SIZE_DEFAULT}')

    def toggle_spellcheck(self, state: bool) -> None:
        self.editor.set_spellcheck(state)

    def set_style_scheme(self, scheme_id: str) -> None:
        self.editor.set_style_scheme(scheme_id)

    def set_autoindent(self, autoindent: bool) -> None:
        self.editor.view.set_auto_indent(autoindent)

    def set_tabs_spaces(self, state: bool) -> None:
        self.editor.view.set_insert_spaces_instead_of_tabs(state)

    def set_indent_width(self, size: int) -> None:
        self.editor.view.set_tab_width(size)

    def zooming(self, direction: Gdk.ScrollDirection) -> None:

        font = self.get_current_font()
        zoom = self.settings.get_int('zoom')

        if direction == Gdk.ScrollDirection.UP:
            zoom += 10
        elif direction == Gdk.ScrollDirection.DOWN:
            zoom -= 10

        font_size = int(FONT_SIZE_DEFAULT * zoom / 100)
        if font_size < FONT_SIZE_MIN or font_size > FONT_SIZE_MAX:
            return

        self.settings.set_string('font', f'{font} {font_size}')
        self.settings.set_int('zoom', zoom)

    def get_current_font(self) -> str:
        font = self.settings.get_string("font")
        return font[:font.rfind(" ")]

    def get_current_font_size(self) -> float:
        font = self.settings.get_string("font")
        return float(font[font.rfind(" ") + 1:])