Example #1
0
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir,
                                      'main_window.glade')
        self.builder = Gtk.Builder()
        if filesystem.IS_WIN:
            import xml.etree.ElementTree as ET
            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if 'translatable' in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding='unicode')
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain('rednotebook')
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object('main_frame')
        self.main_frame.set_title('RedNotebook')
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, 'rednotebook.svg'))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object('vbox3')
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_redo_manager = undo.UndoRedoManager(self)

        self.calendar = MainCalendar(self.journal,
                                     self.builder.get_object('calendar'))
        self.day_text_field = DayEditor(
            self.builder.get_object('day_text_view'), self.undo_redo_manager)
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read('spellcheck'))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == 'MainMenuActionGroup':
                for action in actiongroup.list_actions():
                    if action.get_name() == 'CheckSpelling':
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled
                                          and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object('statusbar'))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object('categories_tree_view'), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object(
            'back_one_day_button')
        self.today_button = self.builder.get_object('today_button')
        self.forward_one_day_button = self.builder.get_object(
            'forward_one_day_button')

        self.edit_pane = self.builder.get_object('edit_pane')
        self.text_vbox = self.builder.get_object('text_vbox')

        if browser.WebKit2:

            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text, 'xhtml')
                    self.load_html(html)

            self.html_editor = Preview(self.journal)
            self.html_editor.connect('button-press-event',
                                     self.on_browser_clicked)
            self.html_editor.connect('decide-policy',
                                     self.on_browser_decide_policy)

            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.hide()
            self.html_editor.set_editable(False)
        else:
            self.html_editor = mock.MagicMock()
            preview_button = self.builder.get_object('preview_button')
            preview_button.set_label(_('Preview in Browser'))

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, 'shrink', False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            'on_back_one_day_button_clicked':
            self.on_back_one_day_button_clicked,
            'on_today_button_clicked': self.on_today_button_clicked,
            'on_forward_one_day_button_clicked':
            self.on_forward_one_day_button_clicked,
            'on_preview_button_clicked': self.on_preview_button_clicked,
            'on_edit_button_clicked': self.on_edit_button_clicked,
            'on_main_frame_configure_event':
            self.on_main_frame_configure_event,
            'on_main_frame_window_state_event':
            self.on_main_frame_window_state_event,
            'on_add_new_entry_button_clicked':
            self.on_add_new_entry_button_clicked,
            'on_main_frame_delete_event': self.on_main_frame_delete_event,

            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object('annotations_pane').set_visible(
            self.journal.config.read('showTagsPane'))
Example #2
0
class MainWindow:
    '''
    Class that holds the reference to the main glade file and handles
    all actions
    '''
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir,
                                      'main_window.glade')
        self.builder = Gtk.Builder()
        if filesystem.IS_WIN:
            import xml.etree.ElementTree as ET
            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if 'translatable' in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding='unicode')
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain('rednotebook')
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object('main_frame')
        self.main_frame.set_title('RedNotebook')
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, 'rednotebook.svg'))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object('vbox3')
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_redo_manager = undo.UndoRedoManager(self)

        self.calendar = MainCalendar(self.journal,
                                     self.builder.get_object('calendar'))
        self.day_text_field = DayEditor(
            self.builder.get_object('day_text_view'), self.undo_redo_manager)
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read('spellcheck'))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == 'MainMenuActionGroup':
                for action in actiongroup.list_actions():
                    if action.get_name() == 'CheckSpelling':
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled
                                          and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object('statusbar'))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object('categories_tree_view'), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object(
            'back_one_day_button')
        self.today_button = self.builder.get_object('today_button')
        self.forward_one_day_button = self.builder.get_object(
            'forward_one_day_button')

        self.edit_pane = self.builder.get_object('edit_pane')
        self.text_vbox = self.builder.get_object('text_vbox')

        if browser.WebKit2:

            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text, 'xhtml')
                    self.load_html(html)

            self.html_editor = Preview(self.journal)
            self.html_editor.connect('button-press-event',
                                     self.on_browser_clicked)
            self.html_editor.connect('decide-policy',
                                     self.on_browser_decide_policy)

            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.hide()
            self.html_editor.set_editable(False)
        else:
            self.html_editor = mock.MagicMock()
            preview_button = self.builder.get_object('preview_button')
            preview_button.set_label(_('Preview in Browser'))

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, 'shrink', False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            'on_back_one_day_button_clicked':
            self.on_back_one_day_button_clicked,
            'on_today_button_clicked': self.on_today_button_clicked,
            'on_forward_one_day_button_clicked':
            self.on_forward_one_day_button_clicked,
            'on_preview_button_clicked': self.on_preview_button_clicked,
            'on_edit_button_clicked': self.on_edit_button_clicked,
            'on_main_frame_configure_event':
            self.on_main_frame_configure_event,
            'on_main_frame_window_state_event':
            self.on_main_frame_window_state_event,
            'on_add_new_entry_button_clicked':
            self.on_add_new_entry_button_clicked,
            'on_main_frame_delete_event': self.on_main_frame_delete_event,

            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object('annotations_pane').set_visible(
            self.journal.config.read('showTagsPane'))

    def set_tooltips(self):
        '''
        Little work-around:
        Tooltips are not shown for menuitems that have been created with uimanager.
        We have to do it manually.
        '''
        groups = self.uimanager.get_action_groups()
        for group in groups:
            actions = group.list_actions()
            for action in actions:
                widgets = action.get_proxies()
                tooltip = action.get_property('tooltip')
                if tooltip:
                    for widget in widgets:
                        widget.set_tooltip_markup(tooltip)

    def set_shortcuts(self):
        '''
        This method actually is not responsible for the Ctrl-C etc. actions
        '''
        self.accel_group = self.builder.get_object('accelgroup1')
        self.main_frame.add_accel_group(self.accel_group)

        self.main_frame.connect('key-press-event', self._on_key_press_event)

        shortcuts = [
            (self.back_one_day_button, 'clicked', '<Ctrl>Page_Up'),
            (self.today_button, 'clicked', '<Alt>Home'),
            (self.forward_one_day_button, 'clicked', '<Ctrl>Page_Down'),
        ]
        for button, signal, shortcut in shortcuts:
            (keyval, mod) = Gtk.accelerator_parse(shortcut)
            button.add_accelerator(signal, self.accel_group, keyval, mod,
                                   Gtk.AccelFlags.VISIBLE)

    def _on_key_press_event(self, widget, event):
        # Exit fullscreen mode with ESC.
        if event.keyval == Gdk.KEY_Escape and self.is_fullscreen:
            self.toggle_fullscreen()

    # TRAY-ICON / CLOSE --------------------------------------------------------

    def setup_tray_icon(self):
        self.tray_icon = Gtk.StatusIcon()
        self.tray_icon.set_name('RedNotebook')
        visible = (self.journal.config.read('closeToTray') == 1)
        self.tray_icon.set_visible(visible)
        logging.debug('Tray icon visible: %s' % visible)

        self.tray_icon.set_tooltip_text('RedNotebook')
        # TODO: Try using the svg here as well.
        icon_file = os.path.join(self.journal.dirs.frame_icon_dir, 'rn-32.png')
        self.tray_icon.set_from_file(icon_file)

        self.tray_icon.connect('activate', self.on_tray_icon_activated)
        self.tray_icon.connect('popup-menu', self.on_tray_popup_menu)

    def on_tray_icon_activated(self, tray_icon):
        if self.main_frame.get_property('visible'):
            self.hide()
        else:
            self.show()

    def on_tray_popup_menu(self, status_icon, button, activate_time):
        '''
        Called when the user right-clicks the tray icon
        '''

        tray_menu_xml = '''
        <ui>
        <popup action="TrayMenu">
            <menuitem action="Show"/>
            <menuitem action="Quit"/>
        </popup>
        </ui>'''

        # Create an ActionGroup
        actiongroup = Gtk.ActionGroup('TrayActionGroup')

        # Create actions
        actiongroup.add_actions([
            ('Show', Gtk.STOCK_MEDIA_PLAY, _('Show RedNotebook'), None, None,
             lambda widget: self.show()),
            ('Quit', Gtk.STOCK_QUIT, None, None, None, self.on_quit_activate),
        ])

        # Add the actiongroup to the uimanager
        self.uimanager.insert_action_group(actiongroup, 0)

        # Add a UI description
        self.uimanager.add_ui_from_string(tray_menu_xml)

        # Create a Menu
        menu = self.uimanager.get_widget('/TrayMenu')

        menu.popup(None, None, Gtk.status_icon_position_menu, button,
                   activate_time, status_icon)

    def show(self):
        self.main_frame.show()
        self.load_values_from_config()

    def hide(self):
        self.add_values_to_config()
        self.journal.save_to_disk()
        self.main_frame.hide()

    def on_main_frame_delete_event(self, widget, event):
        '''
        Exit if not close_to_tray
        '''
        logging.debug('Main frame destroyed')

        if self.journal.config.read('closeToTray'):
            self.hide()
        else:
            self.journal.exit()

        # We never call the default handler. Otherwise, the window would be
        # destroyed, but we might no actually want to exit.
        return True

    def on_quit_activate(self, widget):
        '''
        User selected quit from the menu -> exit unconditionally
        '''
        self.journal.exit()

    # -------------------------------------------------------- TRAY-ICON / CLOSE

    def setup_stats_dialog(self):
        self.stats_dialog = self.builder.get_object('stats_dialog')
        self.stats_dialog.set_transient_for(self.main_frame)
        overall_box = self.builder.get_object('overall_box')
        day_box = self.builder.get_object('day_stats_box')
        columns = [('1', str), ('2', str)]
        overall_list = CustomListView(columns)
        day_list = CustomListView(columns)
        overall_box.pack_start(overall_list, True, True, 0)
        day_box.pack_start(day_list, True, True, 0)
        setattr(self.stats_dialog, 'overall_list', overall_list)
        setattr(self.stats_dialog, 'day_list', day_list)
        for list in [overall_list, day_list]:
            list.set_headers_visible(False)

    # MODE-SWITCHING -----------------------------------------------------------

    def change_mode(self, preview):
        edit_scroll = self.builder.get_object('text_scrolledwindow')
        edit_button = self.builder.get_object('edit_button')
        preview_button = self.builder.get_object('preview_button')

        size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
        size_group.add_widget(edit_button)
        size_group.add_widget(preview_button)

        if preview:
            # Enter preview mode
            edit_scroll.hide()
            self.html_editor.show()

            edit_button.show()
            preview_button.hide()

            self.undo_redo_manager.disable_buttons()
        else:
            # Enter edit mode
            edit_scroll.show()
            self.html_editor.hide()

            preview_button.show()
            edit_button.hide()

            self.undo_redo_manager.update_buttons()

        self.template_manager.set_template_menu_sensitive(not preview)
        self.insert_actiongroup.set_sensitive(not preview)
        self.format_actiongroup.set_sensitive(not preview)
        self.insert_button.set_sensitive(not preview)
        self.format_button.set_sensitive(not preview)
        for action in ['Cut', 'Paste']:
            self.uimanager.get_widget('/MainMenuBar/Edit/%s' %
                                      action).set_sensitive(not preview)

        self.preview_mode = preview

    def on_edit_button_clicked(self, button):
        # The day's text is already in the editor.
        self.change_mode(preview=False)
        # Select (not only highlight) previously selected text by giving focus
        # to the day editor.
        GObject.idle_add(self.day_text_field.day_text_view.grab_focus)

    def on_preview_button_clicked(self, button):
        self.journal.save_old_day()
        if browser.WebKit2:
            self.html_editor.show_day(self.day)
            self.change_mode(preview=True)
        else:
            date_format = self.journal.config.read('exportDateFormat',
                                                   '%A, %x')
            date_string = dates.format_date(date_format, self.day.date)
            markup_string = markup.get_markup_for_day(self.day)
            html = self.journal.convert(
                markup_string,
                'xhtml',
                headers=[date_string + ' - RedNotebook', '', ''],
                options={'toc': 0})
            utils.show_html_in_browser(
                html, os.path.join(self.journal.dirs.temp_dir, 'day.html'))

    def on_browser_clicked(self, webview, event):
        if event.type == Gdk.EventType._2BUTTON_PRESS:
            # Double-click -> Change to edit mode.
            self.change_mode(preview=False)
            # Stop processing this event.
            return True
        elif event.button == 3:
            # Right-click -> don't show context menu.
            return True

    # ----------------------------------------------------------- MODE-SWITCHING

    def setup_search(self):
        always_show_results = not browser.WebKit2
        self.search_tree_view = search.SearchTreeView(self,
                                                      always_show_results)
        self.search_tree_view.show()
        self.search_scroll = Gtk.ScrolledWindow()
        if always_show_results:
            self.search_scroll.show()
        self.search_scroll.add(self.search_tree_view)
        self.search_box = search.SearchComboBox(Gtk.ComboBox.new_with_entry(),
                                                self)
        self.search_box.combo_box.show()
        search_container = self.builder.get_object('search_container')
        search_container.pack_start(self.search_box.combo_box, False, False, 0)
        search_container.pack_start(self.search_scroll, True, True, 0)

    def setup_clouds(self):
        if browser.WebKit2:
            from rednotebook.gui import clouds
            self.cloud = clouds.Cloud(self.journal)
            self.builder.get_object('search_container').pack_end(
                self.cloud, True, True, 0)
        else:
            self.cloud = mock.MagicMock()

    def on_main_frame_configure_event(self, widget, event):
        '''
        Is called when the frame size is changed. Unfortunately this is
        the way to go as asking for frame.get_size() at program termination
        gives strange results.
        '''
        main_frame_width, main_frame_height = self.main_frame.get_size()
        self.journal.config['mainFrameWidth'] = main_frame_width
        self.journal.config['mainFrameHeight'] = main_frame_height

    def on_main_frame_window_state_event(self, widget, event):
        '''
        The "window-state-event" signal is emitted when window state
        of widget changes. For example, for a toplevel window this
        event is signaled when the window is iconified, deiconified,
        minimized, maximized, made sticky, made not sticky, shaded or
        unshaded.
        '''
        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
            maximized = bool(event.new_window_state
                             & Gdk.WindowState.MAXIMIZED)
            self.journal.config['mainFrameMaximized'] = int(maximized)

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.main_frame.unfullscreen()
            self.is_fullscreen = False
        else:
            self.main_frame.fullscreen()
            self.is_fullscreen = True

    def on_back_one_day_button_clicked(self, widget):
        self.journal.go_to_prev_day()

    def on_today_button_clicked(self, widget):
        actual_date = datetime.date.today()
        self.journal.change_date(actual_date)

    def on_forward_one_day_button_clicked(self, widget):
        self.journal.go_to_next_day()

    def on_browser_decide_policy(self, webview, decision, decision_type):
        '''
        We want to load files and links externally.
        '''
        if decision_type == browser.WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
            action = decision.get_navigation_action()
            if action.is_user_gesture():
                uri = action.get_request().get_uri()
                logging.info('Clicked URI "%s"' % uri)
                filesystem.open_url(uri)

                decision.ignore()

        # Stop processing this event.
        return True

    def get_new_journal_dir(self, title, message):
        dir_chooser = self.builder.get_object('dir_chooser')
        dir_chooser.set_transient_for(self.main_frame)
        label = self.builder.get_object('dir_chooser_label')

        label.set_markup('<b>' + message + '</b>')
        dir_chooser.set_current_folder(
            os.path.dirname(self.journal.dirs.data_dir))

        response = dir_chooser.run()
        # Retrieve the dir now, because it will be cleared by the call to hide().
        new_dir = dir_chooser.get_filename()
        dir_chooser.hide()

        if response == Gtk.ResponseType.OK:
            if new_dir is None:
                self.journal.show_message(_('No directory selected.'),
                                          error=True)
                return None
            return new_dir
        return None

    def show_save_error_dialog(self, exit_imminent):
        dialog = self.builder.get_object('save_error_dialog')
        dialog.set_transient_for(self.main_frame)

        exit_without_save_button = self.builder.get_object(
            'exit_without_save_button')
        if exit_imminent:
            exit_without_save_button.show()
        else:
            exit_without_save_button.hide()

        answer = dialog.run()
        dialog.hide()

        if answer == Gtk.ResponseType.OK:
            # Even if the user aborts the Save-As dialog, we don't want to exit.
            self.journal.is_allowed_to_exit = False
            # Let the user select a new directory. Nothing has been saved yet.
            self.menubar_manager.on_save_as_menu_item_activate(None)
        elif answer == Gtk.ResponseType.CANCEL and exit_imminent:
            self.journal.is_allowed_to_exit = False
        # Do nothing if user wants to exit without saving

    def add_values_to_config(self):
        config = self.journal.config

        left_div = self.builder.get_object('main_pane').get_position()
        config['leftDividerPosition'] = left_div

        right_div = self.edit_pane.get_position()
        config['rightDividerPosition'] = right_div

        # Remember if window was maximized in separate method

        # Remember window position
        config['mainFrameX'], config[
            'mainFrameY'] = self.main_frame.get_position()

    def load_values_from_config(self):
        config = self.journal.config
        main_frame_width = config.read('mainFrameWidth')
        main_frame_height = config.read('mainFrameHeight')

        screen_width = Gdk.Screen.width()
        screen_height = Gdk.Screen.height()

        main_frame_width = min(main_frame_width, screen_width)
        main_frame_height = min(main_frame_height, screen_height)

        self.main_frame.resize(main_frame_width, main_frame_height)

        if config.read('mainFrameMaximized'):
            self.main_frame.maximize()
        else:
            # If window is not maximized, restore last position
            x = config.read('mainFrameX')
            y = config.read('mainFrameY')
            try:
                x, y = int(x), int(y)
                # Set to 0 if value is below 0
                if 0 <= x <= screen_width and 0 <= y <= screen_height:
                    self.main_frame.move(x, y)
                else:
                    self.main_frame.set_position(Gtk.WindowPosition.CENTER)
            except (ValueError, TypeError):
                # Values have not been set or are not valid integers
                self.main_frame.set_position(Gtk.WindowPosition.CENTER)

        self.builder.get_object('main_pane').set_position(
            config.read('leftDividerPosition'))
        # By default do not show tags pane.
        self.edit_pane.set_position(
            config.read('rightDividerPosition', main_frame_width))

        self.set_font(config.read('mainFont', editor.DEFAULT_FONT))

    def set_font(self, font_name):
        self.day_text_field.set_font(font_name)
        self.html_editor.set_font_size(
            Pango.FontDescription(font_name).get_size() / Pango.SCALE)

    def setup_template_menu(self):
        def update_menu(button):
            self.template_button.set_menu(self.template_manager.get_menu())

        self.template_button = customwidgets.ToolbarMenuButton(
            Gtk.STOCK_PASTE, self.template_manager.get_menu())
        self.template_button.set_label(_('Template'))
        self.template_button.connect('clicked', update_menu)
        self.template_button.set_tooltip_text(
            _("Insert this weekday's template. "
              "Click the arrow on the right for more options"))
        self.builder.get_object('edit_toolbar').insert(self.template_button, 2)

    def on_add_new_entry_button_clicked(self, widget):
        self.categories_tree_view._on_add_entry_clicked(None)

    def set_date(self, new_month, new_date, day):
        """
        Notes: When switching days in edit mode almost all processing
        time is used for highlighting the markup (searching regexes).
        """
        self.day = day
        self.categories_tree_view.clear()

        self.calendar.set_date(new_date)
        self.calendar.set_month(new_month)

        # Regardless of the mode, we always keep the editor updated, to be able
        # to always save the day.
        self.day_text_field.show_day(day)

        # Only switch mode automatically if set in preferences.
        if self.journal.config.read('autoSwitchMode') and browser.WebKit2:
            if day.has_text and not self.preview_mode:
                self.change_mode(preview=True)
            elif not day.has_text and self.preview_mode:
                self.change_mode(preview=False)

        if self.preview_mode:
            # Converting markup to html takes time, so only do it when necessary
            self.html_editor.show_day(day)

        self.categories_tree_view.set_day_content(day)
        self.undo_redo_manager.set_stack(new_date)

    def get_day_text(self):
        return self.day_text_field.get_text()

    def highlight_text(self, search_text):
        self.html_editor.highlight(search_text)
        self.day_text_field.highlight(search_text)

    def show_message(self, title, msg, msg_type):
        if msg_type == Gtk.MessageType.ERROR:
            self.infobar.show_message(title, msg, msg_type)
        else:
            self.statusbar.show_message(title, msg, msg_type)
Example #3
0
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir,
                                      "main_window.glade")
        self.builder = Gtk.Builder()
        # Register GtkSourceView so builder can use it when loading the file
        # https://stackoverflow.com/q/10524196/434217
        GObject.type_register(GtkSource.View)
        if filesystem.IS_WIN:
            from xml.etree import ElementTree as ET

            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if "translatable" in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding="unicode")
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain("rednotebook")
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object("main_frame")
        self.main_frame.set_title("RedNotebook")
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, "rn-128.png"))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object("vbox3")
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_action = self.uimanager.get_action("/MainMenuBar/Edit/Undo")
        self.redo_action = self.uimanager.get_action("/MainMenuBar/Edit/Redo")

        self.calendar = MainCalendar(self.journal,
                                     self.builder.get_object("calendar"))
        self.day_text_field = DayEditor(
            self.builder.get_object("day_text_view"))
        self.day_text_field.connect("can-undo-redo-changed",
                                    self.update_undo_redo_buttons)
        self.update_undo_redo_buttons()
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read("spellcheck"))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == "MainMenuActionGroup":
                for action in actiongroup.list_actions():
                    if action.get_name() == "CheckSpelling":
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled
                                          and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object("statusbar"))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object("categories_tree_view"), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object(
            "back_one_day_button")
        self.today_button = self.builder.get_object("today_button")
        self.forward_one_day_button = self.builder.get_object(
            "forward_one_day_button")

        self.edit_pane = self.builder.get_object("edit_pane")
        self.text_vbox = self.builder.get_object("text_vbox")

        use_internal_preview = self.journal.config.read(
            "useInternalPreview", 1)
        if use_internal_preview and browser.WebKit2:

            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal
                    self.internal = True

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text,
                                                "xhtml",
                                                use_gtk_theme=True)
                    self.load_html(html)

                def shutdown(self):
                    pass

            self.html_editor = Preview(self.journal)
            self.html_editor.connect("button-press-event",
                                     self.on_browser_clicked)
            self.html_editor.connect("decide-policy",
                                     self.on_browser_decide_policy)
            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.set_editable(False)
        elif use_internal_preview and browser_cef.cef:

            class Preview(browser_cef.HtmlView):
                def __init__(self, journal):
                    super().__init__()
                    self.journal = journal
                    self.internal = True

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text,
                                                "xhtml",
                                                use_gtk_theme=True)
                    self.load_html(html)

                def highlight(self, text):
                    pass

            self.html_editor = Preview(self.journal)
            self.html_editor.connect("on-url-clicked",
                                     lambda _, url: self.navigate_to_uri(url))
            self.text_vbox.pack_start(self.html_editor, True, True, 0)
        else:
            self.html_editor = mock.MagicMock()
            self.html_editor.internal = False
            preview_button = self.builder.get_object("preview_button")
            preview_button.set_label(_("Preview in Browser"))

        self.html_editor.hide()

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, "shrink", False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            "on_back_one_day_button_clicked":
            self.on_back_one_day_button_clicked,
            "on_today_button_clicked": self.on_today_button_clicked,
            "on_forward_one_day_button_clicked":
            self.on_forward_one_day_button_clicked,
            "on_preview_button_clicked": self.on_preview_button_clicked,
            "on_edit_button_clicked": self.on_edit_button_clicked,
            "on_main_frame_configure_event":
            self.on_main_frame_configure_event,
            "on_main_frame_window_state_event":
            self.on_main_frame_window_state_event,
            "on_add_new_entry_button_clicked":
            self.on_add_new_entry_button_clicked,
            "on_main_frame_delete_event": self.on_main_frame_delete_event,
            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object("annotations_pane").set_visible(
            self.journal.config.read("showTagsPane"))
Example #4
0
class MainWindow:
    """
    Class that holds the reference to the main glade file and handles
    all actions
    """
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir,
                                      "main_window.glade")
        self.builder = Gtk.Builder()
        # Register GtkSourceView so builder can use it when loading the file
        # https://stackoverflow.com/q/10524196/434217
        GObject.type_register(GtkSource.View)
        if filesystem.IS_WIN:
            from xml.etree import ElementTree as ET

            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if "translatable" in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding="unicode")
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain("rednotebook")
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object("main_frame")
        self.main_frame.set_title("RedNotebook")
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, "rn-128.png"))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object("vbox3")
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_action = self.uimanager.get_action("/MainMenuBar/Edit/Undo")
        self.redo_action = self.uimanager.get_action("/MainMenuBar/Edit/Redo")

        self.calendar = MainCalendar(self.journal,
                                     self.builder.get_object("calendar"))
        self.day_text_field = DayEditor(
            self.builder.get_object("day_text_view"))
        self.day_text_field.connect("can-undo-redo-changed",
                                    self.update_undo_redo_buttons)
        self.update_undo_redo_buttons()
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read("spellcheck"))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == "MainMenuActionGroup":
                for action in actiongroup.list_actions():
                    if action.get_name() == "CheckSpelling":
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled
                                          and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object("statusbar"))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object("categories_tree_view"), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object(
            "back_one_day_button")
        self.today_button = self.builder.get_object("today_button")
        self.forward_one_day_button = self.builder.get_object(
            "forward_one_day_button")

        self.edit_pane = self.builder.get_object("edit_pane")
        self.text_vbox = self.builder.get_object("text_vbox")

        use_internal_preview = self.journal.config.read(
            "useInternalPreview", 1)
        if use_internal_preview and browser.WebKit2:

            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal
                    self.internal = True

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text,
                                                "xhtml",
                                                use_gtk_theme=True)
                    self.load_html(html)

                def shutdown(self):
                    pass

            self.html_editor = Preview(self.journal)
            self.html_editor.connect("button-press-event",
                                     self.on_browser_clicked)
            self.html_editor.connect("decide-policy",
                                     self.on_browser_decide_policy)
            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.set_editable(False)
        elif use_internal_preview and browser_cef.cef:

            class Preview(browser_cef.HtmlView):
                def __init__(self, journal):
                    super().__init__()
                    self.journal = journal
                    self.internal = True

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text,
                                                "xhtml",
                                                use_gtk_theme=True)
                    self.load_html(html)

                def highlight(self, text):
                    pass

            self.html_editor = Preview(self.journal)
            self.html_editor.connect("on-url-clicked",
                                     lambda _, url: self.navigate_to_uri(url))
            self.text_vbox.pack_start(self.html_editor, True, True, 0)
        else:
            self.html_editor = mock.MagicMock()
            self.html_editor.internal = False
            preview_button = self.builder.get_object("preview_button")
            preview_button.set_label(_("Preview in Browser"))

        self.html_editor.hide()

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, "shrink", False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            "on_back_one_day_button_clicked":
            self.on_back_one_day_button_clicked,
            "on_today_button_clicked": self.on_today_button_clicked,
            "on_forward_one_day_button_clicked":
            self.on_forward_one_day_button_clicked,
            "on_preview_button_clicked": self.on_preview_button_clicked,
            "on_edit_button_clicked": self.on_edit_button_clicked,
            "on_main_frame_configure_event":
            self.on_main_frame_configure_event,
            "on_main_frame_window_state_event":
            self.on_main_frame_window_state_event,
            "on_add_new_entry_button_clicked":
            self.on_add_new_entry_button_clicked,
            "on_main_frame_delete_event": self.on_main_frame_delete_event,
            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object("annotations_pane").set_visible(
            self.journal.config.read("showTagsPane"))

    def set_tooltips(self):
        """
        Little work-around:
        Tooltips are not shown for menuitems that have been created with uimanager.
        We have to do it manually.
        """
        groups = self.uimanager.get_action_groups()
        for group in groups:
            actions = group.list_actions()
            for action in actions:
                widgets = action.get_proxies()
                tooltip = action.get_property("tooltip")
                if tooltip:
                    for widget in widgets:
                        widget.set_tooltip_markup(tooltip)

    def set_shortcuts(self):
        """
        This method actually is not responsible for the Ctrl-C etc. actions
        """
        self.accel_group = self.builder.get_object("accelgroup1")
        self.main_frame.add_accel_group(self.accel_group)

        self.main_frame.connect("key-press-event", self._on_key_press_event)

        shortcuts = [
            (self.back_one_day_button, "clicked", "<Ctrl>Page_Up"),
            (self.today_button, "clicked", "<Alt>Home"),
            (self.forward_one_day_button, "clicked", "<Ctrl>Page_Down"),
        ]
        for button, signal, shortcut in shortcuts:
            (keyval, mod) = Gtk.accelerator_parse(shortcut)
            button.add_accelerator(signal, self.accel_group, keyval, mod,
                                   Gtk.AccelFlags.VISIBLE)

    def _on_key_press_event(self, widget, event):
        # Exit fullscreen mode with ESC.
        if event.keyval == Gdk.KEY_Escape and self.is_fullscreen:
            self.toggle_fullscreen()

    # TRAY-ICON / CLOSE --------------------------------------------------------

    def setup_tray_icon(self):
        self.tray_icon = Gtk.StatusIcon()
        self.tray_icon.set_name("RedNotebook")
        visible = self.journal.config.read("closeToTray") == 1
        self.tray_icon.set_visible(visible)
        logging.debug("Tray icon visible: %s" % visible)

        self.tray_icon.set_tooltip_text("RedNotebook")
        icon_file = os.path.join(self.journal.dirs.frame_icon_dir, "rn-32.png")
        self.tray_icon.set_from_file(icon_file)

        self.tray_icon.connect("activate", self.on_tray_icon_activated)
        self.tray_icon.connect("popup-menu", self.on_tray_popup_menu)

    def on_tray_icon_activated(self, tray_icon):
        if self.main_frame.get_property("visible"):
            self.hide()
        else:
            self.show()

    def on_tray_popup_menu(self, _status_icon, button, activate_time):
        """
        Called when the user right-clicks the tray icon
        """

        tray_menu_xml = """
        <ui>
        <popup action="TrayMenu">
            <menuitem action="Show"/>
            <menuitem action="Quit"/>
        </popup>
        </ui>"""

        # Create an ActionGroup
        actiongroup = Gtk.ActionGroup("TrayActionGroup")

        # Create actions
        actiongroup.add_actions([
            (
                "Show",
                Gtk.STOCK_MEDIA_PLAY,
                _("Show RedNotebook"),
                None,
                None,
                lambda widget: self.show(),
            ),
            ("Quit", Gtk.STOCK_QUIT, None, None, None, self.on_quit_activate),
        ])

        # Add the actiongroup to the uimanager
        self.uimanager.insert_action_group(actiongroup, 0)

        # Add a UI description
        self.uimanager.add_ui_from_string(tray_menu_xml)

        # Create a Menu
        menu = self.uimanager.get_widget("/TrayMenu")

        menu.popup(None, None, None, None, button, activate_time)

    def show(self):
        self.main_frame.show()
        self.load_values_from_config()

    def hide(self):
        self.add_values_to_config()
        self.journal.save_to_disk()
        self.main_frame.hide()

    def on_main_frame_delete_event(self, widget, event):
        """
        Exit if not close_to_tray
        """
        logging.debug("Main frame destroyed")

        if self.journal.config.read("closeToTray"):
            self.hide()
        else:
            self.html_editor.shutdown()
            self.journal.exit()

        # We never call the default handler. Otherwise, the window would be
        # destroyed, but we might no actually want to exit.
        return True

    def on_quit_activate(self, widget):
        """
        User selected quit from the menu -> exit unconditionally
        """
        self.journal.exit()

    # -------------------------------------------------------- TRAY-ICON / CLOSE

    def setup_stats_dialog(self):
        self.stats_dialog = self.builder.get_object("stats_dialog")
        self.stats_dialog.set_transient_for(self.main_frame)
        overall_box = self.builder.get_object("overall_box")
        day_box = self.builder.get_object("day_stats_box")
        columns = [("1", str), ("2", str)]
        overall_list = CustomListView(columns)
        day_list = CustomListView(columns)
        overall_box.pack_start(overall_list, True, True, 0)
        day_box.pack_start(day_list, True, True, 0)
        self.stats_dialog.overall_list = overall_list
        self.stats_dialog.day_list = day_list
        for list in [overall_list, day_list]:
            list.set_headers_visible(False)

    # MODE-SWITCHING -----------------------------------------------------------

    def change_mode(self, preview):
        edit_scroll = self.builder.get_object("text_scrolledwindow")
        edit_button = self.builder.get_object("edit_button")
        preview_button = self.builder.get_object("preview_button")

        size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
        size_group.add_widget(edit_button)
        size_group.add_widget(preview_button)

        if preview:
            # Enter preview mode
            edit_scroll.hide()
            self.html_editor.show()

            edit_button.show()
            preview_button.hide()

            self.disable_undo_redo_buttons()
        else:
            # Enter edit mode
            edit_scroll.show()
            self.html_editor.hide()

            preview_button.show()
            edit_button.hide()

            self.update_undo_redo_buttons()

        # Interacting with the CEF browser makes the main window inactive, so
        # we make it active again.
        self.main_frame.present()

        self.template_manager.set_template_menu_sensitive(not preview)
        self.insert_actiongroup.set_sensitive(not preview)
        self.format_actiongroup.set_sensitive(not preview)
        self.insert_button.set_sensitive(not preview)
        self.format_button.set_sensitive(not preview)
        for action in ["Cut", "Paste"]:
            self.uimanager.get_widget("/MainMenuBar/Edit/%s" %
                                      action).set_sensitive(not preview)

        self.preview_mode = preview

    def on_edit_button_clicked(self, button):
        # The day's text is already in the editor.
        self.change_mode(preview=False)
        # Select (not only highlight) previously selected text by giving focus
        # to the day editor.
        GObject.idle_add(self.day_text_field.day_text_view.grab_focus)

    def on_preview_button_clicked(self, button):
        self.journal.save_old_day()
        if self.html_editor.internal:
            self.html_editor.show_day(self.day)
            self.change_mode(preview=True)
        else:
            date_format = self.journal.config.read("exportDateFormat")
            date_string = dates.format_date(date_format, self.day.date)
            markup_string = markup.get_markup_for_day(self.day, "xhtml")
            html = self.journal.convert(
                markup_string,
                "xhtml",
                headers=[date_string + " - RedNotebook", "", ""],
                options={"toc": 0},
            )
            utils.show_html_in_browser(
                html, os.path.join(self.journal.dirs.temp_dir, "day.html"))

    def on_browser_clicked(self, webview, event):
        if event.type == Gdk.EventType._2BUTTON_PRESS:
            # Double-click -> Change to edit mode.
            self.change_mode(preview=False)
            # Stop processing this event.
            return True
        elif event.button == 3:
            # Right-click -> don't show context menu.
            return True

    # ----------------------------------------------------------- MODE-SWITCHING

    def setup_search(self):
        always_show_results = not browser.WebKit2
        self.search_tree_view = search.SearchTreeView(self,
                                                      always_show_results)
        self.search_tree_view.show()
        self.search_scroll = Gtk.ScrolledWindow()
        if always_show_results:
            self.search_scroll.show()
        self.search_scroll.add(self.search_tree_view)
        self.search_box = search.SearchComboBox(Gtk.ComboBox.new_with_entry(),
                                                self)
        self.search_box.combo_box.show()
        search_container = self.builder.get_object("search_container")
        search_container.pack_start(self.search_box.combo_box, False, False, 0)
        search_container.pack_start(self.search_scroll, True, True, 0)

    def setup_clouds(self):
        if browser.WebKit2:
            from rednotebook.gui import clouds

            self.cloud = clouds.Cloud(self.journal)
            self.builder.get_object("search_container").pack_end(
                self.cloud, True, True, 0)
        else:
            self.cloud = mock.MagicMock()

    def on_main_frame_configure_event(self, widget, event):
        """
        Is called when the frame size is changed. Unfortunately this is
        the way to go as asking for frame.get_size() at program termination
        gives strange results.
        """
        main_frame_width, main_frame_height = self.main_frame.get_size()
        self.journal.config["mainFrameWidth"] = main_frame_width
        self.journal.config["mainFrameHeight"] = main_frame_height

    def on_main_frame_window_state_event(self, widget, event):
        """
        The "window-state-event" signal is emitted when window state
        of widget changes. For example, for a toplevel window this
        event is signaled when the window is iconified, deiconified,
        minimized, maximized, made sticky, made not sticky, shaded or
        unshaded.
        """
        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
            maximized = bool(event.new_window_state
                             & Gdk.WindowState.MAXIMIZED)
            self.journal.config["mainFrameMaximized"] = int(maximized)

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.main_frame.unfullscreen()
            self.is_fullscreen = False
        else:
            self.main_frame.fullscreen()
            self.is_fullscreen = True

    def on_back_one_day_button_clicked(self, widget):
        self.journal.go_to_prev_day()

    def on_today_button_clicked(self, widget):
        actual_date = datetime.date.today()
        self.journal.change_date(actual_date)

    def on_forward_one_day_button_clicked(self, widget):
        self.journal.go_to_next_day()

    def on_browser_decide_policy(self, webview, decision, decision_type):
        """
        We want to load files and links externally.
        """
        if decision_type == browser.WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
            action = decision.get_navigation_action()
            if action.is_user_gesture():
                uri = action.get_request().get_uri()
                self.navigate_to_uri(uri)
                decision.ignore()

        # Stop processing this event.
        return True

    def navigate_to_uri(self, uri):
        logging.info('Navigating to URI "%s"' % uri)
        if urls.is_entry_reference_uri(uri):
            self.navigate_to_referenced_entry(uri)
        else:
            urls.open_url(uri)

    def navigate_to_referenced_entry(self, entry_reference_uri):
        entry_reference_uri = urllib.parse.urlparse(entry_reference_uri)
        date = dates.get_date_from_date_string(entry_reference_uri.fragment)
        self.journal.change_date(date)

    def get_new_journal_dir(self, title, message):
        dir_chooser = self.builder.get_object("dir_chooser")
        dir_chooser.set_transient_for(self.main_frame)
        label = self.builder.get_object("dir_chooser_label")

        label.set_markup("<b>" + message + "</b>")
        dir_chooser.set_current_folder(
            os.path.dirname(self.journal.dirs.data_dir))

        response = dir_chooser.run()
        # Retrieve the dir now, because it will be cleared by the call to hide().
        new_dir = dir_chooser.get_filename()
        dir_chooser.hide()

        if response == Gtk.ResponseType.OK:
            if new_dir is None:
                self.journal.show_message(_("No directory selected."),
                                          error=True)
                return None
            return new_dir
        return None

    def show_save_error_dialog(self, exit_imminent):
        dialog = self.builder.get_object("save_error_dialog")
        dialog.set_transient_for(self.main_frame)

        exit_without_save_button = self.builder.get_object(
            "exit_without_save_button")
        if exit_imminent:
            exit_without_save_button.show()
        else:
            exit_without_save_button.hide()

        answer = dialog.run()
        dialog.hide()

        if answer == Gtk.ResponseType.OK:
            # Even if the user aborts the Save-As dialog, we don't want to exit.
            self.journal.is_allowed_to_exit = False
            # Let the user select a new directory. Nothing has been saved yet.
            self.menubar_manager.on_save_as_menu_item_activate(None)
        elif answer == Gtk.ResponseType.CANCEL and exit_imminent:
            self.journal.is_allowed_to_exit = False
        # Do nothing if user wants to exit without saving

    def add_values_to_config(self):
        config = self.journal.config

        left_div = self.builder.get_object("main_pane").get_position()
        config["leftDividerPosition"] = left_div

        right_div = self.edit_pane.get_position()
        config["rightDividerPosition"] = right_div

        # Remember if window was maximized in separate method

        # Remember window position
        config["mainFrameX"], config[
            "mainFrameY"] = self.main_frame.get_position()

    def load_values_from_config(self):
        config = self.journal.config
        main_frame_width = config.read("mainFrameWidth")
        main_frame_height = config.read("mainFrameHeight")

        screen_width = Gdk.Screen.width()
        screen_height = Gdk.Screen.height()

        main_frame_width = min(main_frame_width, screen_width)
        main_frame_height = min(main_frame_height, screen_height)

        self.main_frame.resize(main_frame_width, main_frame_height)

        if config.read("mainFrameMaximized"):
            self.main_frame.maximize()
        else:
            # If window is not maximized, restore last position
            x = config.read("mainFrameX")
            y = config.read("mainFrameY")
            try:
                x, y = int(x), int(y)
                # Set to 0 if value is below 0
                if 0 <= x <= screen_width and 0 <= y <= screen_height:
                    self.main_frame.move(x, y)
                else:
                    self.main_frame.set_position(Gtk.WindowPosition.CENTER)
            except (ValueError, TypeError):
                # Values have not been set or are not valid integers
                self.main_frame.set_position(Gtk.WindowPosition.CENTER)

        self.builder.get_object("main_pane").set_position(
            config.read("leftDividerPosition"))
        # By default do not show tags pane.
        self.edit_pane.set_position(
            config.read("rightDividerPosition", main_frame_width))

        self.set_font(config.read("mainFont", editor.DEFAULT_FONT))

    def set_font(self, font_name):
        self.day_text_field.set_font(font_name)
        self.html_editor.set_font_size(
            Pango.FontDescription(font_name).get_size() / Pango.SCALE)

    def setup_template_menu(self):
        def update_menu(button):
            self.template_button.set_menu(self.template_manager.get_menu())

        self.template_button = customwidgets.ToolbarMenuButton(
            Gtk.STOCK_PASTE, self.template_manager.get_menu())
        self.template_button.set_label(_("Template"))
        self.template_button.connect("clicked", update_menu)
        self.template_button.set_tooltip_text(
            _("Insert this weekday's template. "
              "Click the arrow on the right for more options"))
        self.builder.get_object("edit_toolbar").insert(self.template_button, 2)

    def on_add_new_entry_button_clicked(self, widget):
        self.categories_tree_view._on_add_entry_clicked(None)

    def set_date(self, new_month, new_date, day):
        """
        Notes: When switching days in edit mode almost all processing
        time is used for highlighting the markup (searching regexes).
        """
        self.day = day
        self.categories_tree_view.clear()

        self.calendar.set_date(new_date)
        self.calendar.set_month(new_month)

        # Regardless of the mode, we always keep the editor updated, to be able
        # to always save the day.
        self.day_text_field.show_day(day)

        # Only switch mode automatically if set in preferences.
        if self.journal.config.read(
                "autoSwitchMode") and self.html_editor.internal:
            if day.has_text and not self.preview_mode:
                self.change_mode(preview=True)
            elif not day.has_text and self.preview_mode:
                self.change_mode(preview=False)

        if self.preview_mode:
            # Converting markup to html takes time, so only do it when necessary
            self.html_editor.show_day(day)

        self.categories_tree_view.set_day_content(day)

    def get_day_text(self):
        return self.day_text_field.get_text()

    def highlight_text(self, search_text):
        self.html_editor.highlight(search_text)
        self.day_text_field.highlight(search_text)

    def show_message(self, title, msg, msg_type):
        if msg_type == Gtk.MessageType.ERROR:
            self.infobar.show_message(title, msg, msg_type)
        else:
            self.statusbar.show_message(title, msg, msg_type)

    def disable_undo_redo_buttons(self):
        self.undo_action.set_sensitive(False)
        self.redo_action.set_sensitive(False)

    def update_undo_redo_buttons(self, _gobject=None):
        """Enable/disable undo+redo actions according to the current text buffer

        The _gobject parameter is unused, but it's necessary for the method
        to be connected to a GObject signal.
        """
        can_undo = self.day_text_field.day_text_buffer.can_undo()
        self.undo_action.set_sensitive(can_undo)
        can_redo = self.day_text_field.day_text_buffer.can_redo()
        self.redo_action.set_sensitive(can_redo)
Example #5
0
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir, 'main_window.glade')
        self.builder = Gtk.Builder()
        if filesystem.IS_WIN:
            import xml.etree.ElementTree as ET
            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if 'translatable' in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding='unicode')
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain('rednotebook')
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object('main_frame')
        self.main_frame.set_title('RedNotebook')
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, 'rednotebook.svg'))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object('vbox3')
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_redo_manager = undo.UndoRedoManager(self)

        self.calendar = MainCalendar(self.journal, self.builder.get_object('calendar'))
        self.day_text_field = DayEditor(self.builder.get_object('day_text_view'),
                                        self.undo_redo_manager)
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read('spellcheck'))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == 'MainMenuActionGroup':
                for action in actiongroup.list_actions():
                    if action.get_name() == 'CheckSpelling':
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object('statusbar'))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object('categories_tree_view'), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object('back_one_day_button')
        self.today_button = self.builder.get_object('today_button')
        self.forward_one_day_button = self.builder.get_object('forward_one_day_button')

        self.edit_pane = self.builder.get_object('edit_pane')
        self.text_vbox = self.builder.get_object('text_vbox')

        if browser.WebKit2:
            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text, 'xhtml')
                    self.load_html(html)

            self.html_editor = Preview(self.journal)
            self.html_editor.connect('button-press-event', self.on_browser_clicked)
            self.html_editor.connect('decide-policy', self.on_browser_decide_policy)

            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.hide()
            self.html_editor.set_editable(False)
        else:
            self.html_editor = mock.MagicMock()
            preview_button = self.builder.get_object('preview_button')
            preview_button.set_label(_('Preview in Browser'))

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, 'shrink', False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            'on_back_one_day_button_clicked': self.on_back_one_day_button_clicked,
            'on_today_button_clicked': self.on_today_button_clicked,
            'on_forward_one_day_button_clicked': self.on_forward_one_day_button_clicked,

            'on_preview_button_clicked': self.on_preview_button_clicked,
            'on_edit_button_clicked': self.on_edit_button_clicked,

            'on_main_frame_configure_event': self.on_main_frame_configure_event,
            'on_main_frame_window_state_event': self.on_main_frame_window_state_event,

            'on_add_new_entry_button_clicked': self.on_add_new_entry_button_clicked,

            'on_main_frame_delete_event': self.on_main_frame_delete_event,

            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object('annotations_pane').set_visible(
            self.journal.config.read('showTagsPane'))
Example #6
0
class MainWindow:
    '''
    Class that holds the reference to the main glade file and handles
    all actions
    '''
    def __init__(self, journal):

        self.journal = journal

        # Load Glade file.
        # TODO: Remove workaround for Windows once it is no longer needed.
        self.gladefile = os.path.join(filesystem.files_dir, 'main_window.glade')
        self.builder = Gtk.Builder()
        if filesystem.IS_WIN:
            import xml.etree.ElementTree as ET
            tree = ET.parse(self.gladefile)
            for node in tree.iter():
                if 'translatable' in node.attrib:
                    node.text = _(node.text)
            xml_text = ET.tostring(tree.getroot(), encoding='unicode')
            self.builder = Gtk.Builder.new_from_string(xml_text, len(xml_text))
        else:
            self.builder.set_translation_domain('rednotebook')
            self.builder.add_from_file(self.gladefile)

        # Get the main window and set the icon
        self.main_frame = self.builder.get_object('main_frame')
        self.main_frame.set_title('RedNotebook')
        icon = GdkPixbuf.Pixbuf.new_from_file(
            os.path.join(filesystem.frame_icon_dir, 'rednotebook.svg'))
        self.main_frame.set_icon(icon)

        self.is_fullscreen = False

        self.uimanager = Gtk.UIManager()

        # Before fetching the menubar, add all menus and actiongroups.
        # Setup the toolbar items first to avoid warnings for missing actions.
        insert_menu.InsertMenu(self)
        format_menu.FormatMenu(self)
        self.menubar_manager = MainMenuBar(self)
        self.menubar = self.menubar_manager.get_menu_bar()
        main_vbox = self.builder.get_object('vbox3')
        main_vbox.pack_start(self.menubar, False, False, 0)
        main_vbox.reorder_child(self.menubar, 0)

        self.undo_redo_manager = undo.UndoRedoManager(self)

        self.calendar = MainCalendar(self.journal, self.builder.get_object('calendar'))
        self.day_text_field = DayEditor(self.builder.get_object('day_text_view'),
                                        self.undo_redo_manager)
        self.day_text_field.day_text_view.grab_focus()
        can_spell_check = self.day_text_field.can_spell_check()
        spell_check_enabled = bool(self.journal.config.read('spellcheck'))
        for actiongroup in self.menubar_manager.uimanager.get_action_groups():
            if actiongroup.get_name() == 'MainMenuActionGroup':
                for action in actiongroup.list_actions():
                    if action.get_name() == 'CheckSpelling':
                        action.set_sensitive(can_spell_check)
                        action.set_active(spell_check_enabled and can_spell_check)
        self.day_text_field.enable_spell_check(spell_check_enabled)

        self.statusbar = Statusbar(self.builder.get_object('statusbar'))

        self.new_entry_dialog = NewEntryDialog(self)

        self.categories_tree_view = categories.CategoriesTreeView(
            self.builder.get_object('categories_tree_view'), self)

        self.new_entry_dialog.categories_tree_view = self.categories_tree_view

        self.back_one_day_button = self.builder.get_object('back_one_day_button')
        self.today_button = self.builder.get_object('today_button')
        self.forward_one_day_button = self.builder.get_object('forward_one_day_button')

        self.edit_pane = self.builder.get_object('edit_pane')
        self.text_vbox = self.builder.get_object('text_vbox')

        if browser.WebKit2:
            class Preview(browser.HtmlView):
                def __init__(self, journal):
                    browser.HtmlView.__init__(self)
                    self.journal = journal

                def show_day(self, new_day):
                    html = self.journal.convert(new_day.text, 'xhtml')
                    self.load_html(html)

            self.html_editor = Preview(self.journal)
            self.html_editor.connect('button-press-event', self.on_browser_clicked)
            self.html_editor.connect('decide-policy', self.on_browser_decide_policy)

            self.text_vbox.pack_start(self.html_editor, True, True, 0)
            self.html_editor.hide()
            self.html_editor.set_editable(False)
        else:
            self.html_editor = mock.MagicMock()
            preview_button = self.builder.get_object('preview_button')
            preview_button.set_label(_('Preview in Browser'))

        self.preview_mode = False

        # Let the edit_paned respect its childs size requests
        self.edit_pane.child_set_property(self.text_vbox, 'shrink', False)

        # Add InfoBar.
        self.infobar = customwidgets.Info()
        self.text_vbox.pack_start(self.infobar, False, False, 0)
        self.text_vbox.reorder_child(self.infobar, 1)

        # Add TemplateBar.
        self.template_bar = customwidgets.TemplateBar()
        self.text_vbox.pack_start(self.template_bar, False, False, 0)
        self.text_vbox.reorder_child(self.template_bar, 1)
        self.template_bar.hide()

        self.load_values_from_config()

        self.main_frame.show()

        self.options_manager = OptionsManager(self)
        self.export_assistant = ExportAssistant(self.journal)
        self.export_assistant.set_transient_for(self.main_frame)

        self.setup_clouds()
        self.setup_search()

        # Create an event->method dictionary and connect it to the widgets
        dic = {
            'on_back_one_day_button_clicked': self.on_back_one_day_button_clicked,
            'on_today_button_clicked': self.on_today_button_clicked,
            'on_forward_one_day_button_clicked': self.on_forward_one_day_button_clicked,

            'on_preview_button_clicked': self.on_preview_button_clicked,
            'on_edit_button_clicked': self.on_edit_button_clicked,

            'on_main_frame_configure_event': self.on_main_frame_configure_event,
            'on_main_frame_window_state_event': self.on_main_frame_window_state_event,

            'on_add_new_entry_button_clicked': self.on_add_new_entry_button_clicked,

            'on_main_frame_delete_event': self.on_main_frame_delete_event,

            # connect_signals can only be called once, it seems
            # Otherwise RuntimeWarnings are raised: RuntimeWarning: missing handler '...'
        }
        self.builder.connect_signals(dic)

        self.set_shortcuts()
        self.setup_stats_dialog()

        self.template_manager = templates.TemplateManager(self)
        self.template_manager.make_empty_template_files()
        self.setup_template_menu()

        self.set_tooltips()
        self.setup_tray_icon()

        # Show/hide the "tags" panel on the right.
        self.builder.get_object('annotations_pane').set_visible(
            self.journal.config.read('showTagsPane'))

    def set_tooltips(self):
        '''
        Little work-around:
        Tooltips are not shown for menuitems that have been created with uimanager.
        We have to do it manually.
        '''
        groups = self.uimanager.get_action_groups()
        for group in groups:
            actions = group.list_actions()
            for action in actions:
                widgets = action.get_proxies()
                tooltip = action.get_property('tooltip')
                if tooltip:
                    for widget in widgets:
                        widget.set_tooltip_markup(tooltip)

    def set_shortcuts(self):
        '''
        This method actually is not responsible for the Ctrl-C etc. actions
        '''
        self.accel_group = self.builder.get_object('accelgroup1')
        self.main_frame.add_accel_group(self.accel_group)

        self.main_frame.connect('key-press-event', self._on_key_press_event)

        shortcuts = [
            (self.back_one_day_button, 'clicked', '<Ctrl>Page_Up'),
            (self.today_button, 'clicked', '<Alt>Home'),
            (self.forward_one_day_button, 'clicked', '<Ctrl>Page_Down'),
        ]
        for button, signal, shortcut in shortcuts:
            (keyval, mod) = Gtk.accelerator_parse(shortcut)
            button.add_accelerator(signal, self.accel_group,
                                   keyval, mod, Gtk.AccelFlags.VISIBLE)

    def _on_key_press_event(self, widget, event):
        # Exit fullscreen mode with ESC.
        if event.keyval == Gdk.KEY_Escape and self.is_fullscreen:
            self.toggle_fullscreen()

    # TRAY-ICON / CLOSE --------------------------------------------------------

    def setup_tray_icon(self):
        self.tray_icon = Gtk.StatusIcon()
        self.tray_icon.set_name('RedNotebook')
        visible = (self.journal.config.read('closeToTray') == 1)
        self.tray_icon.set_visible(visible)
        logging.debug('Tray icon visible: %s' % visible)

        self.tray_icon.set_tooltip_text('RedNotebook')
        # TODO: Try using the svg here as well.
        icon_file = os.path.join(self.journal.dirs.frame_icon_dir, 'rn-32.png')
        self.tray_icon.set_from_file(icon_file)

        self.tray_icon.connect('activate', self.on_tray_icon_activated)
        self.tray_icon.connect('popup-menu', self.on_tray_popup_menu)

    def on_tray_icon_activated(self, tray_icon):
        if self.main_frame.get_property('visible'):
            self.hide()
        else:
            self.show()

    def on_tray_popup_menu(self, status_icon, button, activate_time):
        '''
        Called when the user right-clicks the tray icon
        '''

        tray_menu_xml = '''
        <ui>
        <popup action="TrayMenu">
            <menuitem action="Show"/>
            <menuitem action="Quit"/>
        </popup>
        </ui>'''

        # Create an ActionGroup
        actiongroup = Gtk.ActionGroup('TrayActionGroup')

        # Create actions
        actiongroup.add_actions([
            ('Show', Gtk.STOCK_MEDIA_PLAY, _('Show RedNotebook'),
                None, None, lambda widget: self.show()),
            ('Quit', Gtk.STOCK_QUIT, None, None, None, self.on_quit_activate),
        ])

        # Add the actiongroup to the uimanager
        self.uimanager.insert_action_group(actiongroup, 0)

        # Add a UI description
        self.uimanager.add_ui_from_string(tray_menu_xml)

        # Create a Menu
        menu = self.uimanager.get_widget('/TrayMenu')

        menu.popup(
            None, None, Gtk.status_icon_position_menu, button,
            activate_time, status_icon)

    def show(self):
        self.main_frame.show()
        self.load_values_from_config()

    def hide(self):
        self.add_values_to_config()
        self.journal.save_to_disk()
        self.main_frame.hide()

    def on_main_frame_delete_event(self, widget, event):
        '''
        Exit if not close_to_tray
        '''
        logging.debug('Main frame destroyed')

        if self.journal.config.read('closeToTray'):
            self.hide()
        else:
            self.journal.exit()

        # We never call the default handler. Otherwise, the window would be
        # destroyed, but we might no actually want to exit.
        return True

    def on_quit_activate(self, widget):
        '''
        User selected quit from the menu -> exit unconditionally
        '''
        self.journal.exit()

    # -------------------------------------------------------- TRAY-ICON / CLOSE

    def setup_stats_dialog(self):
        self.stats_dialog = self.builder.get_object('stats_dialog')
        self.stats_dialog.set_transient_for(self.main_frame)
        overall_box = self.builder.get_object('overall_box')
        day_box = self.builder.get_object('day_stats_box')
        columns = [('1', str), ('2', str)]
        overall_list = CustomListView(columns)
        day_list = CustomListView(columns)
        overall_box.pack_start(overall_list, True, True, 0)
        day_box.pack_start(day_list, True, True, 0)
        setattr(self.stats_dialog, 'overall_list', overall_list)
        setattr(self.stats_dialog, 'day_list', day_list)
        for list in [overall_list, day_list]:
            list.set_headers_visible(False)

    # MODE-SWITCHING -----------------------------------------------------------

    def change_mode(self, preview):
        edit_scroll = self.builder.get_object('text_scrolledwindow')
        edit_button = self.builder.get_object('edit_button')
        preview_button = self.builder.get_object('preview_button')

        size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
        size_group.add_widget(edit_button)
        size_group.add_widget(preview_button)

        if preview:
            # Enter preview mode
            edit_scroll.hide()
            self.html_editor.show()

            edit_button.show()
            preview_button.hide()

            self.undo_redo_manager.disable_buttons()
        else:
            # Enter edit mode
            edit_scroll.show()
            self.html_editor.hide()

            preview_button.show()
            edit_button.hide()

            self.undo_redo_manager.update_buttons()

        self.template_manager.set_template_menu_sensitive(not preview)
        self.insert_actiongroup.set_sensitive(not preview)
        self.format_actiongroup.set_sensitive(not preview)
        self.insert_button.set_sensitive(not preview)
        self.format_button.set_sensitive(not preview)
        for action in ['Cut', 'Paste']:
            self.uimanager.get_widget('/MainMenuBar/Edit/%s' % action).set_sensitive(not preview)

        self.preview_mode = preview

    def on_edit_button_clicked(self, button):
        # The day's text is already in the editor.
        self.change_mode(preview=False)
        # Select (not only highlight) previously selected text by giving focus
        # to the day editor.
        GObject.idle_add(self.day_text_field.day_text_view.grab_focus)

    def on_preview_button_clicked(self, button):
        self.journal.save_old_day()
        if browser.WebKit2:
            self.html_editor.show_day(self.day)
            self.change_mode(preview=True)
        else:
            date_format = self.journal.config.read('exportDateFormat')
            date_string = dates.format_date(date_format, self.day.date)
            markup_string = markup.get_markup_for_day(self.day)
            html = self.journal.convert(
                markup_string, 'xhtml',
                headers=[date_string + ' - RedNotebook', '', ''],
                options={'toc': 0})
            utils.show_html_in_browser(
                html, os.path.join(self.journal.dirs.temp_dir, 'day.html'))

    def on_browser_clicked(self, webview, event):
        if event.type == Gdk.EventType._2BUTTON_PRESS:
            # Double-click -> Change to edit mode.
            self.change_mode(preview=False)
            # Stop processing this event.
            return True
        elif event.button == 3:
            # Right-click -> don't show context menu.
            return True

    # ----------------------------------------------------------- MODE-SWITCHING

    def setup_search(self):
        always_show_results = not browser.WebKit2
        self.search_tree_view = search.SearchTreeView(self, always_show_results)
        self.search_tree_view.show()
        self.search_scroll = Gtk.ScrolledWindow()
        if always_show_results:
            self.search_scroll.show()
        self.search_scroll.add(self.search_tree_view)
        self.search_box = search.SearchComboBox(Gtk.ComboBox.new_with_entry(), self)
        self.search_box.combo_box.show()
        search_container = self.builder.get_object('search_container')
        search_container.pack_start(self.search_box.combo_box, False, False, 0)
        search_container.pack_start(self.search_scroll, True, True, 0)

    def setup_clouds(self):
        if browser.WebKit2:
            from rednotebook.gui import clouds
            self.cloud = clouds.Cloud(self.journal)
            self.builder.get_object('search_container').pack_end(self.cloud, True, True, 0)
        else:
            self.cloud = mock.MagicMock()

    def on_main_frame_configure_event(self, widget, event):
        '''
        Is called when the frame size is changed. Unfortunately this is
        the way to go as asking for frame.get_size() at program termination
        gives strange results.
        '''
        main_frame_width, main_frame_height = self.main_frame.get_size()
        self.journal.config['mainFrameWidth'] = main_frame_width
        self.journal.config['mainFrameHeight'] = main_frame_height

    def on_main_frame_window_state_event(self, widget, event):
        '''
        The "window-state-event" signal is emitted when window state
        of widget changes. For example, for a toplevel window this
        event is signaled when the window is iconified, deiconified,
        minimized, maximized, made sticky, made not sticky, shaded or
        unshaded.
        '''
        if event.changed_mask & Gdk.WindowState.MAXIMIZED:
            maximized = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
            self.journal.config['mainFrameMaximized'] = int(maximized)

    def toggle_fullscreen(self):
        if self.is_fullscreen:
            self.main_frame.unfullscreen()
            self.is_fullscreen = False
        else:
            self.main_frame.fullscreen()
            self.is_fullscreen = True

    def on_back_one_day_button_clicked(self, widget):
        self.journal.go_to_prev_day()

    def on_today_button_clicked(self, widget):
        actual_date = datetime.date.today()
        self.journal.change_date(actual_date)

    def on_forward_one_day_button_clicked(self, widget):
        self.journal.go_to_next_day()

    def on_browser_decide_policy(self, webview, decision, decision_type):
        '''
        We want to load files and links externally.
        '''
        if decision_type == browser.WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
            action = decision.get_navigation_action()
            if action.is_user_gesture():
                uri = action.get_request().get_uri()
                logging.info('Clicked URI "%s"' % uri)
                filesystem.open_url(uri)

                decision.ignore()

        # Stop processing this event.
        return True

    def get_new_journal_dir(self, title, message):
        dir_chooser = self.builder.get_object('dir_chooser')
        dir_chooser.set_transient_for(self.main_frame)
        label = self.builder.get_object('dir_chooser_label')

        label.set_markup('<b>' + message + '</b>')
        dir_chooser.set_current_folder(os.path.dirname(self.journal.dirs.data_dir))

        response = dir_chooser.run()
        # Retrieve the dir now, because it will be cleared by the call to hide().
        new_dir = dir_chooser.get_filename()
        dir_chooser.hide()

        if response == Gtk.ResponseType.OK:
            if new_dir is None:
                self.journal.show_message(_('No directory selected.'), error=True)
                return None
            return new_dir
        return None

    def show_save_error_dialog(self, exit_imminent):
        dialog = self.builder.get_object('save_error_dialog')
        dialog.set_transient_for(self.main_frame)

        exit_without_save_button = self.builder.get_object('exit_without_save_button')
        if exit_imminent:
            exit_without_save_button.show()
        else:
            exit_without_save_button.hide()

        answer = dialog.run()
        dialog.hide()

        if answer == Gtk.ResponseType.OK:
            # Even if the user aborts the Save-As dialog, we don't want to exit.
            self.journal.is_allowed_to_exit = False
            # Let the user select a new directory. Nothing has been saved yet.
            self.menubar_manager.on_save_as_menu_item_activate(None)
        elif answer == Gtk.ResponseType.CANCEL and exit_imminent:
            self.journal.is_allowed_to_exit = False
        # Do nothing if user wants to exit without saving

    def add_values_to_config(self):
        config = self.journal.config

        left_div = self.builder.get_object('main_pane').get_position()
        config['leftDividerPosition'] = left_div

        right_div = self.edit_pane.get_position()
        config['rightDividerPosition'] = right_div

        # Remember if window was maximized in separate method

        # Remember window position
        config['mainFrameX'], config['mainFrameY'] = self.main_frame.get_position()

    def load_values_from_config(self):
        config = self.journal.config
        main_frame_width = config.read('mainFrameWidth')
        main_frame_height = config.read('mainFrameHeight')

        screen_width = Gdk.Screen.width()
        screen_height = Gdk.Screen.height()

        main_frame_width = min(main_frame_width, screen_width)
        main_frame_height = min(main_frame_height, screen_height)

        self.main_frame.resize(main_frame_width, main_frame_height)

        if config.read('mainFrameMaximized'):
            self.main_frame.maximize()
        else:
            # If window is not maximized, restore last position
            x = config.read('mainFrameX')
            y = config.read('mainFrameY')
            try:
                x, y = int(x), int(y)
                # Set to 0 if value is below 0
                if 0 <= x <= screen_width and 0 <= y <= screen_height:
                    self.main_frame.move(x, y)
                else:
                    self.main_frame.set_position(Gtk.WindowPosition.CENTER)
            except (ValueError, TypeError):
                # Values have not been set or are not valid integers
                self.main_frame.set_position(Gtk.WindowPosition.CENTER)

        self.builder.get_object('main_pane').set_position(config.read('leftDividerPosition'))
        # By default do not show tags pane.
        self.edit_pane.set_position(config.read('rightDividerPosition', main_frame_width))

        self.set_font(config.read('mainFont', editor.DEFAULT_FONT))

    def set_font(self, font_name):
        self.day_text_field.set_font(font_name)
        self.html_editor.set_font_size(Pango.FontDescription(font_name).get_size() / Pango.SCALE)

    def setup_template_menu(self):
        def update_menu(button):
            self.template_button.set_menu(self.template_manager.get_menu())

        self.template_button = customwidgets.ToolbarMenuButton(
            Gtk.STOCK_PASTE, self.template_manager.get_menu())
        self.template_button.set_label(_('Template'))
        self.template_button.connect('clicked', update_menu)
        self.template_button.set_tooltip_text(_(
            "Insert this weekday's template. "
            "Click the arrow on the right for more options"))
        self.builder.get_object('edit_toolbar').insert(self.template_button, 2)

    def on_add_new_entry_button_clicked(self, widget):
        self.categories_tree_view._on_add_entry_clicked(None)

    def set_date(self, new_month, new_date, day):
        """
        Notes: When switching days in edit mode almost all processing
        time is used for highlighting the markup (searching regexes).
        """
        self.day = day
        self.categories_tree_view.clear()

        self.calendar.set_date(new_date)
        self.calendar.set_month(new_month)

        # Regardless of the mode, we always keep the editor updated, to be able
        # to always save the day.
        self.day_text_field.show_day(day)

        # Only switch mode automatically if set in preferences.
        if self.journal.config.read('autoSwitchMode') and browser.WebKit2:
            if day.has_text and not self.preview_mode:
                self.change_mode(preview=True)
            elif not day.has_text and self.preview_mode:
                self.change_mode(preview=False)

        if self.preview_mode:
            # Converting markup to html takes time, so only do it when necessary
            self.html_editor.show_day(day)

        self.categories_tree_view.set_day_content(day)
        self.undo_redo_manager.set_stack(new_date)

    def get_day_text(self):
        return self.day_text_field.get_text()

    def highlight_text(self, search_text):
        self.html_editor.highlight(search_text)
        self.day_text_field.highlight(search_text)

    def show_message(self, title, msg, msg_type):
        if msg_type == Gtk.MessageType.ERROR:
            self.infobar.show_message(title, msg, msg_type)
        else:
            self.statusbar.show_message(title, msg, msg_type)