JSResourceEditor.cpp 16 KB

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