1 /*
2 * SessionPlots.cpp
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
16 #include "SessionPlots.hpp"
17
18 #include <boost/format.hpp>
19 #include <boost/iostreams/filter/regex.hpp>
20
21 #include <shared_core/Error.hpp>
22 #include <core/Log.hpp>
23 #include <core/Exec.hpp>
24 #include <core/Predicate.hpp>
25 #include <shared_core/FilePath.hpp>
26 #include <core/BoostErrors.hpp>
27 #include <core/FileSerializer.hpp>
28
29 #include <core/system/Environment.hpp>
30
31 #include <core/text/TemplateFilter.hpp>
32
33 #include <core/http/Request.hpp>
34 #include <core/http/Response.hpp>
35
36 #include <r/RSexp.hpp>
37 #include <r/RExec.hpp>
38 #include <r/RRoutines.hpp>
39 #include <r/session/RGraphics.hpp>
40
41 #include <session/SessionModuleContext.hpp>
42
43 using namespace rstudio::core;
44
45 namespace rstudio {
46 namespace session {
47 namespace modules {
48 namespace plots {
49
50 namespace {
51
52 #define MAX_FIG_SIZE 3840*2
53
54 // locations
55 #define kGraphics "/graphics"
56
nextPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)57 Error nextPlot(const json::JsonRpcRequest& request,
58 json::JsonRpcResponse* pResponse)
59 {
60 r::session::graphics::Display& display = r::session::graphics::display();
61 return display.setActivePlot(display.activePlotIndex() + 1);
62 }
63
previousPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)64 Error previousPlot(const json::JsonRpcRequest& request,
65 json::JsonRpcResponse* pResponse)
66 {
67 r::session::graphics::Display& display = r::session::graphics::display();
68 return display.setActivePlot(display.activePlotIndex() - 1);
69 }
70
71
removePlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)72 Error removePlot(const json::JsonRpcRequest& request,
73 json::JsonRpcResponse* pResponse)
74 {
75 r::session::graphics::Display& display = r::session::graphics::display();
76
77 if (display.plotCount() < 1)
78 {
79 return Error(core::json::errc::ParamInvalid, ERROR_LOCATION);
80 }
81 else if (display.plotCount() == 1)
82 {
83 r::session::graphics::display().clear();
84 return Success();
85 }
86 else
87 {
88 int activePlot = display.activePlotIndex();
89 return display.removePlot(activePlot);
90 }
91 }
92
93
clearPlots(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)94 Error clearPlots(const json::JsonRpcRequest& request,
95 json::JsonRpcResponse* pResponse)
96 {
97 r::session::graphics::display().clear();
98 return Success();
99 }
100
refreshPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)101 Error refreshPlot(const json::JsonRpcRequest& request,
102 json::JsonRpcResponse* pResponse)
103 {
104 r::session::graphics::display().refresh();
105 return Success();
106 }
107
boolObject(bool value)108 json::Object boolObject(bool value)
109 {
110 json::Object boolObject;
111 boolObject["value"] = value;
112 return boolObject;
113 }
114
savePlotAs(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)115 Error savePlotAs(const json::JsonRpcRequest& request,
116 json::JsonRpcResponse* pResponse)
117 {
118 // get args
119 std::string path, format;
120 int width, height;
121 bool overwrite;
122 Error error = json::readParams(request.params,
123 &path,
124 &format,
125 &width,
126 &height,
127 &overwrite);
128 if (error)
129 return error;
130
131 // resolve path
132 FilePath plotPath = module_context::resolveAliasedPath(path);
133
134 // if it already exists and we aren't ovewriting then return false
135 if (plotPath.exists() && !overwrite)
136 {
137 pResponse->setResult(boolObject(false));
138 return Success();
139 }
140
141 // save plot
142 using namespace rstudio::r::session::graphics;
143 Display& display = r::session::graphics::display();
144 error = display.savePlotAsImage(plotPath, format, width, height);
145 if (error)
146 {
147 LOG_ERROR(error);
148 return error;
149 }
150
151
152 // set success result
153 pResponse->setResult(boolObject(true));
154 return Success();
155 }
156
157
savePlotAsPdf(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)158 Error savePlotAsPdf(const json::JsonRpcRequest& request,
159 json::JsonRpcResponse* pResponse)
160 {
161 // get args
162 std::string path;
163 double width, height;
164 bool useCairoPdf, overwrite;
165 Error error = json::readParams(request.params,
166 &path,
167 &width,
168 &height,
169 &useCairoPdf,
170 &overwrite);
171 if (error)
172 return error;
173
174 // resolve path
175 FilePath plotPath = module_context::resolveAliasedPath(path);
176
177 // if it already exists and we aren't ovewriting then return false
178 if (plotPath.exists() && !overwrite)
179 {
180 pResponse->setResult(boolObject(false));
181 return Success();
182 }
183
184 // save plot
185 using namespace rstudio::r::session::graphics;
186 Display& display = r::session::graphics::display();
187 error = display.savePlotAsPdf(plotPath, width, height, useCairoPdf);
188 if (error)
189 {
190 LOG_ERROR_MESSAGE(r::endUserErrorMessage(error));
191 return error;
192 }
193
194 // set success result
195 pResponse->setResult(boolObject(true));
196 return Success();
197 }
198
copyPlotToClipboardMetafile(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)199 Error copyPlotToClipboardMetafile(const json::JsonRpcRequest& request,
200 json::JsonRpcResponse* pResponse)
201 {
202 // get args
203 int width, height;
204 Error error = json::readParams(request.params, &width, &height);
205 if (error)
206 return error;
207
208 #if _WIN32
209
210 // create temp file to write to
211 FilePath targetFile = module_context::tempFile("clipboard", "emf");
212
213 // save as metafile
214 using namespace rstudio::r::session::graphics;
215 Display& display = r::session::graphics::display();
216 error = display.savePlotAsMetafile(targetFile, width, height);
217 if (error)
218 return error;
219
220 // copy to clipboard
221 error = system::copyMetafileToClipboard(targetFile);
222 if (error)
223 return error;
224
225 // remove temp file
226 error = targetFile.remove();
227 if (error)
228 LOG_ERROR(error);
229
230 return Success();
231
232 #else
233 return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
234 #endif
235 }
236
copyPlotToCocoaPasteboard(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)237 Error copyPlotToCocoaPasteboard(const json::JsonRpcRequest& request,
238 json::JsonRpcResponse* pResponse)
239 {
240 // get args
241 int width, height;
242 Error error = json::readParams(request.params, &width, &height);
243 if (error)
244 return error;
245
246 #if __APPLE__
247
248 // create temp file to write to
249 FilePath targetFile = module_context::tempFile("clipboard", "png");
250
251 // save as png
252 using namespace rstudio::r::session::graphics;
253 Display& display = r::session::graphics::display();
254 error = display.savePlotAsImage(targetFile, "png", width, height);
255 if (error)
256 return error;
257
258 // copy to pasteboard
259 error = module_context::copyImageToCocoaPasteboard(targetFile);
260 if (error)
261 return error;
262
263 // remove temp file
264 error = targetFile.remove();
265 if (error)
266 LOG_ERROR(error);
267
268 return Success();
269
270 #else
271 return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
272 #endif
273 }
274
275
plotsCreateRPubsHtml(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)276 Error plotsCreateRPubsHtml(const json::JsonRpcRequest& request,
277 json::JsonRpcResponse* pResponse)
278 {
279 // get params
280 std::string title, comment;
281 int width, height;
282 Error error = json::readParams(request.params,
283 &title,
284 &comment,
285 &width,
286 &height);
287 if (error)
288 return error;
289
290 // create a temp directory to work in
291 FilePath tempPath = module_context::tempFile("plots-rpubs", "dir");
292 error = tempPath.ensureDirectory();
293 if (error)
294 return error;
295
296 // save small plot
297 using namespace rstudio::r::session::graphics;
298 Display& display = r::session::graphics::display();
299 FilePath smallPlotPath = tempPath.completeChildPath("plot-small.png");
300 error = display.savePlotAsImage(smallPlotPath, "png", width, height);
301 if (error)
302 {
303 LOG_ERROR(error);
304 return error;
305 }
306
307 // save full plot
308 FilePath fullPlotPath = tempPath.completeChildPath("plot-full.png");
309 error = display.savePlotAsImage(fullPlotPath, "png", 1024, 768, 2.0);
310 if (error)
311 {
312 LOG_ERROR(error);
313 return error;
314 }
315
316 // copy source file to temp dir
317 FilePath sourceFilePath = tempPath.completeChildPath("source.html");
318 FilePath resPath = session::options().rResourcesPath();
319 FilePath plotFilePath = resPath.completePath("plot_publish.html");
320 error = plotFilePath.copy(sourceFilePath);
321
322 // perform the base64 encode using pandoc
323 FilePath targetFilePath = tempPath.completeChildPath("target.html");
324 error = module_context::createSelfContainedHtml(sourceFilePath,
325 targetFilePath);
326 if (error)
327 {
328 LOG_ERROR(error);
329 return error;
330 }
331
332 // return target path
333 pResponse->setResult(module_context::createAliasedPath(targetFilePath));
334
335 // return success
336 return Success();
337 }
338
339
340
341
342
uniqueSavePlotStem(const FilePath & directoryPath,std::string * pStem)343 Error uniqueSavePlotStem(const FilePath& directoryPath, std::string* pStem)
344 {
345 return module_context::uniqueSaveStem(directoryPath, "Rplot", pStem);
346 }
347
getUniqueSavePlotStem(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)348 Error getUniqueSavePlotStem(const json::JsonRpcRequest& request,
349 json::JsonRpcResponse* pResponse)
350 {
351 // get directory arg and convert to path
352 std::string directory;
353 Error error = json::readParam(request.params, 0, &directory);
354 if (error)
355 return error;
356 FilePath directoryPath = module_context::resolveAliasedPath(directory);
357
358 // get stem
359 std::string stem;
360 error = uniqueSavePlotStem(directoryPath, &stem);
361 if (error)
362 return error;
363
364 // set resposne
365 pResponse->setResult(stem);
366 return Success();
367 }
368
supportsSvg()369 bool supportsSvg()
370 {
371 bool supportsSvg = false;
372 Error error = r::exec::RFunction("capabilities", "cairo").call(&supportsSvg);
373 if (error)
374 LOG_ERROR(error);
375 return supportsSvg;
376 }
377
getSavePlotContext(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)378 Error getSavePlotContext(const json::JsonRpcRequest& request,
379 json::JsonRpcResponse* pResponse)
380 {
381 // get directory arg
382 std::string directory;
383 Error error = json::readParam(request.params, 0, &directory);
384 if (error)
385 return error;
386
387 // context
388 json::Object contextJson;
389
390 // get supported formats
391 using namespace module_context;
392 using namespace rstudio::r::session::graphics;
393 json::Array formats;
394 formats.push_back(plotExportFormat("PNG", kPngFormat));
395 formats.push_back(plotExportFormat("JPEG", kJpegFormat));
396 formats.push_back(plotExportFormat("TIFF", kTiffFormat));
397 formats.push_back(plotExportFormat("BMP", kBmpFormat));
398 #if _WIN32
399 formats.push_back(plotExportFormat("Metafile", kMetafileFormat));
400 #endif
401 if(supportsSvg())
402 formats.push_back(plotExportFormat("SVG", kSvgFormat));
403 formats.push_back(plotExportFormat("EPS", kPostscriptFormat));
404 contextJson["formats"] = formats;
405
406 // get directory path -- if it doesn't exist revert to the current
407 // working directory
408 FilePath directoryPath = module_context::resolveAliasedPath(directory);
409 if (!directoryPath.exists())
410 directoryPath = module_context::safeCurrentPath();
411
412 // reflect directory back to caller
413 contextJson["directory"] = module_context::createFileSystemItem(directoryPath);
414
415 // get unique stem
416 std::string stem;
417 error = uniqueSavePlotStem(directoryPath, &stem);
418 if (error)
419 return error;
420 contextJson["uniqueFileStem"] = stem;
421
422 pResponse->setResult(contextJson);
423
424 return Success();
425 }
426
427 template <typename T>
extractSizeParams(const http::Request & request,T min,T max,T * pWidth,T * pHeight,http::Response * pResponse)428 bool extractSizeParams(const http::Request& request,
429 T min,
430 T max,
431 T* pWidth,
432 T* pHeight,
433 http::Response* pResponse)
434 {
435 // get the width and height parameters
436 if (!request.queryParamValue("width",
437 predicate::range(min, max),
438 pWidth))
439 {
440 pResponse->setError(http::status::BadRequest, "invalid width");
441 return false;
442 }
443 if (!request.queryParamValue("height",
444 predicate::range(min, max),
445 pHeight))
446 {
447 pResponse->setError(http::status::BadRequest, "invalid height");
448 return false;
449 }
450
451 // got two valid params
452 return true;
453 }
454
setImageFileResponse(const FilePath & imageFilePath,const http::Request & request,http::Response * pResponse)455 void setImageFileResponse(const FilePath& imageFilePath,
456 const http::Request& request,
457 http::Response* pResponse)
458 {
459 // set content type
460 pResponse->setContentType(imageFilePath.getMimeContentType());
461
462 // attempt gzip
463 if (request.acceptsEncoding(http::kGzipEncoding))
464 pResponse->setContentEncoding(http::kGzipEncoding);
465
466 // set file
467 Error error = pResponse->setBody(imageFilePath);
468 if (error)
469 {
470 if (!core::isPathNotFoundError(error))
471 LOG_ERROR(error);
472 pResponse->setError(http::status::InternalServerError,
473 error.getMessage());
474 }
475 }
476
setTemporaryFileResponse(const FilePath & filePath,const http::Request & request,http::Response * pResponse)477 void setTemporaryFileResponse(const FilePath& filePath,
478 const http::Request& request,
479 http::Response* pResponse)
480 {
481 // no cache (dynamic content)
482 pResponse->setNoCacheHeaders();
483
484 // return the file
485 pResponse->setFile(filePath, request);
486
487 // delete the file
488 Error error = filePath.remove();
489 if (error)
490 LOG_ERROR(error);
491 }
492
handleZoomRequest(const http::Request & request,http::Response * pResponse)493 void handleZoomRequest(const http::Request& request, http::Response* pResponse)
494 {
495 using namespace rstudio::r::session;
496
497 // get the width and height parameters
498 int width, height;
499 if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
500 return;
501
502 // fire off the plot zoom size changed event to notify the client
503 // that a new default size should be established
504 json::Object dataJson;
505 dataJson["width"] = width;
506 dataJson["height"] = height;
507 ClientEvent event(client_events::kPlotsZoomSizeChanged, dataJson);
508 module_context::enqueClientEvent(event);
509
510 // get the scale parameter
511 int scale = request.queryParamValue("scale", 1);
512
513 // define template
514 std::stringstream templateStream;
515 templateStream <<
516 "<html>"
517 "<head>"
518 "<title>Plot Zoom</title>"
519 "<script type=\"text/javascript\">"
520
521 "window.onresize = function() {"
522
523 "var plotEl = document.getElementById('plot');"
524 "if (plotEl && (#scale#==1) ) {"
525 "plotEl.style.width='100%';"
526 "plotEl.style.height='100%';"
527 "}"
528
529 "if(window.activeTimer)"
530 "clearTimeout(window.activeTimer);"
531
532 "window.activeTimer = setTimeout( function() { "
533
534 "window.location.href = "
535 "\"plot_zoom?width=\" + Math.max(Math.min(document.body.clientWidth, " << MAX_FIG_SIZE << "), 100) "
536 " + \"&height=\" + Math.max(Math.min(document.body.clientHeight, " << MAX_FIG_SIZE << "), 100) "
537 " + \"&scale=\" + #scale#;"
538 "}, 300);"
539 "}"
540 "</script>"
541 "</head>"
542 "<body style=\"margin: 0; overflow: hidden\">"
543 "<img id=\"plot\" width=\"100%\" height=\"100%\" src=\"plot_zoom_png?width=#width#&height=#height#\"/>"
544 "</body>"
545 "</html>";
546
547 // define variables
548 std::map<std::string,std::string> variables;
549 variables["width"] = safe_convert::numberToString(width);
550 variables["height"] = safe_convert::numberToString(height);
551 variables["scale"] = safe_convert::numberToString(scale);;
552 text::TemplateFilter filter(variables);
553
554 pResponse->setNoCacheHeaders();
555 pResponse->setBody(templateStream, filter);
556 pResponse->setContentType("text/html");
557 }
558
handleZoomPngRequest(const http::Request & request,http::Response * pResponse)559 void handleZoomPngRequest(const http::Request& request,
560 http::Response* pResponse)
561 {
562 using namespace rstudio::r::session;
563
564 // get the width and height parameters
565 int width, height;
566 if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
567 return;
568
569 // generate the file
570 using namespace rstudio::r::session::graphics;
571 FilePath imagePath = module_context::tempFile("plot", "png");
572 Error saveError = graphics::display().savePlotAsImage(imagePath,
573 kPngFormat,
574 width,
575 height,
576 true);
577 if (saveError)
578 {
579 pResponse->setError(http::status::InternalServerError,
580 saveError.getMessage());
581 return;
582 }
583
584 // send it back
585 setImageFileResponse(imagePath, request, pResponse);
586
587 // delete the temp file
588 Error error = imagePath.remove();
589 if (error)
590 LOG_ERROR(error);
591 }
592
handlePngRequest(const http::Request & request,http::Response * pResponse)593 void handlePngRequest(const http::Request& request,
594 http::Response* pResponse)
595 {
596 // get the width and height parameters
597 int width, height;
598 if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
599 return;
600
601 // generate the image
602 using namespace rstudio::r::session;
603 FilePath imagePath = module_context::tempFile("plot", "png");
604 Error error = graphics::display().savePlotAsImage(imagePath,
605 graphics::kPngFormat,
606 width,
607 height);
608 if (error)
609 {
610 pResponse->setError(http::status::InternalServerError,
611 error.getMessage());
612 return;
613 }
614
615 // check for attachment flag and set content-disposition
616 bool attachment = request.queryParamValue("attachment") == "1";
617 if (attachment)
618 {
619 pResponse->setHeader("Content-Disposition",
620 "attachment; filename=rstudio-plot" +
621 imagePath.getExtension());
622 }
623
624 // return it
625 setTemporaryFileResponse(imagePath, request, pResponse);
626 }
627
628
629 // NOTE: this function assumes it is retreiving the image for the currently
630 // active plot (the assumption is implied by the fact that file not found
631 // on the requested png results in a redirect to the currently active
632 // plot's png). to handle this redirection we should always maintain an
633 // entry point with these semantics. if we wish to have an entry point
634 // for obtaining arbitrary pngs then it should be separate from this.
635
handleGraphicsRequest(const http::Request & request,http::Response * pResponse)636 void handleGraphicsRequest(const http::Request& request,
637 http::Response* pResponse)
638 {
639 // extract plot key from request (take everything after the last /)
640 std::string uri = request.uri();
641 std::size_t lastSlashPos = uri.find_last_of('/');
642 if (lastSlashPos == std::string::npos ||
643 lastSlashPos == (uri.length() - 1))
644 {
645 std::string errmsg = "invalid graphics uri: " + uri;
646 LOG_ERROR_MESSAGE(errmsg);
647 pResponse->setNotFoundError(request);
648 return;
649 }
650 std::string filename = uri.substr(lastSlashPos+1);
651
652 // calculate the path to the png
653 using namespace rstudio::r::session;
654 FilePath imagePath = graphics::display().imagePath(filename);
655
656 // if it exists then return it
657 if (imagePath.exists())
658 {
659 // strong named - cache permanently (in user's browser only)
660 pResponse->setPrivateCacheForeverHeaders();
661
662 // set the file
663 setImageFileResponse(imagePath, request, pResponse);
664 }
665 else
666 {
667 // redirect to png for currently active plot
668 if (graphics::display().hasOutput())
669 {
670 // calculate location of current image
671 std::string imageFilename = graphics::display().imageFilename();
672 std::string imageLocation = std::string(kGraphics "/") +
673 imageFilename;
674
675 // redirect to it
676 pResponse->setMovedTemporarily(request, imageLocation);
677 }
678 else
679 {
680 // not found error
681 pResponse->setNotFoundError(request);
682 }
683 }
684 }
685
686
enquePlotsChanged(const r::session::graphics::DisplayState & displayState,bool activatePlots,bool showManipulator)687 void enquePlotsChanged(const r::session::graphics::DisplayState& displayState,
688 bool activatePlots, bool showManipulator)
689 {
690 // build graphics output event
691 json::Object jsonPlotsState;
692 jsonPlotsState["filename"] = displayState.imageFilename;
693 jsonPlotsState["manipulator"] = displayState.manipulatorJson;
694 jsonPlotsState["width"] = displayState.width;
695 jsonPlotsState["height"] = displayState.height;
696 jsonPlotsState["plotIndex"] = displayState.activePlotIndex;
697 jsonPlotsState["plotCount"] = displayState.plotCount;
698 jsonPlotsState["activatePlots"] = activatePlots &&
699 (displayState.plotCount > 0);
700 jsonPlotsState["showManipulator"] = showManipulator;
701 ClientEvent plotsStateChangedEvent(client_events::kPlotsStateChanged,
702 jsonPlotsState);
703
704 // fire it
705 module_context::enqueClientEvent(plotsStateChangedEvent);
706
707 }
708
renderGraphicsOutput(bool activatePlots,bool showManipulator)709 void renderGraphicsOutput(bool activatePlots, bool showManipulator)
710 {
711 using namespace rstudio::r::session;
712 if (graphics::display().hasOutput())
713 {
714 graphics::display().render(
715 boost::bind(enquePlotsChanged, _1, activatePlots, showManipulator));
716 }
717 }
718
719
onClientInit()720 void onClientInit()
721 {
722 // if we have output make sure the client knows about it
723 renderGraphicsOutput(false, false);
724 }
725
detectChanges(bool activatePlots)726 void detectChanges(bool activatePlots)
727 {
728 // check for changes
729 using namespace rstudio::r::session;
730 if (graphics::display().hasChanges())
731 {
732 graphics::display().render(boost::bind(enquePlotsChanged,
733 _1,
734 activatePlots,
735 false));
736 }
737 }
738
onDetectChanges(module_context::ChangeSource source)739 void onDetectChanges(module_context::ChangeSource source)
740 {
741 bool activatePlots = source == module_context::ChangeSourceREPL;
742 detectChanges(activatePlots);
743 }
744
onBackgroundProcessing(bool isIdle)745 void onBackgroundProcessing(bool isIdle)
746 {
747 using namespace rstudio::r::session;
748 if (graphics::display().isActiveDevice() && graphics::display().hasChanges())
749 {
750 // verify that the last change is more than 50ms old. the reason
751 // we check for changes in the background is so we can respect
752 // plot animations (typically implemented using Sys.sleep). however,
753 // we don't want this to spill over inot incrementally rendering all
754 // plots as this will slow down overall plotting performance
755 // considerably.
756 using namespace boost::posix_time;
757 const int kChangeWindowMs = 50;
758 if ((graphics::display().lastChange() + milliseconds(kChangeWindowMs)) <
759 boost::posix_time::microsec_clock::universal_time())
760 {
761 detectChanges(isIdle); // activate plots only when idle
762 }
763 }
764 }
765
onBeforeExecute()766 void onBeforeExecute()
767 {
768 r::session::graphics::display().onBeforeExecute();
769 }
770
onShowManipulator()771 void onShowManipulator()
772 {
773 // render changes and show manipulator
774 renderGraphicsOutput(true, true);
775 }
776
setManipulatorValues(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)777 Error setManipulatorValues(const json::JsonRpcRequest& request,
778 json::JsonRpcResponse* pResponse)
779 {
780 // read the params
781 json::Object jsObject;
782 Error error = json::readParam(request.params, 0, &jsObject);
783 if (error)
784 return error;
785
786 // set them
787 using namespace rstudio::r::session;
788 graphics::display().setPlotManipulatorValues(jsObject);
789
790 // render
791 renderGraphicsOutput(true, false);
792
793 return Success();
794 }
795
manipulatorPlotClicked(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)796 Error manipulatorPlotClicked(const json::JsonRpcRequest& request,
797 json::JsonRpcResponse* pResponse)
798 {
799 // read the params
800 int x, y;
801 Error error = json::readParams(request.params, &x, &y);
802 if (error)
803 return error;
804
805 // notify the device
806 using namespace rstudio::r::session;
807 graphics::display().manipulatorPlotClicked(x, y);
808
809 // render
810 renderGraphicsOutput(true, false);
811
812 return Success();
813 }
814
rs_emitBeforeNewPlot()815 SEXP rs_emitBeforeNewPlot()
816 {
817 events().onBeforeNewPlot();
818 return R_NilValue;
819 }
820
rs_emitNewPlot()821 SEXP rs_emitNewPlot()
822 {
823 events().onNewPlot();
824 return R_NilValue;
825 }
826
rs_emitBeforeNewGridPage()827 SEXP rs_emitBeforeNewGridPage()
828 {
829 events().onBeforeNewGridPage();
830 return R_NilValue;
831 }
832
rs_savePlotAsImage(SEXP fileSEXP,SEXP formatSEXP,SEXP widthSEXP,SEXP heightSEXP)833 SEXP rs_savePlotAsImage(SEXP fileSEXP,
834 SEXP formatSEXP,
835 SEXP widthSEXP,
836 SEXP heightSEXP)
837 {
838 FilePath filePath(r::sexp::safeAsString(fileSEXP));
839 std::string format = r::sexp::safeAsString(formatSEXP);
840 int width = r::sexp::asInteger(widthSEXP);
841 int height = r::sexp::asInteger(heightSEXP);
842
843 r::session::graphics::Display& display = r::session::graphics::display();
844 if (display.hasOutput())
845 display.savePlotAsImage(filePath, format, width, height);
846
847 return R_NilValue;
848 }
849
850
851 } // anonymous namespace
852
haveCairoPdf()853 bool haveCairoPdf()
854 {
855 // make sure there is a real x server running on osx
856 #ifdef __APPLE__
857 std::string display = core::system::getenv("DISPLAY");
858 if (display.empty() || (display == ":0"))
859 return false;
860 #endif
861
862 SEXP functionSEXP = R_NilValue;
863 r::sexp::Protect rProtect;
864 r::exec::RFunction f(".rs.getPackageFunction", "cairo_pdf", "grDevices");
865 Error error = f.call(&functionSEXP, &rProtect);
866 if (error)
867 {
868 LOG_ERROR(error);
869 return false;
870 }
871
872 return functionSEXP != R_NilValue;
873 }
874
events()875 Events& events()
876 {
877 static Events instance;
878 return instance;
879 }
880
initialize()881 Error initialize()
882 {
883 // subscribe to events
884 using boost::bind;
885 module_context::events().onClientInit.connect(bind(onClientInit));
886 module_context::events().onDetectChanges.connect(bind(onDetectChanges, _1));
887 module_context::events().onBeforeExecute.connect(bind(onBeforeExecute));
888 module_context::events().onBackgroundProcessing.connect(onBackgroundProcessing);
889
890 RS_REGISTER_CALL_METHOD(rs_emitBeforeNewPlot, 0);
891 RS_REGISTER_CALL_METHOD(rs_emitBeforeNewGridPage, 0);
892 RS_REGISTER_CALL_METHOD(rs_emitNewPlot, 0);
893 RS_REGISTER_CALL_METHOD(rs_savePlotAsImage, 4);
894
895 // connect to onShowManipulator
896 using namespace rstudio::r::session;
897 graphics::display().onShowManipulator().connect(bind(onShowManipulator));
898
899 using namespace module_context;
900 ExecBlock initBlock;
901 initBlock.addFunctions()
902 (bind(registerRpcMethod, "next_plot", nextPlot))
903 (bind(registerRpcMethod, "previous_plot", previousPlot))
904 (bind(registerRpcMethod, "remove_plot", removePlot))
905 (bind(registerRpcMethod, "clear_plots", clearPlots))
906 (bind(registerRpcMethod, "refresh_plot", refreshPlot))
907 (bind(registerRpcMethod, "save_plot_as", savePlotAs))
908 (bind(registerRpcMethod, "save_plot_as_pdf", savePlotAsPdf))
909 (bind(registerRpcMethod, "copy_plot_to_clipboard_metafile", copyPlotToClipboardMetafile))
910 (bind(registerRpcMethod, "copy_plot_to_cocoa_pasteboard", copyPlotToCocoaPasteboard))
911 (bind(registerRpcMethod, "plots_create_rpubs_html", plotsCreateRPubsHtml))
912 (bind(registerRpcMethod, "get_unique_save_plot_stem", getUniqueSavePlotStem))
913 (bind(registerRpcMethod, "get_save_plot_context", getSavePlotContext))
914 (bind(registerRpcMethod, "set_manipulator_values", setManipulatorValues))
915 (bind(registerRpcMethod, "manipulator_plot_clicked", manipulatorPlotClicked))
916 (bind(registerUriHandler, kGraphics "/plot_zoom_png", handleZoomPngRequest))
917 (bind(registerUriHandler, kGraphics "/plot_zoom", handleZoomRequest))
918 (bind(registerUriHandler, kGraphics "/plot.png", handlePngRequest))
919 (bind(registerUriHandler, kGraphics, handleGraphicsRequest))
920 (bind(module_context::sourceModuleRFile, "SessionPlots.R"));
921
922 return initBlock.execute();
923 }
924
925 } // namespace plots
926 } // namespace modules
927 } // namespace session
928 } // namespace rstudio
929
930