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