Preferences: add "do not prompt for filename"
[nomnom.git] / src / i / MainWindow.cpp
blobe4260c64e9517f061e00f5b1dfc383f1467024d8
1 /*
2 * Copyright (C) 2010 Toni Gundogdu.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #include <QMainWindow>
19 #include <QCloseEvent>
20 #include <QFileDialog>
21 #include <QRegExp>
22 #include <QDebug>
24 #include "util.h"
25 #include "Log.h"
26 #include "History.h"
27 // UI
28 #include "About.h"
29 #include "Preferences.h"
30 #include "LogView.h"
31 #include "Reminder.h"
32 #include "YoutubeFeed.h"
33 #include "MainWindow.h"
35 #define QSETTINGS_GROUP "MainWindow"
36 #define QSETTINGS_REMINDER_SHOWTIPS "showTips"
38 // main.cpp
40 extern QMap<QString,QStringList> hosts;
41 extern SharedPreferences shPrefs;
42 extern History history;
43 extern Log log;
45 // Modes.
47 enum { StreamVideo=0, DownloadVideo };
49 // Ctor.
51 MainWindow::MainWindow ()
52 : canceled (false)
54 setupUi(this);
56 readSettings();
58 #define _wrap(sig,slot) \
59 do { \
60 connect(&proc, SIGNAL(sig), this, SLOT(slot)); \
61 } while (0)
63 _wrap(started(), onQuviStarted());
65 _wrap(error(QProcess::ProcessError),
66 onQuviError(QProcess::ProcessError));
68 _wrap(readyRead(), onQuviReadyRead());
70 _wrap(finished(int,QProcess::ExitStatus),
71 onQuviFinished(int,QProcess::ExitStatus));
73 #undef _wrap
75 // Stay on top?
77 if (shPrefs.get(SharedPreferences::StayOnTop).toBool())
78 setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
80 // Create Video instance.
82 video = new Video;
84 // Create context menu.
86 createContextMenu();
88 // Create system tray icon.
90 createTray();
92 // Create Download dialog.
94 download = new Download (this);
95 #ifdef _0
96 download->setLabelText (tr ("Copying..."));
97 #endif
98 download->setCancelButtonText (tr ("&Abort"));
100 connect (download, SIGNAL (error (QString)),
101 this, SLOT (onDownloadError (QString)));
103 proc.setProcessChannelMode (QProcess::MergedChannels);
105 log << tr ("Program started.") + "\n";
108 void
109 MainWindow::createContextMenu () {
111 #define creat_a(t,f,c) \
112 do { \
113 QAction *a = new QAction (t, textBrowser); \
114 if (c) { \
115 a->setCheckable (c); \
116 /*connect (a, SIGNAL(toggled (bool)), SLOT(f (bool)));*/ \
118 else \
119 connect (a, SIGNAL(triggered ()), SLOT(f ())); \
120 textBrowser->addAction (a); \
121 actions[t] = a; \
122 } while (0)
124 #define add_s \
125 do { \
126 QAction *a = new QAction (textBrowser); \
127 a->setSeparator (true); \
128 textBrowser->addAction (a); \
129 } while (0)
131 creat_a (tr("Address..."), onAddress, false);
132 creat_a (tr("Feed..."), onFeed, false);
133 #ifdef _0
134 creat_a (tr("Rake..."), onRake, false);
135 #endif
136 creat_a (tr("History..."), onHistory, false);
137 add_s;
138 creat_a (tr("Overwrite"), _, true);
139 creat_a (tr("Stop"), onStop, false);
140 add_s;
141 creat_a (tr("Log..."), onLog, false);
142 creat_a (tr("Preferences..."), onPreferences, false);
143 add_s;
144 creat_a (tr("About..."), onAbout, false);
145 creat_a (tr("Quit"), close, false );
147 #undef add_s
148 #undef creat_a
150 // Add key shortcuts.
152 actions[tr("Address...")]->setShortcut (QKeySequence(tr("Ctrl+A")));
153 actions[tr("Feed...")]->setShortcut (QKeySequence(tr("Ctrl+F")));
154 #ifdef _0
155 actions[tr("Rake...")]->setShortcut (QKeySequence(tr("Ctrl+C")));
156 #endif
157 actions[tr("Overwrite")]->setShortcut(QKeySequence(tr("Ctrl+W")));
158 actions[tr("Stop")]->setShortcut (QKeySequence(tr("Ctrl+S")));
159 actions[tr("Log...")]->setShortcut (QKeySequence(tr("Ctrl+L")));
160 actions[tr("History...")]->setShortcut (QKeySequence(tr("Ctrl+R")));
161 actions[tr("Preferences...")]->setShortcut (QKeySequence(tr("Ctrl+E")));
162 actions[tr("Quit")]->setShortcut (QKeySequence(tr("Ctrl+Q")));
164 // Add the context menu.
166 textBrowser->setContextMenuPolicy (Qt::ActionsContextMenu);
169 // Create tray icon.
171 void
172 MainWindow::createTray () {
174 trayMenu = new QMenu(this);
176 #define creat_a(t,f) \
177 do { \
178 QAction *a = new QAction(t, this); \
179 connect(a, SIGNAL(triggered()), this, SLOT(f)); \
180 actions[t] = a; \
181 trayMenu->addAction(a); \
182 } while (0)
184 creat_a ( tr("Open"), showNormal() );
185 trayMenu->addSeparator();
186 creat_a ( tr("Quit"), close() );
188 #undef creat_a
190 trayIcon = new QSystemTrayIcon(this);
192 trayIcon->setContextMenu(trayMenu);
193 trayIcon->setIcon(QIcon(":img/nomnom.png"));
195 connect(trayIcon, SIGNAL( activated(QSystemTrayIcon::ActivationReason) ),
196 this, SLOT( onTrayActivated(QSystemTrayIcon::ActivationReason) ) );
199 // Show event.
201 void
202 MainWindow::showEvent (QShowEvent *e) {
203 trayIcon->hide();
204 e->accept();
207 // Hide event.
209 void
210 MainWindow::hideEvent (QHideEvent *e) {
212 if (QSystemTrayIcon::isSystemTrayAvailable()
213 && shPrefs.get(SharedPreferences::MinToTray).toBool())
215 trayIcon->show();
216 if (!isMinimized())
217 hide();
219 e->accept();
222 static void
223 still_running (QWidget *p) {
224 NomNom::crit (p,
225 QObject::tr ("Stop the running quvi(1) process first, and try again."));
228 // Close event.
230 void
231 MainWindow::closeEvent (QCloseEvent *e) {
233 int rc = QMessageBox::Yes;
235 if (proc.state() != QProcess::NotRunning) {
236 rc = NomNom::ask(
237 this,
238 tr("quvi is still running, really quit?")
242 if (rc == QMessageBox::Yes)
243 proc.kill();
244 else {
245 e->ignore();
246 return;
249 QSettings s;
250 NomNom::save_size(s, this, QSETTINGS_GROUP);
251 NomNom::save_pos(s, this, QSETTINGS_GROUP);
253 s.beginGroup(QSETTINGS_GROUP);
254 s.setValue("modeCBox", modeCBox->currentIndex());
255 s.endGroup();
257 history.write();
259 e->accept();
262 // Read.
264 void
265 MainWindow::readSettings () {
267 QSettings s;
269 NomNom::restore_size(s, this, QSETTINGS_GROUP, QSize(130,150));
270 NomNom::restore_pos(s, this, QSETTINGS_GROUP);
272 s.beginGroup(QSETTINGS_GROUP);
273 modeCBox->setCurrentIndex(s.value("modeCBox").toInt());
274 s.endGroup();
276 Reminder (this, "SharedPreferences").conditionalExec ();
278 // Re-read (now updated) "showReminder" value. This was previously done
279 // in main.cpp, but has to be done here if we expect the Preferences
280 // "show tips" box value to match the one set in the Reminder dialog.
282 shPrefs.read ();
285 // Handle (dropped) URL.
287 bool
288 MainWindow::handleURL (const QString& url) {
290 if (proc.state() != QProcess::NotRunning) {
291 still_running (this);
292 return false;
295 // Check paths.
297 const QString quviPath =
298 shPrefs.get (SharedPreferences::QuviPath).toString ().simplified ();
300 if (quviPath.isEmpty ()) {
301 NomNom::crit (this, tr ("You must specify path to the quvi command."));
302 onPreferences ();
303 return false;
306 if (hosts.isEmpty ()) {
307 const bool ok = NomNom::parse_quvi_support (this, quviPath);
308 if (!ok) return false;
311 const QString playerPath =
312 shPrefs.get (SharedPreferences::PlayerPath).toString ().simplified ();
314 if (playerPath.isEmpty ()) {
316 NomNom::crit (this,
317 tr("You must specify path to a stream-capable media "
318 "player command."));
320 onPreferences ();
322 return false;
325 // History.
327 history << url;
329 // quvi args.
331 QStringList args =
332 quviPath.split (" ").replaceInStrings ("%u", url);
334 // Choose format.
336 QStringList formats;
338 foreach (QString key, hosts.keys ()) {
339 QRegExp re (key);
340 if (re.indexIn (url) != -1) {
341 formats = hosts.value (key);
342 break;
346 if (formats.size () > 1) { // Assume "default" is always present.
347 bool ok = false;
348 QString s = QInputDialog::getItem (
349 this,
350 tr ("Choose format"),
351 tr ("Format:"),
352 formats,
354 false,
357 if (ok)
358 args << "-f" << s;
359 else
360 return false;
363 log << args.join (" ") + "\n";
365 json.clear ();
366 canceled = false;
368 proc.start (args.takeFirst (), args);
370 return true;
373 // View video (stream).
375 void
376 MainWindow::streamVideo () {
378 QStringList args =
379 shPrefs.get (SharedPreferences::PlayerPath)
380 .toString ().simplified ().split (" ");
382 args = args.replaceInStrings ("%u", video->get (Video::Link).toString ());
384 log << args.join(" ") + "\n";
386 const bool ok = QProcess::startDetached (args.takeFirst (), args);
388 if (!ok) {
389 NomNom::crit (this,
390 tr ("Unable to start player command, check the Preferences."));
394 // Download video (to a file).
396 void
397 MainWindow::downloadVideo () {
399 QString fname =
400 shPrefs.get (SharedPreferences::FilenameFormat).toString ();
402 const QString suffix =
403 video->get (Video::Suffix).toString ().simplified ();
405 bool ok = NomNom::format_filename (
406 this,
407 shPrefs.get (SharedPreferences::Regexp).toString (),
408 video->get (Video::Title).toString ().simplified (),
409 suffix,
410 video->get (Video::Host).toString ().simplified (),
411 video->get (Video::ID).toString ().simplified (),
412 fname
415 if (!ok)
416 return;
418 QString fpath =
419 shPrefs.get (SharedPreferences::SaveDir).toString ();
421 if (fpath.isEmpty ())
422 fpath = QDir::currentPath ();
424 fpath = QDir::toNativeSeparators (fpath +"/"+ fname);
426 const bool dont_prompt =
427 shPrefs.get (SharedPreferences::DontPromptFilename).toBool ();
429 if (!dont_prompt) {
431 const QFileDialog::Options opts =
432 QFileDialog::DontConfirmOverwrite;
434 fpath = QFileDialog::getSaveFileName (
435 this,
436 tr ("Save video as"),
437 fpath,
438 suffix, // Filter.
439 0, // Selected filter.
440 opts
443 if (fpath.isEmpty ())
444 return;
447 if (actions[tr("Overwrite")]->isChecked ())
448 QDir ().remove (fpath);
450 const qint64 expected_bytes =
451 video->get (Video::Length).toLongLong ();
453 if (QFileInfo (fpath).size () != expected_bytes) {
455 const QString cmd =
456 shPrefs.get (SharedPreferences::CurlPath).toString ().simplified ();
458 download->start (cmd, fpath, video);
461 const bool completeFile =
462 QFileInfo (fpath).size () == expected_bytes;
464 const bool playWhenDone =
465 shPrefs.get (SharedPreferences::PlayWhenDone).toBool ();
467 if (completeFile && playWhenDone) {
469 QStringList args =
470 shPrefs.get (SharedPreferences::PlayerPath)
471 .toString ().simplified ().split (" ");
473 args = args.replaceInStrings ("%u", fpath);
475 log << args.join(" ") + "\n";
477 const bool ok = QProcess::startDetached (args.takeFirst (), args);
479 if (!ok) {
480 NomNom::crit (this,
481 tr ("Unable to start player command, check the Preferences."));
488 // Parse JSON data returned by quvi.
490 bool
491 MainWindow::parseOK () {
493 QString error = tr ("Could not find the JSON data in quvi output.");
494 const int n = json.indexOf ("{");
496 if (n != -1) {
497 if (video->fromJSON (json.mid (n), error))
498 return true;
501 NomNom::crit (this, error);
503 return false;
506 // Parse error from data returned by quvi.
508 void
509 MainWindow::parseError () {
511 QRegExp re ("error:\\s+(.*)$");
513 QString error = tr ("Unknown error");
515 if (re.indexIn (json) != -1)
516 error = re.cap (1);
518 NomNom::crit (this, error.simplified ());
521 // Slot: Icon activated.
523 void
524 MainWindow::onTrayActivated (QSystemTrayIcon::ActivationReason r) {
525 switch (r) {
526 case QSystemTrayIcon::DoubleClick: showNormal(); break;
527 default: break;
531 // Slot: Preferences.
533 void
534 MainWindow::onPreferences () {
536 Preferences dlg (this);
538 if (dlg.exec () == QDialog::Accepted) {
540 // User requested app restart.
542 if (dlg.restartAfter ()) {
543 close ();
544 QProcess::startDetached (QCoreApplication::applicationFilePath ());
545 return;
548 Qt::WindowFlags flags = windowFlags ();
550 // Update window flags: Stay on top.
552 if (shPrefs.get (SharedPreferences::StayOnTop).toBool () ) {
553 // Set flag.
554 if (! (flags & Qt::WindowStaysOnTopHint) )
555 flags |= Qt::WindowStaysOnTopHint;
557 else {
558 // Remove flag.
559 if (flags & Qt::WindowStaysOnTopHint)
560 flags &= ~Qt::WindowStaysOnTopHint;
563 if (flags != windowFlags ()) {
564 setWindowFlags (flags);
565 show ();
572 // Slot: About.
574 void
575 MainWindow::onAbout ()
576 { About(this).exec(); }
578 // Slot: History.
580 void
581 MainWindow::onHistory () {
583 const QStringList lst = history.toStringList();
585 if (lst.size() == 0) {
586 NomNom::info (this, tr ("No record of previously visited URLs found."));
587 return;
590 if (proc.state() != QProcess::NotRunning) {
591 still_running (this);
592 return;
595 bool ok = false;
597 const QString s = QInputDialog::getItem(
598 this,
599 tr("History"),
600 tr("Visited URL:"),
601 lst,
603 false,
607 if (!ok)
608 return;
610 handleURL(s);
613 // Slot: Log.
615 void
616 MainWindow::onLog ()
617 { LogView (this).exec (); }
619 // Slot: Download.
621 void
622 MainWindow::onAddress() {
624 if (proc.state() != QProcess::NotRunning) {
625 still_running (this);
626 return;
629 const QString url =
630 QInputDialog::getText (this, tr ("Address"), tr ("Video URL:"));
632 if (url.isEmpty ())
633 return;
635 handleURL (url);
638 // Slot: Stop quvi process.
640 void
641 MainWindow::onStop () {
642 if (proc.state() != QProcess::NotRunning) {
643 canceled = true;
644 proc.kill();
648 // main.cpp
649 extern QHash<QString,QString> feed;
651 // Slot: on feed.
653 void
654 MainWindow::onFeed () {
656 const QString path =
657 shPrefs.get (SharedPreferences::UmphPath).toString ();
659 if (path.isEmpty ()) {
660 NomNom::crit (this,
661 tr ("Specify path to the umph(1) command in the Preferences."));
662 return;
665 if (proc.state() != QProcess::NotRunning) {
666 still_running (this);
667 return;
670 QString url;
672 if (!feed.empty ()) {
674 const int rc = NomNom::ask (this, tr ("Choose from old results?"));
676 if (rc == QMessageBox::Yes) {
677 if (!NomNom::choose_from_feed (this, url))
678 return;
683 if (url.isEmpty ()) {
685 YoutubeFeed d (this);
687 if (d.exec () != QDialog::Accepted || !d.gotItems ())
688 return;
690 if (!NomNom::choose_from_feed (this, url))
691 return;
695 handleURL (url);
698 // Slot: on scan.
700 void
701 MainWindow::onRake () {
704 // Slot: Process started.
706 void
707 MainWindow::onQuviStarted ()
708 { statusLabel->setText (tr ("Starting parser ...")); }
710 // Slot: Process error.
712 void
713 MainWindow::onQuviError (QProcess::ProcessError n) {
714 if (!canceled)
715 NomNom::crit (this, NomNom::to_process_errmsg (n));
718 // Slot: Ready read. Note that we use merged channels (stdout + stderr).
720 void
721 MainWindow::onQuviReadyRead () {
723 static char data[1024];
725 while (proc.readLine (data, sizeof(data))) {
727 QString ln = QString::fromLocal8Bit (data);
729 log << ln;
730 json += ln;
731 ln.remove ("\n");
733 if (ln.startsWith (":: Check"))
734 statusLabel->setText (tr ("Checking..."));
736 else if (ln.startsWith (":: Fetch"))
737 statusLabel->setText (tr ("Fetching..."));
739 else if (ln.startsWith (":: Verify"))
740 statusLabel->setText (tr ("Verifying..."));
742 // Leave JSON and error parsing to finished slot.
746 // Slot: Process finished.
748 void
749 MainWindow::onQuviFinished (int exitCode, QProcess::ExitStatus exitStatus) {
751 statusLabel->setText (tr ("Ready."));
753 if (exitStatus == QProcess::NormalExit && exitCode == 0) {
754 if (parseOK ()) {
755 if (modeCBox->currentIndex() == StreamVideo)
756 streamVideo();
757 else
758 downloadVideo();
761 else {
762 if (!canceled)
763 parseError ();
767 // Slot: download error.
769 void
770 MainWindow::onDownloadError (QString e)
771 { NomNom::crit (this, e); }
773 // Event: DragEnter.
775 void
776 MainWindow::dragEnterEvent (QDragEnterEvent *e) {
778 QUrl url(e->mimeData()->text());
780 if (url.isValid() && url.scheme().toLower() == "http")
781 e->acceptProposedAction();
784 // Event: Drop.
786 void
787 MainWindow::dropEvent (QDropEvent *e) {
789 const QString url = e->mimeData()->text().simplified();
791 if (handleURL(url))
792 e->acceptProposedAction();
793 else
794 e->ignore();