Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Scoped themes to apply only to the editor (#editor-holder) #8447

Merged
merged 4 commits into from
Jul 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/htmlContent/themes-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h1 class="dialog-title">Themes Settings</h1>
<div class="controls">
<select>
{{#themes}}
<option style="text-align:left;" value="{{name}}" data-target="themes">{{displayName}}</option>
<option style="text-align:left;" value="{{name}}" data-target="theme">{{displayName}}</option>
{{/themes}}
</select>
</div>
Expand Down
167 changes: 96 additions & 71 deletions src/view/ThemeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ define(function (require, exports, module) {
prefs = PreferencesManager.getExtensionPrefs("brackets-themes");

var loadedThemes = {},
currentTheme = null,
defaultTheme = "thor-light-theme",
commentRegex = /\/\*([\s\S]*?)\*\//mg,
scrollbarsRegex = /::-webkit-scrollbar(?:[\s\S]*?)\{(?:[\s\S]*?)\}/mg,
Expand Down Expand Up @@ -73,7 +74,7 @@ define(function (require, exports, module) {
* dialog, and to properly add a theme into CodeMirror along with the rest of brackets.
*
* @param {File} file for the theme
* @param {{name: string, className: string, title: string}} options to configure different
* @param {{name: string, title: string}} options to configure different
* properties in the theme
*/
function Theme(file, options) {
Expand All @@ -86,9 +87,9 @@ define(function (require, exports, module) {
// is a theme1.css and a theme1.less that are entirely different themes...

this.file = file;
this.name = options.name || (options.title || fileName).toLocaleLowerCase().replace(/[\W]/g, '-');
this.className = options.className || "theme-" + this.name;
this.displayName = options.title || toDisplayName(fileName);
this.name = options.name || (options.title || fileName).toLocaleLowerCase().replace(/[\W]/g, '-');
this.displayName = options.title || toDisplayName(fileName);
this.dark = options.dark === true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be options.theme.dark === true, I believe.

}


Expand Down Expand Up @@ -156,7 +157,7 @@ define(function (require, exports, module) {
filename: fixPath(theme.file._path)
});

parser.parse("." + theme.className + "{" + content + "}", function (err, tree) {
parser.parse("#editor-holder {" + content + "}", function (err, tree) {
if (err) {
deferred.reject(err);
} else {
Expand Down Expand Up @@ -197,79 +198,83 @@ define(function (require, exports, module) {


/**
* @private
* Get all current theme objects
* Get all current theme objects that are loaded in the editor.
*
* @return {Array.<Theme>} collection of the current theme instances
*/
function getCurrentThemes() {
return _.map(prefs.get("themes").slice(0), function (item) {
return loadedThemes[item] || loadedThemes[defaultTheme];
});
function getCurrentTheme() {
if (!currentTheme) {
currentTheme = loadedThemes[prefs.get("theme")] || loadedThemes[defaultTheme];
}

return currentTheme;
}


/**
* Provides quick access to all available themes
* Gets all available themes that can be loaded in the editor.
* @return {Array.<Theme>} collection of all available themes
*/
function getAllThemes() {
return _.map(loadedThemes, function (item) {
return item;
return loadedThemes.map(function (theme) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, this doesn't work because loadedThemes is an object and doesn't have map. _.map will take an object and iterate on its values.

return theme;
});
}


/**
* @private
* Loads all current themes
* Process and load the current theme into the editor
*
* @return {$.Promise} promise object resolved with the theme object and all
* corresponding new css/less and scrollbar information
*/
function loadCurrentThemes() {
var pendingThemes = _.map(getCurrentThemes(), function (theme) {

return theme && FileUtils.readAsText(theme.file)
.then(function (content) {
var result = extractScrollbars(content);
theme.scrollbar = result.scrollbar;
return result.content;
})
.then(function (content) {
return lessifyTheme(content, theme);
})
.then(function (style) {
return ExtensionUtils.addEmbeddedStyleSheet(style);
})
.then(function (styleNode) {
// Remove after the style has been applied to avoid weird flashes
if (theme.css) {
$(theme.css).remove();
}
function loadCurrentTheme() {
var theme = getCurrentTheme();

var pending = theme && FileUtils.readAsText(theme.file)
.then(function (content) {
var result = extractScrollbars(content);
theme.scrollbar = result.scrollbar;
return result.content;
})
.then(function (content) {
return lessifyTheme(content, theme);
})
.then(function (style) {
return ExtensionUtils.addEmbeddedStyleSheet(style);
})
.then(function (styleNode) {
// Remove after the style has been applied to avoid weird flashes
if (theme.css) {
$(theme.css).remove();
}

theme.css = styleNode;
return theme;
});
});
theme.css = styleNode;
return theme;
});

return $.when.apply(undefined, pendingThemes);
return $.when(pending);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending is just a single promise now, right? So, you can just return it directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes. I commited those changes last night but didn't push. Will do that in a minute

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no what's there is good. There is a the check if the theme is not falsy before it reads the text. The $.when is just a shortcut to always return a promise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see... it's not necessarily a promise if the current theme is null or undefined.

}


/**
* Refresh currently loaded themes
* Refresh current theme in the editor
*
* @param {boolean} force is to force reload the current themes
* @param {boolean} force is to force reload the current theme
*/
function refresh(force) {
$.when(force && loadCurrentThemes()).done(function () {
if (force) {
currentTheme = null;
}

$.when(force && loadCurrentTheme()).done(function () {
var editor = EditorManager.getActiveEditor();
if (!editor || !editor._codeMirror) {
return;
}

var cm = editor._codeMirror;
var cm = editor._codeMirror;
ThemeView.setDocumentMode(cm);
ThemeView.updateThemes(cm);
});
Expand All @@ -285,9 +290,9 @@ define(function (require, exports, module) {
* @return {$.Promise} promise object resolved with the theme to be loaded from fileName
*/
function loadFile(fileName, options) {
var deferred = new $.Deferred(),
file = FileSystem.getFileForPath(fileName),
currentThemes = (prefs.get("themes") || []);
var deferred = new $.Deferred(),
file = FileSystem.getFileForPath(fileName),
currentThemeName = prefs.get("theme");

file.exists(function (err, exists) {
var theme;
Expand All @@ -298,9 +303,9 @@ define(function (require, exports, module) {
ThemeSettings._setThemes(loadedThemes);

// For themes that are loaded after ThemeManager has been loaded,
// we should check if it's the current theme. It is, then we just
// we should check if it's the current theme. If it is, then we just
// load it.
if (currentThemes.indexOf(theme.name) !== -1) {
if (currentThemeName === theme.name) {
refresh(true);
}

Expand All @@ -317,11 +322,11 @@ define(function (require, exports, module) {
/**
* Loads a theme from an extension package.
*
* @param {Object} themePackage is a package for the theme to be loaded.
* @param {Object} themePackage is a package from the extension manager for the theme to be loaded.
* @return {$.Promise} promise object resolved with the theme to be loaded from the pacakge
*/
function loadPackage(themePackage) {
var fileName = themePackage.path + "/" + themePackage.metadata.theme;
var fileName = themePackage.path + "/" + themePackage.metadata.theme.file;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to guard against metadata.theme.file being undefined? I guess the file just wouldn't exist, which is not such a big deal.

Also, the two built-in themes need their metadata updated. (I've done this in my local copy... if the changes required are minor enough, I will likely just make them and then merge so that I can work from this when updating the themes...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah they do. It's a simple change. It's just basically what we had talked about. Converting the theme string into an object with a file field... I am storing off the dark flag but it's not being added to the dom quite yet. I can adjust the default themes if you'd prefer... Or if you already there go ahead and dot it :)

return loadFile(fileName, themePackage.metadata);
}

Expand Down Expand Up @@ -365,31 +370,23 @@ define(function (require, exports, module) {
}
}

function loadThemesFiles(themes) {
function loadThemeFiles(theme) {
// Iterate through each name in the themes and make them theme objects
var deferred = _.map(themes.files, function (themeFile) {
return loadFile(themes.path + "/" + themeFile);
var deferred = theme.files.forEach(function (themeFile) {
return loadFile(theme.path + "/" + themeFile);
});

return $.when.apply(undefined, deferred);
}

FileSystem.getDirectoryForPath(path).getContents(readContent);
return result.then(loadThemesFiles);
return result.then(loadThemeFiles);
}


prefs.on("change", "themes", function () {
refresh(true);
ThemeView.updateScrollbars(getCurrentThemes()[0]);

// Expose event for theme changes
$(exports).trigger("themeChange", getCurrentThemes());
});

prefs.on("change", "customScrollbars", function () {
refresh();
ThemeView.updateScrollbars(getCurrentThemes()[0]);
ThemeView.updateScrollbars(getCurrentTheme());
});

prefs.on("change", "fontSize", function () {
Expand Down Expand Up @@ -421,16 +418,44 @@ define(function (require, exports, module) {
refresh();
});

refresh(true);

// We need to postpone setting the prefs on change event handler for themes
// to prevent flickering
AppInit.appReady(function () {
prefs.on("change", "theme", function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's worth noting about the prefs change events is that they only signal a possible change in the preference value, not a certain one. It may be the case that the flickering you saw was because it was refreshing the editor multiple times when it didn't really need to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless, it does seem like we'd want to confirm that the theme is truly changing so that we don't emit themeChange events when there wasn't a real theme change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flickering was happening because I try to pre-emptively load a theme into the editor while themes are being loaded. This is so that when a theme is removed, and added later, it can be reapplied when it is installed... https://github.com/adobe/brackets/blob/master/src/view/ThemeManager.js#L309

So, that called refresh(true)... Then because I subscribe my theme change listener so early, that would eventually get called, which would in turn call refresh(true)... That double refresh true causes the flickering. I had to be a little more strategic about setting up the listener so that I don't load the same theme twice when brackets is starting up.

I had done the alternative in the past of keeping track of an initted flag and what not, but that got pretty messy. This was the cleanest and least intrusive change I came up with.

// Save the theme that's about to be replaced to allow for some transition to prevent flicker
var oldTheme = getCurrentTheme();

// Refresh editor with the new theme
refresh(true);

// Process the scrollbars for the editor
ThemeView.updateScrollbars(getCurrentTheme());

// Expose event for theme changes
$(exports).trigger("themeChange", getCurrentTheme());

// Delay removing the old style element to avoid flashes when transitioning between themes
if (oldTheme) {
setTimeout(function (theme) {
if (theme.css) {
$(theme.css).remove();
}
}, 0, oldTheme);
}
});
});


ThemeView.updateFonts();
ThemeView.updateScrollbars();

exports.refresh = refresh;
exports.loadFile = loadFile;
exports.loadPackage = loadPackage;
exports.loadDirectory = loadDirectory;
exports.getCurrentThemes = getCurrentThemes;
exports.getAllThemes = getAllThemes;
exports.refresh = refresh;
exports.loadFile = loadFile;
exports.loadPackage = loadPackage;
exports.loadDirectory = loadDirectory;
exports.getCurrentTheme = getCurrentTheme;
exports.getAllThemes = getAllThemes;

// Exposed for testing purposes
exports._toDisplayName = toDisplayName;
Expand Down
23 changes: 8 additions & 15 deletions src/view/ThemeSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ define(function (require, exports, module) {
"lineHeight": 1.25,
"fontFamily": "'SourceCodePro-Medium', MS ゴシック, 'MS Gothic', monospace",
"customScrollbars": true,
"themes": ["thor-light-theme"]
"theme": "thor-light-theme"
};


Expand Down Expand Up @@ -81,11 +81,9 @@ define(function (require, exports, module) {
var $template = $(Mustache.render(template, {"settings": currentSettings, "themes": themes, "Strings": Strings}));

// Select the correct theme.
_.each(currentSettings.themes, function (item) {
$template
.find("[value='" + item + "']")
.attr("selected", "selected");
});
$template
.find("[value='" + currentSettings.theme + "']")
.attr("selected", "selected");

$template
.find("[data-toggle=tab].default")
Expand All @@ -103,16 +101,11 @@ define(function (require, exports, module) {
newSettings[attr] = $target.val();
})
.on("change", function () {
var items;
var $target = $(":selected", this);
var attr = $target.attr("data-target");

if (attr) {
items = $target.map(function (i, item) {
return $(item).val();
});

prefs.set(attr, items.toArray());
prefs.set(attr, $target.val());
}
});

Expand All @@ -123,7 +116,7 @@ define(function (require, exports, module) {
});
} else if (id === "cancel") {
// Make sure we revert any changes to theme selection
prefs.set("themes", currentSettings.themes);
prefs.set("theme", currentSettings.theme);
}
});
}
Expand All @@ -140,15 +133,15 @@ define(function (require, exports, module) {
* Restores themes to factory settings.
*/
function restore() {
prefs.set("themes", defaults.themes);
prefs.set("theme", defaults.theme);
prefs.set("fontSize", defaults.fontSize + "px");
prefs.set("lineHeight", defaults.lineHeight);
prefs.set("fontFamily", defaults.fontFamily);
prefs.set("customScrollbars", defaults.customScrollbars);
}


prefs.definePreference("themes", "array", defaults.themes);
prefs.definePreference("theme", "string", defaults.theme);
prefs.definePreference("fontSize", "string", defaults.fontSize + "px");
prefs.definePreference("lineHeight", "number", defaults.lineHeight);
prefs.definePreference("fontFamily", "string", defaults.fontFamily);
Expand Down
Loading