1 /*
2 * playlist_header.cc
3 * Copyright 2017 John Lindgren and Eugene Paskevich
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 "playlist_header.h"
21 #include "playlist.h"
22 #include "playlist_model.h"
23
24 #include <string.h>
25
26 #include <QAction>
27 #include <QContextMenuEvent>
28 #include <QMenu>
29
30 #include <libaudcore/audstrings.h>
31 #include <libaudcore/hook.h>
32 #include <libaudcore/i18n.h>
33 #include <libaudcore/runtime.h>
34 #include <libaudqt/libaudqt.h>
35
36 namespace Moonstone {
37
38 static const char * const s_col_keys[] = {
39 "playing", "number", "title", "artist", "year", "album",
40 "album-artist", "track", "genre", "queued", "length", "path",
41 "filename", "custom", "bitrate", "comment"};
42
43 static const int s_default_widths[] = {
44 25, // now playing
45 25, // entry number
46 275, // title
47 175, // artist
48 50, // year
49 175, // album
50 175, // album artist
51 75, // track
52 100, // genre
53 25, // queue position
54 75, // length
55 275, // path
56 275, // filename
57 275, // custom title
58 75, // bitrate
59 275 // comment
60 };
61
62 static const Playlist::SortType s_sort_types[] = {
63 Playlist::n_sort_types, // now playing
64 Playlist::n_sort_types, // entry number
65 Playlist::Title, // title
66 Playlist::Artist, // artist
67 Playlist::Date, // year
68 Playlist::Album, // album
69 Playlist::AlbumArtist, // album artist
70 Playlist::Track, // track
71 Playlist::Genre, // genre
72 Playlist::n_sort_types, // queue position
73 Playlist::Length, // length
74 Playlist::Path, // path
75 Playlist::Filename, // file name
76 Playlist::FormattedTitle, // custom title
77 Playlist::n_sort_types, // bitrate
78 Playlist::Comment // comment
79 };
80
81 static_assert(aud::n_elems(s_col_keys) == PlaylistModel::n_cols,
82 "update s_col_keys");
83 static_assert(aud::n_elems(s_default_widths) == PlaylistModel::n_cols,
84 "update s_default_widths");
85 static_assert(aud::n_elems(s_sort_types) == PlaylistModel::n_cols,
86 "update s_sort_types");
87
88 static Index<int> s_cols;
89 static int s_col_widths[PlaylistModel::n_cols];
90
loadConfig(bool force=false)91 static void loadConfig(bool force = false)
92 {
93 static bool loaded = false;
94
95 if (loaded && !force)
96 return;
97
98 auto columns =
99 str_list_to_index("playing number artist album title length", " ");
100 int n_columns = aud::min(columns.len(), (int)PlaylistModel::n_cols);
101
102 s_cols.clear();
103
104 for (int c = 0; c < n_columns; c++)
105 {
106 int i = 0;
107 while (i < PlaylistModel::n_cols && strcmp(columns[c], s_col_keys[i]))
108 i++;
109
110 if (i < PlaylistModel::n_cols)
111 s_cols.append(i);
112 }
113
114 for (int i = 0; i < PlaylistModel::n_cols; i++)
115 s_col_widths[i] = audqt::to_native_dpi(s_default_widths[i]);
116
117 loaded = true;
118 }
119
saveConfig()120 static void saveConfig()
121 {
122 Index<String> index;
123 for (int col : s_cols)
124 index.append(String(s_col_keys[col]));
125
126 int widths[PlaylistModel::n_cols];
127 for (int i = 0; i < PlaylistModel::n_cols; i++)
128 widths[i] = audqt::to_portable_dpi(s_col_widths[i]);
129
130 aud_set_str("qtui", "playlist_columns", index_to_str_list(index, " "));
131 aud_set_str("qtui", "column_widths",
132 int_array_to_str(widths, PlaylistModel::n_cols));
133 }
134
PlaylistHeader(PlaylistWidget * playlist)135 PlaylistHeader::PlaylistHeader(PlaylistWidget * playlist)
136 : QHeaderView(Qt::Horizontal, playlist), m_playlist(playlist)
137 {
138 loadConfig();
139
140 setSectionsMovable(true);
141 setStretchLastSection(true);
142
143 connect(this, &QHeaderView::sectionClicked, this,
144 &PlaylistHeader::sectionClicked);
145 connect(this, &QHeaderView::sectionResized, this,
146 &PlaylistHeader::sectionResized);
147 connect(this, &QHeaderView::sectionMoved, this,
148 &PlaylistHeader::sectionMoved);
149 }
150
toggleColumn(int col,bool on)151 static void toggleColumn(int col, bool on)
152 {
153 int pos = s_cols.find(col);
154
155 if (on)
156 {
157 if (pos >= 0)
158 return;
159
160 s_cols.append(col);
161 }
162 else
163 {
164 if (pos < 0)
165 return;
166
167 s_cols.remove(pos, 1);
168 }
169
170 saveConfig();
171
172 // update all playlists
173 hook_call("qtui update playlist columns", nullptr);
174 }
175
resetToDefaults()176 static void resetToDefaults()
177 {
178 aud_set_str("qtui", "playlist_columns", "artist title");
179 aud_set_str("qtui", "column_widths", "");
180
181 loadConfig(true);
182
183 // update all playlists
184 hook_call("qtui update playlist columns", nullptr);
185 }
186
contextMenuEvent(QContextMenuEvent * event)187 void PlaylistHeader::contextMenuEvent(QContextMenuEvent * event)
188 {
189 auto menu = new QMenu(this);
190 QAction * actions[PlaylistModel::n_cols];
191
192 for (int col = 0; col < PlaylistModel::n_cols; col++)
193 {
194 actions[col] = new QAction(_(PlaylistModel::labels[col]), menu);
195 actions[col]->setCheckable(true);
196
197 connect(actions[col], &QAction::toggled,
198 [col](bool on) { toggleColumn(col, on); });
199
200 menu->addAction(actions[col]);
201 }
202
203 for (int col : s_cols)
204 actions[col]->setChecked(true);
205
206 auto sep = new QAction(menu);
207 sep->setSeparator(true);
208 menu->addAction(sep);
209
210 auto reset = new QAction(_("Reset to Defaults"), menu);
211 connect(reset, &QAction::triggered, resetToDefaults);
212 menu->addAction(reset);
213
214 menu->popup(event->globalPos());
215 }
216
updateColumns()217 void PlaylistHeader::updateColumns()
218 {
219 m_inUpdate = true;
220
221 int n_shown = s_cols.len();
222
223 // Due to QTBUG-33974, column #0 cannot be moved by the user.
224 // As a workaround, hide column #0 and start the real columns at #1.
225 // However, Qt will hide the header completely if no columns are visible.
226 // This is bad since the user can't right-click to add any columns again.
227 // To prevent this, show column #0 if no real columns are visible.
228 m_playlist->setColumnHidden(0, (n_shown > 0));
229
230 bool shown[PlaylistModel::n_cols]{};
231
232 for (int i = 0; i < n_shown; i++)
233 {
234 int col = s_cols[i];
235 moveSection(visualIndex(1 + col), 1 + i);
236 shown[col] = true;
237 }
238
239 // last column expands to fit, so size is not restored
240 int last = (n_shown > 0) ? s_cols[n_shown - 1] : -1;
241
242 for (int col = 0; col < PlaylistModel::n_cols; col++)
243 {
244 if (col != last)
245 m_playlist->setColumnWidth(1 + col, s_col_widths[col]);
246
247 m_playlist->setColumnHidden(1 + col, !shown[col]);
248 }
249
250 // width of last column should be set to 0 initially,
251 // but doing so repeatedly causes flicker
252 if (last >= 0 && last != m_lastCol)
253 m_playlist->setColumnWidth(1 + last, 0);
254
255 // this should come after all setColumnHidden() calls
256 m_playlist->setFirstVisibleColumn((n_shown > 0) ? 1 + s_cols[0] : 0);
257
258 m_inUpdate = false;
259 m_lastCol = last;
260 }
261
sectionClicked(int logicalIndex)262 void PlaylistHeader::sectionClicked(int logicalIndex)
263 {
264 int col = logicalIndex - 1;
265 if (col < 0 || col >= PlaylistModel::n_cols)
266 return;
267
268 if (s_sort_types[col] != Playlist::n_sort_types)
269 m_playlist->playlist().sort_entries(s_sort_types[col]);
270 }
271
sectionMoved(int logicalIndex,int oldVisualIndex,int newVisualIndex)272 void PlaylistHeader::sectionMoved(int logicalIndex, int oldVisualIndex,
273 int newVisualIndex)
274 {
275 if (m_inUpdate)
276 return;
277
278 int old_pos = oldVisualIndex - 1;
279 int new_pos = newVisualIndex - 1;
280
281 if (old_pos < 0 || old_pos > s_cols.len() || new_pos < 0 ||
282 new_pos > s_cols.len())
283 return;
284
285 int col = logicalIndex - 1;
286 if (col != s_cols[old_pos])
287 return;
288
289 s_cols.remove(old_pos, 1);
290 s_cols.insert(&col, new_pos, 1);
291
292 saveConfig();
293
294 // update all the other playlists
295 hook_call("qtui update playlist columns", nullptr);
296 }
297
sectionResized(int logicalIndex,int,int newSize)298 void PlaylistHeader::sectionResized(int logicalIndex, int /*oldSize*/,
299 int newSize)
300 {
301 if (m_inUpdate)
302 return;
303
304 int col = logicalIndex - 1;
305 if (col < 0 || col >= PlaylistModel::n_cols)
306 return;
307
308 // last column expands to fit, so size is not saved
309 int pos = s_cols.find(col);
310 if (pos < 0 || pos == s_cols.len() - 1)
311 return;
312
313 s_col_widths[col] = newSize;
314
315 saveConfig();
316
317 // update all the other playlists
318 hook_call("qtui update playlist columns", nullptr);
319 }
320
321 }
322