diff --git a/nvpy/nvpy.py b/nvpy/nvpy.py index 8c680a7..e053852 100644 --- a/nvpy/nvpy.py +++ b/nvpy/nvpy.py @@ -44,6 +44,7 @@ import re import collections +import tk from utils import KeyValueObject, SubjectMixin import view import webbrowser @@ -327,66 +328,70 @@ def __init__(self, config): # create the interface self.view = view.View(self.config, self.notes_list_model) - # read our database of notes into memory - # and sync with simplenote. try: - self.notes_db = NotesDB(self.config) - - except ReadError, e: - emsg = "Please check nvpy.log.\n" + str(e) - self.view.show_error('Sync error', emsg) - exit(1) - - self.notes_db.add_observer('synced:note', self.observer_notes_db_synced_note) - self.notes_db.add_observer('change:note-status', self.observer_notes_db_change_note_status) - - if self.config.simplenote_sync: - self.notes_db.add_observer('progress:sync_full', self.observer_notes_db_sync_full) - self.notes_db.add_observer('error:sync_full', self.observer_notes_db_error_sync_full) - self.notes_db.add_observer('complete:sync_full', self.observer_notes_db_complete_sync_full) - - # we want to be notified when the user does stuff - self.view.add_observer('click:notelink', - self.observer_view_click_notelink) - self.view.add_observer('delete:note', self.observer_view_delete_note) - self.view.add_observer('select:note', self.observer_view_select_note) - self.view.add_observer('change:entry', self.observer_view_change_entry) - self.view.add_observer('change:text', self.observer_view_change_text) - self.view.add_observer('change:pinned', self.observer_view_change_pinned) - self.view.add_observer('create:note', self.observer_view_create_note) - self.view.add_observer('keep:house', self.observer_view_keep_house) - self.view.add_observer('command:markdown', self.observer_view_markdown) - self.view.add_observer('command:rest', self.observer_view_rest) - self.view.add_observer('delete:tag', self.observer_view_delete_tag) - self.view.add_observer('add:tag', self.observer_view_add_tag) - - if self.config.simplenote_sync: - self.view.add_observer('command:sync_full', lambda v, et, e: self.sync_full()) - self.view.add_observer('command:sync_current_note', self.observer_view_sync_current_note) - - self.view.add_observer('close', self.observer_view_close) - - # setup UI to reflect our search mode and case sensitivity - self.view.set_cs(self.config.case_sensitive, silent=True) - self.view.set_search_mode(self.config.search_mode, silent=True) - - self.view.add_observer('change:cs', self.observer_view_change_cs) - self.view.add_observer('change:search_mode', self.observer_view_change_search_mode) - - # nn is a list of (key, note) objects - nn, match_regexp, active_notes = self.notes_db.filter_notes() - # this will trigger the list_change event - self.notes_list_model.set_list(nn) - self.notes_list_model.match_regexp = match_regexp - self.view.set_note_tally(len(nn), active_notes, len(self.notes_db.notes)) + # read our database of notes into memory + # and sync with simplenote. + try: + self.notes_db = NotesDB(self.config) + except ReadError, e: + emsg = "Please check nvpy.log.\n" + str(e) + self.view.show_error('Sync error', emsg) + exit(1) + + self.notes_db.add_observer('synced:note', self.observer_notes_db_synced_note) + self.notes_db.add_observer('change:note-status', self.observer_notes_db_change_note_status) + + if self.config.simplenote_sync: + self.notes_db.add_observer('progress:sync_full', self.observer_notes_db_sync_full) + self.notes_db.add_observer('error:sync_full', self.observer_notes_db_error_sync_full) + self.notes_db.add_observer('complete:sync_full', self.observer_notes_db_complete_sync_full) + + # we want to be notified when the user does stuff + self.view.add_observer('click:notelink', + self.observer_view_click_notelink) + self.view.add_observer('delete:note', self.observer_view_delete_note) + self.view.add_observer('select:note', self.observer_view_select_note) + self.view.add_observer('change:entry', self.observer_view_change_entry) + self.view.add_observer('change:text', self.observer_view_change_text) + self.view.add_observer('change:pinned', self.observer_view_change_pinned) + self.view.add_observer('create:note', self.observer_view_create_note) + self.view.add_observer('keep:house', self.observer_view_keep_house) + self.view.add_observer('command:markdown', self.observer_view_markdown) + self.view.add_observer('command:rest', self.observer_view_rest) + self.view.add_observer('delete:tag', self.observer_view_delete_tag) + self.view.add_observer('add:tag', self.observer_view_add_tag) + + if self.config.simplenote_sync: + self.view.add_observer('command:sync_full', lambda v, et, e: self.sync_full()) + self.view.add_observer('command:sync_current_note', self.observer_view_sync_current_note) + + self.view.add_observer('close', self.observer_view_close) + + # setup UI to reflect our search mode and case sensitivity + self.view.set_cs(self.config.case_sensitive, silent=True) + self.view.set_search_mode(self.config.search_mode, silent=True) + + self.view.add_observer('change:cs', self.observer_view_change_cs) + self.view.add_observer('change:search_mode', self.observer_view_change_search_mode) + + # nn is a list of (key, note) objects + nn, match_regexp, active_notes = self.notes_db.filter_notes() + # this will trigger the list_change event + self.notes_list_model.set_list(nn) + self.notes_list_model.match_regexp = match_regexp + self.view.set_note_tally(len(nn), active_notes, len(self.notes_db.notes)) + + # we'll use this to keep track of the currently selected note + # we only use idx, because key could change from right under us. + self.selected_note_key = None + self.view.select_note(0) - # we'll use this to keep track of the currently selected note - # we only use idx, because key could change from right under us. - self.selected_note_key = None - self.view.select_note(0) - - if self.config.simplenote_sync: - self.view.after(0, self.sync_full) + if self.config.simplenote_sync: + self.view.after(0, self.sync_full) + except BaseException: + # Initialization failed. Stop all timers. + self.view.cancel_timers() + raise def main_loop(self): if not self.config.files_read: @@ -404,7 +409,11 @@ def poll_notifies(): self.notes_db.handle_notifies() self.view.after(0, poll_notifies) - self.view.main_loop() + try: + self.view.main_loop() + finally: + # Cancel all timers before stop this program. + self.view.cancel_timers() def observer_notes_db_change_note_status(self, notes_db, evt_type, evt): skey = self.selected_note_key @@ -855,8 +864,14 @@ def main(): config = Config(appdir_full_path) - controller = Controller(config) - controller.main_loop() + try: + controller = Controller(config) + controller.main_loop() + except tk.Ucs4NotSupportedError as e: + logging.error(str(e)) + import tkMessageBox + tkMessageBox.showerror('UCS-4 not supported', str(e)) + raise if __name__ == '__main__': diff --git a/nvpy/tk.py b/nvpy/tk.py index 48f1b79..4262b0a 100644 --- a/nvpy/tk.py +++ b/nvpy/tk.py @@ -1,10 +1,53 @@ -# nvPY: cross-platform note-taking app with simplenote syncing -# copyright 2012 by Charl P. Botha -# new BSD license - -# Tkinter and ttk documentation recommend pulling all symbols into client -# module namespace. I don't like that, so first pulling into this module -# tk, then can use tk.whatever in main module. - -from Tkinter import * -from ttk import * +# nvPY: cross-platform note-taking app with simplenote syncing +# copyright 2012 by Charl P. Botha +# new BSD license + +# Tkinter and ttk documentation recommend pulling all symbols into client +# module namespace. I don't like that, so first pulling into this module +# tk, then can use tk.whatever in main module. + +from Tkinter import * +from ttk import * + + +class Ucs4NotSupportedError(BaseException): + def __init__(self, char): + self.char = char + + def __str__(self): + return ( + 'non-BMP character {} is not supported. ' + 'Please rebuild python interpreter and libraries with UCS-4 support. ' + 'See https://github.com/cpbotha/nvpy/blob/master/docs/ucs-4.rst' + ).format(self.char) + + +def with_ucs4_error_handling(fn): + """ Catch the non-BMP character error and reraise the Ucs4NotSupportedError. """ + import functools + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except TclError as e: + import re + result = re.match(r'character (U\+[0-9a-f]+) is above the range \(U\+0000-U\+FFFF\) allowed by Tcl', str(e)) + if result: + char = result.group(1) + raise Ucs4NotSupportedError(char) + raise + + return wrapper + + +######################################################################## +# Apply the monkey patches for convert TclError to Ucs4NotSupportedError + +_Text = Text + + +class Text(_Text): + @with_ucs4_error_handling + def insert(self, *args, **kwargs): + return _Text.insert(self, *args, **kwargs) diff --git a/nvpy/utils.py b/nvpy/utils.py index e000750..f245e29 100644 --- a/nvpy/utils.py +++ b/nvpy/utils.py @@ -10,6 +10,8 @@ import threading from Queue import Queue, Empty as QueueEmpty +import tk + # first line with non-whitespace should be the title note_title_re = re.compile('\s*(.*)\n?') @@ -202,6 +204,7 @@ def __init__(self): def add_observer(self, evt_type, o): from .debug import wrap_buggy_function + o = tk.with_ucs4_error_handling(o) o = wrap_buggy_function(o) if evt_type not in self.observers: @@ -216,8 +219,7 @@ def notify_observers(self, evt_type, evt): if threading.current_thread() == self.MAIN_THREAD: for o in self.observers[evt_type]: - # invoke observers with ourselves as first param - o(self, evt_type, evt) + self.__invoke_observer(o, evt_type, evt) else: # Tkinter is not thread safe. so, observers must be executed on MAIN_THREAD. @@ -234,12 +236,15 @@ def handle_notifies(self): evt_type, evt = self.notifies.get_nowait() for o in self.observers[evt_type]: - # invoke observers with ourselves as first param - o(self, evt_type, evt) + self.__invoke_observer(o, evt_type, evt) except QueueEmpty: pass + def __invoke_observer(self, observer, event_type, event): + # invoke observers with ourselves as first param + observer(self, event_type, event) + def mute(self, evt_type): self.mutes[evt_type] = True diff --git a/nvpy/view.py b/nvpy/view.py index 927255a..28c0d19 100644 --- a/nvpy/view.py +++ b/nvpy/view.py @@ -11,6 +11,7 @@ import tkMessageBox import utils import webbrowser +import threading class WidgetRedirector: @@ -940,9 +941,12 @@ def __init__(self, config, notes_list_model): notes_list_model.add_observer('set:list', self.observer_notes_list) self.notes_list_model = notes_list_model + self.timer_ids_lock = threading.Lock() + self.timer_ids = set() self.root = None + tk.Tk.report_callback_exception = self.handle_unexpected_error self._create_ui() self._bind_events() @@ -958,6 +962,22 @@ def __init__(self, config, notes_list_model): self.search_entry.focus_set() + def handle_unexpected_error(self, *args): + # An unexpected error has occurred. The program MUST be stop immediately. + self.cancel_timers() + + err = args[1] + if isinstance(err, tk.Ucs4NotSupportedError): + title, msg = 'UCS-4 not supported', str(err) + else: + import traceback + stackTrace = ''.join(traceback.format_exception(*args)) + title, msg = "Unexpected Error", stackTrace + + logging.error(msg) + self.show_error(title, msg) + exit(1) + def askyesno(self, title, msg): return tkMessageBox.askyesno(title, msg) @@ -1154,7 +1174,7 @@ def _bind_events(self): self.pinned_checkbutton_var.trace('w', self.handler_pinned_checkbutton) - self.root.after(self.config.housekeeping_interval_ms, self.handler_housekeeper) + self.after(self.config.housekeeping_interval_ms, self.handler_housekeeper) def _create_menu(self): """Utility function to setup main menu. @@ -1658,7 +1678,7 @@ def handler_housekeeper(self): if refresh_notes_list: self.refresh_notes_list() - self.root.after(self.config.housekeeping_interval_ms, self.handler_housekeeper) + self.after(self.config.housekeeping_interval_ms, self.handler_housekeeper) except Exception as e: self.show_error('Housekeeper error', 'An error occurred during housekeeping.\n' + str(e)) raise @@ -1843,9 +1863,9 @@ def is_note_different(self, note): return True tags = note.get('tags', []) - + # get list of string tags from ui - tag_elements = self.note_existing_tags_frame.children.values() + tag_elements = self.note_existing_tags_frame.children.values() ui_tags = [element['text'].replace(' x', '') for element in tag_elements] if sorted(ui_tags) != sorted(tags): @@ -2054,4 +2074,21 @@ def word_count(self): self.show_info('Word Count', '%d words in total\n%d words in selection' % (tlen, slen)) def after(self, ms, callback): - self.root.after(ms, callback) + timer_id = 'dummy_value' + + def fn(): + with self.timer_ids_lock: + # timer_id is updated to actual value by self.root.after(). + self.timer_ids.remove(timer_id) + + callback() + + with self.timer_ids_lock: + timer_id = self.root.after(ms, fn) + self.timer_ids.add(timer_id) + return timer_id + + def cancel_timers(self): + with self.timer_ids_lock: + for timer_id in self.timer_ids: + self.root.after_cancel(timer_id)