1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 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/ui/font_popup.h"
13
14#include "app/commands/cmd_set_palette.h"
15#include "app/commands/commands.h"
16#include "app/console.h"
17#include "app/font_path.h"
18#include "app/i18n/strings.h"
19#include "app/match_words.h"
20#include "app/ui/search_entry.h"
21#include "app/ui/skin/skin_theme.h"
22#include "app/ui_context.h"
23#include "app/util/conversion_to_surface.h"
24#include "app/util/freetype_utils.h"
25#include "base/fs.h"
26#include "base/string.h"
27#include "doc/image.h"
28#include "doc/image_ref.h"
29#include "os/surface.h"
30#include "os/system.h"
31#include "ui/box.h"
32#include "ui/button.h"
33#include "ui/fit_bounds.h"
34#include "ui/graphics.h"
35#include "ui/listitem.h"
36#include "ui/paint_event.h"
37#include "ui/size_hint_event.h"
38#include "ui/theme.h"
39#include "ui/view.h"
40
41#include "font_popup.xml.h"
42
43#ifdef _WIN32
44 #include <shlobj.h>
45 #include <windows.h>
46 #undef max
47#endif
48
49#include <algorithm>
50#include <map>
51
52namespace app {
53
54using namespace ui;
55
56static std::map<std::string, doc::ImageRef> g_thumbnails;
57
58class FontItem : public ListItem {
59public:
60 FontItem(const std::string& fn)
61 : ListItem(base::get_file_title(fn))
62 , m_image(g_thumbnails[fn])
63 , m_filename(fn) {
64 }
65
66 const std::string& filename() const {
67 return m_filename;
68 }
69
70private:
71 void onPaint(PaintEvent& ev) override {
72 ListItem::onPaint(ev);
73
74 if (m_image) {
75 Graphics* g = ev.graphics();
76 os::SurfaceRef sur = os::instance()->makeRgbaSurface(m_image->width(),
77 m_image->height());
78
79 convert_image_to_surface(
80 m_image.get(), nullptr, sur.get(),
81 0, 0, 0, 0, m_image->width(), m_image->height());
82
83 g->drawRgbaSurface(sur.get(), textWidth()+4, 0);
84 }
85 }
86
87 void onSizeHint(SizeHintEvent& ev) override {
88 ListItem::onSizeHint(ev);
89 if (m_image) {
90 gfx::Size sz = ev.sizeHint();
91 ev.setSizeHint(
92 sz.w + 4 + m_image->width(),
93 std::max(sz.h, m_image->height()));
94 }
95 }
96
97 void onSelect(bool selected) override {
98 if (!selected || m_image)
99 return;
100
101 ListBox* listbox = static_cast<ListBox*>(parent());
102 if (!listbox)
103 return;
104
105 auto theme = app::skin::SkinTheme::get(this);
106 gfx::Color color = theme->colors.text();
107
108 try {
109 m_image.reset(
110 render_text(
111 m_filename, 16,
112 "ABCDEabcde", // TODO custom text
113 doc::rgba(gfx::getr(color),
114 gfx::getg(color),
115 gfx::getb(color),
116 gfx::geta(color)),
117 true)); // antialias
118
119 View* view = View::getView(listbox);
120 view->updateView();
121 listbox->makeChildVisible(this);
122
123 // Save the thumbnail for future FontPopups
124 g_thumbnails[m_filename] = m_image;
125 }
126 catch (const std::exception&) {
127 // Ignore errors
128 }
129 }
130
131private:
132 doc::ImageRef m_image;
133 std::string m_filename;
134};
135
136FontPopup::FontPopup()
137 : PopupWindow(Strings::font_popup_title(),
138 ClickBehavior::CloseOnClickInOtherWindow,
139 EnterBehavior::DoNothingOnEnter)
140 , m_popup(new gen::FontPopup())
141{
142 setAutoRemap(false);
143 setBorder(gfx::Border(4*guiscale()));
144
145 addChild(m_popup);
146
147 m_popup->search()->Change.connect([this]{ onSearchChange(); });
148 m_popup->loadFont()->Click.connect([this]{ onLoadFont(); });
149 m_listBox.setFocusMagnet(true);
150 m_listBox.Change.connect([this]{ onChangeFont(); });
151 m_listBox.DoubleClickItem.connect([this]{ onLoadFont(); });
152
153 m_popup->view()->attachToView(&m_listBox);
154
155 base::paths fontDirs;
156 get_font_dirs(fontDirs);
157
158 // Create a list of fullpaths to every font found in all font
159 // directories (fontDirs)
160 base::paths files;
161 for (const auto& fontDir : fontDirs) {
162 for (const auto& file : base::list_files(fontDir)) {
163 std::string fullpath = base::join_path(fontDir, file);
164 if (base::is_file(fullpath))
165 files.push_back(fullpath);
166 }
167 }
168
169 // Sort all files by "file title"
170 std::sort(
171 files.begin(), files.end(),
172 [](const std::string& a, const std::string& b){
173 return base::utf8_icmp(base::get_file_title(a), base::get_file_title(b)) < 0;
174 });
175
176 // Create one FontItem for each font
177 for (auto& file : files) {
178 std::string ext = base::string_to_lower(base::get_file_extension(file));
179 if (ext == "ttf" || ext == "ttc" ||
180 ext == "otf" || ext == "dfont")
181 m_listBox.addChild(new FontItem(file));
182 }
183
184 if (m_listBox.children().empty())
185 m_listBox.addChild(new ListItem(Strings::font_popup_empty_fonts()));
186}
187
188void FontPopup::showPopup(Display* display,
189 const gfx::Rect& buttonBounds)
190{
191 m_popup->loadFont()->setEnabled(false);
192 m_listBox.selectChild(NULL);
193
194 ui::fit_bounds(display, this,
195 gfx::Rect(buttonBounds.x, buttonBounds.y2(), 32, 32),
196 [](const gfx::Rect& workarea,
197 gfx::Rect& bounds,
198 std::function<gfx::Rect(Widget*)> getWidgetBounds) {
199 bounds.w = workarea.w / 2;
200 bounds.h = workarea.h / 2;
201 });
202
203 // Setup the hot-region
204 setHotRegion(gfx::Region(gfx::Rect(boundsOnScreen()).enlarge(32*guiscale()*display->scale())));
205
206 openWindow();
207}
208
209void FontPopup::onSearchChange()
210{
211 std::string searchText = m_popup->search()->text();
212 Widget* firstItem = nullptr;
213
214 MatchWords match(searchText);
215 for (auto child : m_listBox.children()) {
216 bool visible = match(child->text());
217 if (visible && !firstItem)
218 firstItem = child;
219 child->setVisible(visible);
220 }
221
222 m_listBox.selectChild(firstItem);
223 layout();
224}
225
226void FontPopup::onChangeFont()
227{
228 m_popup->loadFont()->setEnabled(true);
229}
230
231void FontPopup::onLoadFont()
232{
233 FontItem* child = dynamic_cast<FontItem*>(m_listBox.getSelectedChild());
234 if (!child)
235 return;
236
237 std::string filename = child->filename();
238 if (base::is_file(filename))
239 Load(filename); // Fire Load signal
240
241 closeWindow(nullptr);
242}
243
244} // namespace app
245