1 /*
2 * search-model.cc
3 * Copyright 2011-2019 John Lindgren and René J.V. Bertin
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions, and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions, and the following disclaimer in the documentation
13 * provided with the distribution.
14 *
15 * This software is provided "as is" and without any warranty, express or
16 * implied. In no event shall the authors be liable for any damages arising from
17 * the use of this software.
18 */
19
20 #include "search-model.h"
21
22 #include <QMimeData>
23 #include <QUrl>
24
25 #include <libaudcore/i18n.h>
26
create_item_label(const Item & item)27 static QString create_item_label (const Item & item)
28 {
29 static constexpr aud::array<SearchField, const char *> start_tags =
30 {"", "<b>", "<i>", ""};
31 static constexpr aud::array<SearchField, const char *> end_tags =
32 {"", "</b>", "</i>", ""};
33
34 QString string = start_tags[item.field];
35
36 string += QString ((item.field == SearchField::Genre) ?
37 str_toupper_utf8 (item.name) : item.name).toHtmlEscaped ();
38
39 string += end_tags[item.field];
40
41 #ifdef Q_OS_MAC // Mac-specific font tweaks
42 string += "<br> ";
43 #else
44 string += "<br><small> ";
45 #endif
46
47 if (item.field != SearchField::Title)
48 {
49 string += str_printf (dngettext (PACKAGE, "%d song", "%d songs",
50 item.matches.len ()), item.matches.len ());
51
52 if (item.field == SearchField::Genre || item.parent)
53 string += ' ';
54 }
55
56 if (item.field == SearchField::Genre)
57 {
58 string += _("of this genre");
59 }
60 else if (item.parent)
61 {
62 auto parent = (item.parent->parent ? item.parent->parent : item.parent);
63
64 string += (parent->field == SearchField::Album) ? _("on") : _("by");
65 string += ' ';
66 string += start_tags[parent->field];
67 string += QString (parent->name).toHtmlEscaped ();
68 string += end_tags[parent->field];
69 }
70
71 #ifndef Q_OS_MAC // Mac-specific font tweaks
72 string += "</small>";
73 #endif
74
75 return string;
76 }
77
data(const QModelIndex & index,int role) const78 QVariant SearchModel::data (const QModelIndex & index, int role) const
79 {
80 if (role == Qt::DisplayRole)
81 {
82 int row = index.row ();
83 if (row < 0 || row >= m_items.len ())
84 return QVariant ();
85
86 return create_item_label (* m_items[row]);
87 }
88
89 return QVariant ();
90 }
91
mimeData(const QModelIndexList & indexes) const92 QMimeData * SearchModel::mimeData (const QModelIndexList & indexes) const
93 {
94 m_playlist.select_all (false);
95
96 QList<QUrl> urls;
97 for (auto & index : indexes)
98 {
99 int row = index.row ();
100 if (row < 0 || row >= m_items.len ())
101 continue;
102
103 for (int entry : m_items[row]->matches)
104 {
105 urls.append (QString (m_playlist.entry_filename (entry)));
106 m_playlist.select_entry (entry, true);
107 }
108 }
109
110 m_playlist.cache_selected ();
111
112 auto data = new QMimeData;
113 data->setUrls (urls);
114 return data;
115 }
116
update()117 void SearchModel::update ()
118 {
119 int rows = m_items.len ();
120 int keep = aud::min (rows, m_rows);
121
122 if (rows < m_rows)
123 {
124 beginRemoveRows (QModelIndex (), rows, m_rows - 1);
125 m_rows = rows;
126 endRemoveRows ();
127 }
128 else if (rows > m_rows)
129 {
130 beginInsertRows (QModelIndex (), m_rows, rows - 1);
131 m_rows = rows;
132 endInsertRows ();
133 }
134
135 if (keep > 0)
136 {
137 auto topLeft = createIndex (0, 0);
138 auto bottomRight = createIndex (keep - 1, 0);
139 emit dataChanged (topLeft, bottomRight);
140 }
141 }
142
destroy_database()143 void SearchModel::destroy_database ()
144 {
145 m_playlist = Playlist ();
146 m_items.clear ();
147 m_hidden_items = 0;
148 m_database.clear ();
149 }
150
create_database(Playlist playlist)151 void SearchModel::create_database (Playlist playlist)
152 {
153 destroy_database ();
154
155 int entries = playlist.n_entries ();
156
157 for (int e = 0; e < entries; e ++)
158 {
159 Tuple tuple = playlist.entry_tuple (e, Playlist::NoWait);
160
161 aud::array<SearchField, String> fields;
162 fields[SearchField::Genre] = tuple.get_str (Tuple::Genre);
163 fields[SearchField::Artist] = tuple.get_str (Tuple::Artist);
164 fields[SearchField::Album] = tuple.get_str (Tuple::Album);
165 fields[SearchField::Title] = tuple.get_str (Tuple::Title);
166
167 Item * parent = nullptr;
168 SimpleHash<Key, Item> * hash = & m_database;
169
170 for (auto f : aud::range<SearchField> ())
171 {
172 if (fields[f])
173 {
174 Key key = {f, fields[f]};
175 Item * item = hash->lookup (key);
176
177 if (! item)
178 item = hash->add (key, Item (f, fields[f], parent));
179
180 item->matches.append (e);
181
182 /* genre is outside the normal hierarchy */
183 if (f != SearchField::Genre)
184 {
185 parent = item;
186 hash = & item->children;
187 }
188 }
189 }
190 }
191
192 m_playlist = playlist;
193 }
194
search_recurse(SimpleHash<Key,Item> & domain,const Index<String> & terms,int mask,Index<const Item * > & results)195 static void search_recurse (SimpleHash<Key, Item> & domain,
196 const Index<String> & terms, int mask, Index<const Item *> & results)
197 {
198 domain.iterate ([&] (const Key & key, Item & item)
199 {
200 int count = terms.len ();
201 int new_mask = mask;
202
203 for (int t = 0, bit = 1; t < count; t ++, bit <<= 1)
204 {
205 if (! (new_mask & bit))
206 continue; /* skip term if it is already found */
207
208 if (strstr (item.folded, terms[t]))
209 new_mask &= ~bit; /* we found it */
210 else if (! item.children.n_items ())
211 break; /* quit early if there are no children to search */
212 }
213
214 /* adding an item with exactly one child is redundant, so avoid it */
215 if (! new_mask && item.children.n_items () != 1)
216 results.append (& item);
217
218 search_recurse (item.children, terms, new_mask, results);
219 });
220 }
221
item_compare(const Item * const & a,const Item * const & b)222 static int item_compare (const Item * const & a, const Item * const & b)
223 {
224 if (a->field < b->field)
225 return -1;
226 if (a->field > b->field)
227 return 1;
228
229 int val = str_compare (a->name, b->name);
230 if (val)
231 return val;
232
233 if (a->parent)
234 return b->parent ? item_compare (a->parent, b->parent) : 1;
235 else
236 return b->parent ? -1 : 0;
237 }
238
item_compare_pass1(const Item * const & a,const Item * const & b)239 static int item_compare_pass1 (const Item * const & a, const Item * const & b)
240 {
241 if (a->matches.len () > b->matches.len ())
242 return -1;
243 if (a->matches.len () < b->matches.len ())
244 return 1;
245
246 return item_compare (a, b);
247 }
248
do_search(const Index<String> & terms,int max_results)249 void SearchModel::do_search (const Index<String> & terms, int max_results)
250 {
251 m_items.clear ();
252 m_hidden_items = 0;
253
254 /* effectively limits number of search terms to 32 */
255 search_recurse (m_database, terms, (1 << terms.len ()) - 1, m_items);
256
257 /* first sort by number of songs per item */
258 m_items.sort (item_compare_pass1);
259
260 /* limit to items with most songs */
261 if (m_items.len () > max_results)
262 {
263 m_hidden_items = m_items.len () - max_results;
264 m_items.remove (max_results, -1);
265 }
266
267 /* sort by item type, then item name */
268 m_items.sort (item_compare);
269 }
270