def test_Theme_get_syntax_color(): config = Config(None, { "theme": { "text_color": ColorString("000000"), "syntax": { "default": { "builtin": ColorString("bbbbbb"), "comment": ColorString("cccccc"), "keyword": ColorString("eeeeee"), "string": ColorString("accafe"), }, "B": { "builtin": ColorString("BBBBBB"), "keyword": ColorString("EEEEEE"), }, }, }}) config.data = {"theme": {"syntax": { "default": { "comment": "112233", "keyword": "445566", }, "C": { "builtin": "665544", "keyword": "332211", }, }}} theme = mod.Theme(config) get_color = lambda v: v def test(name, color): with replattr(mod, "get_color", get_color, sigcheck=False): eq_(theme.get_syntax_color(name), color) yield test, "A builtin", "bbbbbb" yield test, "A comment", "112233" yield test, "A keyword", "445566" yield test, "A string", "accafe" yield test, "B builtin", "BBBBBB" yield test, "B comment", "112233" yield test, "B keyword", "EEEEEE" yield test, "B string", "accafe" yield test, "C builtin", "665544" yield test, "C comment", "112233" yield test, "C keyword", "332211" yield test, "C string", "accafe"
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 __init__(self, profile=None): self.launch_fault = False self.launching = True 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.documents = DocumentController(self) self.terminating = False self.errlog = ErrorLog(self) self.errlog_handler = LogViewHandler(self) self.errlog_handler.setLevel(logging.INFO) self.errlog_handler.setFormatter( logging.Formatter("%(levelname).7s %(name)s - %(message)s")) with self.logger(): self.panels = [] self._setup_profile = set() self.windows = [] self.path_opener = None self.config_callbacks = WeakValueDictionary() self.config = Config( os.path.join(self.profile_path, const.CONFIG_FILENAME)) logging_config = self.config["logging_config"] if logging_config: try: logging.config.dictConfig(logging_config) log.debug("logging config: %r", logging_config) except Exception: log.exception("bad logging config: %r", logging_config) self.context = ContextMap() self.syntax_factory = None self.theme = Theme(self.config) state_dir = os.path.join(self.profile_path, const.STATE_DIR) command_history = CommandHistory(state_dir) self.text_commander = CommandManager(command_history, app=self)
class Application(object): def __init__(self, profile=None): self.launch_fault = False self.launching = True 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.documents = DocumentController(self) self.terminating = False self.errlog = ErrorLog(self) self.errlog_handler = LogViewHandler(self) self.errlog_handler.setLevel(logging.INFO) self.errlog_handler.setFormatter( logging.Formatter("%(levelname).7s %(name)s - %(message)s")) with self.logger(): self.panels = [] self._setup_profile = set() self.windows = [] self.path_opener = None self.config_callbacks = WeakValueDictionary() self.config = Config( os.path.join(self.profile_path, const.CONFIG_FILENAME)) logging_config = self.config["logging_config"] if logging_config: try: logging.config.dictConfig(logging_config) log.debug("logging config: %r", logging_config) except Exception: log.exception("bad logging config: %r", logging_config) self.context = ContextMap() self.syntax_factory = None self.theme = Theme(self.config) state_dir = os.path.join(self.profile_path, const.STATE_DIR) command_history = CommandHistory(state_dir) self.text_commander = CommandManager(command_history, app=self) @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() @contextmanager def logger(self): root = logging.getLogger() root.addHandler(self.errlog_handler) try: yield self.errlog finally: root.removeHandler(self.errlog_handler) 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 @property def default_font(self, cache=[]): """Get the default application font Uses the value specified in the config file (all fields are optinal): ``` font: face: Inconsolata size: 13 smooth: true ``` Falling back to the default system fixed width font for unspecified or unknown values. :returns: A `editxt.datatypes.Font` instance. """ cfg = self.config["font"] key = (cfg["face"], cfg["size"], cfg["smooth"]) if not (cache and cache[0] == key): cache[:] = [key, get_font(*key)] return cache[1] def reload_config(self): old_theme = self.config.get("theme") self.config.reload() self.theme.reset() class event: theme_changed = self.config.get("theme") != old_theme for callback in self.config_callbacks: # without call_later this could cause # [NSMachPort sendBeforeDate:] destination port invalid # due to forced syntax highlighting taking a long time call_later(0, callback, event=event) def on_reload_config(self, callback, owner): self.config_callbacks[callback] = owner def application_will_finish_launching(self, app, delegate): self.init_syntax_definitions() self.text_commander.load_commands(delegate.textMenu) self.text_commander.load_shortcuts(delegate.shortcutsMenu) states = list(self.iter_saved_window_states()) errors = [] if states: for state in reversed(states): if isinstance(state, StateLoadFailure): errors.append(state) else: self.create_window(state) else: self.create_window() self.launching = False if self.launch_fault or errors: self.open_error_log(set_current=False) def create_window(self, state=None): from editxt.window import Window if state is None: state_dir = os.path.join(self.profile_path, const.STATE_DIR, const.CLOSED_DIR) for x, path in enumerate_state_paths(state_dir, reverse=True): try: with open(path, encoding="utf-8") as f: state = load_yaml(f) except Exception: log.warn("cannot load editor state: %s", path) continue try: os.remove(path) except Exception: log.exception('cannot remove %s', path) break window = Window(self, state) self.windows.append(window) window.show(self) return window def open_path_dialog(self): if self.path_opener is None: opc = OpenPathController(self) opc.showWindow_(self) self.path_opener = opc else: self.path_opener.window().makeKeyAndOrderFront_(self) self.path_opener.populateWithClipboard() def new_document(self): self.open_documents_with_paths([None]) def new_project(self): window = self.current_window() if window is not None: return window.new_project() def document_with_path(self, path): """Get a text document with the given path Documents returned by this method have been added to the document controllers list of documents. """ from editxt.document import TextDocument docs = self.documents if path is None: # untitled document will get associated with DocumentController on save doc = TextDocument(self) else: doc = docs.get_document(path) return doc def open_documents_with_paths(self, paths): window = self.current_window() if window is None: window = self.create_window() return window.open_paths(paths) def open_config_file(self): items = self.open_documents_with_paths([self.config.path]) assert len(items) == 1, items editor = items[0] assert editor.file_path == self.config.path, \ (editor.file_path, self.config.path) if not (os.path.exists(editor.file_path) or editor.document.text): editor.document.text = self.config.default_config def open_error_log(self, set_current=True): """Open the error log document This method should only be called while this application's ``logger`` context manager is active. """ doc = self.errlog.document try: editor = next(self.iter_editors_of_document(doc)) except StopIteration: window = self.current_window() if window is None: window = self.create_window() window.insert_items([doc], focus=set_current) else: if set_current: self.set_current_editor(editor) def get_internal_document(self, name): if name == "errlog": return self.errlog.document raise Error("unknown internal document: {}".format(name)) def iter_dirty_editors(self): seen = set() for window in self.iter_windows(): for proj in window.projects: dirty = False for editor in proj.dirty_editors(): if editor.document.id not in seen: seen.add(editor.document.id) yield editor dirty = True if dirty: yield proj def set_current_editor(self, editor): window = self.find_window_with_editor(editor) window.current_editor = editor def close_current_document(self): window = self.current_window() if window is not None: editor = window.current_editor if editor is not None: window.close_item(editor) def document_closed(self, document): """Remove document from the list of open documents""" self.documents.discard(document) def iter_editors_of_document(self, doc): for window in self.iter_windows(): for editor in window.iter_editors_of_document(doc): yield editor def iter_windows_with_editor_of_document(self, document): for window in self.iter_windows(): try: next(window.iter_editors_of_document(document)) except StopIteration: pass else: yield window def find_window_with_editor(self, editor): for window in self.iter_windows(): for proj in window.projects: for ed in proj.editors: if ed is editor: return window return None def find_windows_with_project(self, project): return [ed for ed in self.windows if project in ed.projects] def find_project_with_path(self, path): for ed in self.windows: 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 window in self.windows: for proj in window.projects: if proj.id == ident: return proj for doc in proj.editors: if doc.id == ident: return doc return None def iter_windows(self, app=None): """Iterate over windows in on-screen z-order starting with the front-most window""" from editxt.window import WindowController if app is None: app = ak.NSApp() z_ordered_eds = set() for win in app.orderedWindows(): wc = win.windowController() if isinstance(wc, WindowController) and wc.window_ in self.windows: z_ordered_eds.add(wc.window_) yield wc.window_ for ed in self.windows: if ed not in z_ordered_eds: yield ed def add_window(self, window): self.windows.append(window) def current_window(self): try: return next(self.iter_windows()) except StopIteration: return None def discard_window(self, window): try: self.windows.remove(window) except ValueError: pass if self.setup_profile(windows=True) and not self.terminating: state_dir = os.path.join(self.profile_path, const.STATE_DIR, const.CLOSED_DIR) ident = next(enumerate_state_paths(state_dir, reverse=True), [-1])[0] + 1 self.save_window_state(window, state_dir, ident) window.close() def setup_profile(self, windows=False): """Ensure that profile dir exists This will create the profile directory if it does not exist. :returns: True on success, otherwise False. """ def setup(path, name): if name not in self._setup_profile and not os.path.isdir(path): try: os.mkdir(path) except OSError: log.error('cannot create %s', path, exc_info=True) return False self._setup_profile.add(name) return True state_path = os.path.join(self.profile_path, const.STATE_DIR) closed_path = os.path.join(state_path, const.CLOSED_DIR) return setup(self.profile_path, ".") and ( not windows or (setup(state_path, "editor") and setup(closed_path, "closed")) ) def _legacy_window_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) or [] settings = defaults.arrayForKey_(const.WINDOW_SETTINGS_DEFAULTS_KEY) or [] 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_window_states(self): """Yield saved window states""" state_dir = os.path.join(self.profile_path, const.STATE_DIR) if not os.path.exists(state_dir): if self.profile_path == os.path.expanduser(self.default_profile()): # TODO remove once all users have upgraded for state in self._legacy_window_states(): yield state return for x, path in enumerate_state_paths(state_dir): try: with open(path, encoding="utf-8") as f: yield load_yaml(f) except Exception: log.error('cannot load %s', path, exc_info=True) yield StateLoadFailure(path) def save_window_state(self, window, state_dir, ident): """Save a single window's state :param window: The window with state to be saved. :param state_dir: The directory in which to write the state file. :param ident: The identifier to use when saving window state. :returns: The name of the state file. """ state_name = const.EDITOR_STATE.format(ident) state_file = os.path.join(state_dir, state_name) state = window.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_window_states(self): """Save all windows' states""" state_dir = os.path.join(self.profile_path, const.STATE_DIR) old = {os.path.basename(x[1]) for x in enumerate_state_paths(state_dir)} for i, window in enumerate(self.iter_windows()): if i == 0: self.setup_profile(windows=True) state_name = self.save_window_state(window, state_dir, i) old.discard(state_name) for name in old: state_file = os.path.join(state_dir, name) try: os.remove(state_file) except Exception: log.error('cannot remove %s', state_file, exc_info=True) def should_terminate(self, callback): """Check if application can terminate :param callback: A callback to be used as the return value to signal a delayed termination. If this object is returned it will be called with a single boolean argument at some point in the future when it is known if the application should terminate or not. :returns: ``True``, ``False`` or ``callback``. ``True`` means the application should terminate immediately. ``False`` means the application should not terminate, and ``callback`` means the application will perform an extended check to determine if it should terminate before calling ``callback`` with either ``True`` or ``False``. """ if next(self.iter_dirty_editors(), None) is not None: self.async_interactive_close(self.iter_dirty_editors(), callback) return callback return True @staticmethod def async_interactive_close(dirty_editors, callback): """Visit each dirty document and prompt for close Calls ``callback(False)`` if any unsaved document cancels the close operation. Otherwise calls ``callback(True)``. """ class RecursiveCall(Exception): pass def continue_closing(): def _callback(ok_to_close): if not ok_to_close: callback(False) return if recursive_call: raise RecursiveCall() continue_closing() recursive_call = True for editor in dirty_editors: try: editor.should_close(_callback) except RecursiveCall: continue except Exception: log.exception("termination sequence failed") callback(False) return recursive_call = False return callback(True) dirty_editors = iter(dirty_editors) continue_closing() def will_terminate(self): self.terminating = True self.save_window_states()