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 | |
22 | namespace { |
23 | |
24 | enum { kPinnedFiles, kRecentFiles, |
25 | kPinnedPaths, kRecentPaths }; |
26 | |
27 | const 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. |
34 | const char* kConversionKey = "_" ; |
35 | |
36 | struct 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 | |
46 | namespace app { |
47 | |
48 | RecentFiles::RecentFiles(const int limit) |
49 | : m_limit(limit) |
50 | { |
51 | load(); |
52 | } |
53 | |
54 | RecentFiles::~RecentFiles() |
55 | { |
56 | save(); |
57 | } |
58 | |
59 | void 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 | |
82 | void 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 | |
94 | void RecentFiles::removeRecentFolder(const std::string& dir) |
95 | { |
96 | std::string fn = normalizePath(dir); |
97 | removeItem(m_paths[kRecentFolders], fn); |
98 | |
99 | Changed(); |
100 | } |
101 | |
102 | void 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 | |
118 | void 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 | |
127 | void RecentFiles::setFiles(const base::paths& pinnedFiles, |
128 | const base::paths& recentFiles) |
129 | { |
130 | m_paths[kPinnedFiles] = pinnedFiles; |
131 | m_paths[kRecentFiles] = recentFiles; |
132 | } |
133 | |
134 | void RecentFiles::setFolders(const base::paths& pinnedFolders, |
135 | const base::paths& recentFolders) |
136 | { |
137 | m_paths[kPinnedFolders] = pinnedFolders; |
138 | m_paths[kRecentFolders] = recentFolders; |
139 | } |
140 | |
141 | std::string RecentFiles::normalizePath(const std::string& filename) |
142 | { |
143 | return base::normalize_path(filename); |
144 | } |
145 | |
146 | void 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 | |
165 | void 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 | |
172 | void 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 | |
209 | void 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 | |