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(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)
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()