1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "streamssettings.h"
25 #include "models/streamsmodel.h"
26 #include "streamproviderlistdialog.h"
27 #include "widgets/basicitemdelegate.h"
28 #include "widgets/icons.h"
29 #include "support/icon.h"
30 #include "tar.h"
31 #include "support/messagebox.h"
32 #include "support/utils.h"
33 #include "digitallyimportedsettings.h"
34 #include <QListWidget>
35 #include <QMenu>
36 #include <QFileInfo>
37 #include <QFileDialog>
38 #include <QTimer>
39
40 enum Roles {
41 KeyRole = Qt::UserRole,
42 BuiltInRole,
43 ConfigurableRole
44 };
45
removeDir(const QString & d,const QStringList & types)46 static bool removeDir(const QString &d, const QStringList &types)
47 {
48 QDir dir(d);
49 if (dir.exists()) {
50 QFileInfoList files=dir.entryInfoList(types, QDir::Files|QDir::NoDotAndDotDot);
51 for (const QFileInfo &file: files) {
52 if (!QFile::remove(file.absoluteFilePath())) {
53 return false;
54 }
55 }
56 return dir.rmdir(d);
57 }
58 return true; // Does not exist...
59 }
60
StreamsSettings(QWidget * p)61 StreamsSettings::StreamsSettings(QWidget *p)
62 : Dialog(p, "StreamsDialog", QSize(400, 500))
63 , providerDialog(nullptr)
64 {
65 setCaption(tr("Configure Streams"));
66 QWidget *mw=new QWidget(this);
67 setupUi(mw);
68 setMainWidget(mw);
69 categories->setItemDelegate(new BasicItemDelegate(categories));
70 categories->setSortingEnabled(true);
71 int iSize=Icon::stdSize(QApplication::fontMetrics().height()*1.25);
72 QMenu *installMenu=new QMenu(this);
73 QAction *installFromFileAct=installMenu->addAction(tr("From File..."));
74 QAction *installFromWebAct=installMenu->addAction(tr("Download..."));
75 categories->setIconSize(QSize(iSize, iSize));
76 connect(categories, SIGNAL(currentRowChanged(int)), SLOT(currentCategoryChanged(int)));
77 connect(installFromFileAct, SIGNAL(triggered()), this, SLOT(installFromFile()));
78 connect(installFromWebAct, SIGNAL(triggered()), this, SLOT(installFromWeb()));
79
80 setButtons(Close|User1|User2|User3);
81 setButtonGuiItem(User1, GuiItem(tr("Configure Provider")));
82 setButtonGuiItem(User2, GuiItem(tr("Install")));
83 setButtonGuiItem(User3, GuiItem(tr("Remove")));
84 setButtonMenu(User2, installMenu, InstantPopup);
85 enableButton(User3, false);
86 enableButton(User1, false);
87 }
88
load()89 void StreamsSettings::load()
90 {
91 QList<StreamsModel::Category> cats=StreamsModel::self()->getCategories();
92 QFont f(font());
93 f.setItalic(true);
94 categories->clear();
95 for (const StreamsModel::Category &cat: cats) {
96 QListWidgetItem *item=new QListWidgetItem(cat.name, categories);
97 item->setCheckState(cat.hidden ? Qt::Unchecked : Qt::Checked);
98 item->setData(KeyRole, cat.key);
99 item->setData(BuiltInRole, cat.builtin);
100 item->setData(ConfigurableRole, cat.configurable);
101 item->setIcon(cat.icon);
102 if (cat.builtin) {
103 item->setFont(f);
104 }
105 }
106 }
107
save()108 void StreamsSettings::save()
109 {
110 QSet<QString> disabled;
111 for (int i=0; i<categories->count(); ++i) {
112 QListWidgetItem *item=categories->item(i);
113 if (Qt::Unchecked==item->checkState()) {
114 disabled.insert(item->data(Qt::UserRole).toString());
115 }
116 }
117 StreamsModel::self()->setHiddenCategories(disabled);
118 }
119
currentCategoryChanged(int row)120 void StreamsSettings::currentCategoryChanged(int row)
121 {
122 bool enableRemove=false;
123 bool enableConfigure=false;
124
125 if (row>=0) {
126 QListWidgetItem *item=categories->item(row);
127 enableRemove=!item->data(BuiltInRole).toBool();
128 enableConfigure=item->data(ConfigurableRole).toBool();
129 }
130 enableButton(User3, enableRemove);
131 enableButton(User1, enableConfigure);
132 }
133
installFromFile()134 void StreamsSettings::installFromFile()
135 {
136 QString fileName=QFileDialog::getOpenFileName(this, tr("Install Streams"), QDir::homePath(), tr("Cantata Streams (*.streams)"));
137 if (fileName.isEmpty()) {
138 return;
139 }
140
141 QString name=QFileInfo(fileName).baseName();
142 if (name.isEmpty()) {
143 return;
144 }
145 name=name.replace(Utils::constDirSep, "_");
146 #ifdef Q_OS_WIN
147 name=name.replace("\\", "_");
148 #endif
149
150 if (get(name) && MessageBox::No==MessageBox::warningYesNo(this, tr("A category named '%1' already exists!\n\nOverwrite?").arg(name))) {
151 return;
152 }
153 install(fileName, name);
154 }
155
installFromWeb()156 void StreamsSettings::installFromWeb()
157 {
158 if (!providerDialog) {
159 providerDialog=new StreamProviderListDialog(this);
160 }
161
162 QSet<QString> installed;
163 for (int i=0; i<categories->count(); ++i) {
164 QListWidgetItem *item=categories->item(i);
165 installed.insert(item->text());
166 }
167
168 providerDialog->show(installed);
169 #ifdef Q_OS_MAC
170 // Under OSX when stream providers are installed/updated, and the dialog closed, it
171 // puts the pref dialog below the main window! This hack fixes this...
172 QTimer::singleShot(0, this, SLOT(raiseWindow()));
173 #endif
174 }
175
install(const QString & fileName,const QString & name,bool showErrors)176 bool StreamsSettings::install(const QString &fileName, const QString &name, bool showErrors)
177 {
178 Tar tar(fileName);
179 if (!tar.open()) {
180 if (showErrors) {
181 MessageBox::error(this, tr("Failed to open package file."));
182 }
183 return false;
184 }
185
186 QMap<QString, QByteArray> files=tar.extract(QStringList() << StreamsModel::constXmlFile << StreamsModel::constCompressedXmlFile
187 << StreamsModel::constSettingsFile
188 << StreamsModel::constPngIcon << StreamsModel::constSvgIcon
189 << ".png" << ".svg");
190 QString streamsName=files.contains(StreamsModel::constCompressedXmlFile)
191 ? StreamsModel::constCompressedXmlFile
192 : files.contains(StreamsModel::constXmlFile)
193 ? StreamsModel::constXmlFile
194 : StreamsModel::constSettingsFile;
195 QString iconName=files.contains(StreamsModel::constSvgIcon) ? StreamsModel::constSvgIcon : StreamsModel::constPngIcon;
196 QByteArray streamFile=files[streamsName];
197 QByteArray icon=files[iconName];
198
199 if (streamFile.isEmpty()) {
200 if (showErrors) {
201 MessageBox::error(this, tr("Invalid file format!"));
202 }
203 return false;
204 }
205
206 QString streamsDir=Utils::dataDir(StreamsModel::constSubDir, true);
207 QString dir=streamsDir+name;
208 if (!QDir(dir).exists() && !QDir(dir).mkpath(dir)) {
209 if (showErrors) {
210 MessageBox::error(this, tr("Failed to create stream category folder!"));
211 }
212 return false;
213 }
214
215 QFile streamsFile(dir+Utils::constDirSep+streamsName);
216 if (!streamsFile.open(QIODevice::WriteOnly)) {
217 if (showErrors) {
218 MessageBox::error(this, tr("Failed to save stream list!"));
219 }
220 return false;
221 }
222 streamsFile.write(streamFile);
223 streamsFile.close();
224
225 QIcon icn;
226 if (!icon.isEmpty()) {
227 QFile iconFile(dir+Utils::constDirSep+iconName);
228 if (iconFile.open(QIODevice::WriteOnly)) {
229 iconFile.write(icon);
230 iconFile.close();
231 icn.addFile(dir+Utils::constDirSep+iconName);
232 }
233 }
234
235 // Write all other png and svg files...
236 QMap<QString, QByteArray>::ConstIterator it=files.constBegin();
237 QMap<QString, QByteArray>::ConstIterator end=files.constEnd();
238 for (; it!=end; ++it) {
239 if (it.key()!=iconName && (it.key().endsWith(".png") || it.key().endsWith(".svg"))) {
240 QFile f(dir+Utils::constDirSep+it.key());
241 if (f.open(QIODevice::WriteOnly)) {
242 f.write(it.value());
243 }
244 }
245 }
246
247 StreamsModel::CategoryItem *cat=StreamsModel::self()->addInstalledProvider(name, icn, dir+Utils::constDirSep+streamsName, true);
248 if (!cat) {
249 if (showErrors) {
250 MessageBox::error(this, tr("Invalid file format!"));
251 }
252 return false;
253 }
254 QListWidgetItem *existing=get(name);
255 if (existing) {
256 delete existing;
257 }
258
259 QListWidgetItem *item=new QListWidgetItem(name, categories);
260 item->setCheckState(Qt::Checked);
261 item->setData(KeyRole, cat->configName);
262 item->setData(BuiltInRole, false);
263 item->setData(ConfigurableRole, cat->isDi());
264 item->setIcon(icn);
265 return true;
266 }
267
remove()268 void StreamsSettings::remove()
269 {
270 int row=categories->currentRow();
271 if (row<0) {
272 return;
273 }
274
275 QListWidgetItem *item=categories->item(row);
276 if (!item->data(BuiltInRole).toBool() && MessageBox::No==MessageBox::warningYesNo(this, tr("Are you sure you wish to remove '%1'?").arg(item->text()))) {
277 return;
278 }
279
280 QString dir=Utils::dataDir(StreamsModel::constSubDir);
281 if (!dir.isEmpty() && !removeDir(dir+item->text(), QStringList() << StreamsModel::constXmlFile << StreamsModel::constCompressedXmlFile
282 << StreamsModel::constSettingsFile << "*.png" << "*.svg")) {
283 MessageBox::error(this, tr("Failed to remove streams folder!"));
284 return;
285 }
286
287 StreamsModel::self()->removeInstalledProvider(item->data(KeyRole).toString());
288 delete item;
289 }
290
configure()291 void StreamsSettings::configure()
292 {
293 int row=categories->currentRow();
294 if (row<0) {
295 return;
296 }
297
298 QListWidgetItem *item=categories->item(row);
299 if (!item->data(ConfigurableRole).toBool()) {
300 return;
301 }
302
303 // TODO: Currently only digitally imported can be configured...
304 DigitallyImportedSettings(this).show();
305 }
306
slotButtonClicked(int button)307 void StreamsSettings::slotButtonClicked(int button)
308 {
309 switch (button) {
310 case User1:
311 configure();
312 break;
313 case User3:
314 remove();
315 break;
316 case Close:
317 save();
318 reject();
319 // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
320 Dialog::slotButtonClicked(button);
321 break;
322 default:
323 break;
324 }
325 }
326
raiseWindow()327 void StreamsSettings::raiseWindow()
328 {
329 Utils::raiseWindow(topLevelWidget());
330 }
331
get(const QString & name)332 QListWidgetItem * StreamsSettings::get(const QString &name)
333 {
334 for (int i=0; i<categories->count(); ++i) {
335 QListWidgetItem *item=categories->item(i);
336 if (!item->data(BuiltInRole).toBool() && item->text()==name) {
337 return item;
338 }
339 }
340 return nullptr;
341 }
342
343 #include "moc_streamssettings.cpp"
344