Skip to content

Commit

Permalink
Merge branch 'ucs4'
Browse files Browse the repository at this point in the history
  • Loading branch information
yuuki0xff committed Aug 26, 2019
2 parents ddfc18a + 992a40c commit 12461aa
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 80 deletions.
137 changes: 76 additions & 61 deletions nvpy/nvpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import re
import collections

import tk
from utils import KeyValueObject, SubjectMixin
import view
import webbrowser
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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__':
Expand Down
63 changes: 53 additions & 10 deletions nvpy/tk.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
# nvPY: cross-platform note-taking app with simplenote syncing
# copyright 2012 by Charl P. Botha <cpbotha@vxlabs.com>
# 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 <cpbotha@vxlabs.com>
# 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)
13 changes: 9 additions & 4 deletions nvpy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?')

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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

Expand Down
47 changes: 42 additions & 5 deletions nvpy/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import tkMessageBox
import utils
import webbrowser
import threading


class WidgetRedirector:
Expand Down Expand Up @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

0 comments on commit 12461aa

Please sign in to comment.