Dynamically load CodeMirror themes

Most users run with the default theme, which does not require any
additional CSS. The total set of themes costs 7.3 KiB of download
when added into the default CSS file.

Split the themes into their own files and only load the theme when
it is required by the browser.

Change-Id: I48f274347e1ca94895c0756fa17479661c78fd57
This commit is contained in:
Shawn Pearce 2014-12-31 12:51:45 -05:00
parent 3a52cede92
commit a2346d1f0a
9 changed files with 184 additions and 53 deletions

View File

@ -20,6 +20,7 @@ public enum Theme {
ECLIPSE,
ELEGANT,
NEAT,
// Dark themes
MIDNIGHT,
NIGHT,

View File

@ -56,6 +56,7 @@ import com.google.gwt.user.client.ui.ToggleButton;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
/** Displays current diff preferences. */
class PreferencesBox extends Composite {
@ -386,18 +387,30 @@ class PreferencesBox extends Composite {
@UiHandler("theme")
void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
prefs.theme(Theme.valueOf(theme.getValue(theme.getSelectedIndex())));
view.setThemeStyles(prefs.theme().isDark());
view.operation(new Runnable() {
final Theme newTheme = getSelectedTheme();
prefs.theme(newTheme);
ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
@Override
public void run() {
String t = prefs.theme().name().toLowerCase();
view.getCmFromSide(DisplaySide.A).setOption("theme", t);
view.getCmFromSide(DisplaySide.B).setOption("theme", t);
public void onSuccess(Void result) {
view.operation(new Runnable() {
@Override
public void run() {
if (getSelectedTheme() == newTheme && isAttached()) {
String t = newTheme.name().toLowerCase();
view.getCmFromSide(DisplaySide.A).setOption("theme", t);
view.getCmFromSide(DisplaySide.B).setOption("theme", t);
view.setThemeStyles(newTheme.isDark());
}
}
});
}
});
}
private Theme getSelectedTheme() {
return Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
}
@UiHandler("apply")
void onApply(@SuppressWarnings("unused") ClickEvent e) {
close();
@ -493,26 +506,8 @@ class PreferencesBox extends Composite {
}
private void initTheme() {
theme.addItem(
Theme.DEFAULT.name().toLowerCase(),
Theme.DEFAULT.name());
theme.addItem(
Theme.ECLIPSE.name().toLowerCase(),
Theme.ECLIPSE.name());
theme.addItem(
Theme.ELEGANT.name().toLowerCase(),
Theme.ELEGANT.name());
theme.addItem(
Theme.NEAT.name().toLowerCase(),
Theme.NEAT.name());
theme.addItem(
Theme.MIDNIGHT.name().toLowerCase(),
Theme.MIDNIGHT.name());
theme.addItem(
Theme.NIGHT.name().toLowerCase(),
Theme.NIGHT.name());
theme.addItem(
Theme.TWILIGHT.name().toLowerCase(),
Theme.TWILIGHT.name());
for (Theme t : Theme.values()) {
theme.addItem(t.name().toLowerCase(), t.name());
}
}
}

View File

@ -78,6 +78,7 @@ import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
import java.util.ArrayList;
import java.util.Collections;
@ -175,7 +176,10 @@ public class SideBySide2 extends Screen {
CallbackGroup cmGroup = new CallbackGroup();
CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback()));
final CallbackGroup group = new CallbackGroup();
final AsyncCallback<Void> themeCallback =
group.add(CallbackGroup.<Void> emptyCallback());
final AsyncCallback<Void> modeInjectorCb =
group.add(CallbackGroup.<Void> emptyCallback());
@ -189,6 +193,9 @@ public class SideBySide2 extends Screen {
public void onSuccess(DiffInfo diffInfo) {
diff = diffInfo;
fileSize = bucketFileSize(diffInfo);
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
if (prefs.syntaxHighlighting()) {
if (fileSize.compareTo(FileSize.SMALL) > 0) {
modeInjectorCb.onSuccess(null);

View File

@ -20,4 +20,5 @@
<source path='lib'/>
<source path='keymap'/>
<source path='mode'/>
<source path='theme'/>
</module>

View File

@ -0,0 +1,83 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.codemirror.theme;
import com.google.gerrit.extensions.common.Theme;
import com.google.gwt.dom.client.StyleInjector;
import com.google.gwt.resources.client.ExternalTextResource;
import com.google.gwt.resources.client.ResourceCallback;
import com.google.gwt.resources.client.ResourceException;
import com.google.gwt.resources.client.TextResource;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.EnumSet;
/** Dynamically loads a known CodeMirror theme's CSS */
public class ThemeLoader {
private static final ExternalTextResource[] THEMES = {
Themes.I.eclipse(),
Themes.I.elegant(),
Themes.I.midnight(),
Themes.I.neat(),
Themes.I.night(),
Themes.I.twilight(),
};
private static final EnumSet<Theme> loaded = EnumSet.of(Theme.DEFAULT);
public static final void loadTheme(final Theme theme,
final AsyncCallback<Void> cb) {
if (loaded.contains(theme)) {
cb.onSuccess(null);
return;
}
ExternalTextResource resource = findTheme(theme);
if (resource == null) {
cb.onFailure(new Exception("unknown theme " + theme));
return;
}
try {
resource.getText(new ResourceCallback<TextResource>() {
@Override
public void onSuccess(TextResource resource) {
StyleInjector.inject(resource.getText());
loaded.add(theme);
cb.onSuccess(null);
}
@Override
public void onError(ResourceException e) {
cb.onFailure(e);
}
});
} catch (ResourceException e) {
cb.onFailure(e);
}
}
private static final ExternalTextResource findTheme(Theme theme) {
for (ExternalTextResource r : THEMES) {
if (theme.name().toLowerCase().equals(r.getName())) {
return r;
}
}
return null;
}
private ThemeLoader() {
}
}

View File

@ -0,0 +1,34 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.codemirror.theme;
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ExternalTextResource;
public interface Themes extends ClientBundle {
public static final Themes I = GWT.create(Themes.class);
@Source("eclipse.css") ExternalTextResource eclipse();
@Source("elegant.css") ExternalTextResource elegant();
@Source("midnight.css") ExternalTextResource midnight();
@Source("neat.css") ExternalTextResource neat();
@Source("night.css") ExternalTextResource night();
@Source("twilight.css") ExternalTextResource twilight();
// When adding a resource, update:
// - static initializer in ThemeLoader
// - enum value in com.google.gerrit.extensions.common.Theme
}

View File

@ -19,18 +19,29 @@ CLOSURE_COMPILER_ARGS = [
genrule(
name = 'css',
cmd = ';'.join([
':>$OUT',
"echo '/** @license' >>$OUT",
"echo '/** @license' >$OUT",
'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
"echo '*/' >>$OUT",
] +
['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n)
for n in CM_CSS + CM_THEMES]
for n in CM_CSS]
),
deps = [':zip'],
out = 'cm.css',
)
for n in CM_THEMES:
genrule(
name = 'theme_%s' % n,
cmd = ';'.join([
"echo '/** @license' >$OUT",
'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
"echo '*/' >>$OUT",
'unzip -p $(location :zip) %s/theme/%s.css >>$OUT' % (TOP, n)
]
),
out = 'theme_%s.css' % n,
)
genrule(
name = 'cm-verbose',
cmd = ';'.join([
@ -42,7 +53,6 @@ genrule(
['unzip -p $(location :zip) %s/addon/%s >>$OUT' % (TOP, n)
for n in CM_ADDONS]
),
deps = [':zip'],
out = 'cm-verbose.js',
)
@ -62,7 +72,6 @@ for n in CM_MODES:
"echo '*/' >>$OUT",
'unzip -p $(location :zip) %s/mode/%s/%s.js >>$OUT' % (TOP, n, n),
]),
deps = [':zip'],
out = 'mode_%s_src.js' %n,
)
js_minify(
@ -86,18 +95,14 @@ genrule(
name = 'jar',
cmd = ';'.join([
'cd $TMP',
'mkdir -p net/codemirror/{lib,mode}',
'mkdir -p net/codemirror/{lib,mode,theme}',
'cp $(location :css) net/codemirror/lib',
'cp $(location :js) net/codemirror/lib']
+ ['cp $(location :mode_%s_js) net/codemirror/mode/%s.js' % (n, n)
for n in CM_MODES]
+ ['zip -qr $OUT net/codemirror/{lib,mode}']),
deps = [
':css',
':js',
':js_minifier',
':zip',
] + [':mode_%s_js' % n for n in CM_MODES],
+ ['cp $(location :theme_%s) net/codemirror/theme/%s.css' % (n, n)
for n in CM_THEMES]
+ ['zip -qr $OUT net/codemirror/{lib,mode,theme}']),
out = 'codemirror.jar',
)
@ -107,7 +112,6 @@ genrule(
' -o $OUT' +
' -u ' + URL +
' -v ' + SHA1,
deps = ['//tools:download_file'],
out = ZIP,
)

View File

@ -4,15 +4,6 @@ CM_CSS = [
'addon/scroll/simplescrollbars.css',
]
CM_THEMES = [
'theme/eclipse.css',
'theme/elegant.css',
'theme/midnight.css',
'theme/neat.css',
'theme/night.css',
'theme/twilight.css',
]
CM_JS = [
'lib/codemirror.js',
'mode/meta.js',
@ -30,6 +21,21 @@ CM_ADDONS = [
'mode/simple.js',
]
# Available themes must be enumerated here,
# in gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.java,
# in gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
CM_THEMES = [
'eclipse',
'elegant',
'midnight',
'neat',
'night',
'twilight',
]
# Available modes must be enumerated here,
# in gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java,
# and in CodeMirror's own mode/meta.js script.
CM_MODES = [
'clike',
'clojure',

@ -1 +1 @@
Subproject commit 80373ef2e4c1978b8c9a68a12c05ec05c4063f8a
Subproject commit 39ffdc20f021484c1ce1dca2ae9f00fa10a482f4