1// Aseprite
2// Copyright (C) 2018-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/recent_listbox.h"
13
14#include "app/app.h"
15#include "app/commands/commands.h"
16#include "app/commands/params.h"
17#include "app/i18n/strings.h"
18#include "app/pref/preferences.h"
19#include "app/recent_files.h"
20#include "app/ui/draggable_widget.h"
21#include "app/ui/skin/skin_theme.h"
22#include "app/ui_context.h"
23#include "base/fs.h"
24#include "ui/alert.h"
25#include "ui/graphics.h"
26#include "ui/link_label.h"
27#include "ui/listitem.h"
28#include "ui/message.h"
29#include "ui/paint_event.h"
30#include "ui/scroll_region_event.h"
31#include "ui/size_hint_event.h"
32#include "ui/system.h"
33#include "ui/view.h"
34
35namespace app {
36
37using namespace ui;
38using namespace skin;
39
40//////////////////////////////////////////////////////////////////////
41// RecentFileItem
42
43class RecentFileItem : public DraggableWidget<LinkLabel> {
44public:
45 RecentFileItem(const std::string& file,
46 const bool pinned)
47 : DraggableWidget<LinkLabel>("")
48 , m_fullpath(file)
49 , m_name(base::get_file_name(file))
50 , m_path(base::get_file_path(file))
51 , m_pinned(pinned) {
52 initTheme();
53 }
54
55 const std::string& fullpath() const { return m_fullpath; }
56 bool pinned() const { return m_pinned; }
57
58 void pin() {
59 m_pinned = true;
60 invalidate();
61 }
62
63 void onScrollRegion(ui::ScrollRegionEvent& ev) {
64 ev.region() -= gfx::Region(pinBounds(bounds()));
65 }
66
67protected:
68 void onInitTheme(InitThemeEvent& ev) override {
69 LinkLabel::onInitTheme(ev);
70 auto theme = SkinTheme::get(this);
71 setStyle(theme->styles.recentItem());
72 }
73
74 void onSizeHint(SizeHintEvent& ev) override {
75 auto theme = SkinTheme::get(this);
76 ui::Style* style = theme->styles.recentFile();
77 ui::Style* styleDetail = theme->styles.recentFileDetail();
78
79 setTextQuiet(m_name);
80 gfx::Size sz1 = theme->calcSizeHint(this, style);
81
82 setTextQuiet(m_path);
83 gfx::Size sz2 = theme->calcSizeHint(this, styleDetail);
84
85 ev.setSizeHint(gfx::Size(sz1.w+sz2.w, std::max(sz1.h, sz2.h)));
86 }
87
88 bool onProcessMessage(Message* msg) override {
89 switch (msg->type()) {
90 case kMouseDownMessage: {
91 const gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
92 gfx::Rect rc = pinBounds(bounds());
93 rc.y = bounds().y;
94 rc.h = bounds().h;
95 if (rc.contains(mousePos)) {
96 m_pinned = !m_pinned;
97 invalidate();
98
99 auto parent = this->parent();
100 const auto& children = parent->children();
101 auto end = children.end();
102 auto moveTo = parent->firstChild();
103 if (m_pinned) {
104 for (auto it=children.begin(); it != end; ++it) {
105 if (*it == this || !static_cast<RecentFileItem*>(*it)->pinned()) {
106 moveTo = *it;
107 break;
108 }
109 }
110 }
111 else {
112 auto it = std::find(children.begin(), end, this);
113 if (it != end) {
114 auto prevIt = it++;
115 for (; it != end; prevIt=it++) {
116 if (!static_cast<RecentFileItem*>(*it)->pinned())
117 break;
118 }
119 moveTo = *prevIt;
120 }
121 }
122 if (this != moveTo) {
123 parent->moveChildTo(this, moveTo);
124 parent->layout();
125 }
126 saveConfig();
127 return true;
128 }
129 break;
130 }
131 }
132 return DraggableWidget<LinkLabel>::onProcessMessage(msg);
133 }
134
135 void onPaint(PaintEvent& ev) override {
136 auto theme = SkinTheme::get(this);
137 Graphics* g = ev.graphics();
138 gfx::Rect bounds = clientBounds();
139 ui::Style* style = theme->styles.recentFile();
140 ui::Style* styleDetail = theme->styles.recentFileDetail();
141
142 setTextQuiet(m_name.c_str());
143 theme->paintWidget(g, this, style, bounds);
144
145 if (Preferences::instance().general.showFullPath()) {
146 gfx::Size textSize = theme->calcSizeHint(this, style);
147 gfx::Rect detailsBounds(
148 bounds.x+textSize.w, bounds.y,
149 bounds.w-textSize.w, bounds.h);
150 setTextQuiet(m_path.c_str());
151 theme->paintWidget(g, this, styleDetail, detailsBounds);
152 }
153
154 if (!isDragging() && (m_pinned || hasMouse())) {
155 ui::Style* pinStyle = theme->styles.recentFilePin();
156 const gfx::Rect pinBounds = this->pinBounds(bounds);
157 PaintWidgetPartInfo pi;
158 pi.styleFlags =
159 (isSelected() ? ui::Style::Layer::kSelected: 0) |
160 (m_pinned ? ui::Style::Layer::kFocus: 0) |
161 (hasMouse() ? ui::Style::Layer::kMouse: 0);
162 theme->paintWidgetPart(g, pinStyle, pinBounds, pi);
163 }
164 }
165
166 void onClick() override {
167 if (!wasDragged())
168 static_cast<RecentListBox*>(parent())->onClick(m_fullpath);
169 }
170
171 void onReorderWidgets(const gfx::Point& mousePos, bool inside) override {
172 auto parent = this->parent();
173 auto other = manager()->pick(mousePos);
174 if (other && other != this && other->parent() == parent) {
175 parent->moveChildTo(this, other);
176 parent->layout();
177 }
178 }
179
180 void onFinalDrop(bool inside) override {
181 if (!wasDragged())
182 return;
183
184 if (inside) {
185 // Pin all elements to keep the order
186 const auto& children = parent()->children();
187 for (auto it=children.rbegin(), end=children.rend(); it!=end; ++it) {
188 if (this == *it) {
189 for (; it!=end; ++it)
190 static_cast<RecentFileItem*>(*it)->pin();
191 break;
192 }
193 }
194 }
195 else {
196 setVisible(false);
197 parent()->layout();
198 }
199
200 saveConfig();
201
202 if (!inside)
203 deferDelete();
204 }
205
206private:
207 gfx::Rect pinBounds(const gfx::Rect& bounds) {
208 auto theme = SkinTheme::get(this);
209 ui::Style* pinStyle = theme->styles.recentFilePin();
210 ui::View* view = View::getView(parent());
211 const gfx::Size pinSize = theme->calcSizeHint(this, pinStyle);
212 const gfx::Rect vp = view->viewportBounds();
213 const gfx::Point scroll = view->viewScroll();
214 return gfx::Rect(scroll.x+bounds.x+vp.w-pinSize.w,
215 bounds.y+bounds.h/2-pinSize.h/2,
216 pinSize.w, pinSize.h);
217 }
218
219 void saveConfig() {
220 static_cast<RecentListBox*>(parent())->updateRecentListFromUIItems();
221 }
222
223 std::string m_fullpath;
224 std::string m_name;
225 std::string m_path;
226 bool m_pinned;
227};
228
229//////////////////////////////////////////////////////////////////////
230// RecentListBox
231
232RecentListBox::RecentListBox()
233{
234 m_recentFilesConn =
235 App::instance()->recentFiles()->Changed.connect(
236 [this]{ rebuildList(); });
237
238 m_showFullPathConn =
239 Preferences::instance().general.showFullPath.AfterChange.connect(
240 [this]{ invalidate(); });
241}
242
243void RecentListBox::rebuildList()
244{
245 while (auto child = lastChild()) {
246 removeChild(child);
247 child->deferDelete();
248 }
249
250 onRebuildList();
251
252 View* view = View::getView(this);
253 if (view)
254 view->layout();
255 else
256 layout();
257}
258
259void RecentListBox::updateRecentListFromUIItems()
260{
261 base::paths pinnedPaths;
262 base::paths recentPaths;
263 for (auto item : children()) {
264 auto fi = static_cast<RecentFileItem*>(item);
265 if (fi->hasFlags(ui::HIDDEN))
266 continue;
267 if (fi->pinned())
268 pinnedPaths.push_back(fi->fullpath());
269 else
270 recentPaths.push_back(fi->fullpath());
271 }
272 onUpdateRecentListFromUIItems(pinnedPaths,
273 recentPaths);
274}
275
276void RecentListBox::onScrollRegion(ui::ScrollRegionEvent& ev)
277{
278 for (auto item : children())
279 static_cast<RecentFileItem*>(item)->onScrollRegion(ev);
280}
281
282//////////////////////////////////////////////////////////////////////
283// RecentFilesListBox
284
285RecentFilesListBox::RecentFilesListBox()
286{
287 onRebuildList();
288}
289
290void RecentFilesListBox::onRebuildList()
291{
292 auto recent = App::instance()->recentFiles();
293 for (const auto& fn : recent->pinnedFiles())
294 addChild(new RecentFileItem(fn, true));
295 for (const auto& fn : recent->recentFiles())
296 addChild(new RecentFileItem(fn, false));
297}
298
299void RecentFilesListBox::onClick(const std::string& path)
300{
301 if (!base::is_file(path)) {
302 ui::Alert::show(Strings::alerts_recent_file_doesnt_exist());
303 App::instance()->recentFiles()->removeRecentFile(path);
304 return;
305 }
306
307 Command* command = Commands::instance()->byId(CommandId::OpenFile());
308 Params params;
309 params.set("filename", path.c_str());
310 UIContext::instance()->executeCommandFromMenuOrShortcut(command, params);
311}
312
313void RecentFilesListBox::onUpdateRecentListFromUIItems(const base::paths& pinnedPaths,
314 const base::paths& recentPaths)
315{
316 App::instance()->recentFiles()->setFiles(pinnedPaths,
317 recentPaths);
318}
319
320//////////////////////////////////////////////////////////////////////
321// RecentFoldersListBox
322
323RecentFoldersListBox::RecentFoldersListBox()
324{
325 onRebuildList();
326}
327
328void RecentFoldersListBox::onRebuildList()
329{
330 auto recent = App::instance()->recentFiles();
331 for (const auto& fn : recent->pinnedFolders())
332 addChild(new RecentFileItem(fn, true));
333 for (const auto& fn : recent->recentFolders())
334 addChild(new RecentFileItem(fn, false));
335}
336
337void RecentFoldersListBox::onClick(const std::string& path)
338{
339 if (!base::is_directory(path)) {
340 ui::Alert::show(Strings::alerts_recent_folder_doesnt_exist());
341 App::instance()->recentFiles()->removeRecentFolder(path);
342 return;
343 }
344
345 Command* command = Commands::instance()->byId(CommandId::OpenFile());
346 Params params;
347 params.set("folder", path.c_str());
348 UIContext::instance()->executeCommandFromMenuOrShortcut(command, params);
349}
350
351void RecentFoldersListBox::onUpdateRecentListFromUIItems(const base::paths& pinnedPaths,
352 const base::paths& recentPaths)
353{
354 App::instance()->recentFiles()->setFolders(pinnedPaths,
355 recentPaths);
356}
357
358} // namespace app
359