1// Aseprite
2// Copyright (C) 2018-2020 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/recent_files.h"
13
14#include "app/ini_file.h"
15#include "base/fs.h"
16#include "fmt/format.h"
17
18#include <cstdio>
19#include <cstring>
20#include <set>
21
22namespace {
23
24enum { kPinnedFiles, kRecentFiles,
25 kPinnedPaths, kRecentPaths };
26
27const char* kSectionName[] = { "PinnedFiles",
28 "RecentFiles",
29 "PinnedPaths",
30 "RecentPaths" };
31
32// Special key used in recent sections (files/paths) to indicate that
33// the section was already converted at least one time.
34const char* kConversionKey = "_";
35
36struct compare_path {
37 std::string a;
38 compare_path(const std::string& a) : a(a) { }
39 bool operator()(const std::string& b) const {
40 return base::compare_filenames(a, b) == 0;
41 }
42};
43
44}
45
46namespace app {
47
48RecentFiles::RecentFiles(const int limit)
49 : m_limit(limit)
50{
51 load();
52}
53
54RecentFiles::~RecentFiles()
55{
56 save();
57}
58
59void RecentFiles::addRecentFile(const std::string& filename)
60{
61 std::string fn = normalizePath(filename);
62
63 // If the filename is already pinned, we don't add it in the
64 // collection of recent files collection.
65 auto it = std::find(m_paths[kPinnedFiles].begin(),
66 m_paths[kPinnedFiles].end(), fn);
67 if (it != m_paths[kPinnedFiles].end())
68 return;
69 addItem(m_paths[kRecentFiles], fn);
70
71 // Add recent folder
72 std::string path = base::get_file_path(fn);
73 it = std::find(m_paths[kPinnedFolders].begin(),
74 m_paths[kPinnedFolders].end(), path);
75 if (it == m_paths[kPinnedFolders].end()) {
76 addItem(m_paths[kRecentFolders], path);
77 }
78
79 Changed();
80}
81
82void RecentFiles::removeRecentFile(const std::string& filename)
83{
84 std::string fn = normalizePath(filename);
85 removeItem(m_paths[kRecentFiles], fn);
86
87 std::string dir = base::get_file_path(fn);
88 if (!base::is_directory(dir))
89 removeRecentFolder(dir);
90
91 Changed();
92}
93
94void RecentFiles::removeRecentFolder(const std::string& dir)
95{
96 std::string fn = normalizePath(dir);
97 removeItem(m_paths[kRecentFolders], fn);
98
99 Changed();
100}
101
102void RecentFiles::setLimit(const int newLimit)
103{
104 ASSERT(newLimit >= 0);
105
106 for (auto& list : m_paths) {
107 if (newLimit < list.size()) {
108 auto it = list.begin();
109 std::advance(it, newLimit);
110 list.erase(it, list.end());
111 }
112 }
113
114 m_limit = newLimit;
115 Changed();
116}
117
118void RecentFiles::clear()
119{
120 // Clear only recent items (not pinned items)
121 m_paths[kRecentFiles].clear();
122 m_paths[kRecentFolders].clear();
123
124 Changed();
125}
126
127void RecentFiles::setFiles(const base::paths& pinnedFiles,
128 const base::paths& recentFiles)
129{
130 m_paths[kPinnedFiles] = pinnedFiles;
131 m_paths[kRecentFiles] = recentFiles;
132}
133
134void RecentFiles::setFolders(const base::paths& pinnedFolders,
135 const base::paths& recentFolders)
136{
137 m_paths[kPinnedFolders] = pinnedFolders;
138 m_paths[kRecentFolders] = recentFolders;
139}
140
141std::string RecentFiles::normalizePath(const std::string& filename)
142{
143 return base::normalize_path(filename);
144}
145
146void RecentFiles::addItem(base::paths& list, const std::string& fn)
147{
148 auto it = std::find_if(list.begin(), list.end(), compare_path(fn));
149
150 // If the item already exist in the list...
151 if (it != list.end()) {
152 // Move it to the first position
153 list.erase(it);
154 list.insert(list.begin(), fn);
155 return;
156 }
157
158 if (m_limit > 0)
159 list.insert(list.begin(), fn);
160
161 while (list.size() > m_limit)
162 list.erase(--list.end());
163}
164
165void RecentFiles::removeItem(base::paths& list, const std::string& fn)
166{
167 auto it = std::find_if(list.begin(), list.end(), compare_path(fn));
168 if (it != list.end())
169 list.erase(it);
170}
171
172void RecentFiles::load()
173{
174 for (int i=0; i<kCollections; ++i) {
175 const char* section = kSectionName[i];
176
177 // For recent files: If there is an item called "Filename00" and no "0" key
178 // For recent paths: If there is an item called "Path00" and no "0" key
179 // -> We are migrating from and old version to a new one
180 const bool processOldFilenames =
181 (i == kRecentFiles &&
182 get_config_string(section, "Filename00", nullptr) &&
183 !get_config_bool(section, kConversionKey, false));
184
185 const bool processOldPaths =
186 (i == kRecentPaths &&
187 get_config_string(section, "Path00", nullptr) &&
188 !get_config_bool(section, kConversionKey, false));
189
190 for (const auto& key : enum_config_keys(section)) {
191 if ((!processOldFilenames && std::strncmp(key.c_str(), "Filename", 8) == 0)
192 ||
193 (!processOldPaths && std::strncmp(key.c_str(), "Path", 4) == 0)) {
194 // Ignore old entries if we are going to read the new ones
195 continue;
196 }
197
198 const char* fn = get_config_string(section, key.c_str(), nullptr);
199 if (fn && *fn &&
200 ((i < 2 && base::is_file(fn)) ||
201 (i >= 2 && base::is_directory(fn)))) {
202 std::string normalFn = normalizePath(fn);
203 m_paths[i].push_back(normalFn);
204 }
205 }
206 }
207}
208
209void RecentFiles::save()
210{
211 for (int i=0; i<kCollections; ++i) {
212 const char* section = kSectionName[i];
213
214 for (const auto& key : enum_config_keys(section)) {
215 if ((i == kRecentFiles &&
216 (std::strncmp(key.c_str(), "Filename", 8) == 0 || key == kConversionKey))
217 ||
218 (i == kRecentPaths &&
219 (std::strncmp(key.c_str(), "Path", 4) == 0 || key == kConversionKey))) {
220 // Ignore old entries if we are going to read the new ones
221 continue;
222 }
223 del_config_value(section, key.c_str());
224 }
225
226 for (int j=0; j<m_paths[i].size(); ++j) {
227 set_config_string(section,
228 fmt::format("{:04d}", j).c_str(),
229 m_paths[i][j].c_str());
230 }
231 // Special entry that indicates that we've already converted
232 if ((i == kRecentFiles || i == kRecentPaths) &&
233 !get_config_bool(section, kConversionKey, false)) {
234 set_config_bool(section, kConversionKey, true);
235 }
236 }
237}
238
239} // namespace app
240