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