Files
atom/src/theme-manager.coffee
Nathan Sobo 99b08826dd Style non-shadow DOM editor with it’s own style sheet
Trying to style both modes of the text editor with the same style sheet
is proving to be more trouble than it’s worth. This prepares the shadow
DOM style sheet to diverge more radically to enable the background color
to be overridden from the outside more easily.
2014-11-13 16:04:24 -08:00

362 lines
12 KiB
CoffeeScript

path = require 'path'
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter, Disposable} = require 'event-kit'
{File} = require 'pathwatcher'
fs = require 'fs-plus'
Q = require 'q'
{deprecate} = require 'grim'
Package = require './package'
# Extended: Handles loading and activating available themes.
#
# An instance of this class is always available as the `atom.themes` global.
module.exports =
class ThemeManager
EmitterMixin.includeInto(this)
constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) ->
@emitter = new Emitter
@styleSheetDisposablesBySourcePath = {}
@lessCache = null
@initialLoadComplete = false
@packageManager.registerPackageActivator(this, ['theme'])
@sheetsByStyleElement = new WeakMap
stylesElement = document.head.querySelector('atom-styles')
stylesElement.onDidAddStyleElement @styleElementAdded.bind(this)
stylesElement.onDidRemoveStyleElement @styleElementRemoved.bind(this)
stylesElement.onDidUpdateStyleElement @styleElementUpdated.bind(this)
styleElementAdded: (styleElement) ->
{sheet} = styleElement
@sheetsByStyleElement.set(styleElement, sheet)
@emit 'stylesheet-added', sheet
@emitter.emit 'did-add-stylesheet', sheet
@emit 'stylesheets-changed'
@emitter.emit 'did-change-stylesheets'
styleElementRemoved: (styleElement) ->
sheet = @sheetsByStyleElement.get(styleElement)
@emit 'stylesheet-removed', sheet
@emitter.emit 'did-remove-stylesheet', sheet
@emit 'stylesheets-changed'
@emitter.emit 'did-change-stylesheets'
styleElementUpdated: ({sheet}) ->
@emit 'stylesheet-removed', sheet
@emitter.emit 'did-remove-stylesheet', sheet
@emit 'stylesheet-added', sheet
@emitter.emit 'did-add-stylesheet', sheet
@emit 'stylesheets-changed'
@emitter.emit 'did-change-stylesheets'
###
Section: Event Subscription
###
# Essential: Invoke `callback` when all styles have been reloaded.
#
# * `callback` {Function}
onDidReloadAll: (callback) ->
@emitter.on 'did-reload-all', callback
# Essential: Invoke `callback` when a stylesheet has been added to the dom.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddStylesheet: (callback) ->
@emitter.on 'did-add-stylesheet', callback
# Essential: Invoke `callback` when a stylesheet has been removed from the dom.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveStylesheet: (callback) ->
@emitter.on 'did-remove-stylesheet', callback
# Essential: Invoke `callback` when a stylesheet has been updated.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidUpdateStylesheet: (callback) ->
@emitter.on 'did-update-stylesheet', callback
# Essential: Invoke `callback` when any stylesheet has been updated, added, or removed.
#
# * `callback` {Function}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStylesheets: (callback) ->
@emitter.on 'did-change-stylesheets', callback
on: (eventName) ->
switch eventName
when 'reloaded'
deprecate 'Use ThemeManager::onDidReloadAll instead'
when 'stylesheet-added'
deprecate 'Use ThemeManager::onDidAddStylesheet instead'
when 'stylesheet-removed'
deprecate 'Use ThemeManager::onDidRemoveStylesheet instead'
when 'stylesheet-updated'
deprecate 'Use ThemeManager::onDidUpdateStylesheet instead'
when 'stylesheets-changed'
deprecate 'Use ThemeManager::onDidChangeStylesheets instead'
else
deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
###
Section: Accessing Available Themes
###
getAvailableNames: ->
# TODO: Maybe should change to list all the available themes out there?
@getLoadedNames()
###
Section: Accessing Loaded Themes
###
# Public: Get an array of all the loaded theme names.
getLoadedNames: ->
theme.name for theme in @getLoadedThemes()
# Public: Get an array of all the loaded themes.
getLoadedThemes: ->
pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()
###
Section: Accessing Active Themes
###
# Public: Get an array of all the active theme names.
getActiveNames: ->
theme.name for theme in @getActiveThemes()
# Public: Get an array of all the active themes.
getActiveThemes: ->
pack for pack in @packageManager.getActivePackages() when pack.isTheme()
activatePackages: -> @activateThemes()
###
Section: Managing Enabled Themes
###
# Public: Get the enabled theme names from the config.
#
# Returns an array of theme names in the order that they should be activated.
getEnabledThemeNames: ->
themeNames = atom.config.get('core.themes') ? []
themeNames = [themeNames] unless _.isArray(themeNames)
themeNames = themeNames.filter (themeName) ->
if themeName and typeof themeName is 'string'
return true if atom.packages.resolvePackagePath(themeName)
console.warn("Enabled theme '#{themeName}' is not installed.")
false
# Use a built-in syntax and UI theme any time the configured themes are not
# available.
if themeNames.length < 2
builtInThemeNames = [
'atom-dark-syntax'
'atom-dark-ui'
'atom-light-syntax'
'atom-light-ui'
'base16-tomorrow-dark-theme'
'base16-tomorrow-light-theme'
'solarized-dark-syntax'
'solarized-light-syntax'
]
themeNames = _.intersection(themeNames, builtInThemeNames)
if themeNames.length is 0
themeNames = ['atom-dark-syntax', 'atom-dark-ui']
else if themeNames.length is 1
if _.endsWith(themeNames[0], '-ui')
themeNames.unshift('atom-dark-syntax')
else
themeNames.push('atom-dark-ui')
# Reverse so the first (top) theme is loaded after the others. We want
# the first/top theme to override later themes in the stack.
themeNames.reverse()
# Public: Set the list of enabled themes.
#
# * `enabledThemeNames` An {Array} of {String} theme names.
setEnabledThemes: (enabledThemeNames) ->
atom.config.set('core.themes', enabledThemeNames)
###
Section: Managing Stylesheets
###
# Public: Returns the {String} path to the user's stylesheet under ~/.atom
getUserStylesheetPath: ->
stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less'])
if fs.isFileSync(stylesheetPath)
stylesheetPath
else
path.join(@configDirPath, 'styles.less')
# Public: Resolve and apply the stylesheet specified by the path.
#
# This supports both CSS and Less stylsheets.
#
# * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
# path or a relative path that will be resolved against the load path.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# required stylesheet.
requireStylesheet: (stylesheetPath, type='bundled') ->
if fullPath = @resolveStylesheet(stylesheetPath)
content = @loadStylesheet(fullPath)
@applyStylesheet(fullPath, content, type)
else
throw new Error("Could not find a file at path '#{stylesheetPath}'")
unwatchUserStylesheet: ->
@userStylesheetFile?.off()
@userStylesheetFile = null
@removeStylesheet(@userStylesheetPath) if @userStylesheetPath?
loadUserStylesheet: ->
@unwatchUserStylesheet()
userStylesheetPath = @getUserStylesheetPath()
return unless fs.isFileSync(userStylesheetPath)
@userStylesheetPath = userStylesheetPath
@userStylesheetFile = new File(userStylesheetPath)
@userStylesheetFile.on 'contents-changed moved removed', => @loadUserStylesheet()
userStylesheetContents = @loadStylesheet(userStylesheetPath, true)
@applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme')
loadBaseStylesheets: ->
@requireStylesheet('../static/bootstrap')
@reloadBaseStylesheets()
reloadBaseStylesheets: ->
@requireStylesheet('../static/atom')
if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less'])
@requireStylesheet(nativeStylesheetPath)
stylesheetElementForId: (id) ->
document.head.querySelector("atom-styles style[source-path=\"#{id}\"]")
resolveStylesheet: (stylesheetPath) ->
if path.extname(stylesheetPath).length > 0
fs.resolveOnLoadPath(stylesheetPath)
else
fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
loadStylesheet: (stylesheetPath, importFallbackVariables) ->
if path.extname(stylesheetPath) is '.less'
@loadLessStylesheet(stylesheetPath, importFallbackVariables)
else
fs.readFileSync(stylesheetPath, 'utf8')
loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) ->
unless @lessCache?
LessCompileCache = require './less-compile-cache'
@lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()})
try
if importFallbackVariables
baseVarImports = """
@import "variables/ui-variables";
@import "variables/syntax-variables";
"""
less = fs.readFileSync(lessStylesheetPath, 'utf8')
@lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n'))
else
@lessCache.read(lessStylesheetPath)
catch error
console.error """
Error compiling Less stylesheet: #{lessStylesheetPath}
Line number: #{error.line}
#{error.message}
"""
removeStylesheet: (stylesheetPath) ->
@styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose()
applyStylesheet: (path, text, type='bundled') ->
@styleSheetDisposablesBySourcePath[path] = atom.styles.addStyleSheet(text, sourcePath: path, group: type)
###
Section: Private
###
stringToId: (string) ->
string.replace(/\\/g, '/')
activateThemes: ->
deferred = Q.defer()
# atom.config.observe runs the callback once, then on subsequent changes.
atom.config.observe 'core.themes', =>
@deactivateThemes()
@refreshLessCache() # Update cache for packages in core.themes config
promises = []
for themeName in @getEnabledThemeNames()
if @packageManager.resolvePackagePath(themeName)
promises.push(@packageManager.activatePackage(themeName))
else
console.warn("Failed to activate theme '#{themeName}' because it isn't installed.")
Q.all(promises).then =>
@addActiveThemeClasses()
@refreshLessCache() # Update cache again now that @getActiveThemes() is populated
@loadUserStylesheet()
@reloadBaseStylesheets()
@initialLoadComplete = true
@emit 'reloaded'
@emitter.emit 'did-reload-all'
deferred.resolve()
deferred.promise
deactivateThemes: ->
@removeActiveThemeClasses()
@unwatchUserStylesheet()
@packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()
null
isInitialLoadComplete: -> @initialLoadComplete
addActiveThemeClasses: ->
for pack in @getActiveThemes()
atom.workspaceView?[0]?.classList.add("theme-#{pack.name}")
return
removeActiveThemeClasses: ->
for pack in @getActiveThemes()
atom.workspaceView?[0]?.classList.remove("theme-#{pack.name}")
return
refreshLessCache: ->
@lessCache?.setImportPaths(@getImportPaths())
getImportPaths: ->
activeThemes = @getActiveThemes()
if activeThemes.length > 0
themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme)
else
themePaths = []
for themeName in @getEnabledThemeNames()
if themePath = @packageManager.resolvePackagePath(themeName)
themePaths.push(path.join(themePath, Package.stylesheetsDir))
themePaths.filter (themePath) -> fs.isDirectorySync(themePath)