JSResourceEditor.cpp 16 KB

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