JSResourceEditor.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. // Copyright (c) 2014-2015, THUNDERBEAST GAMES LLC All rights reserved
  2. // Please see LICENSE.md in repository root for license information
  3. // https://github.com/AtomicGameEngine/AtomicGameEngine
  4. #include "AtomicEditor.h"
  5. #include <Atomic/Container/ArrayPtr.h>
  6. #include <Atomic/UI/UI.h>
  7. #include <Atomic/IO/Log.h>
  8. #include <Atomic/IO/File.h>
  9. #include <Atomic/IO/FileSystem.h>
  10. #include <Atomic/Resource/ResourceCache.h>
  11. #include <Atomic/Core/CoreEvents.h>
  12. #include <AtomicJS/Javascript/JSVM.h>
  13. /*
  14. #include "../AEEvents.h"
  15. #include "../UI/UIFindTextWidget.h"
  16. #include "../AEJavascript.h"
  17. */
  18. #include "JSResourceEditor.h"
  19. #include "../Javascript/JSAutocomplete.h"
  20. #include "../Javascript/JSTheme.h"
  21. #include "../Javascript/JSASTSyntaxColorVisitor.h"
  22. #include <TurboBadger/tb_message_window.h>
  23. #include <TurboBadger/tb_editfield.h>
  24. #include <TurboBadger/tb_style_edit.h>
  25. #include <TurboBadger/tb_style_edit_content.h>
  26. using namespace tb;
  27. namespace AtomicEditor
  28. {
  29. JSResourceEditor ::JSResourceEditor(Context* context, const String &fullpath, UITabContainer *container) :
  30. ResourceEditor(context, fullpath, container),
  31. styleEdit_(0),
  32. lineNumberList_(0),
  33. editField_(0),
  34. autocomplete_(0),
  35. textDirty_(true),
  36. textDelta_(0.0f),
  37. modified_(false),
  38. currentFindPos_(-1)
  39. {
  40. TBLayout* layout = new TBLayout();
  41. layout->SetLayoutSize(LAYOUT_SIZE_GRAVITY);
  42. layout->SetGravity(WIDGET_GRAVITY_ALL);
  43. layout->SetLayoutDistribution(LAYOUT_DISTRIBUTION_GRAVITY);
  44. rootContentWidget_->GetInternalWidget()->AddChild(layout);
  45. TBContainer* c = new TBContainer();
  46. c->SetGravity(WIDGET_GRAVITY_ALL);
  47. TBEditField* text = editField_ = new TBEditField();
  48. text->SetMultiline(true);
  49. text->SetWrapping(true);
  50. text->SetGravity(WIDGET_GRAVITY_ALL);
  51. text->SetStyling(true);
  52. text->SetSkinBg(TBIDC("TextCode"));
  53. TBFontDescription fd;
  54. fd.SetID(TBIDC("Monaco"));
  55. fd.SetSize(12);
  56. text->SetFontDescription(fd);
  57. SharedPtr<File> jsFile(GetSubsystem<ResourceCache>()->GetFile(fullpath));
  58. assert(jsFile);
  59. String source;
  60. jsFile->ReadText(source);
  61. String json;
  62. JSASTProgram* program = NULL;
  63. if (ParseJavascriptToJSON(source.CString(), json))
  64. {
  65. program = JSASTProgram::ParseFromJSON(fullpath, json);
  66. }
  67. text->SetText(source.CString());
  68. lineNumberList_ = new TBSelectList();
  69. lineNumberList_->SetFontDescription(fd);
  70. lineNumberList_->SetSkinBg(TBIDC("LineNumberSelectList"));
  71. lineNumberList_->GetScrollContainer()->SetScrollMode(SCROLL_MODE_OFF);
  72. //lineNumberList_->GetScrollContainer()->SetIgnoreScrollEvents(true);
  73. lineNumberList_->SetGravity(WIDGET_GRAVITY_ALL);
  74. LayoutParams lp;
  75. lp.max_w = 48;
  76. lineNumberList_->SetLayoutParams(lp);
  77. c->AddChild(text);
  78. layout->AddChild(lineNumberList_);
  79. layout->AddChild(c);
  80. layout->SetSpacing(0);
  81. TBStyleEdit* sedit = text->GetStyleEdit();
  82. TBTextTheme* theme = new TBTextTheme();
  83. for (unsigned i = 0; i < TB_MAX_TEXT_THEME_COLORS; i++)
  84. theme->themeColors[i] = TBColor(255, 255, 255);
  85. theme->themeColors[JSTHEME_LITERAL_STRING].SetFromString("#E6DB74", 7);
  86. theme->themeColors[JSTHEME_LITERAL_NUMBER].SetFromString("#AE81FF", 7);
  87. theme->themeColors[JSTHEME_LITERAL_REGEX].SetFromString("#AE81FF", 7);
  88. theme->themeColors[JSTHEME_LITERAL_BOOLEAN].SetFromString("#AE81FF", 7);
  89. theme->themeColors[JSTHEME_LITERAL_NULL].SetFromString("#AE81FF", 7);
  90. theme->themeColors[JSTHEME_FUNCTION].SetFromString("#66D9EF", 7);
  91. theme->themeColors[JSTHEME_VAR].SetFromString("#66D9EF", 7);
  92. theme->themeColors[JSTHEME_KEYWORD].SetFromString("#f92672", 7);
  93. theme->themeColors[JSTHEME_OPERATOR].SetFromString("#f92672", 7);
  94. theme->themeColors[JSTHEME_CODE].SetFromString("#a6e22e", 7);
  95. theme->themeColors[JSTHEME_COMMENT].SetFromString("#75715e", 7);
  96. theme->themeColors[JSTHEME_FUNCTIONDECLARG].SetFromString("#FF9800", 7);
  97. sedit->SetTextTheme(theme);
  98. sedit->text_change_listener = this;
  99. styleEdit_ = sedit;
  100. UpdateLineNumbers();
  101. if (program)
  102. {
  103. JSASTSyntaxColorVisitor syntaxColor(sedit);
  104. syntaxColor.visit(program);
  105. }
  106. autocomplete_ = new JSAutocomplete(text);
  107. autocomplete_->UpdateLocals();
  108. SubscribeToEvent(E_UPDATE, HANDLER(JSResourceEditor, HandleUpdate));
  109. // FIXME: Set the size at the end of setup, so all children are updated accordingly
  110. // future size changes will be handled automatically
  111. IntRect rect = container_->GetContentRoot()->GetRect();
  112. rootContentWidget_->SetSize(rect.Width(), rect.Height());
  113. }
  114. JSResourceEditor::~JSResourceEditor()
  115. {
  116. }
  117. void JSResourceEditor::UpdateLineNumbers()
  118. {
  119. if (!styleEdit_)
  120. return;
  121. TBGenericStringItemSource* lineSource = lineNumberList_->GetDefaultSource();
  122. int lines = lineSource->GetNumItems();
  123. int lineCount = styleEdit_->blocks.CountLinks();
  124. if (lines == lineCount)
  125. return;
  126. while (lines > lineCount)
  127. {
  128. lineSource->DeleteItem(lineSource->GetNumItems() - 1);
  129. lines --;
  130. }
  131. for (int i = lines; i < lineCount; i++)
  132. {
  133. String sline;
  134. sline.AppendWithFormat("%i ", i + 1);
  135. TBGenericStringItem* item = new TBGenericStringItem(sline.CString());
  136. lineSource->AddItem(item);
  137. }
  138. // item widgets don't exist until ValidateList
  139. lineNumberList_->ValidateList();
  140. for (int i = 0; i < lineCount; i++)
  141. {
  142. TBTextField* textField = (TBTextField* )lineNumberList_->GetItemWidget(i);
  143. if (textField)
  144. {
  145. textField->SetTextAlign(TB_TEXT_ALIGN_RIGHT);
  146. textField->SetSkinBg(TBIDC("TBSelectItemLineNumber"));
  147. }
  148. }
  149. }
  150. void JSResourceEditor::OnChange(TBStyleEdit* styleEdit)
  151. {
  152. textDelta_ = 0.25f;
  153. textDirty_ = true;
  154. modified_ = true;
  155. String filename = GetFileNameAndExtension(fullpath_);
  156. filename += "*";
  157. button_->SetText(filename.CString());
  158. autocomplete_->Hide();
  159. TBTextFragment* fragment = 0;
  160. int ofs = styleEdit_->caret.pos.ofs;
  161. fragment = styleEdit_->caret.pos.block->FindFragment(ofs, true);
  162. if (fragment && fragment->len && (styleEdit_->caret.pos.ofs == (fragment->ofs + fragment->len)))
  163. {
  164. String value(fragment->Str(), fragment->len);
  165. bool hasCompletions = autocomplete_->UpdateCompletions(value);
  166. if (hasCompletions)
  167. {
  168. autocomplete_->SetPosition(TBPoint(fragment->xpos, (styleEdit_->caret.y - styleEdit_->scroll_y) + fragment->line_height));
  169. autocomplete_->Show();
  170. }
  171. }
  172. UpdateLineNumbers();
  173. }
  174. bool JSResourceEditor::OnEvent(const TBWidgetEvent &ev)
  175. {
  176. if (ev.type == EVENT_TYPE_KEY_DOWN)
  177. {
  178. if (autocomplete_ && autocomplete_->Visible())
  179. {
  180. return autocomplete_->OnEvent(ev);
  181. }
  182. if (ev.special_key == TB_KEY_ESC)
  183. {
  184. //SendEvent(E_FINDTEXTCLOSE);
  185. }
  186. }
  187. if (ev.type == EVENT_TYPE_SHORTCUT)
  188. {
  189. if (ev.ref_id == TBIDC("close"))
  190. {
  191. if (modified_)
  192. {
  193. TBMessageWindow *msg_win = new TBMessageWindow(container_->GetInternalWidget(), TBIDC("unsaved_jsmodifications_dialog"));
  194. TBMessageWindowSettings settings(TB_MSG_OK_CANCEL, TBID(uint32(0)));
  195. settings.dimmer = true;
  196. settings.styling = true;
  197. msg_win->Show("Unsaved Modifications", "There are unsaved modications.\nDo you wish to discard them and close?", &settings, 640, 360);
  198. }
  199. else
  200. {
  201. Close();
  202. }
  203. }
  204. if (ev.ref_id == TBIDC("save") && modified_)
  205. {
  206. TBStr text;
  207. styleEdit_->GetText(text);
  208. File file(context_, fullpath_, FILE_WRITE);
  209. file.Write((void*) text.CStr(), text.Length());
  210. file.Close();
  211. String filename = GetFileNameAndExtension(fullpath_);
  212. button_->SetText(filename.CString());
  213. modified_ = false;
  214. //SendEvent(E_JAVASCRIPTSAVED);
  215. return true;
  216. }
  217. else if (ev.ref_id == TBIDC("find"))
  218. {
  219. //using namespace FindTextOpen;
  220. //SendEvent(E_FINDTEXTOPEN);
  221. }
  222. else if (ev.ref_id == TBIDC("findnext") || ev.ref_id == TBIDC("findprev"))
  223. {
  224. /*
  225. String text;
  226. FindTextWidget* finder = GetSubsystem<FindTextWidget>();
  227. finder->GetFindText(text);
  228. // TODO: get flags from finder
  229. unsigned flags = FINDTEXT_FLAG_NONE;
  230. if (ev.ref_id == TBIDC("findnext"))
  231. flags |= FINDTEXT_FLAG_NEXT;
  232. else if (ev.ref_id == TBIDC("findprev"))
  233. flags |= FINDTEXT_FLAG_PREV;
  234. flags |= FINDTEXT_FLAG_WRAP;
  235. finder->Find(text, flags);
  236. */
  237. }
  238. else if (ev.ref_id == TBIDC("beautify"))
  239. {
  240. TBStr text;
  241. styleEdit_->GetText(text);
  242. if (text.Length())
  243. {
  244. String output;
  245. if (BeautifyJavascript(text.CStr(), output))
  246. {
  247. if (output.Length())
  248. {
  249. styleEdit_->selection.SelectAll();
  250. styleEdit_->InsertText(output.CString(), output.Length());
  251. }
  252. }
  253. }
  254. }
  255. else if (ev.ref_id == TBIDC("cut") || ev.ref_id == TBIDC("copy") || ev.ref_id == TBIDC("paste")
  256. || ev.ref_id == TBIDC("selectall") || ev.ref_id == TBIDC("undo") || ev.ref_id == TBIDC("redo") )
  257. {
  258. editField_->OnEvent(ev);
  259. }
  260. }
  261. if (ev.type == EVENT_TYPE_CLICK)
  262. {
  263. if (ev.target->GetID() == TBIDC("unsaved_jsmodifications_dialog"))
  264. {
  265. if (ev.ref_id == TBIDC("TBMessageWindow.ok"))
  266. {
  267. Close();
  268. }
  269. else
  270. {
  271. SetFocus();
  272. }
  273. return true;
  274. }
  275. }
  276. return false;
  277. }
  278. void JSResourceEditor::HandleUpdate(StringHash eventType, VariantMap& eventData)
  279. {
  280. if (!styleEdit_)
  281. return;
  282. // sync line number
  283. lineNumberList_->GetScrollContainer()->ScrollTo(0, styleEdit_->scroll_y);
  284. lineNumberList_->SetValue(styleEdit_->GetCaretLine());
  285. if (autocomplete_->Visible())
  286. {
  287. TBTextFragment* fragment = 0;
  288. int ofs = styleEdit_->caret.pos.ofs;
  289. fragment = styleEdit_->caret.pos.block->FindFragment(ofs, true);
  290. if (fragment && (styleEdit_->caret.pos.ofs == (fragment->ofs + fragment->len)))
  291. {
  292. String value(fragment->Str(), fragment->len);
  293. bool hasCompletions = autocomplete_->UpdateCompletions(value);
  294. if (!hasCompletions)
  295. {
  296. autocomplete_->Hide();
  297. }
  298. }
  299. }
  300. // Timestep parameter is same no matter what event is being listened to
  301. float timeStep = eventData[Update::P_TIMESTEP].GetFloat();
  302. if (!textDirty_)
  303. return;
  304. if (textDelta_ > 0.0f)
  305. {
  306. textDelta_ -= timeStep;
  307. if (textDelta_ < 0.0f)
  308. {
  309. textDelta_ = 0.0f;
  310. }
  311. else
  312. {
  313. return;
  314. }
  315. }
  316. TBStr text;
  317. styleEdit_->GetText(text);
  318. JSASTProgram* program = NULL;
  319. String json;
  320. if (ParseJavascriptToJSON(text.CStr(), json))
  321. {
  322. program = JSASTProgram::ParseFromJSON("fullpath", json);
  323. if (program)
  324. {
  325. JSASTSyntaxColorVisitor syntaxColor(styleEdit_);
  326. syntaxColor.visit(program);
  327. delete program;
  328. }
  329. }
  330. textDirty_ = false;
  331. editField_->SetFocus(WIDGET_FOCUS_REASON_UNKNOWN);
  332. }
  333. void JSResourceEditor::FindTextClose()
  334. {
  335. editField_->SetFocus(WIDGET_FOCUS_REASON_UNKNOWN);
  336. styleEdit_->selection.SelectNothing();
  337. }
  338. bool JSResourceEditor::FindText(const String& findText, unsigned flags)
  339. {
  340. /*
  341. unsigned findLength = findText.Length();
  342. if (!findLength)
  343. return true;
  344. TBStr _source;
  345. styleEdit_->GetText(_source);
  346. String source = _source.CStr();
  347. unsigned pos = String::NPOS;
  348. int startPos = currentFindPos_;
  349. if (currentFindPos_ == -1)
  350. startPos = styleEdit_->caret.GetGlobalOfs();
  351. else
  352. {
  353. if (flags & FINDTEXT_FLAG_NEXT)
  354. startPos += findLength;
  355. }
  356. if (flags & FINDTEXT_FLAG_PREV)
  357. {
  358. String pretext = source.Substring(0, startPos);
  359. pos = pretext.FindLast(findText, String::NPOS, flags & FINDTEXT_FLAG_CASESENSITIVE ? true : false);
  360. }
  361. else
  362. {
  363. pos = source.Find(findText, startPos, flags & FINDTEXT_FLAG_CASESENSITIVE ? true : false);
  364. }
  365. if (pos == String::NPOS)
  366. {
  367. if (flags & FINDTEXT_FLAG_WRAP)
  368. {
  369. if (flags & FINDTEXT_FLAG_PREV)
  370. {
  371. pos = source.FindLast(findText, String::NPOS, flags & FINDTEXT_FLAG_CASESENSITIVE ? true : false);
  372. }
  373. else
  374. {
  375. pos = source.Find(findText, 0, flags & FINDTEXT_FLAG_CASESENSITIVE ? true : false);
  376. }
  377. }
  378. if (pos == String::NPOS)
  379. {
  380. styleEdit_->selection.SelectNothing();
  381. return true;
  382. }
  383. }
  384. currentFindPos_ = pos;
  385. styleEdit_->caret.SetGlobalOfs((int) pos + findLength);
  386. int height = styleEdit_->layout_height;
  387. int newy = styleEdit_->caret.y - height/2;
  388. styleEdit_->SetScrollPos(styleEdit_->scroll_x, newy);
  389. styleEdit_->selection.Select(pos, pos + findLength);
  390. */
  391. return true;
  392. }
  393. void JSResourceEditor::SetFocus()
  394. {
  395. editField_->SetFocus(WIDGET_FOCUS_REASON_UNKNOWN);
  396. }
  397. void JSResourceEditor::GotoTokenPos(int tokenPos)
  398. {
  399. styleEdit_->caret.SetGlobalOfs(tokenPos);
  400. int height = styleEdit_->layout_height;
  401. int newy = styleEdit_->caret.y - height/2;
  402. styleEdit_->SetScrollPos(styleEdit_->scroll_x, newy);
  403. }
  404. void JSResourceEditor::GotoLineNumber(int lineNumber)
  405. {
  406. int line = 0;
  407. TBBlock *block = NULL;
  408. for (block = styleEdit_->blocks.GetFirst(); block; block = block->GetNext())
  409. {
  410. if (lineNumber == line)
  411. break;
  412. line++;
  413. }
  414. if (!block)
  415. return;
  416. styleEdit_->caret.Place(block, 0);
  417. int height = styleEdit_->layout_height;
  418. int newy = styleEdit_->caret.y - height/2;
  419. styleEdit_->SetScrollPos(styleEdit_->scroll_x, newy);
  420. }
  421. bool JSResourceEditor::HasUnsavedModifications()
  422. {
  423. return modified_;
  424. }
  425. bool JSResourceEditor::ParseJavascriptToJSON(const char* source, String& json, bool loose)
  426. {
  427. JSVM* vm = JSVM::GetJSVM(NULL);
  428. duk_context* ctx = vm->GetJSContext();
  429. int top = duk_get_top(ctx);
  430. json.Clear();
  431. duk_get_global_string(ctx, "require");
  432. duk_push_string(ctx, "AtomicEditor/typescript/modules/jsutils");
  433. if (duk_pcall(ctx, 1))
  434. {
  435. printf("Error: %s\n", duk_safe_to_string(ctx, -1));
  436. duk_set_top(ctx, top);
  437. return false;
  438. }
  439. duk_get_prop_string(ctx, -1, "parseToJSON");
  440. duk_push_string(ctx, source);
  441. bool ok = true;
  442. if (duk_pcall(ctx, 1))
  443. {
  444. ok = false;
  445. printf("Error: %s\n", duk_safe_to_string(ctx, -1));
  446. }
  447. else
  448. {
  449. json = duk_to_string(ctx, -1);
  450. }
  451. duk_set_top(ctx, top);
  452. return ok;
  453. }
  454. bool JSResourceEditor::BeautifyJavascript(const char* source, String& output)
  455. {
  456. JSVM* vm = JSVM::GetJSVM(NULL);
  457. duk_context* ctx = vm->GetJSContext();
  458. int top = duk_get_top(ctx);
  459. output.Clear();
  460. duk_get_global_string(ctx, "require");
  461. duk_push_string(ctx, "AtomicEditor/typescript/modules/jsutils");
  462. if (duk_pcall(ctx, 1))
  463. {
  464. printf("Error: %s\n", duk_safe_to_string(ctx, -1));
  465. duk_set_top(ctx, top);
  466. return false;
  467. }
  468. duk_get_prop_string(ctx, -1, "jsBeautify");
  469. duk_push_string(ctx, source);
  470. bool ok = true;
  471. if (duk_pcall(ctx, 1))
  472. {
  473. ok = false;
  474. printf("Error: %s\n", duk_safe_to_string(ctx, -1));
  475. }
  476. else
  477. {
  478. output = duk_to_string(ctx, -1);
  479. }
  480. // ignore result
  481. duk_set_top(ctx, top);
  482. return ok;
  483. }
  484. }