1 /*
2  * AceEditorPreview.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.workbench.prefs.views;
16 
17 import com.google.gwt.core.client.GWT;
18 import com.google.gwt.dom.client.*;
19 import com.google.gwt.dom.client.Style.BorderStyle;
20 import com.google.gwt.dom.client.Style.Unit;
21 import com.google.gwt.resources.client.ClientBundle;
22 import com.google.gwt.resources.client.TextResource;
23 
24 import org.rstudio.core.client.ExternalJavaScriptLoader;
25 import org.rstudio.core.client.ExternalJavaScriptLoader.Callback;
26 import org.rstudio.core.client.theme.ThemeFonts;
27 import org.rstudio.core.client.widget.DynamicIFrame;
28 import org.rstudio.core.client.widget.FontSizer;
29 import org.rstudio.studio.client.application.Desktop;
30 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceResources;
31 
32 public class AceEditorPreview extends DynamicIFrame
33 {
AceEditorPreview(String code)34    public AceEditorPreview(String code)
35    {
36       super("Editor Theme Preview");
37       code_ = code;
38       Style style = getStyleElement().getStyle();
39       style.setBorderColor("#CCC");
40       style.setBorderWidth(1, Unit.PX);
41       style.setBorderStyle(BorderStyle.SOLID);
42    }
43 
44    @Override
onFrameLoaded()45    protected void onFrameLoaded()
46    {
47       isFrameLoaded_ = true;
48       final Document doc = getDocument();
49 
50       // NOTE: There is an interesting 'feature' in Firefox whereby an
51       // initialized IFrame will report that it has successfully initialized the
52       // window / underlying document (readyState is 'complete') but, in fact,
53       // there is still some initialization left to occur and any changes made
54       // before complete initialization will cause it to be swept out from under
55       // our feet. To work around this, we double-check that the document we are
56       // working with is the _same document_ after each JavaScript load
57       // iteration.
58       new ExternalJavaScriptLoader(getDocument(), AceResources.INSTANCE.acejs().getSafeUri().asString())
59             .addCallback(new Callback()
60       {
61          public void onLoaded()
62          {
63             if (getDocument() != doc)
64             {
65                onFrameLoaded();
66                return;
67             }
68 
69             new ExternalJavaScriptLoader(getDocument(), AceResources.INSTANCE.acesupportjs().getSafeUri().asString())
70                   .addCallback(new Callback()
71                   {
72                      public void onLoaded()
73                      {
74 
75                         if (getDocument() != doc)
76                         {
77                            onFrameLoaded();
78                            return;
79                         }
80 
81                         final Document doc = getDocument();
82                         final BodyElement body = doc.getBody();
83 
84                         if (themeUrl_ != null)
85                            setTheme(themeUrl_);
86                         if (fontSize_ != null)
87                            setFontSize(fontSize_);
88                         if (zoomLevel_ != null)
89                            setZoomLevel(zoomLevel_);
90 
91                         doc.getHead().getParentElement().setLang("en"); // accessibility requirement
92 
93                         body.getStyle().setMargin(0, Unit.PX);
94                         body.getStyle().setBackgroundColor("white");
95 
96                         StyleElement style = doc.createStyleElement();
97                         style.setType("text/css");
98                         style.setInnerText(
99                               ".ace_editor {\n" +
100                                     "border: none !important;\n" +
101                               "}");
102                         if (Desktop.isDesktop())
103                            setFont(ThemeFonts.getFixedWidthFont(), false);
104                         else if (webFont_ != null)
105                            setFont(webFont_, true);
106                         body.appendChild(style);
107 
108                         DivElement div = doc.createDivElement();
109                         div.setId("editor");
110                         div.getStyle().setWidth(100, Unit.PCT);
111                         div.getStyle().setHeight(100, Unit.PCT);
112                         div.setInnerText(code_);
113                         body.appendChild(div);
114 
115                         FontSizer.injectStylesIntoDocument(doc);
116                         FontSizer.applyNormalFontSize(div);
117 
118                         body.appendChild(doc.createScriptElement(RES.loader().getText()));
119                      }
120                   });
121          }
122       });
123    }
124 
setTheme(String themeUrl)125    public void setTheme(String themeUrl)
126    {
127       themeUrl_ = themeUrl;
128       if (!isFrameLoaded_)
129          return;
130 
131       if (currentStyleLink_ != null)
132          currentStyleLink_.removeFromParent();
133 
134       Document doc = getDocument();
135       currentStyleLink_ = doc.createLinkElement();
136       currentStyleLink_.setRel("stylesheet");
137       currentStyleLink_.setType("text/css");
138       currentStyleLink_.setHref(themeUrl);
139       doc.getBody().appendChild(currentStyleLink_);
140    }
141 
setFontSize(double fontSize)142    public void setFontSize(double fontSize)
143    {
144       fontSize_ = fontSize;
145       if (!isFrameLoaded_)
146          return;
147 
148       if (zoomLevel_ == null)
149          FontSizer.setNormalFontSize(getDocument(), fontSize_);
150       else
151          FontSizer.setNormalFontSize(getDocument(), fontSize_ * zoomLevel_);
152    }
153 
setFont(String font, boolean webFont)154    public void setFont(String font, boolean webFont)
155    {
156       final String STYLE_EL_ID = "__rstudio_font_family";
157       final String LINK_EL_ID = "__rstudio_font_link";
158       Document document = getDocument();
159 
160       Element oldStyle = document.getElementById(STYLE_EL_ID);
161       Element oldLink = document.getElementById(LINK_EL_ID);
162 
163       if (webFont)
164       {
165          LinkElement link = document.createLinkElement();
166          link.setRel("stylesheet");
167          link.setHref("fonts/css/" + font + ".css");
168          link.setId(LINK_EL_ID);
169          document.getHead().appendChild(link);
170          webFont_ = font;
171       }
172 
173       StyleElement style = document.createStyleElement();
174       style.setAttribute("type", "text/css");
175       style.setInnerText(".ace_editor, .ace_text-layer {\n" +
176                          "font-family: \"" + font + "\" !important;\n" +
177                          "}");
178 
179       document.getBody().appendChild(style);
180 
181       if (oldStyle != null)
182          oldStyle.removeFromParent();
183       if (oldLink != null)
184          oldLink.removeFromParent();
185 
186       style.setId(STYLE_EL_ID);
187    }
188 
setZoomLevel(double zoomLevel)189    public void setZoomLevel(double zoomLevel)
190    {
191       zoomLevel_ = zoomLevel;
192       if (!isFrameLoaded_)
193          return;
194 
195       if (fontSize_ != null)
196          setFontSize(fontSize_);
197    }
198 
199    private LinkElement currentStyleLink_;
200    private boolean isFrameLoaded_;
201    private String themeUrl_;
202    private String webFont_;
203    private Double fontSize_;
204    private Double zoomLevel_;
205    private final String code_;
206 
207    public interface Resources extends ClientBundle
208    {
209       @Source("AceEditorPreview.js")
loader()210       TextResource loader();
211    }
212 
213    private static Resources RES = GWT.create(Resources.class);
214 
215 }
216