def __init__(self, app): """Set up the main window""" Gtk.ApplicationWindow.__init__(self, application=Gio.Application.get_default(), title="Uberwriter") self.builder = get_builder('UberwriterWindow') self.add(self.builder.get_object("FullscreenOverlay")) self.set_default_size(850, 500) # preferences self.settings = Settings.new() self.set_name('UberwriterWindow') # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FsHeaderbar(self.builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.focusmode = False self.word_count = self.builder.get_object('word_count') self.char_count = self.builder.get_object('char_count') # Setup status bar hide after 3 seconds self.status_bar = self.builder.get_object('status_bar_box') self.statusbar_revealer = self.builder.get_object('status_bar_revealer') self.status_bar.get_style_context().add_class('status_bar_box') self.status_bar_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) # Setup text editor self.text_editor = TextEditor() self.text_editor.set_name('UberwriterEditor') self.get_style_context().add_class('uberwriter_window') base_leftmargin = 100 self.text_editor.set_left_margin(base_leftmargin) self.text_editor.set_left_margin(40) self.text_editor.set_top_margin(80) self.text_editor.props.width_request = 600 self.text_editor.props.halign = Gtk.Align.CENTER self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1')) self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.text_editor.connect('focus-out-event', self.focus_out) self.text_editor.get_style_context().connect('changed', self.style_changed) self.text_editor.set_top_margin(80) self.text_editor.set_bottom_margin(16) self.text_editor.set_pixels_above_lines(4) self.text_editor.set_pixels_below_lines(4) self.text_editor.set_pixels_inside_wrap(8) tab_array = Pango.TabArray.new(1, True) tab_array.set_tab(0, Pango.TabAlign.LEFT, 20) self.text_editor.set_tabs(tab_array) self.text_editor.show() self.text_editor.grab_focus() # Setup preview webview self.preview_webview = None self.editor_alignment = self.builder.get_object('editor_alignment') self.scrolled_window = self.builder.get_object('editor_scrolledwindow') self.scrolled_window.props.width_request = 600 self.scrolled_window.add(self.text_editor) self.alignment_padding = 40 self.editor_viewport = self.builder.get_object('editor_viewport') # some people seems to have performance problems with the overlay. # Let them disable it if self.settings.get_value("gradient-overlay"): self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient) self.smooth_scroll_starttime = 0 self.smooth_scroll_endtime = 0 self.smooth_scroll_acttarget = 0 self.smooth_scroll_data = { 'target_pos': -1, 'source_pos': -1, 'duration': 0 } self.smooth_scroll_tickid = -1 self.text_buffer = self.text_editor.get_buffer() self.text_buffer.set_text('') # Init Window height for top/bottom padding self.window_height = self.get_size()[1] self.text_change_event = self.text_buffer.connect( 'changed', self.text_changed) # Init file name with None self.set_filename() # Markup and Shortcuts for the TextBuffer self.markup_buffer = MarkupBuffer( self, self.text_buffer, base_leftmargin) self.markup_buffer.markup_buffer() # Set current theme self.apply_current_theme() # Scrolling -> Dark or not? self.textchange = False self.scroll_count = 0 self.timestamp_last_mouse_motion = 0 self.text_buffer.connect_after('mark-set', self.mark_set) # Drag and drop # self.TextEditor.drag_dest_unset() # self.TextEditor.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) self.target_list = Gtk.TargetList.new([]) self.target_list.add_uri_targets(1) self.target_list.add_text_targets(2) self.text_editor.drag_dest_set_target_list(self.target_list) self.text_editor.connect_after( 'drag-data-received', self.on_drag_data_received) def on_drop(_widget, *_args): print("drop") self.text_editor.connect('drag-drop', on_drop) self.text_buffer.connect('paste-done', self.paste_done) # self.connect('key-press-event', self.alt_mod) # Events for Typewriter mode # Setting up inline preview self.inline_preview = InlinePreview( self.text_editor, self.text_buffer) # Vertical scrolling self.vadjustment = self.scrolled_window.get_vadjustment() self.vadjustment.connect('value-changed', self.scrolled) # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = self.builder.get_object("main_pained") self.sidebar_box = self.builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self) # Window resize self.window_resize(self) self.connect("configure-event", self.window_resize) self.connect("delete-event", self.on_delete_called)
def __init__(self, app): """Set up the main window""" super().__init__(application=Gio.Application.get_default(), title="Uberwriter") self.get_style_context().add_class('uberwriter-window') # Set UI builder = Gtk.Builder() builder.add_from_resource("/de/wolfvollprecht/UberWriter/ui/Window.ui") root = builder.get_object("FullscreenOverlay") self.connect("delete-event", self.on_delete_called) self.add(root) self.set_default_size(1000, 600) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) self.scrolled_window = builder.get_object('editor_scrolledwindow') # Setup text editor self.text_view = TextView(self.settings.get_int("characters-per-line")) self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() self.scrolled_window.add(self.text_view) # Setup stats counter self.stats_revealer = builder.get_object('editor_stats_revealer') self.stats_button = builder.get_object('editor_stats_button') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup preview content = builder.get_object('content') editor = builder.get_object('editor') self.preview_handler = PreviewHandler(self, content, editor, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay( self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = builder.get_object("main_paned") self.sidebar_box = builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view, builder)
class Window(Gtk.ApplicationWindow): WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE) def __init__(self, app): """Set up the main window""" Gtk.ApplicationWindow.__init__(self, application=Gio.Application.get_default(), title="Uberwriter") self.builder = get_builder('UberwriterWindow') self.add(self.builder.get_object("FullscreenOverlay")) self.set_default_size(850, 500) # preferences self.settings = Settings.new() self.set_name('UberwriterWindow') # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FsHeaderbar(self.builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.focusmode = False self.word_count = self.builder.get_object('word_count') self.char_count = self.builder.get_object('char_count') # Setup status bar hide after 3 seconds self.status_bar = self.builder.get_object('status_bar_box') self.statusbar_revealer = self.builder.get_object('status_bar_revealer') self.status_bar.get_style_context().add_class('status_bar_box') self.status_bar_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) # Setup text editor self.text_editor = TextEditor() self.text_editor.set_name('UberwriterEditor') self.get_style_context().add_class('uberwriter_window') base_leftmargin = 100 self.text_editor.set_left_margin(base_leftmargin) self.text_editor.set_left_margin(40) self.text_editor.set_top_margin(80) self.text_editor.props.width_request = 600 self.text_editor.props.halign = Gtk.Align.CENTER self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1')) self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.text_editor.connect('focus-out-event', self.focus_out) self.text_editor.get_style_context().connect('changed', self.style_changed) self.text_editor.set_top_margin(80) self.text_editor.set_bottom_margin(16) self.text_editor.set_pixels_above_lines(4) self.text_editor.set_pixels_below_lines(4) self.text_editor.set_pixels_inside_wrap(8) tab_array = Pango.TabArray.new(1, True) tab_array.set_tab(0, Pango.TabAlign.LEFT, 20) self.text_editor.set_tabs(tab_array) self.text_editor.show() self.text_editor.grab_focus() # Setup preview webview self.preview_webview = None self.editor_alignment = self.builder.get_object('editor_alignment') self.scrolled_window = self.builder.get_object('editor_scrolledwindow') self.scrolled_window.props.width_request = 600 self.scrolled_window.add(self.text_editor) self.alignment_padding = 40 self.editor_viewport = self.builder.get_object('editor_viewport') # some people seems to have performance problems with the overlay. # Let them disable it if self.settings.get_value("gradient-overlay"): self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient) self.smooth_scroll_starttime = 0 self.smooth_scroll_endtime = 0 self.smooth_scroll_acttarget = 0 self.smooth_scroll_data = { 'target_pos': -1, 'source_pos': -1, 'duration': 0 } self.smooth_scroll_tickid = -1 self.text_buffer = self.text_editor.get_buffer() self.text_buffer.set_text('') # Init Window height for top/bottom padding self.window_height = self.get_size()[1] self.text_change_event = self.text_buffer.connect( 'changed', self.text_changed) # Init file name with None self.set_filename() # Markup and Shortcuts for the TextBuffer self.markup_buffer = MarkupBuffer( self, self.text_buffer, base_leftmargin) self.markup_buffer.markup_buffer() # Set current theme self.apply_current_theme() # Scrolling -> Dark or not? self.textchange = False self.scroll_count = 0 self.timestamp_last_mouse_motion = 0 self.text_buffer.connect_after('mark-set', self.mark_set) # Drag and drop # self.TextEditor.drag_dest_unset() # self.TextEditor.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) self.target_list = Gtk.TargetList.new([]) self.target_list.add_uri_targets(1) self.target_list.add_text_targets(2) self.text_editor.drag_dest_set_target_list(self.target_list) self.text_editor.connect_after( 'drag-data-received', self.on_drag_data_received) def on_drop(_widget, *_args): print("drop") self.text_editor.connect('drag-drop', on_drop) self.text_buffer.connect('paste-done', self.paste_done) # self.connect('key-press-event', self.alt_mod) # Events for Typewriter mode # Setting up inline preview self.inline_preview = InlinePreview( self.text_editor, self.text_buffer) # Vertical scrolling self.vadjustment = self.scrolled_window.get_vadjustment() self.vadjustment.connect('value-changed', self.scrolled) # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = self.builder.get_object("main_pained") self.sidebar_box = self.builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self) # Window resize self.window_resize(self) self.connect("configure-event", self.window_resize) self.connect("delete-event", self.on_delete_called) __gsignals__ = { 'save-file': (GObject.SIGNAL_ACTION, None, ()), 'open-file': (GObject.SIGNAL_ACTION, None, ()), 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), 'new-file': (GObject.SIGNAL_ACTION, None, ()), 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), 'close-window': (GObject.SIGNAL_ACTION, None, ()) } def apply_current_theme(self): """Adjusts both the window and the CSD for the current theme. """ self.markup_buffer.update_style() # Reload preview if it exists, otherwise redraw contents of window (self) if self.preview_webview: self.show_preview() else: self.queue_draw() def scrolled(self, widget): """if window scrolled + focusmode make font black again""" # if self.focusmode: # if self.textchange == False: # if self.scroll_count >= 4: # self.TextBuffer.apply_tag( # self.MarkupBuffer.blackfont, # self.TextBuffer.get_start_iter(), # self.TextBuffer.get_end_iter()) # else: # self.scroll_count += 1 # else: # self.scroll_count = 0 # self.textchange = False def paste_done(self, *_): self.markup_buffer.markup_buffer(0) def init_typewriter(self): """put the cursor at the center of the screen by setting top and bottom margins to height/2 """ editor_height = self.text_editor.get_allocation().height self.text_editor.props.top_margin = editor_height / 2 self.text_editor.props.bottom_margin = editor_height / 2 def remove_typewriter(self): """set margins to default values """ self.text_editor.props.top_margin = 80 self.text_editor.props.bottom_margin = 16 self.text_change_event = self.text_buffer.connect( 'changed', self.text_changed) def get_text(self): """get text from self.text_buffer """ start_iter = self.text_buffer.get_start_iter() end_iter = self.text_buffer.get_end_iter() return self.text_buffer.get_text(start_iter, end_iter, False) def update_line_and_char_count(self): """it... it updates line and characters count """ if self.status_bar_visible is False: return self.char_count.set_text(str(self.text_buffer.get_char_count())) text = self.get_text() words = re.split(self.WORDCOUNT, text) length = len(words) # Last word a "space" if not words[-1]: length = length - 1 # First word a "space" (happens in focus mode...) if not words[0]: length = length - 1 if length == -1: length = 0 self.word_count.set_text(str(length)) def mark_set(self, _buffer, _location, mark, _data=None): if mark.get_name() in ['insert', 'gtk_drag_target']: self.check_scroll(mark) return True def text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that """ if self.did_change is False: self.did_change = True title = self.get_title() self.set_headerbar_title("* " + title) self.markup_buffer.markup_buffer(1) self.textchange = True self.buffer_modified_for_status_bar = True self.update_line_and_char_count() self.check_scroll(self.text_buffer.get_insert()) def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border Arguments: state {almost bool} -- The desired fullscreen state of the window """ if state.get_boolean(): self.fullscreen() self.fs_headerbar.events.show() else: self.unfullscreen() self.fs_headerbar.events.hide() self.text_editor.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ if state.get_boolean(): self.init_typewriter() self.markup_buffer.focusmode_highlight() self.focusmode = True self.text_editor.grab_focus() self.check_scroll(self.text_buffer.get_insert()) if self.spell_checker: self.spell_checker._misspelled.set_property('underline', 0) self.click_event = self.text_editor.connect("button-release-event", self.on_focusmode_click) else: self.remove_typewriter() self.focusmode = False self.text_buffer.remove_tag(self.markup_buffer.unfocused_text, self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter()) self.text_buffer.remove_tag(self.markup_buffer.blackfont, self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter()) self.markup_buffer.markup_buffer(1) self.text_editor.grab_focus() self.update_line_and_char_count() self.check_scroll() if self.spell_checker: self.spell_checker._misspelled.set_property('underline', 4) _click_event = self.text_editor.disconnect(self.click_event) def set_hemingway_mode(self, state): """toggle hemingwaymode """ self.text_editor.can_delete = not state.get_boolean() self.text_editor.grab_focus() def on_focusmode_click(self, *_args): """call MarkupBuffer to mark as bold the line where the cursor is """ self.markup_buffer.markup_buffer(1) def scroll_smoothly(self, widget, frame_clock, _data=None): if self.smooth_scroll_data['target_pos'] == -1: return True def ease_out_cubic(time): time = time - 1 return pow(time, 3) + 1 now = frame_clock.get_frame_time() if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']: self.smooth_scroll_starttime = now self.smooth_scroll_endtime = now + \ self.smooth_scroll_data['duration'] * 100 self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos'] if now < self.smooth_scroll_endtime: time = float(now - self.smooth_scroll_starttime) / float( self.smooth_scroll_endtime - self.smooth_scroll_starttime) else: time = 1 pos = self.smooth_scroll_data['source_pos'] \ + (time * (self.smooth_scroll_data['target_pos'] - self.smooth_scroll_data['source_pos'])) widget.get_vadjustment().props.value = pos self.smooth_scroll_data['target_pos'] = -1 return True time = ease_out_cubic(time) pos = self.smooth_scroll_data['source_pos'] \ + (time * (self.smooth_scroll_data['target_pos'] - self.smooth_scroll_data['source_pos'])) widget.get_vadjustment().props.value = pos return True # continue ticking def check_scroll(self, mark=None): gradient_offset = 80 buf = self.text_editor.get_buffer() if mark: ins_it = buf.get_iter_at_mark(mark) else: ins_it = buf.get_iter_at_mark(buf.get_insert()) loc_rect = self.text_editor.get_iter_location(ins_it) # alignment offset added from top pos_y = loc_rect.y + loc_rect.height + self.text_editor.props.top_margin # pylint: disable=no-member ha = self.scrolled_window.get_vadjustment() if ha.props.page_size < gradient_offset: return pos = pos_y - ha.props.value # print("pos: %i, pos_y %i, page_sz: %i, val: %i" % (pos, pos_y, ha.props.page_size # - gradient_offset, ha.props.value)) # global t, amount, initvadjustment target_pos = -1 if self.focusmode: # print("pos: %i > %i" % (pos, ha.props.page_size * 0.5)) if pos != (ha.props.page_size * 0.5): target_pos = pos_y - (ha.props.page_size * 0.5) elif pos > ha.props.page_size - gradient_offset - 60: target_pos = pos_y - ha.props.page_size + gradient_offset + 40 elif pos < gradient_offset: target_pos = pos_y - gradient_offset self.smooth_scroll_data = { 'target_pos': target_pos, 'source_pos': ha.props.value, 'duration': 2000 } if self.smooth_scroll_tickid == -1: self.smooth_scroll_tickid = self.scrolled_window.add_tick_callback( self.scroll_smoothly) def window_resize(self, widget, _data=None): """set paddings dependant of the window size """ # To calc padding top / bottom self.window_height = widget.get_allocation().height w_width = widget.get_allocation().width # Calculate left / right margin if w_width < 900: width_request = 600 self.markup_buffer.set_multiplier(8) self.current_font_size = 12 self.alignment_padding = 30 lm = 7 * 8 self.get_style_context().remove_class("medium") self.get_style_context().remove_class("large") self.get_style_context().add_class("small") elif w_width < 1400: width_request = 800 self.markup_buffer.set_multiplier(10) self.current_font_size = 15 self.alignment_padding = 40 lm = 7 * 10 self.get_style_context().remove_class("small") self.get_style_context().remove_class("large") self.get_style_context().add_class("medium") else: width_request = 1000 self.markup_buffer.set_multiplier(13) self.current_font_size = 17 self.alignment_padding = 60 lm = 7 * 13 self.get_style_context().remove_class("medium") self.get_style_context().remove_class("small") self.get_style_context().add_class("large") self.editor_alignment.props.margin_bottom = 0 self.editor_alignment.props.margin_top = 0 self.text_editor.set_left_margin(lm) self.text_editor.set_right_margin(lm) self.markup_buffer.recalculate(lm) if self.focusmode: self.remove_typewriter() self.init_typewriter() if self.text_editor.props.width_request != width_request: # pylint: disable=no-member self.text_editor.props.width_request = width_request self.scrolled_window.props.width_request = width_request alloc = self.text_editor.get_allocation() alloc.width = width_request self.text_editor.size_allocate(alloc) def style_changed(self, _widget, _data=None): pgc = self.text_editor.get_pango_context() mets = pgc.get_metrics() self.markup_buffer.set_multiplier( Pango.units_to_double(mets.get_approximate_char_width()) + 1) # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ if self.filename: LOGGER.info("saving") filename = self.filename file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.get_text()) file_to_save.close() if self.did_change: self.did_change = False title = self.get_title() self.set_headerbar_title(title[2:]) return Gtk.ResponseType.OK filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') filefilter.set_name('MarkDown (.md)') filechooser = Gtk.FileChooserDialog( _("Save your File"), self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK) ) filechooser.set_do_overwrite_confirmation(True) filechooser.add_filter(filefilter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.did_change = False filechooser.destroy() return response filechooser.destroy() return Gtk.ResponseType.CANCEL def save_document_as(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ filechooser = Gtk.FileChooserDialog( "Save your File", self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK) ) filechooser.set_do_overwrite_confirmation(True) if self.filename: filechooser.set_filename(self.filename) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.remove_item("file:/" + filename) self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) try: self.recent_manager.add_item(filename) except: pass filechooser.destroy() self.did_change = False else: filechooser.destroy() return Gtk.ResponseType.CANCEL def copy_html_to_clipboard(self, _widget=None, _date=None): """Copies only html without headers etc. to Clipboard """ args = ['pandoc', '--from=markdown', '--to=html5'] proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) text = bytes(self.get_text(), "utf-8") output = proc.communicate(text)[0] clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(output.decode("utf-8"), -1) clipboard.store() def open_document(self, _widget=None): """open the desired file """ if self.check_change() == Gtk.ResponseType.CANCEL: return filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') filefilter.set_name(_('MarkDown or Plain Text')) filechooser = Gtk.FileChooserDialog( _("Open a .md-File"), self, Gtk.FileChooserAction.OPEN, ("_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.OK) ) filechooser.add_filter(filefilter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() self.load_file(filename) filechooser.destroy() elif response == Gtk.ResponseType.CANCEL: filechooser.destroy() def check_change(self): """Show dialog to prevent loss of unsaved changes """ if self.did_change and self.get_text(): dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("You have not saved your changes.") ) dialog.add_button(_("Close without Saving"), Gtk.ResponseType.NO) dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("Save now"), Gtk.ResponseType.YES) # dialog.set_default_size(200, 60) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() if response == Gtk.ResponseType.YES: if self.save_document() == Gtk.ResponseType.CANCEL: dialog.destroy() return self.check_change() dialog.destroy() return response if response == Gtk.ResponseType.NO: dialog.destroy() return response dialog.destroy() return Gtk.ResponseType.CANCEL def new_document(self, _widget=None): """create new document """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.text_buffer.set_text('') self.text_editor.undos = [] self.text_editor.redos = [] self.did_change = False self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) def menu_toggle_sidebar(self, _widget=None): """WIP """ self.sidebar.toggle_sidebar() def toggle_spellcheck(self, status): """Enable/disable the autospellchecking Arguments: status {gtk bool} -- Desired status of the spellchecking """ if status.get_boolean(): try: self.spell_checker.enable() except: try: self.spell_checker = SpellChecker( self.text_editor, locale.getdefaultlocale()[0], collapse=False) if self.auto_correct: self.auto_correct.set_language(self.spell_checker.language) self.spell_checker.connect_language_change( # pylint: disable=no-member self.auto_correct.set_language) except: self.spell_checker = None dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL \ | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.NONE, _("You can not enable the Spell Checker.") ) dialog.format_secondary_text( _("Please install 'hunspell' or 'aspell' dictionarys" + " for your language from the software center.")) _response = dialog.run() return return else: try: self.spell_checker.disable() except: pass return def on_drag_data_received(self, _widget, drag_context, _x, _y, data, info, time): """Handle drag and drop events""" if info == 1: # uri target uris = data.get_uris() for uri in uris: uri = urllib.parse.unquote_plus(uri) mime = mimetypes.guess_type(uri) if mime[0] is not None and mime[0].startswith('image'): if uri.startswith("file://"): uri = uri[7:] text = "![Insert image title here](%s)" % uri limit_left = 2 limit_right = 23 else: text = "[Insert link title here](%s)" % uri limit_left = 1 limit_right = 22 self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark( self.text_buffer.get_mark('gtk_drag_target'))) self.text_buffer.insert_at_cursor(text) insert_mark = self.text_buffer.get_insert() selection_bound = self.text_buffer.get_selection_bound() cursor_iter = self.text_buffer.get_iter_at_mark(insert_mark) cursor_iter.backward_chars(len(text) - limit_left) self.text_buffer.move_mark(insert_mark, cursor_iter) cursor_iter.forward_chars(limit_right) self.text_buffer.move_mark(selection_bound, cursor_iter) elif info == 2: # Text target self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark( self.text_buffer.get_mark('gtk_drag_target'))) self.text_buffer.insert_at_cursor(data.get_text()) Gtk.drag_finish(drag_context, True, True, time) self.present() return False def toggle_preview(self, state): """Toggle the preview mode Arguments: state {gtk bool} -- Desired state of the preview mode (enabled/disabled) """ if state.get_boolean(): self.show_preview() else: self.show_text_editor() return True def show_text_editor(self): self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.text_editor) self.text_editor.show() self.preview_webview.destroy() self.preview_webview = None self.queue_draw() def show_preview(self, loaded=False): if loaded: self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.preview_webview) self.preview_webview.show() self.queue_draw() else: # Insert a tag with ID to scroll to # self.TextBuffer.insert_at_cursor('<span id="scroll_mark"></span>') # TODO # Find a way to find the next header, scroll to the next header. # TODO: provide a local version of mathjax # We need to convert relative routes to absolute ones # For that first we need to know if the file is saved: if self.filename: base_path = os.path.dirname(self.filename) else: base_path = '' os.environ['PANDOC_PREFIX'] = base_path + '/' args = ['pandoc', '-s', '--from=markdown', '--to=html5', '--mathjax', '--css=' + Theme.get_current().web_css_path, '--quiet', '--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'), '--lua-filter=' + helpers.get_script_path('task-list.lua')] # TODO: find a way to pass something like this instead of the quiet arg #'--metadata pagetitle="test"', proc = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) text = bytes(self.get_text(), "utf-8") output = proc.communicate(text)[0] if self.preview_webview is None: self.preview_webview = WebKit.WebView() self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True) # Delete the cursor-scroll mark again # cursor_iter = self.TextBuffer.get_iter_at_mark(self.TextBuffer.get_insert()) # begin_del = cursor_iter.copy() # begin_del.backward_chars(30) # self.TextBuffer.delete(begin_del, cursor_iter) # Show preview once the load is finished self.preview_webview.connect("load-changed", self.on_preview_load_change) # This saying that all links will be opened in default browser, \ # but local files are opened in appropriate apps: self.preview_webview.connect("decide-policy", self.on_click_link) self.preview_webview.load_html(output.decode("utf-8"), 'file://localhost/') def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" if self.check_change() == Gtk.ResponseType.CANCEL: return if filename: if filename.startswith('file://'): filename = filename[7:] filename = urllib.parse.unquote_plus(filename) try: if not os.path.exists(filename): self.text_buffer.set_text("") else: current_file = codecs.open(filename, encoding="utf-8", mode='r') self.text_buffer.set_text(current_file.read()) current_file.close() self.markup_buffer.markup_buffer(0) self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.text_editor.undo_stack = [] self.text_editor.redo_stack = [] self.set_filename(filename) except Exception: LOGGER.warning("Error Reading File: %r" % Exception) self.did_change = False else: LOGGER.warning("No File arg") def open_uberwriter_markdown(self, _widget=None, _data=None): """open a markdown mini tutorial """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(helpers.get_media_file('uberwriter_markdown.md')) def open_search_and_replace(self): """toggle the search box """ self.searchreplace.toggle_search() def open_advanced_export(self, _widget=None, _data=None): """open the export and advanced export dialog """ self.export = Export(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() if response == 1: self.export.export(bytes(self.get_text(), "utf-8")) self.export.dialog.destroy() def open_recent(self, _widget, data=None): """open the given recent document """ print("open") if data: if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(data) def poll_for_motion(self): """check if the user has moved the cursor to show the headerbar Returns: True -- Gtk things """ if (self.was_motion is False and self.status_bar_visible and self.buffer_modified_for_status_bar and self.text_editor.props.has_focus): # pylint: disable=no-member # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) self.statusbar_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) self.status_bar_visible = False self.buffer_modified_for_status_bar = False self.was_motion = False return True def on_motion_notify(self, _widget, event, _data=None): """check the motion of the mouse to fade in the headerbar """ now = event.get_time() if now - self.timestamp_last_mouse_motion > 150: # filter out accidental motions self.timestamp_last_mouse_motion = now return if now - self.timestamp_last_mouse_motion < 100: # filter out accidental motion return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar if self.status_bar_visible is False: self.statusbar_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False self.update_line_and_char_count() # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ if self.status_bar_visible is False: self.statusbar_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False self.update_line_and_char_count() def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the TextWindow """ bg_color = self.get_style_context().get_background_color(Gtk.StateFlags.ACTIVE) lg_top = cairo.LinearGradient(0, 0, 0, 35) # pylint: disable=no-member lg_top.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 0) width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height cr.rectangle(0, 0, width, 35) cr.set_source(lg_top) cr.fill() cr.rectangle(0, height - 35, width, height) lg_btm = cairo.LinearGradient(0, height - 35, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 0) cr.set_source(lg_btm) cr.fill() def use_experimental_features(self, _val): """use experimental features """ pass # try: # self.auto_correct = AutoCorrect( # self.text_editor, self.text_buffer) # except: # LOGGER.debug("Couldn't install autocorrect.") # self.plugins = [BibTex(self)] # def alt_mod(self, _widget, event, _data=None): # # TODO: Click and open when alt is pressed # if event.state & Gdk.ModifierType.MOD2_MASK: # LOGGER.info("Alt pressed") # return def on_delete_called(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ LOGGER.info('delete called') if self.check_change() == Gtk.ResponseType.CANCEL: return True return False def on_mnu_close_activate(self, _widget, _data=None): """Signal handler for closing the UberwriterWindow. Overriden from parent Window Class """ if self.on_delete_called(self): # Really destroy? return self.destroy() return def on_destroy(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ # Clean up code for saving application state should be added here. Gtk.main_quit() def set_headerbar_title(self, title): """set the desired headerbar title """ self.headerbar.hb.props.title = title self.fs_headerbar.hb.props.title = title self.set_title(title) def set_filename(self, filename=None): """set filename """ if filename: self.filename = filename base_path = os.path.dirname(self.filename) else: self.filename = None base_path = "/" self.settings.set_value("open-file-path", GLib.Variant("s", base_path)) def on_preview_load_change(self, webview, event): """swaps text editor with preview once the load is complete """ if event == WebKit.LoadEvent.FINISHED: self.show_preview(loaded=True) def on_click_link(self, web_view, decision, _decision_type): """provide ability for self.webview to open links in default browser """ if web_view.get_uri().startswith(("http://", "https://", "www.")): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up"
class MainWindow(StyledWindow): __gsignals__ = { 'save-file': (GObject.SIGNAL_ACTION, None, ()), 'open-file': (GObject.SIGNAL_ACTION, None, ()), 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), 'new-file': (GObject.SIGNAL_ACTION, None, ()), 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), 'close-window': (GObject.SIGNAL_ACTION, None, ()) } def __init__(self, app): """Set up the main window""" super().__init__(application=Gio.Application.get_default(), title="Uberwriter") self.get_style_context().add_class('uberwriter-window') # Set UI builder = Gtk.Builder() builder.add_from_resource("/de/wolfvollprecht/UberWriter/ui/Window.ui") root = builder.get_object("FullscreenOverlay") self.connect("delete-event", self.on_delete_called) self.add(root) self.set_default_size(1000, 600) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) self.scrolled_window = builder.get_object('editor_scrolledwindow') # Setup text editor self.text_view = TextView(self.settings.get_int("characters-per-line")) self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() self.scrolled_window.add(self.text_view) # Setup stats counter self.stats_revealer = builder.get_object('editor_stats_revealer') self.stats_button = builder.get_object('editor_stats_button') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup preview content = builder.get_object('content') editor = builder.get_object('editor') self.preview_handler = PreviewHandler(self, content, editor, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay( self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = builder.get_object("main_paned") self.sidebar_box = builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view, builder) def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that """ if self.did_change is False: self.did_change = True title = self.get_title() self.set_headerbar_title("* " + title) self.buffer_modified_for_status_bar = True def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border Arguments: state {almost bool} -- The desired fullscreen state of the window """ if state.get_boolean(): self.fullscreen() self.fs_headerbar.events.show() else: self.unfullscreen() self.fs_headerbar.events.hide() self.text_view.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ self.text_view.set_focus_mode(state.get_boolean()) self.text_view.grab_focus() def set_hemingway_mode(self, state): """toggle hemingwaymode """ self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() def toggle_preview(self, state): """Toggle the preview mode Arguments: state {gtk bool} -- Desired state of the preview mode (enabled/disabled) """ if state.get_boolean(): self.text_view.grab_focus() self.preview_handler.show() else: self.preview_handler.hide() self.text_view.grab_focus() return True # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ if self.filename: LOGGER.info("saving") filename = self.filename file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() if self.did_change: self.did_change = False title = self.get_title() self.set_headerbar_title(title[2:]) return Gtk.ResponseType.OK filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') filefilter.set_name('Markdown (.md)') filechooser = Gtk.FileChooserDialog( _("Save your File"), self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK)) filechooser.set_do_overwrite_confirmation(True) filechooser.add_filter(filefilter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.did_change = False filechooser.destroy() return response filechooser.destroy() return Gtk.ResponseType.CANCEL def save_document_as(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ filechooser = Gtk.FileChooserDialog( "Save your File", self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK)) filechooser.set_do_overwrite_confirmation(True) if self.filename: filechooser.set_filename(self.filename) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.remove_item("file:/" + filename) self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) try: self.recent_manager.add_item(filename) except: pass filechooser.destroy() self.did_change = False else: filechooser.destroy() return Gtk.ResponseType.CANCEL def copy_html_to_clipboard(self, _widget=None, _date=None): """Copies only html without headers etc. to Clipboard """ output = helpers.pandoc_convert(self.text_view.get_text()) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(output, -1) clipboard.store() def open_document(self, _widget=None): """open the desired file """ if self.check_change() == Gtk.ResponseType.CANCEL: return markdown_filter = Gtk.FileFilter.new() markdown_filter.add_mime_type('text/markdown') markdown_filter.add_mime_type('text/x-markdown') markdown_filter.set_name(_('Markdown Files')) plaintext_filter = Gtk.FileFilter.new() plaintext_filter.add_mime_type('text/plain') plaintext_filter.set_name(_('Plain Text Files')) filechooser = Gtk.FileChooserDialog( _("Open a .md file"), self, Gtk.FileChooserAction.OPEN, ("_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.OK)) filechooser.add_filter(markdown_filter) filechooser.add_filter(plaintext_filter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() self.load_file(filename) filechooser.destroy() elif response == Gtk.ResponseType.CANCEL: filechooser.destroy() def check_change(self): """Show dialog to prevent loss of unsaved changes """ if self.did_change and self.text_view.get_text(): dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("You have not saved your changes.")) dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO) dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("Save now"), Gtk.ResponseType.YES) # dialog.set_default_size(200, 60) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() if response == Gtk.ResponseType.YES: if self.save_document() == Gtk.ResponseType.CANCEL: dialog.destroy() return self.check_change() dialog.destroy() return response if response == Gtk.ResponseType.NO: dialog.destroy() return response dialog.destroy() return Gtk.ResponseType.CANCEL def new_document(self, _widget=None): """create new document """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.text_view.clear() self.did_change = False self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) def update_default_stat(self): self.stats_handler.update_default_stat() def update_preview_mode(self): self.preview_handler.update_preview_mode() def menu_toggle_sidebar(self, _widget=None): """WIP """ self.sidebar.toggle_sidebar() def toggle_spellcheck(self, state): """Enable/disable the autospellchecking Arguments: status {gtk bool} -- Desired status of the spellchecking """ self.text_view.set_spellcheck(state.get_boolean()) def toggle_gradient_overlay(self, state): """Toggle the gradient overlay Arguments: state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled) """ if state.get_boolean(): self.overlay_id = self.scrolled_window.connect_after( "draw", self.draw_gradient) elif self.overlay_id: self.scrolled_window.disconnect(self.overlay_id) def reload_preview(self, reshow=False): self.preview_handler.reload(reshow=reshow) def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" if self.check_change() == Gtk.ResponseType.CANCEL: return if filename: if filename.startswith('file://'): filename = filename[7:] self.text_view.clear() try: if os.path.exists(filename): with codecs.open(filename, encoding="utf-8", mode='r') as current_file: self.text_view.set_text(current_file.read()) else: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, _("The file you tried to open doesn't exist.\ \nA new file will be created in its place when you save the current one." )) dialog.run() dialog.destroy() self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.set_filename(filename) except Exception as e: LOGGER.warning(_("Error Reading File: %r") % e) dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, _("Error reading file:\ \n%r" % e)) dialog.run() dialog.destroy() self.did_change = False else: LOGGER.warning("No File arg") def open_uberwriter_markdown(self, _widget=None, _data=None): """open a markdown mini tutorial """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(helpers.get_media_file('uberwriter_markdown.md')) def open_search(self, replace=False): """toggle the search box """ self.searchreplace.toggle_search(replace=replace) def open_advanced_export(self, _widget=None, _data=None): """open the export and advanced export dialog """ self.export = Export(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() if response == 1: try: self.export.export(bytes(self.text_view.get_text(), "utf-8")) except Exception as e: dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, _("An error happened while trying to export:\n\n{err_msg}" ).format( err_msg=str(e).encode().decode("unicode-escape"))) dialog.run() dialog.destroy() self.export.dialog.destroy() def open_recent(self, _widget, data=None): """open the given recent document """ print("open") if data: if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(data) def poll_for_motion(self): """check if the user has moved the cursor to show the headerbar Returns: True -- Gtk things """ if (not self.was_motion and self.buffer_modified_for_status_bar and self.text_view.props.has_focus): self.reveal_top_bottom_bars(False) self.was_motion = False return True def on_motion_notify(self, _widget, event, _data=None): """check the motion of the mouse to fade in the headerbar """ now = event.get_time() if now - self.timestamp_last_mouse_motion > 150: # filter out accidental motions self.timestamp_last_mouse_motion = now return if now - self.timestamp_last_mouse_motion < 100: # filter out accidental motion return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar self.reveal_top_bottom_bars(True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ self.reveal_top_bottom_bars(True) def reveal_top_bottom_bars(self, reveal): if self.top_bottom_bars_visible != reveal: self.headerbar.hb_revealer.set_reveal_child(reveal) self.stats_revealer.set_reveal_child(reveal) for revealer in self.preview_handler.get_top_bottom_bar_revealers( ): revealer.set_reveal_child(reveal) self.top_bottom_bars_visible = reveal self.buffer_modified_for_status_bar = reveal def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the TextWindow """ bg_color = self.get_style_context().get_background_color( Gtk.StateFlags.ACTIVE) lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member lg_top.add_color_stop_rgba(0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba(1, bg_color.red, bg_color.green, bg_color.blue, 0) width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height cr.rectangle(0, 0, width, 32) cr.set_source(lg_top) cr.fill() cr.rectangle(0, height - 32, width, height) lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba(1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba(0, bg_color.red, bg_color.green, bg_color.blue, 0) cr.set_source(lg_btm) cr.fill() def on_delete_called(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ LOGGER.info('delete called') if self.check_change() == Gtk.ResponseType.CANCEL: return True return False def on_mnu_close_activate(self, _widget, _data=None): """Signal handler for closing the Window. Overriden from parent Window Class """ if self.on_delete_called(self): # Really destroy? return self.destroy() return def set_headerbar_title(self, title): """set the desired headerbar title """ self.headerbar.hb.props.title = title self.fs_headerbar.hb.props.title = title self.set_title(title) def set_filename(self, filename=None): """set filename """ if filename: self.filename = filename base_path = os.path.dirname(self.filename) else: self.filename = None base_path = "/" self.settings.set_string("open-file-path", base_path)
def __init__(self, app): """Set up the main window""" super().__init__(application=Gio.Application.get_default(), title="Uberwriter") self.get_style_context().add_class('uberwriter-window') # Set UI builder = get_builder('Window') root = builder.get_object("FullscreenOverlay") self.connect("delete-event", self.on_delete_called) self.add(root) self.set_default_size(1000, 600) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) self.scrolled_window = builder.get_object('editor_scrolledwindow') # Setup text editor self.text_view = TextView(self.settings.get_int("characters-per-line")) self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() self.scrolled_window.add(self.text_view) # Setup stats counter self.stats_revealer = builder.get_object('editor_stats_revealer') self.stats_button = builder.get_object('editor_stats_button') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup preview content = builder.get_object('content') editor = builder.get_object('editor') self.preview_handler = PreviewHandler(self, content, editor, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = builder.get_object("main_paned") self.sidebar_box = builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view, builder)
class MainWindow(StyledWindow): __gsignals__ = { 'save-file': (GObject.SIGNAL_ACTION, None, ()), 'open-file': (GObject.SIGNAL_ACTION, None, ()), 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), 'new-file': (GObject.SIGNAL_ACTION, None, ()), 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), 'close-window': (GObject.SIGNAL_ACTION, None, ()) } def __init__(self, app): """Set up the main window""" super().__init__(application=Gio.Application.get_default(), title="Uberwriter") self.get_style_context().add_class('uberwriter-window') # Set UI builder = get_builder('Window') root = builder.get_object("FullscreenOverlay") self.connect("delete-event", self.on_delete_called) self.add(root) self.set_default_size(1000, 600) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) self.scrolled_window = builder.get_object('editor_scrolledwindow') # Setup text editor self.text_view = TextView(self.settings.get_int("characters-per-line")) self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() self.scrolled_window.add(self.text_view) # Setup stats counter self.stats_revealer = builder.get_object('editor_stats_revealer') self.stats_button = builder.get_object('editor_stats_button') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup preview content = builder.get_object('content') editor = builder.get_object('editor') self.preview_handler = PreviewHandler(self, content, editor, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = builder.get_object("main_paned") self.sidebar_box = builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view, builder) def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that """ if self.did_change is False: self.did_change = True title = self.get_title() self.set_headerbar_title("* " + title) self.buffer_modified_for_status_bar = True def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border Arguments: state {almost bool} -- The desired fullscreen state of the window """ if state.get_boolean(): self.fullscreen() self.fs_headerbar.events.show() else: self.unfullscreen() self.fs_headerbar.events.hide() self.text_view.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ self.text_view.set_focus_mode(state.get_boolean()) self.text_view.grab_focus() def set_hemingway_mode(self, state): """toggle hemingwaymode """ self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() def toggle_preview(self, state): """Toggle the preview mode Arguments: state {gtk bool} -- Desired state of the preview mode (enabled/disabled) """ if state.get_boolean(): self.text_view.grab_focus() self.preview_handler.show() else: self.preview_handler.hide() self.text_view.grab_focus() return True # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ if self.filename: LOGGER.info("saving") filename = self.filename file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() if self.did_change: self.did_change = False title = self.get_title() self.set_headerbar_title(title[2:]) return Gtk.ResponseType.OK filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') filefilter.set_name('Markdown (.md)') filechooser = Gtk.FileChooserDialog( _("Save your File"), self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK) ) filechooser.set_do_overwrite_confirmation(True) filechooser.add_filter(filefilter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.did_change = False filechooser.destroy() return response filechooser.destroy() return Gtk.ResponseType.CANCEL def save_document_as(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ filechooser = Gtk.FileChooserDialog( "Save your File", self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK) ) filechooser.set_do_overwrite_confirmation(True) if self.filename: filechooser.set_filename(self.filename) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.remove_item("file:/" + filename) self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) try: self.recent_manager.add_item(filename) except: pass filechooser.destroy() self.did_change = False else: filechooser.destroy() return Gtk.ResponseType.CANCEL def copy_html_to_clipboard(self, _widget=None, _date=None): """Copies only html without headers etc. to Clipboard """ output = helpers.pandoc_convert(self.text_view.get_text()) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(output, -1) clipboard.store() def open_document(self, _widget=None): """open the desired file """ if self.check_change() == Gtk.ResponseType.CANCEL: return markdown_filter = Gtk.FileFilter.new() markdown_filter.add_mime_type('text/markdown') markdown_filter.add_mime_type('text/x-markdown') markdown_filter.set_name(_('Markdown Files')) plaintext_filter = Gtk.FileFilter.new() plaintext_filter.add_mime_type('text/plain') plaintext_filter.set_name(_('Plain Text Files')) filechooser = Gtk.FileChooserDialog( _("Open a .md file"), self, Gtk.FileChooserAction.OPEN, ("_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.OK) ) filechooser.add_filter(markdown_filter) filechooser.add_filter(plaintext_filter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() self.load_file(filename) filechooser.destroy() elif response == Gtk.ResponseType.CANCEL: filechooser.destroy() def check_change(self): """Show dialog to prevent loss of unsaved changes """ if self.did_change and self.text_view.get_text(): dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("You have not saved your changes.") ) dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO) dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("Save now"), Gtk.ResponseType.YES) # dialog.set_default_size(200, 60) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() if response == Gtk.ResponseType.YES: if self.save_document() == Gtk.ResponseType.CANCEL: dialog.destroy() return self.check_change() dialog.destroy() return response if response == Gtk.ResponseType.NO: dialog.destroy() return response dialog.destroy() return Gtk.ResponseType.CANCEL def new_document(self, _widget=None): """create new document """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.text_view.clear() self.did_change = False self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) def update_default_stat(self): self.stats_handler.update_default_stat() def update_preview_mode(self): self.preview_handler.update_preview_mode() def menu_toggle_sidebar(self, _widget=None): """WIP """ self.sidebar.toggle_sidebar() def toggle_spellcheck(self, state): """Enable/disable the autospellchecking Arguments: status {gtk bool} -- Desired status of the spellchecking """ self.text_view.set_spellcheck(state.get_boolean()) def toggle_gradient_overlay(self, state): """Toggle the gradient overlay Arguments: state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled) """ if state.get_boolean(): self.overlay_id = self.scrolled_window.connect_after("draw", self.draw_gradient) elif self.overlay_id: self.scrolled_window.disconnect(self.overlay_id) def reload_preview(self, reshow=False): self.preview_handler.reload(reshow=reshow) def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" if self.check_change() == Gtk.ResponseType.CANCEL: return if filename: if filename.startswith('file://'): filename = filename[7:] filename = urllib.parse.unquote_plus(filename) self.text_view.clear() try: if os.path.exists(filename): current_file = codecs.open(filename, encoding="utf-8", mode='r') self.text_view.set_text(current_file.read()) current_file.close() self.set_headerbar_title(os.path.basename(filename) + self.title_end) self.set_filename(filename) except Exception: LOGGER.warning("Error Reading File: %r" % Exception) self.did_change = False else: LOGGER.warning("No File arg") def open_uberwriter_markdown(self, _widget=None, _data=None): """open a markdown mini tutorial """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(helpers.get_media_file('uberwriter_markdown.md')) def open_search_and_replace(self): """toggle the search box """ self.searchreplace.toggle_search() def open_advanced_export(self, _widget=None, _data=None): """open the export and advanced export dialog """ self.export = Export(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() if response == 1: self.export.export(bytes(self.text_view.get_text(), "utf-8")) self.export.dialog.destroy() def open_recent(self, _widget, data=None): """open the given recent document """ print("open") if data: if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(data) def poll_for_motion(self): """check if the user has moved the cursor to show the headerbar Returns: True -- Gtk things """ if (not self.was_motion and self.buffer_modified_for_status_bar and self.text_view.props.has_focus): self.reveal_top_bottom_bars(False) self.was_motion = False return True def on_motion_notify(self, _widget, event, _data=None): """check the motion of the mouse to fade in the headerbar """ now = event.get_time() if now - self.timestamp_last_mouse_motion > 150: # filter out accidental motions self.timestamp_last_mouse_motion = now return if now - self.timestamp_last_mouse_motion < 100: # filter out accidental motion return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar self.reveal_top_bottom_bars(True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ self.reveal_top_bottom_bars(True) def reveal_top_bottom_bars(self, reveal): if self.top_bottom_bars_visible != reveal: self.headerbar.hb_revealer.set_reveal_child(reveal) self.stats_revealer.set_reveal_child(reveal) for revealer in self.preview_handler.get_top_bottom_bar_revealers(): revealer.set_reveal_child(reveal) self.top_bottom_bars_visible = reveal self.buffer_modified_for_status_bar = reveal def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the TextWindow """ bg_color = self.get_style_context().get_background_color(Gtk.StateFlags.ACTIVE) lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member lg_top.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 0) width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height cr.rectangle(0, 0, width, 32) cr.set_source(lg_top) cr.fill() cr.rectangle(0, height - 32, width, height) lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 0) cr.set_source(lg_btm) cr.fill() def on_delete_called(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ LOGGER.info('delete called') if self.check_change() == Gtk.ResponseType.CANCEL: return True return False def on_mnu_close_activate(self, _widget, _data=None): """Signal handler for closing the Window. Overriden from parent Window Class """ if self.on_delete_called(self): # Really destroy? return self.destroy() return def set_headerbar_title(self, title): """set the desired headerbar title """ self.headerbar.hb.props.title = title self.fs_headerbar.hb.props.title = title self.set_title(title) def set_filename(self, filename=None): """set filename """ if filename: self.filename = filename base_path = os.path.dirname(self.filename) else: self.filename = None base_path = "/" self.settings.set_string("open-file-path", base_path)
def __init__(self, app): """Set up the main window""" Gtk.ApplicationWindow.__init__( self, application=Gio.Application.get_default(), title="Uberwriter") # Set UI self.builder = get_builder('Window') root = self.builder.get_object("FullscreenOverlay") root.connect('style-updated', self.apply_current_theme) self.add(root) self.set_default_size(900, 500) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) # Setup text editor self.text_view = TextView() self.text_view.props.halign = Gtk.Align.CENTER self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() # Setup preview webview self.preview_webview = None self.scrolled_window = self.builder.get_object('editor_scrolledwindow') self.scrolled_window.get_style_context().add_class( 'uberwriter-scrolled-window') self.scrolled_window.add(self.text_view) self.editor_viewport = self.builder.get_object('editor_viewport') # Stats counter self.stats_counter_revealer = self.builder.get_object( 'stats_counter_revealer') self.stats_button = self.builder.get_object('stats_counter') self.stats_button.get_style_context().add_class('stats-counter') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay( self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = self.builder.get_object("main_pained") self.sidebar_box = self.builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view) # Window resize self.window_resize(self) self.connect("configure-event", self.window_resize) self.connect("delete-event", self.on_delete_called) # Set current theme self.apply_current_theme() self.get_style_context().add_class('uberwriter-window')
class Window(Gtk.ApplicationWindow): __gsignals__ = { 'save-file': (GObject.SIGNAL_ACTION, None, ()), 'open-file': (GObject.SIGNAL_ACTION, None, ()), 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), 'new-file': (GObject.SIGNAL_ACTION, None, ()), 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), 'close-window': (GObject.SIGNAL_ACTION, None, ()) } def __init__(self, app): """Set up the main window""" Gtk.ApplicationWindow.__init__( self, application=Gio.Application.get_default(), title="Uberwriter") # Set UI self.builder = get_builder('Window') root = self.builder.get_object("FullscreenOverlay") root.connect('style-updated', self.apply_current_theme) self.add(root) self.set_default_size(900, 500) # Preferences self.settings = Settings.new() # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) self.accel_group = Gtk.AccelGroup() self.add_accel_group(self.accel_group) # Setup text editor self.text_view = TextView() self.text_view.props.halign = Gtk.Align.CENTER self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() self.text_view.grab_focus() # Setup preview webview self.preview_webview = None self.scrolled_window = self.builder.get_object('editor_scrolledwindow') self.scrolled_window.get_style_context().add_class( 'uberwriter-scrolled-window') self.scrolled_window.add(self.text_view) self.editor_viewport = self.builder.get_object('editor_viewport') # Stats counter self.stats_counter_revealer = self.builder.get_object( 'stats_counter_revealer') self.stats_button = self.builder.get_object('stats_counter') self.stats_button.get_style_context().add_class('stats-counter') self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None self.toggle_gradient_overlay( self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) self.did_change = False ### # Sidebar initialization test ### self.paned_window = self.builder.get_object("main_pained") self.sidebar_box = self.builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() ### # Search and replace initialization # Same interface as Sidebar ;) ### self.searchreplace = SearchAndReplace(self, self.text_view) # Window resize self.window_resize(self) self.connect("configure-event", self.window_resize) self.connect("delete-event", self.on_delete_called) # Set current theme self.apply_current_theme() self.get_style_context().add_class('uberwriter-window') def apply_current_theme(self, *_): """Adjusts the window, CSD and preview for the current theme. """ # Get current theme theme, changed = Theme.get_current_changed() if changed: # Set theme variant (dark/light) Gtk.Settings.get_default().set_property( "gtk-application-prefer-dark-theme", GLib.Variant("b", theme.is_dark)) # Set theme css style_provider = Gtk.CssProvider() style_provider.load_from_path(helpers.get_css_path("gtk/base.css")) Gtk.StyleContext.add_provider_for_screen( self.get_screen(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # Reload preview if it exists self.reload_preview() # Redraw contents of window self.queue_draw() def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that """ if self.did_change is False: self.did_change = True title = self.get_title() self.set_headerbar_title("* " + title) self.buffer_modified_for_status_bar = True def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border Arguments: state {almost bool} -- The desired fullscreen state of the window """ if state.get_boolean(): self.fullscreen() self.fs_headerbar.events.show() else: self.unfullscreen() self.fs_headerbar.events.hide() self.text_view.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ focus_mode = state.get_boolean() self.text_view.set_focus_mode(focus_mode) if self.spell_checker: self.spell_checker._misspelled.set_property( 'underline', 0 if focus_mode else 4) self.text_view.grab_focus() def set_hemingway_mode(self, state): """toggle hemingwaymode """ self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() def window_resize(self, window, event=None): """set paddings dependant of the window size """ # Ensure the window receiving the event is the one we care about, ie. the main window. # On Wayland (bug?), sub-windows such as the recents popover will also trigger this. if event and event.window != window.get_window(): return # Adjust text editor width depending on window width, so that: # - The number of characters per line is adequate (http://webtypography.net/2.1.2) # - The number of characters stays constant while resizing the window / font # - There is enough text margin for MarkupBuffer to apply indents / negative margins # # TODO: Avoid hard-coding. Font size is clearer than unclear dimensions, but not ideal. w_width = event.width if event else window.get_allocation().width if w_width < 900: font_size = 14 self.get_style_context().add_class("small") self.get_style_context().remove_class("large") elif w_width < 1280: font_size = 16 self.get_style_context().remove_class("small") self.get_style_context().remove_class("large") else: font_size = 18 self.get_style_context().remove_class("small") self.get_style_context().add_class("large") font_width = int(font_size * 1 / 1.6) # Ratio specific to Fira Mono width = 67 * font_width - 1 # 66 characters horizontal_margin = 8 * font_width # 8 characters width_request = width + horizontal_margin * 2 if self.text_view.props.width_request != width_request: self.text_view.props.width_request = width_request self.text_view.set_left_margin(horizontal_margin) self.text_view.set_right_margin(horizontal_margin) self.scrolled_window.props.width_request = width_request # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ if self.filename: LOGGER.info("saving") filename = self.filename file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() if self.did_change: self.did_change = False title = self.get_title() self.set_headerbar_title(title[2:]) return Gtk.ResponseType.OK filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') filefilter.set_name('Markdown (.md)') filechooser = Gtk.FileChooserDialog( _("Save your File"), self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK)) filechooser.set_do_overwrite_confirmation(True) filechooser.add_filter(filefilter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.did_change = False filechooser.destroy() return response filechooser.destroy() return Gtk.ResponseType.CANCEL def save_document_as(self, _widget=None, _data=None): """provide to the user a filechooser and save the document where he wants. Call set_headbar_title after that """ filechooser = Gtk.FileChooserDialog( "Save your File", self, Gtk.FileChooserAction.SAVE, ("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK)) filechooser.set_do_overwrite_confirmation(True) if self.filename: filechooser.set_filename(self.filename) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() if filename[-3:] != ".md": filename = filename + ".md" try: self.recent_manager.remove_item("file:/" + filename) self.recent_manager.add_item("file:/ " + filename) except: pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) self.set_headerbar_title( os.path.basename(filename) + self.title_end) try: self.recent_manager.add_item(filename) except: pass filechooser.destroy() self.did_change = False else: filechooser.destroy() return Gtk.ResponseType.CANCEL def copy_html_to_clipboard(self, _widget=None, _date=None): """Copies only html without headers etc. to Clipboard """ output = helpers.pandoc_convert(self.text_view.get_text()) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(output, -1) clipboard.store() def open_document(self, _widget=None): """open the desired file """ if self.check_change() == Gtk.ResponseType.CANCEL: return markdown_filter = Gtk.FileFilter.new() markdown_filter.add_mime_type('text/markdown') markdown_filter.add_mime_type('text/x-markdown') markdown_filter.set_name(_('Markdown Files')) plaintext_filter = Gtk.FileFilter.new() plaintext_filter.add_mime_type('text/plain') plaintext_filter.set_name(_('Plain Text Files')) filechooser = Gtk.FileChooserDialog( _("Open a .md file"), self, Gtk.FileChooserAction.OPEN, ("_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.OK)) filechooser.add_filter(markdown_filter) filechooser.add_filter(plaintext_filter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() self.load_file(filename) filechooser.destroy() elif response == Gtk.ResponseType.CANCEL: filechooser.destroy() def check_change(self): """Show dialog to prevent loss of unsaved changes """ if self.did_change and self.text_view.get_text(): dialog = Gtk.MessageDialog( self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("You have not saved your changes.")) dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO) dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("Save now"), Gtk.ResponseType.YES) # dialog.set_default_size(200, 60) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() if response == Gtk.ResponseType.YES: if self.save_document() == Gtk.ResponseType.CANCEL: dialog.destroy() return self.check_change() dialog.destroy() return response if response == Gtk.ResponseType.NO: dialog.destroy() return response dialog.destroy() return Gtk.ResponseType.CANCEL def new_document(self, _widget=None): """create new document """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.text_view.clear() self.did_change = False self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) def update_default_stat(self): self.stats_handler.update_default_stat() def menu_toggle_sidebar(self, _widget=None): """WIP """ self.sidebar.toggle_sidebar() def toggle_spellcheck(self, state): """Enable/disable the autospellchecking Arguments: status {gtk bool} -- Desired status of the spellchecking """ if state.get_boolean(): try: self.spell_checker.enable() except: try: self.spell_checker = SpellChecker( self.text_view, locale.getdefaultlocale()[0], collapse=False) if self.auto_correct: self.auto_correct.set_language( self.spell_checker.language) self.spell_checker.connect_language_change( # pylint: disable=no-member self.auto_correct.set_language) except: self.spell_checker = None dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL \ | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.NONE, _("You can not enable the Spell Checker.") ) dialog.format_secondary_text( _("Please install 'hunspell' or 'aspell' dictionaries" + " for your language from the software center.")) _response = dialog.run() return return else: try: self.spell_checker.disable() except: pass return def toggle_gradient_overlay(self, state): """Toggle the gradient overlay Arguments: state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled) """ if state.get_boolean(): self.overlay_id = self.scrolled_window.connect_after( "draw", self.draw_gradient) elif self.overlay_id: self.scrolled_window.disconnect(self.overlay_id) def toggle_preview(self, state): """Toggle the preview mode Arguments: state {gtk bool} -- Desired state of the preview mode (enabled/disabled) """ if state.get_boolean(): self.show_preview() else: self.show_text_editor() return True def show_text_editor(self): self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.text_view) self.text_view.show() self.preview_webview.destroy() self.preview_webview = None self.queue_draw() def show_preview(self, loaded=False): if loaded: self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.preview_webview) self.preview_webview.show() self.queue_draw() else: args = [ '--standalone', '--mathjax', '--css=' + Theme.get_current().web_css_path, '--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'), '--lua-filter=' + helpers.get_script_path('task-list.lua') ] output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args) if self.preview_webview is None: self.preview_webview = WebKit.WebView() self.preview_webview.get_settings( ).set_allow_universal_access_from_file_urls(True) # Show preview once the load is finished self.preview_webview.connect("load-changed", self.on_preview_load_change) # This saying that all links will be opened in default browser, \ # but local files are opened in appropriate apps: self.preview_webview.connect("decide-policy", self.on_click_link) self.preview_webview.load_html(output, 'file://localhost/') def reload_preview(self): if self.preview_webview: self.show_preview() def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" if self.check_change() == Gtk.ResponseType.CANCEL: return if filename: if filename.startswith('file://'): filename = filename[7:] filename = urllib.parse.unquote_plus(filename) self.text_view.clear() try: if os.path.exists(filename): current_file = codecs.open(filename, encoding="utf-8", mode='r') self.text_view.set_text(current_file.read()) current_file.close() self.set_headerbar_title( os.path.basename(filename) + self.title_end) self.set_filename(filename) except Exception: LOGGER.warning("Error Reading File: %r" % Exception) self.did_change = False else: LOGGER.warning("No File arg") def open_uberwriter_markdown(self, _widget=None, _data=None): """open a markdown mini tutorial """ if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(helpers.get_media_file('uberwriter_markdown.md')) def open_search_and_replace(self): """toggle the search box """ self.searchreplace.toggle_search() def open_advanced_export(self, _widget=None, _data=None): """open the export and advanced export dialog """ self.export = Export(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() if response == 1: self.export.export(bytes(self.text_view.get_text(), "utf-8")) self.export.dialog.destroy() def open_recent(self, _widget, data=None): """open the given recent document """ print("open") if data: if self.check_change() == Gtk.ResponseType.CANCEL: return self.load_file(data) def poll_for_motion(self): """check if the user has moved the cursor to show the headerbar Returns: True -- Gtk things """ if (self.was_motion is False and self.top_bottom_bars_visible and self.buffer_modified_for_status_bar and self.text_view.props.has_focus): # pylint: disable=no-member # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) self.stats_counter_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) self.top_bottom_bars_visible = False self.buffer_modified_for_status_bar = False self.was_motion = False return True def on_motion_notify(self, _widget, event, _data=None): """check the motion of the mouse to fade in the headerbar """ now = event.get_time() if now - self.timestamp_last_mouse_motion > 150: # filter out accidental motions self.timestamp_last_mouse_motion = now return if now - self.timestamp_last_mouse_motion < 100: # filter out accidental motion return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar if self.top_bottom_bars_visible is False: self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ if self.top_bottom_bars_visible is False: self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the TextWindow """ bg_color = self.get_style_context().get_background_color( Gtk.StateFlags.ACTIVE) lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member lg_top.add_color_stop_rgba(0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba(1, bg_color.red, bg_color.green, bg_color.blue, 0) width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height cr.rectangle(0, 0, width, 32) cr.set_source(lg_top) cr.fill() cr.rectangle(0, height - 32, width, height) lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba(1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba(0, bg_color.red, bg_color.green, bg_color.blue, 0) cr.set_source(lg_btm) cr.fill() def on_delete_called(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ LOGGER.info('delete called') if self.check_change() == Gtk.ResponseType.CANCEL: return True return False def on_mnu_close_activate(self, _widget, _data=None): """Signal handler for closing the Window. Overriden from parent Window Class """ if self.on_delete_called(self): # Really destroy? return self.destroy() return def set_headerbar_title(self, title): """set the desired headerbar title """ self.headerbar.hb.props.title = title self.fs_headerbar.hb.props.title = title self.set_title(title) def set_filename(self, filename=None): """set filename """ if filename: self.filename = filename base_path = os.path.dirname(self.filename) else: self.filename = None base_path = "/" self.settings.set_value("open-file-path", GLib.Variant("s", base_path)) def on_preview_load_change(self, webview, event): """swaps text editor with preview once the load is complete """ if event == WebKit.LoadEvent.FINISHED: self.show_preview(loaded=True) def on_click_link(self, web_view, decision, _decision_type): """provide ability for self.webview to open links in default browser """ if web_view.get_uri().startswith(("http://", "https://", "www.")): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up"