示例#1
0
 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)
示例#2
0
 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)
示例#3
0
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()
示例#4
0
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()