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 | |
42 | namespace app { |
43 | |
44 | using namespace ui; |
45 | using namespace app::skin; |
46 | |
47 | namespace { |
48 | |
49 | RegisterMessage kLoadFileMessage; |
50 | |
51 | class LoadFileMessage : public Message { |
52 | public: |
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 | |
60 | private: |
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. |
68 | class 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 | |
82 | public: |
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 | |
153 | private: |
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 | |
549 | BrowserView::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 | |
569 | BrowserView::~BrowserView() |
570 | { |
571 | delete m_textBox; |
572 | } |
573 | |
574 | void BrowserView::loadFile(const std::string& file, |
575 | const std::string& section) |
576 | { |
577 | m_textBox->loadFile(file, section); |
578 | } |
579 | |
580 | std::string BrowserView::getTabText() |
581 | { |
582 | return base::get_file_title(m_textBox->file()); |
583 | } |
584 | |
585 | TabIcon BrowserView::getTabIcon() |
586 | { |
587 | return TabIcon::NONE; |
588 | } |
589 | |
590 | gfx::Color BrowserView::getTabColor() |
591 | { |
592 | return gfx::ColorNone; |
593 | } |
594 | |
595 | WorkspaceView* BrowserView::cloneWorkspaceView() |
596 | { |
597 | return new BrowserView(); |
598 | } |
599 | |
600 | void BrowserView::onWorkspaceViewSelected() |
601 | { |
602 | if (auto statusBar = StatusBar::instance()) |
603 | statusBar->clearText(); |
604 | |
605 | if (m_textBox) |
606 | m_textBox->focusSection(); |
607 | } |
608 | |
609 | bool BrowserView::onCloseView(Workspace* workspace, bool quitting) |
610 | { |
611 | workspace->removeView(this); |
612 | return true; |
613 | } |
614 | |
615 | void BrowserView::(Workspace* workspace) |
616 | { |
617 | Menu* = AppMenus::instance()->getTabPopupMenu(); |
618 | if (!menu) |
619 | return; |
620 | |
621 | menu->showPopup(mousePosInDisplay(), display()); |
622 | } |
623 | |
624 | } // namespace app |
625 | |