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 | |
35 | namespace app { |
36 | |
37 | using namespace ui; |
38 | using namespace skin; |
39 | |
40 | ////////////////////////////////////////////////////////////////////// |
41 | // RecentFileItem |
42 | |
43 | class RecentFileItem : public DraggableWidget<LinkLabel> { |
44 | public: |
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 | |
67 | protected: |
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 | |
206 | private: |
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 | |
232 | RecentListBox::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 | |
243 | void 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 | |
259 | void 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 | |
276 | void RecentListBox::onScrollRegion(ui::ScrollRegionEvent& ev) |
277 | { |
278 | for (auto item : children()) |
279 | static_cast<RecentFileItem*>(item)->onScrollRegion(ev); |
280 | } |
281 | |
282 | ////////////////////////////////////////////////////////////////////// |
283 | // RecentFilesListBox |
284 | |
285 | RecentFilesListBox::RecentFilesListBox() |
286 | { |
287 | onRebuildList(); |
288 | } |
289 | |
290 | void 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 | |
299 | void 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 | |
313 | void 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 | |
323 | RecentFoldersListBox::RecentFoldersListBox() |
324 | { |
325 | onRebuildList(); |
326 | } |
327 | |
328 | void 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 | |
337 | void 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 | |
351 | void 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 | |