1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2016-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/app.h"
13#include "app/app_menus.h"
14#include "app/resource_finder.h"
15#include "app/ui/browser_view.h"
16#include "app/ui/main_window.h"
17#include "app/ui/separator_in_view.h"
18#include "app/ui/skin/skin_theme.h"
19#include "app/ui/status_bar.h"
20#include "app/ui/workspace.h"
21#include "base/file_handle.h"
22#include "base/fs.h"
23#include "base/split_string.h"
24#include "os/font.h"
25#include "ui/alert.h"
26#include "ui/link_label.h"
27#include "ui/menu.h"
28#include "ui/message.h"
29#include "ui/paint_event.h"
30#include "ui/resize_event.h"
31#include "ui/size_hint_event.h"
32#include "ui/system.h"
33#include "ui/textbox.h"
34
35#include "cmark.h"
36
37#include <array>
38#include <cstring>
39#include <string>
40#include <vector>
41
42namespace app {
43
44using namespace ui;
45using namespace app::skin;
46
47namespace {
48
49RegisterMessage kLoadFileMessage;
50
51class LoadFileMessage : public Message {
52public:
53 LoadFileMessage(const std::string& file)
54 : Message(kLoadFileMessage)
55 , m_file(file) {
56 }
57
58 const std::string& file() const { return m_file; }
59
60private:
61 std::string m_file;
62};
63
64} // annonymous namespace
65
66// TODO This is not the best implementation, but it's "good enough"
67// for a first version.
68class BrowserView::CMarkBox : public Widget {
69 class Break : public Widget {
70 public:
71 Break() {
72 setMinSize(gfx::Size(0, font()->height()));
73 }
74 };
75 class OpenList : public Widget { };
76 class CloseList : public Widget { };
77 class Item : public Label {
78 public:
79 Item(const std::string& text) : Label(text) { }
80 };
81
82public:
83 obs::signal<void()> FileChange;
84
85 CMarkBox() {
86 initTheme();
87 }
88
89 const std::string& file() {
90 return m_file;
91 }
92
93 void loadFile(const std::string& inputFile,
94 const std::string& section = std::string()) {
95 std::string file = inputFile;
96 {
97 ResourceFinder rf;
98 rf.includeDataDir(file.c_str());
99 if (rf.findFirst())
100 file = rf.filename();
101 }
102 m_file = file;
103
104 cmark_parser* parser = cmark_parser_new(CMARK_OPT_DEFAULT);
105 FILE* fp = base::open_file_raw(file, "rb");
106 if (fp) {
107 std::array<char, 4096> buffer;
108 size_t bytes;
109 bool isTxt = (base::get_file_extension(file) == "txt");
110
111 if (isTxt)
112 cmark_parser_feed(parser, "```\n", 4);
113
114 while ((bytes = std::fread(&buffer[0], 1, buffer.size(), fp)) > 0) {
115 cmark_parser_feed(parser, &buffer[0], bytes);
116 if (bytes < buffer.size()) {
117 break;
118 }
119 }
120
121 if (isTxt)
122 cmark_parser_feed(parser, "\n```\n", 5);
123
124 cmark_node* root = cmark_parser_finish(parser);
125 if (root) {
126 processNode(root, section);
127 cmark_node_free(root);
128 }
129 fclose(fp);
130 }
131 else {
132 clear();
133 addText("File not found:");
134 addBreak();
135 addCodeBlock(file);
136 }
137 cmark_parser_free(parser);
138
139 relayout();
140 FileChange();
141 }
142
143 void focusSection() {
144 View* view = View::getView(this);
145 if (m_sectionWidget) {
146 int y = m_sectionWidget->bounds().y - bounds().y;
147 view->setViewScroll(gfx::Point(0, y));
148
149 m_sectionWidget = nullptr;
150 }
151 }
152
153private:
154 void layoutElements(int width,
155 std::function<void(const gfx::Rect& bounds,
156 Widget* child)> callback) {
157 const WidgetsList& children = this->children();
158 const gfx::Rect cpos = childrenBounds();
159
160 gfx::Point p = cpos.origin();
161 int maxH = 0;
162 int itemLevel = 0;
163 //Widget* prevChild = nullptr;
164
165 for (auto child : children) {
166 gfx::Size sz = child->sizeHint(gfx::Size(width, 0));
167
168 bool isBreak = (dynamic_cast<Break*>(child) ? true: false);
169 bool isOpenList = (dynamic_cast<OpenList*>(child) ? true: false);
170 bool isCloseList = (dynamic_cast<CloseList*>(child) ? true: false);
171 bool isItem = (dynamic_cast<Item*>(child) ? true: false);
172
173 if (isOpenList) {
174 ++itemLevel;
175 }
176 else if (isCloseList) {
177 --itemLevel;
178 }
179 else if (isItem) {
180 p.x -= sz.w;
181 }
182
183 if (child->isExpansive() ||
184 p.x+sz.w > cpos.x+width ||
185 isBreak || isOpenList || isCloseList) {
186 p.x = cpos.x + itemLevel*font()->textLength(" - ");
187 p.y += maxH;
188 maxH = 0;
189 //prevChild = nullptr;
190 }
191
192 if (child->isExpansive())
193 sz.w = std::max(sz.w, width);
194
195 callback(gfx::Rect(p, sz), child);
196
197 //if (!isItem) prevChild = child;
198 //if (isBreak) prevChild = nullptr;
199
200 maxH = std::max(maxH, sz.h);
201 p.x += sz.w;
202 }
203 }
204
205 void onSizeHint(SizeHintEvent& ev) override {
206 gfx::Size sz;
207
208 layoutElements(
209 View::getView(this)->viewportBounds().w - border().width(),
210 [&](const gfx::Rect& rc, Widget* child) {
211 sz.w = std::max(sz.w, rc.x+rc.w-this->bounds().x);
212 sz.h = std::max(sz.h, rc.y+rc.h-this->bounds().y);
213 });
214 sz.w += border().right();
215 sz.h += border().bottom();
216
217 ev.setSizeHint(sz);
218 }
219
220 void onResize(ResizeEvent& ev) override {
221 setBoundsQuietly(ev.bounds());
222
223 layoutElements(
224 View::getView(this)->viewportBounds().w - border().width(),
225 [](const gfx::Rect& rc, Widget* child) {
226 child->setBounds(rc);
227 });
228 }
229
230 void onPaint(PaintEvent& ev) override {
231 Graphics* g = ev.graphics();
232 gfx::Rect rc = clientBounds();
233 auto theme = SkinTheme::get(this);
234
235 g->fillRect(theme->colors.textboxFace(), rc);
236 }
237
238 bool onProcessMessage(Message* msg) override {
239 if (msg->type() == kLoadFileMessage) {
240 loadFile(static_cast<LoadFileMessage*>(msg)->file());
241 return true;
242 }
243
244 switch (msg->type()) {
245
246 case kMouseWheelMessage: {
247 View* view = View::getView(this);
248 if (view) {
249 auto mouseMsg = static_cast<MouseMessage*>(msg);
250 gfx::Point scroll = view->viewScroll();
251
252 if (mouseMsg->preciseWheel())
253 scroll += mouseMsg->wheelDelta();
254 else
255 scroll += mouseMsg->wheelDelta() * textHeight()*3;
256
257 view->setViewScroll(scroll);
258 }
259 break;
260 }
261 }
262
263 return Widget::onProcessMessage(msg);
264 }
265
266 void onInitTheme(InitThemeEvent& ev) override {
267 Widget::onInitTheme(ev);
268
269 auto theme = SkinTheme::get(this);
270 setBgColor(theme->colors.textboxFace());
271 setBorder(gfx::Border(4*guiscale()));
272 }
273
274 void clear() {
275 // Delete all children
276 while (auto child = lastChild())
277 delete child;
278 }
279
280 void processNode(cmark_node* root,
281 const std::string& section) {
282 clear();
283
284 m_content.clear();
285
286 bool inHeading = false;
287 bool inImage = false;
288 const char* inLink = nullptr;
289
290 cmark_iter* iter = cmark_iter_new(root);
291 cmark_event_type ev_type;
292 while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
293 cmark_node* cur = cmark_iter_get_node(iter);
294
295 switch (cmark_node_get_type(cur)) {
296
297 case CMARK_NODE_TEXT: {
298 const char* text = cmark_node_get_literal(cur);
299 if (!inImage && text) {
300 if (inLink) {
301 if (!m_content.empty() &&
302 m_content[m_content.size()-1] != ' ') {
303 m_content += " ";
304 }
305 m_content += text;
306 }
307 else {
308 m_content += text;
309 if (inHeading) {
310 closeContent();
311 if (section == text)
312 m_sectionWidget = lastChild();
313 }
314 }
315 }
316 break;
317 }
318
319 case CMARK_NODE_INLINE_HTML: {
320 const char* text = cmark_node_get_literal(cur);
321 if (text && std::strncmp(text, "<br />", 6) == 0) {
322 closeContent();
323 addBreak();
324 }
325 break;
326 }
327
328 case CMARK_NODE_CODE: {
329 const char* text = cmark_node_get_literal(cur);
330 if (text) {
331 closeContent();
332 addCodeInline(text);
333 }
334 break;
335 }
336
337 case CMARK_NODE_CODE_BLOCK: {
338 const char* text = cmark_node_get_literal(cur);
339 if (text) {
340 closeContent();
341 addCodeBlock(text);
342 }
343 break;
344 }
345
346 case CMARK_NODE_SOFTBREAK: {
347 m_content += " ";
348 break;
349 }
350
351 case CMARK_NODE_LINEBREAK: {
352 closeContent();
353 addBreak();
354 break;
355 }
356
357 case CMARK_NODE_LIST: {
358 if (ev_type == CMARK_EVENT_ENTER) {
359 closeContent();
360 addChild(new OpenList);
361 }
362 else if (ev_type == CMARK_EVENT_EXIT) {
363 closeContent();
364 addChild(new CloseList);
365 }
366 break;
367 }
368
369 case CMARK_NODE_ITEM: {
370 if (ev_type == CMARK_EVENT_ENTER) {
371 closeContent();
372 addChild(new Item(" - "));
373 }
374 break;
375 }
376
377 case CMARK_NODE_THEMATIC_BREAK: {
378 if (ev_type == CMARK_EVENT_ENTER) {
379 closeContent();
380 addSeparator();
381 }
382 else if (ev_type == CMARK_EVENT_EXIT) {
383 closeContent();
384 addBreak();
385 addBreak();
386 }
387 break;
388 }
389
390 case CMARK_NODE_HEADING: {
391 if (ev_type == CMARK_EVENT_ENTER) {
392 inHeading = true;
393
394 closeContent();
395 addSeparator();
396 }
397 else if (ev_type == CMARK_EVENT_EXIT) {
398 inHeading = false;
399
400 closeContent();
401 addBreak();
402 addBreak();
403 }
404 break;
405 }
406
407 case CMARK_NODE_PARAGRAPH: {
408 if (ev_type == CMARK_EVENT_EXIT) {
409 closeContent();
410 addBreak();
411 }
412 break;
413 }
414
415 case CMARK_NODE_IMAGE: {
416 if (ev_type == CMARK_EVENT_ENTER)
417 inImage = true;
418 else if (ev_type == CMARK_EVENT_EXIT)
419 inImage = false;
420 break;
421 }
422
423 case CMARK_NODE_LINK: {
424 if (ev_type == CMARK_EVENT_ENTER) {
425 inLink = cmark_node_get_url(cur);
426 if (inLink)
427 closeContent();
428 }
429 else if (ev_type == CMARK_EVENT_EXIT) {
430 if (inLink) {
431 if (!m_content.empty()) {
432 addLink(inLink, m_content);
433 m_content.clear();
434 }
435 }
436 inLink = nullptr;
437 }
438 break;
439 }
440
441 }
442 }
443 cmark_iter_free(iter);
444
445 closeContent();
446 }
447
448 void closeContent() {
449 if (!m_content.empty()) {
450 addText(m_content);
451 m_content.clear();
452 }
453 }
454
455 void addSeparator() {
456 auto sep = new SeparatorInView(std::string(), HORIZONTAL);
457 sep->setBorder(gfx::Border(0, font()->height(), 0, font()->height()));
458 sep->setExpansive(true);
459 addChild(sep);
460 }
461
462 void addBreak() {
463 addChild(new Break);
464 }
465
466 void addText(const std::string& content) {
467 auto theme = SkinTheme::get(this);
468
469 std::vector<std::string> words;
470 base::split_string(content, words, " ");
471 for (const auto& word : words)
472 if (!word.empty()) {
473 Label* label;
474
475 if (word.size() > 4 &&
476 std::strncmp(word.c_str(), "http", 4) == 0) {
477 label = new LinkLabel(word);
478 label->setStyle(theme->styles.browserLink());
479 }
480 else
481 label = new Label(word);
482
483 // Uncomment this line to debug labels
484 //label->setBgColor(gfx::rgba((rand()%128)+128, 128, 128));
485
486 addChild(label);
487 }
488 }
489
490 void addCodeInline(const std::string& content) {
491 auto theme = SkinTheme::get(this);
492 auto label = new Label(content);
493 label->setBgColor(theme->colors.textboxCodeFace());
494 addChild(label);
495 }
496
497 void addCodeBlock(const std::string& content) {
498 auto textBox = new TextBox(content, LEFT);
499 textBox->InitTheme.connect(
500 [textBox]{
501 auto theme = SkinTheme::get(textBox);
502 textBox->setBgColor(theme->colors.textboxCodeFace());
503 textBox->setBorder(gfx::Border(4*guiscale()));
504 });
505 textBox->initTheme();
506 addChild(textBox);
507 }
508
509 void addLink(const std::string& url, const std::string& text) {
510 auto label = new LinkLabel(url, text);
511 label->InitTheme.connect(
512 [label]{
513 auto theme = SkinTheme::get(label);
514 label->setStyle(theme->styles.browserLink());
515 });
516 label->initTheme();
517
518 if (url.find(':') == std::string::npos) {
519 label->setUrl("");
520 label->Click.connect(
521 [this, url]{
522 Message* msg = new LoadFileMessage(url);
523 msg->setRecipient(this);
524 Manager::getDefault()->enqueueMessage(msg);
525 });
526 }
527
528 // Uncomment this line to debug labels
529 //label->setBgColor(gfx::rgba((rand()%128)+128, 128, 128));
530
531 addChild(label);
532 }
533
534 void relayout() {
535 layout();
536 auto view = View::getView(this);
537 if (view) {
538 view->updateView();
539 view->setViewScroll(gfx::Point(0, 0));
540 }
541 invalidate();
542 }
543
544 std::string m_file;
545 std::string m_content;
546 Widget* m_sectionWidget = nullptr;
547};
548
549BrowserView::BrowserView()
550 : m_textBox(new CMarkBox)
551{
552 addChild(&m_view);
553
554 m_view.attachToView(m_textBox);
555 m_view.setExpansive(true);
556 m_view.InitTheme.connect(
557 [this]{
558 auto theme = SkinTheme::get(this);
559 m_view.setStyle(theme->styles.workspaceView());
560 });
561 m_view.initTheme();
562
563 m_textBox->FileChange.connect(
564 []{
565 App::instance()->workspace()->updateTabs();
566 });
567}
568
569BrowserView::~BrowserView()
570{
571 delete m_textBox;
572}
573
574void BrowserView::loadFile(const std::string& file,
575 const std::string& section)
576{
577 m_textBox->loadFile(file, section);
578}
579
580std::string BrowserView::getTabText()
581{
582 return base::get_file_title(m_textBox->file());
583}
584
585TabIcon BrowserView::getTabIcon()
586{
587 return TabIcon::NONE;
588}
589
590gfx::Color BrowserView::getTabColor()
591{
592 return gfx::ColorNone;
593}
594
595WorkspaceView* BrowserView::cloneWorkspaceView()
596{
597 return new BrowserView();
598}
599
600void BrowserView::onWorkspaceViewSelected()
601{
602 if (auto statusBar = StatusBar::instance())
603 statusBar->clearText();
604
605 if (m_textBox)
606 m_textBox->focusSection();
607}
608
609bool BrowserView::onCloseView(Workspace* workspace, bool quitting)
610{
611 workspace->removeView(this);
612 return true;
613}
614
615void BrowserView::onTabPopup(Workspace* workspace)
616{
617 Menu* menu = AppMenus::instance()->getTabPopupMenu();
618 if (!menu)
619 return;
620
621 menu->showPopup(mousePosInDisplay(), display());
622}
623
624} // namespace app
625