def test(c): m = Mocker() menu = m.mock(ak.NSMenu) ctl = TextCommandController("<history>") for command in c.commands: ctl.add_command(command, None, menu) eq_(ctl.lookup(c.lookup), c.result)
def test(c): m = Mocker() menu = m.mock(ak.NSMenu) ctl = TextCommandController("<history>") for command in c.commands: ctl.add_command(command, None, menu) menu.insertItem_atIndex_(ANY, ANY) eq_(ctl.lookup_full_command(c.lookup), c.result)
def __init__(self, profile=None): if profile is None: profile = self.default_profile() self.profile_path = os.path.expanduser(profile) assert os.path.isabs(self.profile_path), \ 'profile path cannot be relative (%s)' % self.profile_path self._setup_profile = set() self.editors = [] self.path_opener = None self.config = Config(self.profile_path) self.context = ContextMap() self.syntax_factory = None state_dir = os.path.join(self.profile_path, const.STATE_DIR) command_history = CommandHistory(state_dir) self.text_commander = TextCommandController(command_history) register_value_transformers()
def test(c): m = Mocker() lg = m.replace("editxt.textcommand.log") mi = m.mock(ak.NSMenuItem) tv = m.mock(ak.NSTextView) tc = m.mock() tcc = TextCommandController("<history>") cmds = m.replace(tcc, 'commands') cmds.get(mi.tag() >> 42) >> (tc if c.has_command else None) if c.has_command: tc(tv, mi, None) if c.error: m.throw(Exception) lg.error("%s.execute failed", ANY, exc_info=True) with m: tcc.do_textview_command(tv, mi)
def test(c): m = Mocker() lg = m.replace("editxt.textcommand.log", passthrough=False) mi = m.mock(NSMenuItem) tv = m.mock(NSTextView) tc = m.mock(TextCommand) tcc = TextCommandController(None) cmds = m.replace(tcc.commands) cmd = cmds.get(mi.tag() >> 42) >> (tc if c.has_command else None) if c.has_command: cmd.execute(tv, mi) if c.error: m.throw(Exception) lg.error("%s.execute failed", ANY, exc_info=True) with m: tcc.do_textview_command(tv, mi)
def test(c): m = Mocker() lg = m.replace("editxt.textcommand.log") tv = m.mock(ak.NSTextView) tcc = TextCommandController("<history>") sel = "<selector>" callback = m.mock() handlers = m.replace(tcc, 'input_handlers') cmd = handlers.get(sel) >> (callback if c.has_selector else None) if c.has_selector: callback(tv, None, None) if c.error: m.throw(Exception) lg.error("%s failed", callback, exc_info=True) with m: result = tcc.do_textview_command_by_selector(tv, sel) eq_(result, c.result)
def test_CommandBar_get_history_concurrently(): with tempdir() as tmp: history = mod.CommandHistory(tmp) for item in reversed("abc"): history.append(item) editor = type("FakeEditor", (object, ), {})() commander = TextCommandController(history) bar1 = mod.CommandBar(editor, commander) bar2 = mod.CommandBar(editor, commander) bar3 = mod.CommandBar(editor, commander) bar4 = mod.CommandBar(editor, commander) eq_(bar1.get_history("x"), "a") eq_(bar2.get_history(""), "a") eq_(bar2.get_history("y"), "b") eq_(bar3.get_history(""), "a") eq_(bar3.get_history("a"), "b") eq_(bar3.get_history("z"), "c") # <-- "z" will move to 0 (with "b") history.append("b") # current index "a", "x" in new command buffer eq_(bar1.get_history("a"), "c") eq_(bar1.get_history("c"), None) eq_(bar1.get_history("c", True), "a") eq_(bar1.get_history("a", True), "b") eq_(bar1.get_history("b", True), "x") eq_(bar1.get_history("x", True), None) # current index "b", "y" at 0 eq_(bar2.get_history("B"), "y") # <-- "B" now at 0 eq_(bar2.get_history("y"), "c") eq_(bar2.get_history("c"), None) eq_(bar2.get_history("c", True), "y") eq_(bar2.get_history("y", True), "B") eq_(bar2.get_history("B", True), "") eq_(bar2.get_history("", True), None) # current index "c", "z" at 1 eq_(bar3.get_history("c"), None) eq_(bar3.get_history("C", True), "a") eq_(bar3.get_history("a"), "C") eq_(bar3.get_history("C", True), "a") eq_(bar3.get_history("a", True), "z") eq_(bar3.get_history("z", True), "") # <-- "z" moved to 0 eq_(bar3.get_history("", True), None) eq_(bar4.get_history("A", True), None) eq_(bar4.get_history("A"), "b") eq_(bar4.get_history("b"), "a") eq_(bar4.get_history("a"), "c") eq_(bar4.get_history("c"), None) eq_(bar4.get_history("c", True), "a") eq_(bar4.get_history("a", True), "b") eq_(bar4.get_history("b", True), "A") eq_(bar4.get_history("A", True), None)
def test(c): m = Mocker() lg = m.replace("editxt.textcommand.log") mi = m.mock(ak.NSMenuItem) tv = m.mock(ak.NSTextView) tc = m.mock() tcc = TextCommandController("<history>") cmds = m.replace(tcc, 'commands') cmds.get(mi.tag() >> 42) >> (tc if c.has_command else None) if c.has_command: if c.error: expect(tc.is_enabled(tv, mi)).throw(Exception) lg.error("%s.is_enabled failed", ANY, exc_info=True) else: tc.is_enabled(tv, mi) >> c.enabled with m: result = tcc.is_textview_command_enabled(tv, mi) eq_(result, c.enabled)
def test(c): m = Mocker() menu = m.mock(ak.NSMenu) ctl = TextCommandController("<history>") handlers = ctl.input_handlers = m.mock(dict) add = m.method(ctl.add_command) mod = m.mock(dict) m.method(ctl.iter_command_modules)() >> [("<path>", mod)] cmds = []; mod.get("text_menu_commands", []) >> cmds for i in range(c.commands): cmd = "<command %s>" % i add(cmd, "<path>", menu) cmds.append(cmd) hnds = mod.get("input_handlers", {}) >> {} for i in range(c.handlers): hnds["handle%s" % i] = "<handle %s>" % i handlers.update(hnds) with m: ctl.load_commands(menu)
def test_TextCommandController_init(): m = Mocker() menu = m.mock(ak.NSMenu) with m: ctl = TextCommandController("<history>") eq_(ctl.history, "<history>") eq_(ctl.commands, {}) eq_(ctl.commands_by_path, {}) eq_(ctl.input_handlers, {}) eq_(ctl.editems, {})
def test(c): m = Mocker() menu = m.mock(ak.NSMenu) ctl = TextCommandController("<history>") handlers = ctl.input_handlers = m.mock(dict) add = m.method(ctl.add_command) mod = m.mock(dict) m.method(ctl.iter_command_modules)() >> [("<path>", mod)] cmds = [] mod.get("text_menu_commands", []) >> cmds for i in range(c.commands): cmd = "<command %s>" % i add(cmd, "<path>", menu) cmds.append(cmd) hnds = mod.get("input_handlers", {}) >> {} for i in range(c.handlers): hnds["handle%s" % i] = "<handle %s>" % i handlers.update(hnds) with m: ctl.load_commands(menu)
def test(c): m = Mocker() menu = m.mock(NSMenu) mi_class = m.replace(NSMenuItem, passthrough=False) ctl = TextCommandController(menu) cmds = m.replace(ctl.commands) handlers = m.replace(ctl.input_handlers, passthrough=False) validate = m.method(ctl.validate_hotkey) cmd = m.mock(TextCommand) tag = cmd._TextCommandController__tag = ctl.tagger.next() + 1 validate(cmd.preferred_hotkey() >> "<hotkey>") >> ("<hotkey>", "<keymask>") mi = mi_class.alloc() >> m.mock(NSMenuItem) mi.initWithTitle_action_keyEquivalent_( cmd.title() >> "<title>", "performTextCommand:" ,"<hotkey>") >> mi mi.setKeyEquivalentModifierMask_("<keymask>") mi.setTag_(tag) menu.insertItem_atIndex_(mi, tag) ctl.commands[tag] = cmd with m: ctl.add_command(cmd, None)
def test_CommandBar_history_reset_on_execute(): from editxt.document import TextDocumentView from editxt.textcommand import CommandHistory, TextCommandController with tempdir() as tmp: m = Mocker() editor = m.mock() history = CommandHistory(tmp) commander = TextCommandController(history) bar = mod.CommandBar(editor, commander) args = ["cmd"] view = editor.current_view >> m.mock(TextDocumentView) view.text_view >> '<view>' @command def cmd(textview, sender, args): pass commander.add_command(cmd, None, None) with m: bar.get_history("cmd") bar.execute("cmd") eq_(bar.history_view, None) eq_(list(history), ["cmd"])
def test(c): m = Mocker() menu = m.mock(ak.NSMenu) mi_class = m.replace(ak, 'NSMenuItem') ctl = TextCommandController("<history>") handlers = m.replace(ctl, 'input_handlers') validate = m.method(ctl.validate_hotkey) cmd = m.mock() cmd.names >> [] cmd.lookup_with_arg_parser >> False tag = cmd._TextCommandController__tag = next(ctl.tagger) + 1 validate(cmd.hotkey >> "<hotkey>") >> ("<hotkey>", "<keymask>") mi = mi_class.alloc() >> m.mock(ak.NSMenuItem) (cmd.title << "<title>").count(2) mi.initWithTitle_action_keyEquivalent_( '<title>', "performTextCommand:" ,"<hotkey>") >> mi mi.setKeyEquivalentModifierMask_("<keymask>") mi.setTag_(tag) menu.insertItem_atIndex_(mi, tag) with m: ctl.add_command(cmd, None, menu) assert ctl.commands[tag] is cmd, (ctl.commands[tag], cmd)
def test(c): m = Mocker() menu = m.mock(ak.NSMenu) mi_class = m.replace(ak, 'NSMenuItem') ctl = TextCommandController("<history>") handlers = m.replace(ctl, 'input_handlers') validate = m.method(ctl.validate_hotkey) cmd = m.mock() cmd.names >> [] cmd.lookup_with_arg_parser >> False tag = cmd._TextCommandController__tag = next(ctl.tagger) + 1 validate(cmd.hotkey >> "<hotkey>") >> ("<hotkey>", "<keymask>") mi = mi_class.alloc() >> m.mock(ak.NSMenuItem) (cmd.title << "<title>").count(2) mi.initWithTitle_action_keyEquivalent_( '<title>', "performTextCommand:", "<hotkey>") >> mi mi.setKeyEquivalentModifierMask_("<keymask>") mi.setTag_(tag) menu.insertItem_atIndex_(mi, tag) with m: ctl.add_command(cmd, None, menu) assert ctl.commands[tag] is cmd, (ctl.commands[tag], cmd)
def test(nav): with tempdir() as tmp: history = mod.CommandHistory(tmp) for item in reversed("abc"): history.append(item) editor = type("FakeEditor", (object, ), {})() commander = TextCommandController(history) bar = mod.CommandBar(editor, commander) for input, direction, history in nav: dirchar = "v" if direction else "A" print("{}({!r}, {!r})".format(dirchar, input, history)) eq_(bar.get_history(input, forward=direction), history)
class Application(object): def __init__(self, profile=None): if profile is None: profile = self.default_profile() self.profile_path = os.path.expanduser(profile) assert os.path.isabs(self.profile_path), \ 'profile path cannot be relative (%s)' % self.profile_path self._setup_profile = set() self.editors = [] self.path_opener = None self.config = Config(self.profile_path) self.context = ContextMap() self.syntax_factory = None state_dir = os.path.join(self.profile_path, const.STATE_DIR) command_history = CommandHistory(state_dir) self.text_commander = TextCommandController(command_history) register_value_transformers() @classmethod def name(cls): return fn.NSBundle.mainBundle().objectForInfoDictionaryKey_("CFBundleName") @classmethod def resource_path(cls): return fn.NSBundle.mainBundle().resourcePath() @classmethod def default_profile(cls): return '~/.' + cls.name().lower() def init_syntax_definitions(self): from editxt.syntax import SyntaxFactory self.syntax_factory = sf = SyntaxFactory() paths = [(self.resource_path(), False), (self.profile_path, True)] for path, log_info in paths: path = os.path.join(path, const.SYNTAX_DEFS_DIR) sf.load_definitions(path, log_info) sf.index_definitions() @property def syntaxdefs(self): return self.syntax_factory.definitions def application_will_finish_launching(self, app, doc_ctrl): self.init_syntax_definitions() self.text_commander.load_commands(doc_ctrl.textMenu) states = list(self.iter_saved_editor_states()) if states: for state in reversed(states): self.create_editor(state) else: self.create_editor() def create_editor(self, state=None): from editxt.editor import EditorWindowController, Editor wc = EditorWindowController.alloc().initWithWindowNibName_("EditorWindow") ed = Editor(self, wc, state) wc.editor = ed self.editors.append(ed) wc.showWindow_(self) return ed def open_path_dialog(self): if self.path_opener is None: opc = OpenPathController.alloc().initWithWindowNibName_("OpenPath") opc.showWindow_(self) self.path_opener = opc else: self.path_opener.window().makeKeyAndOrderFront_(self) self.path_opener.populateWithClipboard() def new_project(self): editor = self.current_editor() if editor is not None: return editor.new_project() def open_documents_with_paths(self, paths): from editxt.document import TextDocumentView editor = self.current_editor() if editor is None: editor = self.create_editor() focus = None views = [] for path in paths: if os.path.isfile(path) or not os.path.exists(path): view = TextDocumentView.create_with_path(path) focus = editor.add_document_view(view) views.append(view) else: log.info("cannot open path: %s", path) if focus is not None: editor.current_view = focus return views def open_config_file(self): views = self.open_documents_with_paths([self.config.path]) if not os.path.exists(self.config.path): assert len(views) == 1, views views[0].document.text = self.config.default_config def open_error_log(self, set_current=True): from editxt.document import TextDocumentView doc = errlog.document try: view = next(self.iter_views_of_document(doc)) except StopIteration: editor = self.current_editor() if editor is None: editor = self.create_editor() view = TextDocumentView.create_with_document(doc) editor.add_document_view(view) if set_current: editor.current_view = view else: if set_current: self.set_current_document_view(view) def iter_dirty_documents(self): seen = set() for editor in self.iter_editors(): for proj in editor.projects: dirty = False for view in proj.dirty_documents(): if view.document.id not in seen: seen.add(view.document.id) yield view dirty = True if dirty: yield proj def set_current_document_view(self, doc_view): ed = self.find_editor_with_document_view(doc_view) ed.current_view = doc_view def close_current_document(self): editor = self.current_editor() if editor is not None: view = editor.current_view if view is not None: view.perform_close(editor) def iter_views_of_document(self, doc): for editor in self.iter_editors(): for view in editor.iter_views_of_document(doc): yield view # def find_view_with_document(self, doc): # """find a view of the given document # # Returns a view in the topmost window with the given document, or None # if there are no views of this document. # """ # try: # return next(self.iter_views_of_document(doc)) # except StopIteration: # return None def count_views_of_document(self, doc): return len(list(self.iter_views_of_document(doc))) def iter_editors_with_view_of_document(self, document): for editor in self.iter_editors(): try: next(editor.iter_views_of_document(document)) except StopIteration: pass else: yield editor def find_editor_with_document_view(self, doc_view): for editor in self.iter_editors(): for proj in editor.projects: for dv in proj.documents(): if dv is doc_view: return editor return None def find_editors_with_project(self, project): return [ed for ed in self.editors if project in ed.projects] def find_project_with_path(self, path): for ed in self.editors: proj = ed.find_project_with_path(path) if proj is not None: return proj return None def find_item_with_id(self, ident): # HACK slow implementation, violates encapsulation for editor in self.editors: for proj in editor.projects: if proj.id == ident: return proj for doc in proj.documents(): if doc.id == ident: return doc return None def item_changed(self, item, change_type=None): for editor in self.editors: editor.item_changed(item, change_type) def iter_editors(self, app=None): """Iterate over editors in on-screen z-order starting with the front-most editor window""" from editxt.editor import EditorWindowController if app is None: app = ak.NSApp() z_ordered_eds = set() for win in app.orderedWindows(): wc = win.windowController() if isinstance(wc, EditorWindowController) and wc.editor in self.editors: z_ordered_eds.add(wc.editor) yield wc.editor for ed in self.editors: if ed not in z_ordered_eds: yield ed def add_editor(self, editor): self.editors.append(editor) def current_editor(self): try: return next(self.iter_editors()) except StopIteration: return None def discard_editor(self, editor): try: self.editors.remove(editor) except ValueError: pass editor.close() def setup_profile(self, editors=False): """Ensure that profile dir exists This will create the profile directory if it does not exist. :returns: True on success, otherwise False. """ if not ('.' in self._setup_profile or os.path.isdir(self.profile_path)): try: os.mkdir(self.profile_path) except OSError: log.error('cannot create %s', self.profile_path, exc_info=True) return False self._setup_profile.add('.') if editors and 'editors' not in self._setup_profile: state_path = os.path.join(self.profile_path, const.STATE_DIR) if not os.path.exists(state_path): try: os.mkdir(state_path) except OSError: log.error('cannot create %s', state_path, exc_info=True) return False self._setup_profile.add('editors') return True def _legacy_editor_states(self): # TODO remove once all users have upraded to new state persistence def pythonify(value): if isinstance(value, (str, int, float, bool)): return value if isinstance(value, (dict, fn.NSDictionary)): return {k: pythonify(v) for k, v in value.items()} if isinstance(value, (list, fn.NSArray)): return [pythonify(v) for v in value] raise ValueError('unknown value type: {} {}' .format(type(value), repr(value))) defaults = fn.NSUserDefaults.standardUserDefaults() serials = defaults.arrayForKey_(const.WINDOW_CONTROLLERS_DEFAULTS_KEY) settings = defaults.arrayForKey_(const.WINDOW_SETTINGS_DEFAULTS_KEY) serials = list(reversed(serials)) for serial, setting in zip(serials, chain(settings, repeat(None))): try: state = dict(serial) if setting is not None: state['window_settings'] = setting yield pythonify(state) except Exception: log.warn('cannot load legacy state: %r', serial, exc_info=True) def iter_saved_editor_states(self): """Yield saved editor states""" state_path = os.path.join(self.profile_path, const.STATE_DIR) if not os.path.exists(state_path): if self.profile_path == os.path.expanduser(self.default_profile()): # TODO remove once all users have upraded for state in self._legacy_editor_states(): yield state return state_glob = os.path.join(state_path, const.EDITOR_STATE.format('*')) for path in sorted(glob.glob(state_glob)): try: with open(path) as f: yield load_yaml(f) except Exception: log.error('cannot load %s', path, exc_info=True) def save_editor_state(self, editor, ident=None): """Save a single editor's state :param editor: The editor with state to be saved. :param ident: The identifier to use when saving editor state. It is assumed that the profile has been setup when this argument is provided; ``editor.id`` will be used when not provided. :returns: The name of the state file. """ if ident is None: raise NotImplementedError ident = editor.id self.setup_profile(editors=True) state_name = const.EDITOR_STATE.format(ident) state_file = os.path.join( self.profile_path, const.STATE_DIR, state_name) state = editor.state try: with atomicfile(state_file, encoding="utf-8") as fh: dump_yaml(state, fh) except Exception: log.error('cannot write %s\n%s\n', state_file, state, exc_info=True) return state_name def save_editor_states(self): """Save all editors' states""" state_path = os.path.join(self.profile_path, const.STATE_DIR) old_glob = os.path.join(state_path, const.EDITOR_STATE.format('*')) old = {os.path.basename(name) for name in glob.glob(old_glob)} for i, editor in enumerate(self.iter_editors()): state_name = self.save_editor_state(editor, i) old.discard(state_name) for name in old: state_file = os.path.join(state_path, name) try: os.remove(state_file) except Exception: log.error('cannot remove %s', state_file, exc_info=True) def app_will_terminate(self, app): self.save_editor_states()
class Application(object): def __init__(self, profile=None): if profile is None: profile = self.default_profile() self.profile_path = os.path.expanduser(profile) assert os.path.isabs(self.profile_path), \ 'profile path cannot be relative (%s)' % self.profile_path self._setup_profile = set() self.editors = [] self.path_opener = None self.config = Config(self.profile_path) self.context = ContextMap() self.syntax_factory = None state_dir = os.path.join(self.profile_path, const.STATE_DIR) command_history = CommandHistory(state_dir) self.text_commander = TextCommandController(command_history) register_value_transformers() @classmethod def name(cls): return fn.NSBundle.mainBundle().objectForInfoDictionaryKey_( "CFBundleName") @classmethod def resource_path(cls): return fn.NSBundle.mainBundle().resourcePath() @classmethod def default_profile(cls): return '~/.' + cls.name().lower() def init_syntax_definitions(self): from editxt.syntax import SyntaxFactory self.syntax_factory = sf = SyntaxFactory() paths = [(self.resource_path(), False), (self.profile_path, True)] for path, log_info in paths: path = os.path.join(path, const.SYNTAX_DEFS_DIR) sf.load_definitions(path, log_info) sf.index_definitions() @property def syntaxdefs(self): return self.syntax_factory.definitions def application_will_finish_launching(self, app, doc_ctrl): self.init_syntax_definitions() self.text_commander.load_commands(doc_ctrl.textMenu) states = list(self.iter_saved_editor_states()) if states: for state in reversed(states): self.create_editor(state) else: self.create_editor() def create_editor(self, state=None): from editxt.editor import EditorWindowController, Editor wc = EditorWindowController.alloc().initWithWindowNibName_( "EditorWindow") ed = Editor(self, wc, state) wc.editor = ed self.editors.append(ed) wc.showWindow_(self) return ed def open_path_dialog(self): if self.path_opener is None: opc = OpenPathController.alloc().initWithWindowNibName_("OpenPath") opc.showWindow_(self) self.path_opener = opc else: self.path_opener.window().makeKeyAndOrderFront_(self) self.path_opener.populateWithClipboard() def new_project(self): editor = self.current_editor() if editor is not None: return editor.new_project() def open_documents_with_paths(self, paths): from editxt.document import TextDocumentView editor = self.current_editor() if editor is None: editor = self.create_editor() focus = None views = [] for path in paths: if os.path.isfile(path) or not os.path.exists(path): view = TextDocumentView.create_with_path(path) focus = editor.add_document_view(view) views.append(view) else: log.info("cannot open path: %s", path) if focus is not None: editor.current_view = focus return views def open_config_file(self): views = self.open_documents_with_paths([self.config.path]) if not os.path.exists(self.config.path): assert len(views) == 1, views views[0].document.text = self.config.default_config def open_error_log(self, set_current=True): from editxt.document import TextDocumentView doc = errlog.document try: view = next(self.iter_views_of_document(doc)) except StopIteration: editor = self.current_editor() if editor is None: editor = self.create_editor() view = TextDocumentView.create_with_document(doc) editor.add_document_view(view) if set_current: editor.current_view = view else: if set_current: self.set_current_document_view(view) def iter_dirty_documents(self): seen = set() for editor in self.iter_editors(): for proj in editor.projects: dirty = False for view in proj.dirty_documents(): if view.document.id not in seen: seen.add(view.document.id) yield view dirty = True if dirty: yield proj def set_current_document_view(self, doc_view): ed = self.find_editor_with_document_view(doc_view) ed.current_view = doc_view def close_current_document(self): editor = self.current_editor() if editor is not None: view = editor.current_view if view is not None: view.perform_close(editor) def iter_views_of_document(self, doc): for editor in self.iter_editors(): for view in editor.iter_views_of_document(doc): yield view # def find_view_with_document(self, doc): # """find a view of the given document # # Returns a view in the topmost window with the given document, or None # if there are no views of this document. # """ # try: # return next(self.iter_views_of_document(doc)) # except StopIteration: # return None def count_views_of_document(self, doc): return len(list(self.iter_views_of_document(doc))) def iter_editors_with_view_of_document(self, document): for editor in self.iter_editors(): try: next(editor.iter_views_of_document(document)) except StopIteration: pass else: yield editor def find_editor_with_document_view(self, doc_view): for editor in self.iter_editors(): for proj in editor.projects: for dv in proj.documents(): if dv is doc_view: return editor return None def find_editors_with_project(self, project): return [ed for ed in self.editors if project in ed.projects] def find_project_with_path(self, path): for ed in self.editors: proj = ed.find_project_with_path(path) if proj is not None: return proj return None def find_item_with_id(self, ident): # HACK slow implementation, violates encapsulation for editor in self.editors: for proj in editor.projects: if proj.id == ident: return proj for doc in proj.documents(): if doc.id == ident: return doc return None def item_changed(self, item, change_type=None): for editor in self.editors: editor.item_changed(item, change_type) def iter_editors(self, app=None): """Iterate over editors in on-screen z-order starting with the front-most editor window""" from editxt.editor import EditorWindowController if app is None: app = ak.NSApp() z_ordered_eds = set() for win in app.orderedWindows(): wc = win.windowController() if isinstance( wc, EditorWindowController) and wc.editor in self.editors: z_ordered_eds.add(wc.editor) yield wc.editor for ed in self.editors: if ed not in z_ordered_eds: yield ed def add_editor(self, editor): self.editors.append(editor) def current_editor(self): try: return next(self.iter_editors()) except StopIteration: return None def discard_editor(self, editor): try: self.editors.remove(editor) except ValueError: pass editor.close() def setup_profile(self, editors=False): """Ensure that profile dir exists This will create the profile directory if it does not exist. :returns: True on success, otherwise False. """ if not ('.' in self._setup_profile or os.path.isdir(self.profile_path)): try: os.mkdir(self.profile_path) except OSError: log.error('cannot create %s', self.profile_path, exc_info=True) return False self._setup_profile.add('.') if editors and 'editors' not in self._setup_profile: state_path = os.path.join(self.profile_path, const.STATE_DIR) if not os.path.exists(state_path): try: os.mkdir(state_path) except OSError: log.error('cannot create %s', state_path, exc_info=True) return False self._setup_profile.add('editors') return True def _legacy_editor_states(self): # TODO remove once all users have upraded to new state persistence def pythonify(value): if isinstance(value, (str, int, float, bool)): return value if isinstance(value, (dict, fn.NSDictionary)): return {k: pythonify(v) for k, v in value.items()} if isinstance(value, (list, fn.NSArray)): return [pythonify(v) for v in value] raise ValueError('unknown value type: {} {}'.format( type(value), repr(value))) defaults = fn.NSUserDefaults.standardUserDefaults() serials = defaults.arrayForKey_(const.WINDOW_CONTROLLERS_DEFAULTS_KEY) settings = defaults.arrayForKey_(const.WINDOW_SETTINGS_DEFAULTS_KEY) serials = list(reversed(serials)) for serial, setting in zip(serials, chain(settings, repeat(None))): try: state = dict(serial) if setting is not None: state['window_settings'] = setting yield pythonify(state) except Exception: log.warn('cannot load legacy state: %r', serial, exc_info=True) def iter_saved_editor_states(self): """Yield saved editor states""" state_path = os.path.join(self.profile_path, const.STATE_DIR) if not os.path.exists(state_path): if self.profile_path == os.path.expanduser(self.default_profile()): # TODO remove once all users have upraded for state in self._legacy_editor_states(): yield state return state_glob = os.path.join(state_path, const.EDITOR_STATE.format('*')) for path in sorted(glob.glob(state_glob)): try: with open(path) as f: yield load_yaml(f) except Exception: log.error('cannot load %s', path, exc_info=True) def save_editor_state(self, editor, ident=None): """Save a single editor's state :param editor: The editor with state to be saved. :param ident: The identifier to use when saving editor state. It is assumed that the profile has been setup when this argument is provided; ``editor.id`` will be used when not provided. :returns: The name of the state file. """ if ident is None: raise NotImplementedError ident = editor.id self.setup_profile(editors=True) state_name = const.EDITOR_STATE.format(ident) state_file = os.path.join(self.profile_path, const.STATE_DIR, state_name) state = editor.state try: with atomicfile(state_file, encoding="utf-8") as fh: dump_yaml(state, fh) except Exception: log.error('cannot write %s\n%s\n', state_file, state, exc_info=True) return state_name def save_editor_states(self): """Save all editors' states""" state_path = os.path.join(self.profile_path, const.STATE_DIR) old_glob = os.path.join(state_path, const.EDITOR_STATE.format('*')) old = {os.path.basename(name) for name in glob.glob(old_glob)} for i, editor in enumerate(self.iter_editors()): state_name = self.save_editor_state(editor, i) old.discard(state_name) for name in old: state_file = os.path.join(state_path, name) try: os.remove(state_file) except Exception: log.error('cannot remove %s', state_file, exc_info=True) def app_will_terminate(self, app): self.save_editor_states()
def test_TextCommandController_validate_hotkey(): tc = TextCommandController("<history>") eq_(tc.validate_hotkey(None), ("", 0)) eq_(tc.validate_hotkey(("a", 1)), ("a", 1)) assert_raises(AssertionError, tc.validate_hotkey, ("a", "b", "c"))