1 #include "RecentManager.h"
2
3 #include <array>
4 #include <limits>
5
6 #include <filesystem.h>
7 #include <gtk/gtk.h>
8 #include <util/safe_casts.h>
9
10 #include "PathUtil.h"
11 #include "StringUtils.h"
12 #include "i18n.h"
13
14 #define MIME "application/x-xoj"
15 #define MIME_PDF "application/x-pdf"
16 #define GROUP "xournal++"
17
18 RecentManagerListener::~RecentManagerListener() = default;
19
RecentManager()20 RecentManager::RecentManager() {
21 this->menu = gtk_menu_new();
22
23 GtkRecentManager* recentManager = gtk_recent_manager_get_default();
24 this->recentHandlerId = g_signal_connect(recentManager, "changed", G_CALLBACK(recentManagerChangedCallback), this);
25
26 updateMenu();
27 }
28
~RecentManager()29 RecentManager::~RecentManager() {
30 if (this->recentHandlerId) {
31 GtkRecentManager* recentManager = gtk_recent_manager_get_default();
32 g_signal_handler_disconnect(recentManager, this->recentHandlerId);
33 this->recentHandlerId = 0;
34 }
35 this->menu = nullptr;
36 }
37
addListener(RecentManagerListener * l)38 void RecentManager::addListener(RecentManagerListener* l) { this->listener.push_back(l); }
39
recentManagerChangedCallback(GtkRecentManager *,RecentManager * recentManager)40 void RecentManager::recentManagerChangedCallback(GtkRecentManager* /*manager*/, RecentManager* recentManager) {
41 // regenerate the menu when the model changes
42 recentManager->updateMenu();
43 }
44
addRecentFileFilename(const fs::path & filepath)45 void RecentManager::addRecentFileFilename(const fs::path& filepath) {
46 GtkRecentManager* recentManager = gtk_recent_manager_get_default();
47
48 std::string group_name = GROUP;
49 std::array<gchar*, 2> groups = {group_name.data(), nullptr};
50 std::string app_name = g_get_application_name();
51 std::string app_exec = std::string(g_get_prgname()) + " %u";
52 std::string mime_type = (filepath.extension() == ".pdf") ? std::string(MIME_PDF) : std::string(MIME);
53
54 GtkRecentData recentData{};
55 recentData.display_name = nullptr;
56 recentData.description = nullptr;
57 recentData.app_name = app_name.data();
58 recentData.app_exec = app_exec.data();
59 recentData.groups = groups.data();
60 recentData.mime_type = mime_type.data();
61 recentData.is_private = false;
62
63 auto uri = Util::toUri(filepath);
64 if (!uri) {
65 return;
66 }
67 gtk_recent_manager_add_full(recentManager, (*uri).c_str(), &recentData);
68 }
69
removeRecentFileFilename(const fs::path & filename)70 void RecentManager::removeRecentFileFilename(const fs::path& filename) {
71 auto uri = Util::toUri(filename);
72 if (!uri) {
73 return;
74 }
75 GtkRecentManager* recentManager = gtk_recent_manager_get_default();
76 gtk_recent_manager_remove_item(recentManager, uri->c_str(), nullptr);
77 }
78
openRecent(const fs::path & p)79 void RecentManager::openRecent(const fs::path& p) {
80 if (p.empty()) {
81 return;
82 }
83
84 for (RecentManagerListener* l: this->listener) { l->fileOpened(p); }
85 }
86
getMenu()87 auto RecentManager::getMenu() -> GtkWidget* { return menu; }
88
freeOldMenus()89 void RecentManager::freeOldMenus() {
90 for (GtkWidget* w: menuItemList) { gtk_widget_destroy(w); }
91
92 this->menuItemList.clear();
93 }
94
95 using stime_t = std::make_signed<time_t>;
96
97 // Todo: replace with <=> in c++ 20
sortRecentsEntries(GtkRecentInfo * a,GtkRecentInfo * b)98 auto RecentManager::sortRecentsEntries(GtkRecentInfo* a, GtkRecentInfo* b) -> gint {
99 auto tp_a = gtk_recent_info_get_modified(a);
100 auto tp_b = gtk_recent_info_get_modified(b);
101 return tp_a != tp_b ? (tp_a < tp_b ? 1 : -1) : 0;
102 }
103
getMostRecent()104 auto RecentManager::getMostRecent() -> GtkRecentInfo* {
105 GList* filteredItemsXoj = filterRecent(gtk_recent_manager_get_items(gtk_recent_manager_get_default()), true);
106 auto mostRecent = static_cast<GtkRecentInfo*>(filteredItemsXoj->data);
107
108 gtk_recent_info_ref(mostRecent);
109 for (GList* l = filteredItemsXoj; l != nullptr; l = l->next) {
110 gtk_recent_info_unref(static_cast<GtkRecentInfo*>(l->data));
111 }
112 g_list_free(filteredItemsXoj);
113
114 return mostRecent;
115 }
116
filterRecent(GList * items,bool xoj)117 auto RecentManager::filterRecent(GList* items, bool xoj) -> GList* {
118 GList* filteredItems = nullptr;
119
120 // filter
121 for (GList* l = items; l != nullptr; l = l->next) {
122 auto* info = static_cast<GtkRecentInfo*>(l->data);
123
124 const gchar* uri = gtk_recent_info_get_uri(info);
125 if (!uri) // issue #1071
126 {
127 continue;
128 }
129
130 auto p = Util::fromUri(uri);
131
132 // Skip remote files
133 if (!p) {
134 continue;
135 }
136
137 if (xoj && Util::hasXournalFileExt(*p)) {
138 filteredItems = g_list_prepend(filteredItems, info);
139 }
140 if (!xoj && p->extension() == ".pdf") {
141 filteredItems = g_list_prepend(filteredItems, info);
142 }
143 }
144
145 // sort
146 filteredItems = g_list_sort(filteredItems, reinterpret_cast<GCompareFunc>(sortRecentsEntries));
147
148 return filteredItems;
149 }
150
recentsMenuActivateCallback(GtkAction * action,RecentManager * recentManager)151 void RecentManager::recentsMenuActivateCallback(GtkAction* action, RecentManager* recentManager) {
152 auto* info = static_cast<GtkRecentInfo*>(g_object_get_data(G_OBJECT(action), "gtk-recent-info"));
153 g_return_if_fail(info != nullptr);
154
155 auto p = Util::fromUri(gtk_recent_info_get_uri(info));
156 if (p) {
157 recentManager->openRecent(*p);
158 }
159 }
160
addRecentMenu(GtkRecentInfo * info,int i)161 void RecentManager::addRecentMenu(GtkRecentInfo* info, int i) {
162 string display_name = gtk_recent_info_get_display_name(info);
163
164 // escape underscore
165 StringUtils::replaceAllChars(display_name, {replace_pair('_', "__")});
166
167 string label =
168 (i >= 10 ? FS(FORMAT_STR("{1}. {2}") % i % display_name) : FS(FORMAT_STR("_{1}. {2}") % i % display_name));
169
170 /* gtk_recent_info_get_uri_display (info) is buggy and
171 * works only for local files */
172 GFile* gfile = g_file_new_for_uri(gtk_recent_info_get_uri(info));
173 char* fileUri = g_file_get_parse_name(gfile);
174 string ruri = fileUri;
175 g_free(fileUri);
176
177 g_object_unref(gfile);
178
179 if (StringUtils::startsWith(ruri, "~/")) {
180 ruri = string(g_get_home_dir()) + ruri.substr(1);
181 }
182
183 string tip = FS(C_F("{1} is a URI", "Open {1}") % ruri);
184
185
186 GtkWidget* item = gtk_menu_item_new_with_mnemonic(label.c_str());
187
188 gtk_widget_set_tooltip_text(item, tip.c_str());
189
190 g_object_set_data_full(G_OBJECT(item), "gtk-recent-info", gtk_recent_info_ref(info),
191 reinterpret_cast<GDestroyNotify>(gtk_recent_info_unref));
192
193 g_signal_connect(item, "activate", G_CALLBACK(recentsMenuActivateCallback), this);
194
195 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
196 gtk_widget_set_visible(GTK_WIDGET(item), true);
197
198 this->menuItemList.push_back(item);
199 }
200
updateMenu()201 void RecentManager::updateMenu() {
202 GtkRecentManager* recentManager = gtk_recent_manager_get_default();
203 GList* items = gtk_recent_manager_get_items(recentManager);
204 GList* filteredItemsXoj = filterRecent(items, true);
205 GList* filteredItemsPdf = filterRecent(items, false);
206
207 freeOldMenus();
208
209 int xojCount = 0;
210 for (GList* l = filteredItemsXoj; l != nullptr; l = l->next) {
211 auto* info = static_cast<GtkRecentInfo*>(l->data);
212
213 if (xojCount >= maxRecent) {
214 break;
215 }
216 xojCount++;
217
218 addRecentMenu(info, xojCount);
219 }
220 g_list_free(filteredItemsXoj);
221
222 GtkWidget* separator = gtk_separator_menu_item_new();
223 gtk_menu_shell_append(GTK_MENU_SHELL(menu), separator);
224 gtk_widget_set_visible(GTK_WIDGET(separator), true);
225
226 this->menuItemList.push_back(separator);
227
228 int pdfCount = 0;
229 for (GList* l = filteredItemsPdf; l != nullptr; l = l->next) {
230 auto* info = static_cast<GtkRecentInfo*>(l->data);
231
232 if (pdfCount >= maxRecent) {
233 break;
234 }
235 pdfCount++;
236
237 addRecentMenu(info, pdfCount + xojCount);
238 }
239 g_list_free(filteredItemsPdf);
240
241 g_list_foreach(items, reinterpret_cast<GFunc>(gtk_recent_info_unref), nullptr);
242 g_list_free(items);
243 }
244