// Copyright (C) 2013 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 com.google.gerrit.client.diff; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.DEFAULT_CONTEXT; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_NONE; import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_TRAILING; import static com.google.gwt.event.dom.client.KeyCodes.KEY_ESCAPE; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.account.AccountApi; import com.google.gerrit.client.account.DiffPreferences; import com.google.gerrit.client.patches.PatchUtil; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.NpIntTextBox; import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import com.google.gerrit.extensions.client.Theme; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.resources.client.CssResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.ToggleButton; import com.google.gwt.user.client.ui.UIObject; import java.util.Objects; import net.codemirror.lib.CodeMirror; import net.codemirror.mode.ModeInfo; import net.codemirror.mode.ModeInjector; import net.codemirror.theme.ThemeLoader; /** Displays current diff preferences. */ public class PreferencesBox extends Composite { interface Binder extends UiBinder<HTMLPanel, PreferencesBox> {} private static final Binder uiBinder = GWT.create(Binder.class); public interface Style extends CssResource { String dialog(); } private final DiffScreen view; private DiffPreferences prefs; private int contextLastValue; private Timer updateContextTimer; @UiField Style style; @UiField Element header; @UiField Anchor close; @UiField ListBox ignoreWhitespace; @UiField NpIntTextBox tabWidth; @UiField NpIntTextBox lineLength; @UiField NpIntTextBox context; @UiField NpIntTextBox cursorBlinkRate; @UiField CheckBox contextEntireFile; @UiField ToggleButton intralineDifference; @UiField ToggleButton syntaxHighlighting; @UiField ToggleButton whitespaceErrors; @UiField ToggleButton showTabs; @UiField ToggleButton lineNumbers; @UiField Element leftSideLabel; @UiField ToggleButton leftSide; @UiField ToggleButton emptyPane; @UiField ToggleButton topMenu; @UiField ToggleButton autoHideDiffTableHeader; @UiField ToggleButton manualReview; @UiField ToggleButton expandAllComments; @UiField ToggleButton renderEntireFile; @UiField ToggleButton matchBrackets; @UiField ToggleButton lineWrapping; @UiField ToggleButton skipDeleted; @UiField ToggleButton skipUnchanged; @UiField ToggleButton skipUncommented; @UiField ListBox theme; @UiField Element modeLabel; @UiField ListBox mode; @UiField Button apply; @UiField Button save; public PreferencesBox(DiffScreen view) { this.view = view; initWidget(uiBinder.createAndBindUi(this)); initIgnoreWhitespace(); initTheme(); if (view != null) { initMode(); } else { UIObject.setVisible(header, false); apply.getElement().getStyle().setVisibility(Visibility.HIDDEN); } } @Override public void onLoad() { super.onLoad(); save.setVisible(Gerrit.isSignedIn()); if (view != null) { addDomHandler( new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { if (event.getNativeKeyCode() == KEY_ESCAPE || event.getNativeKeyCode() == ',') { close(); } } }, KeyDownEvent.getType()); updateContextTimer = new Timer() { @Override public void run() { if (prefs.context() == WHOLE_FILE_CONTEXT) { contextEntireFile.setValue(true); } if (view.canRenderEntireFile(prefs)) { renderEntireFile.setEnabled(true); renderEntireFile.setValue(prefs.renderEntireFile()); } else { renderEntireFile.setValue(false); renderEntireFile.setEnabled(false); } view.setContext(prefs.context()); } }; } } public Style getStyle() { return style; } public void set(DiffPreferences prefs) { this.prefs = prefs; setIgnoreWhitespace(prefs.ignoreWhitespace()); tabWidth.setIntValue(prefs.tabSize()); if (view != null && Patch.COMMIT_MSG.equals(view.path)) { lineLength.setEnabled(false); lineLength.setIntValue(72); } else { lineLength.setEnabled(true); lineLength.setIntValue(prefs.lineLength()); } cursorBlinkRate.setIntValue(prefs.cursorBlinkRate()); syntaxHighlighting.setValue(prefs.syntaxHighlighting()); whitespaceErrors.setValue(prefs.showWhitespaceErrors()); showTabs.setValue(prefs.showTabs()); lineNumbers.setValue(prefs.showLineNumbers()); emptyPane.setValue(!prefs.hideEmptyPane()); if (view != null) { leftSide.setValue(view.getDiffTable().isVisibleA()); leftSide.setEnabled( !(prefs.hideEmptyPane() && view.getDiffTable().getChangeType() == ChangeType.ADDED)); } else { UIObject.setVisible(leftSideLabel, false); leftSide.setVisible(false); } topMenu.setValue(!prefs.hideTopMenu()); autoHideDiffTableHeader.setValue(!prefs.autoHideDiffTableHeader()); manualReview.setValue(prefs.manualReview()); expandAllComments.setValue(prefs.expandAllComments()); matchBrackets.setValue(prefs.matchBrackets()); lineWrapping.setValue(prefs.lineWrapping()); skipDeleted.setValue(!prefs.skipDeleted()); skipUnchanged.setValue(!prefs.skipUnchanged()); skipUncommented.setValue(!prefs.skipUncommented()); setTheme(prefs.theme()); if (view == null || view.canRenderEntireFile(prefs)) { renderEntireFile.setValue(prefs.renderEntireFile()); renderEntireFile.setEnabled(true); } else { renderEntireFile.setValue(false); renderEntireFile.setEnabled(false); } if (view != null) { mode.setEnabled(prefs.syntaxHighlighting()); if (prefs.syntaxHighlighting()) { setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode")); } } else { UIObject.setVisible(modeLabel, false); mode.setVisible(false); } if (view != null) { switch (view.getIntraLineStatus()) { case OFF: case OK: intralineDifference.setValue(prefs.intralineDifference()); break; case TIMEOUT: case FAILURE: intralineDifference.setValue(false); intralineDifference.setEnabled(false); break; } } else { intralineDifference.setValue(prefs.intralineDifference()); } if (prefs.context() == WHOLE_FILE_CONTEXT) { contextLastValue = DEFAULT_CONTEXT; context.setText(""); contextEntireFile.setValue(true); } else { context.setIntValue(prefs.context()); contextEntireFile.setValue(false); } } @UiHandler("ignoreWhitespace") void onIgnoreWhitespace(@SuppressWarnings("unused") ChangeEvent e) { prefs.ignoreWhitespace( Whitespace.valueOf(ignoreWhitespace.getValue(ignoreWhitespace.getSelectedIndex()))); if (view != null) { view.reloadDiffInfo(); } } @UiHandler("intralineDifference") void onIntralineDifference(ValueChangeEvent<Boolean> e) { prefs.intralineDifference(Boolean.valueOf(e.getValue())); if (view != null) { view.setShowIntraline(prefs.intralineDifference()); } } @UiHandler("context") void onContextKey(KeyPressEvent e) { if (contextEntireFile.getValue()) { char c = e.getCharCode(); if ('0' <= c && c <= '9') { contextEntireFile.setValue(false); } } } @UiHandler("context") void onContext(ValueChangeEvent<String> e) { String v = e.getValue(); int c; if (v != null && v.length() > 0) { c = Math.min(Math.max(0, Integer.parseInt(v)), 32767); contextEntireFile.setValue(false); } else if (v == null || v.isEmpty()) { c = WHOLE_FILE_CONTEXT; } else { return; } prefs.context(c); if (view != null) { updateContextTimer.schedule(200); } } @UiHandler("contextEntireFile") void onContextEntireFile(ValueChangeEvent<Boolean> e) { // If a click arrives too fast after onContext applied an update // the user committed the context line update by clicking on the // whole file checkmark. Drop this event, but transfer focus. if (e.getValue()) { contextLastValue = context.getIntValue(); context.setText(""); prefs.context(WHOLE_FILE_CONTEXT); } else { prefs.context(contextLastValue > 0 ? contextLastValue : DEFAULT_CONTEXT); context.setIntValue(prefs.context()); context.setFocus(true); context.setSelectionRange(0, context.getText().length()); } if (view != null) { updateContextTimer.schedule(200); } } @UiHandler("tabWidth") void onTabWidth(ValueChangeEvent<String> e) { String v = e.getValue(); if (v != null && v.length() > 0) { prefs.tabSize(Math.max(1, Integer.parseInt(v))); if (view != null) { view.operation( () -> { int size = prefs.tabSize(); for (CodeMirror cm : view.getCms()) { cm.setOption("tabSize", size); } }); } } } @UiHandler("lineLength") void onLineLength(ValueChangeEvent<String> e) { String v = e.getValue(); if (v != null && v.length() > 0) { prefs.lineLength(Math.max(1, Integer.parseInt(v))); if (view != null) { view.operation(() -> view.setLineLength(prefs.lineLength())); } } } @UiHandler("expandAllComments") void onExpandAllComments(ValueChangeEvent<Boolean> e) { prefs.expandAllComments(e.getValue()); if (view != null) { view.getCommentManager().setExpandAllComments(prefs.expandAllComments()); } } @UiHandler("cursorBlinkRate") void onCursoBlinkRate(ValueChangeEvent<String> e) { String v = e.getValue(); if (v != null && v.length() > 0) { // A negative value hides the cursor entirely: // don't let user shoot himself in the foot. prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v))); view.getCmFromSide(DisplaySide.A).setOption("cursorBlinkRate", prefs.cursorBlinkRate()); view.getCmFromSide(DisplaySide.B).setOption("cursorBlinkRate", prefs.cursorBlinkRate()); } } @UiHandler("showTabs") void onShowTabs(ValueChangeEvent<Boolean> e) { prefs.showTabs(e.getValue()); if (view != null) { view.setShowTabs(prefs.showTabs()); } } @UiHandler("lineNumbers") void onLineNumbers(ValueChangeEvent<Boolean> e) { prefs.showLineNumbers(e.getValue()); if (view != null) { view.setShowLineNumbers(prefs.showLineNumbers()); } } @UiHandler("leftSide") void onLeftSide(ValueChangeEvent<Boolean> e) { if (view.getDiffTable() instanceof SideBySideTable) { ((SideBySideTable) view.getDiffTable()).setVisibleA(e.getValue()); } } @UiHandler("emptyPane") void onHideEmptyPane(ValueChangeEvent<Boolean> e) { prefs.hideEmptyPane(!e.getValue()); if (view != null) { view.getDiffTable().setHideEmptyPane(prefs.hideEmptyPane()); if (prefs.hideEmptyPane()) { if (view.getDiffTable().getChangeType() == ChangeType.ADDED) { leftSide.setValue(false); leftSide.setEnabled(false); } } else { leftSide.setValue(view.getDiffTable().isVisibleA()); leftSide.setEnabled(true); } } } @UiHandler("topMenu") void onTopMenu(ValueChangeEvent<Boolean> e) { prefs.hideTopMenu(!e.getValue()); if (view != null) { Gerrit.setHeaderVisible(!prefs.hideTopMenu()); view.resizeCodeMirror(); } } @UiHandler("autoHideDiffTableHeader") void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) { prefs.autoHideDiffTableHeader(!e.getValue()); if (view != null) { view.setAutoHideDiffHeader(!e.getValue()); } } @UiHandler("manualReview") void onManualReview(ValueChangeEvent<Boolean> e) { prefs.manualReview(e.getValue()); } @UiHandler("syntaxHighlighting") void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) { prefs.syntaxHighlighting(e.getValue()); if (view != null) { mode.setEnabled(prefs.syntaxHighlighting()); if (prefs.syntaxHighlighting()) { setMode(view.getContentType()); } view.setSyntaxHighlighting(prefs.syntaxHighlighting()); } } @UiHandler("mode") void onMode(@SuppressWarnings("unused") ChangeEvent e) { String mode = getSelectedMode(); prefs.syntaxHighlighting(true); syntaxHighlighting.setValue(true, false); new ModeInjector() .add(mode) .inject( new GerritCallback<Void>() { @Override public void onSuccess(Void result) { if (prefs.syntaxHighlighting() && Objects.equals(mode, getSelectedMode()) && view.isAttached()) { view.operation( () -> { view.getCmFromSide(DisplaySide.A).setOption("mode", mode); view.getCmFromSide(DisplaySide.B).setOption("mode", mode); }); } } }); } private String getSelectedMode() { String m = mode.getValue(mode.getSelectedIndex()); return m != null && !m.isEmpty() ? m : null; } @UiHandler("whitespaceErrors") void onWhitespaceErrors(ValueChangeEvent<Boolean> e) { prefs.showWhitespaceErrors(e.getValue()); if (view != null) { view.operation( () -> { boolean s = prefs.showWhitespaceErrors(); for (CodeMirror cm : view.getCms()) { cm.setOption("showTrailingSpace", s); } }); } } @UiHandler("renderEntireFile") void onRenderEntireFile(ValueChangeEvent<Boolean> e) { prefs.renderEntireFile(e.getValue()); if (view != null) { view.updateRenderEntireFile(); } } @UiHandler("matchBrackets") void onMatchBrackets(ValueChangeEvent<Boolean> e) { prefs.matchBrackets(e.getValue()); view.getCmFromSide(DisplaySide.A).setOption("matchBrackets", prefs.matchBrackets()); view.getCmFromSide(DisplaySide.B).setOption("matchBrackets", prefs.matchBrackets()); } @UiHandler("lineWrapping") void onLineWrapping(ValueChangeEvent<Boolean> e) { prefs.lineWrapping(e.getValue()); view.getCmFromSide(DisplaySide.A).setOption("lineWrapping", prefs.lineWrapping()); view.getCmFromSide(DisplaySide.B).setOption("lineWrapping", prefs.lineWrapping()); } @UiHandler("skipDeleted") void onSkipDeleted(ValueChangeEvent<Boolean> e) { prefs.skipDeleted(!e.getValue()); // TODO: Update the navigation links on the current DiffScreen } @UiHandler("skipUnchanged") void onSkipUnchanged(ValueChangeEvent<Boolean> e) { prefs.skipUnchanged(!e.getValue()); // TODO: Update the navigation links on the current DiffScreen } @UiHandler("skipUncommented") void onSkipUncommented(ValueChangeEvent<Boolean> e) { prefs.skipUncommented(!e.getValue()); // TODO: Update the navigation links on the current DiffScreen } @UiHandler("theme") void onTheme(@SuppressWarnings("unused") ChangeEvent e) { Theme newTheme = getSelectedTheme(); prefs.theme(newTheme); if (view != null) { ThemeLoader.loadTheme( newTheme, new GerritCallback<Void>() { @Override public void onSuccess(Void result) { view.operation( () -> { 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(); } @UiHandler("save") void onSave(@SuppressWarnings("unused") ClickEvent e) { AccountApi.putDiffPreferences( prefs, new GerritCallback<DiffPreferences>() { @Override public void onSuccess(DiffPreferences result) { DiffPreferencesInfo p = new DiffPreferencesInfo(); result.copyTo(p); Gerrit.setDiffPreferences(p); } }); if (view != null) { close(); } } @UiHandler("close") void onClose(ClickEvent e) { e.preventDefault(); close(); } void setFocus(boolean focus) { ignoreWhitespace.setFocus(focus); } private void close() { ((PopupPanel) getParent()).hide(); } private void setIgnoreWhitespace(Whitespace v) { String name = v != null ? v.name() : IGNORE_NONE.name(); for (int i = 0; i < ignoreWhitespace.getItemCount(); i++) { if (ignoreWhitespace.getValue(i).equals(name)) { ignoreWhitespace.setSelectedIndex(i); return; } } ignoreWhitespace.setSelectedIndex(0); } private void initIgnoreWhitespace() { ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), IGNORE_NONE.name()); ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_TRAILING(), IGNORE_TRAILING.name()); ignoreWhitespace.addItem( PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(), IGNORE_LEADING_AND_TRAILING.name()); ignoreWhitespace.addItem(PatchUtil.C.whitespaceIGNORE_ALL(), IGNORE_ALL.name()); } private void initMode() { mode.addItem("", ""); for (ModeInfo m : Natives.asList(ModeInfo.all())) { mode.addItem(m.name(), m.mime()); } } private void setMode(String modeType) { if (modeType != null && !modeType.isEmpty()) { ModeInfo m = ModeInfo.findModeByMIME(modeType); if (m != null) { for (int i = 0; i < mode.getItemCount(); i++) { if (mode.getValue(i).equals(m.mime())) { mode.setSelectedIndex(i); return; } } } } mode.setSelectedIndex(0); } private void setTheme(Theme v) { String name = v != null ? v.name() : Theme.DEFAULT.name(); for (int i = 0; i < theme.getItemCount(); i++) { if (theme.getValue(i).equals(name)) { theme.setSelectedIndex(i); return; } } theme.setSelectedIndex(0); } private void initTheme() { for (Theme t : Theme.values()) { theme.addItem(t.name().toLowerCase(), t.name()); } } }