Fix doc path
[opentx.git] / companion / src / mainwindow.cpp
blob1689cebbd244e82fccea213ad23b608264ee3038
1 /*
2 * Copyright (C) OpenTX
4 * Based on code named
5 * th9x - http://code.google.com/p/th9x
6 * er9x - http://code.google.com/p/er9x
7 * gruvin9x - http://code.google.com/p/gruvin9x
9 * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2 as
13 * published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
21 #include "mainwindow.h"
22 #include "mdichild.h"
23 #include "burnconfigdialog.h"
24 #include "comparedialog.h"
25 #include "logsdialog.h"
26 #include "apppreferencesdialog.h"
27 #include "fwpreferencesdialog.h"
28 #include "firmwareinterface.h"
29 #include "fusesdialog.h"
30 #include "downloaddialog.h"
31 #include "printdialog.h"
32 #include "version.h"
33 #include "creditsdialog.h"
34 #include "releasenotesdialog.h"
35 #include "releasenotesfirmwaredialog.h"
36 #include "customizesplashdialog.h"
37 #include "flasheepromdialog.h"
38 #include "flashfirmwaredialog.h"
39 #include "hexinterface.h"
40 #include "warnings.h"
41 #include "helpers.h"
42 #include "appdata.h"
43 #include "radionotfound.h"
44 #include "process_sync.h"
45 #include "radiointerface.h"
46 #include "progressdialog.h"
47 #include "progresswidget.h"
48 #include "storage.h"
49 #include "translations.h"
51 #include <QtGui>
52 #include <QNetworkProxyFactory>
53 #include <QFileInfo>
54 #include <QDesktopServices>
56 #define OPENTX_DOWNLOADS_PAGE_URL "http://www.open-tx.org/downloads"
57 #define DONATE_STR "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QUZ48K4SEXDP2"
59 #ifdef __APPLE__
60 #define COMPANION_STAMP "companion-macosx.stamp"
61 #define COMPANION_INSTALLER "macosx/opentx-companion-%1.dmg"
62 #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Diskimage (*.dmg)")
63 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to open the disk image to install the new version?")
64 #elif WIN32
65 #define COMPANION_STAMP "companion-windows.stamp"
66 #define COMPANION_INSTALLER "windows/companion-windows-%1.exe"
67 #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Executable (*.exe)")
68 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?")
69 #else
70 #define COMPANION_STAMP "companion-linux.stamp"
71 #define COMPANION_INSTALLER "" // no automated updates for linux
72 #define COMPANION_FILEMASK "*.*"
73 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?")
74 #endif
76 const char * const OPENTX_COMPANION_DOWNLOAD_URL[] = {
77 "https://downloads.open-tx.org/2.2/release/companion",
78 "https://downloads.open-tx.org/2.2/rc/companion",
79 "https://downloads.open-tx.org/2.2/nightlies/companion"
82 MainWindow::MainWindow():
83 downloadDialog_forWait(nullptr),
84 checkForUpdatesState(0),
85 networkManager(nullptr),
86 windowsListActions(new QActionGroup(this))
88 // setUnifiedTitleAndToolBarOnMac(true);
89 this->setWindowIcon(QIcon(":/icon.png"));
90 QNetworkProxyFactory::setUseSystemConfiguration(true);
91 setAcceptDrops(true);
93 mdiArea = new QMdiArea(this);
94 mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
95 mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
96 mdiArea->setTabsClosable(true);
97 mdiArea->setTabsMovable(true);
98 mdiArea->setDocumentMode(true);
99 connect(mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::updateMenus);
101 setCentralWidget(mdiArea);
103 createActions();
104 createMenus();
105 createToolBars();
106 retranslateUi();
107 updateMenus();
109 setIconThemeSize(g.iconSize());
110 restoreGeometry(g.mainWinGeo());
111 restoreState(g.mainWinState());
112 setTabbedWindows(g.tabbedMdi());
114 connect(windowsListActions, &QActionGroup::triggered, this, &MainWindow::onChangeWindowAction);
116 // give time to the splash to disappear and main window to open before starting updates
117 int updateDelay = 1000;
118 bool showSplash = g.showSplash();
119 if (showSplash) {
120 updateDelay += (SPLASH_TIME*1000);
123 if (g.isFirstUse()) {
124 g.warningId(g.warningId() | AppMessages::MSG_WELCOME);
125 QTimer::singleShot(updateDelay-500, this, SLOT(appPrefs())); // must be shown before warnings dialog but after splash
127 else {
128 if (!g.previousVersion().isEmpty())
129 g.warningId(g.warningId() | AppMessages::MSG_UPGRADED);
130 QTimer::singleShot(updateDelay, this, SLOT(doAutoUpdates()));
132 QTimer::singleShot(updateDelay, this, SLOT(displayWarnings()));
134 QStringList strl = QApplication::arguments();
135 QString str;
136 QString printfilename;
137 int printing = strl.contains("--print");
138 int model = -1;
139 int count = 0;
140 foreach(QString arg, strl) {
141 count++;
142 if (arg.contains("--model")) {
143 model = strl[count].toInt() - 1;
145 if (arg.contains("--filename")) {
146 printfilename = strl[count];
149 if (strl.count() > 1)
150 str = strl[1];
151 if (!str.isEmpty()) {
152 int fileType = getStorageType(str);
154 if (fileType==STORAGE_TYPE_EEPE || fileType==STORAGE_TYPE_EEPM || fileType==STORAGE_TYPE_BIN || fileType==STORAGE_TYPE_OTX) {
155 MdiChild * child = createMdiChild();
156 if (child->loadFile(str)) {
157 if (!(printing && model >= 0 && (getCurrentFirmware()->getCapability(Models) == 0 || model<getCurrentFirmware()->getCapability(Models)) && !printfilename.isEmpty())) {
158 statusBar()->showMessage(tr("File loaded"), 2000);
159 child->show();
161 else {
162 child->show();
163 child->print(model, printfilename);
164 child->close();
167 else {
168 child->closeFile(true);
172 if (printing) {
173 QTimer::singleShot(0, this, SLOT(autoClose()));
177 MainWindow::~MainWindow()
179 if (windowsListActions) {
180 delete windowsListActions;
181 windowsListActions = NULL;
185 void MainWindow::displayWarnings()
187 using namespace AppMessages;
188 static uint shownMsgs = 0;
189 int showMsgs = g.warningId();
190 int msgId;
191 QString infoTxt;
193 if ((showMsgs & MSG_WELCOME) && !(shownMsgs & MSG_WELCOME)) {
194 infoTxt = CPN_STR_MSG_WELCOME.arg(VERSION);
195 msgId = MSG_WELCOME;
197 else if ((showMsgs & MSG_UPGRADED) && !(shownMsgs & MSG_UPGRADED)) {
198 infoTxt = CPN_STR_MSG_UPGRADED.arg(VERSION);
199 msgId = MSG_UPGRADED;
201 else {
202 return;
205 QMessageBox msgBox(this);
206 msgBox.setWindowTitle(CPN_STR_APP_NAME);
207 msgBox.setIcon(QMessageBox::Information);
208 msgBox.setStandardButtons(QMessageBox::Ok);
209 msgBox.setInformativeText(infoTxt);
210 QCheckBox * cb = new QCheckBox(tr("Show this message again at next startup?"), &msgBox);
211 msgBox.setCheckBox(cb);
213 msgBox.exec();
215 shownMsgs |= msgId;
216 if (!cb->isChecked())
217 g.warningId(showMsgs & ~msgId);
219 displayWarnings(); // in case more warnings need showing
222 void MainWindow::doAutoUpdates()
224 if (g.autoCheckApp())
225 checkForUpdatesState |= CHECK_COMPANION;
226 if (g.autoCheckFw())
227 checkForUpdatesState |= CHECK_FIRMWARE;
228 checkForUpdates();
231 void MainWindow::doUpdates()
233 checkForUpdatesState = CHECK_COMPANION | CHECK_FIRMWARE | SHOW_DIALOG_WAIT;
234 checkForUpdates();
237 void MainWindow::checkForFirmwareUpdate()
239 checkForUpdatesState = CHECK_FIRMWARE | SHOW_DIALOG_WAIT;
240 checkForUpdates();
243 void MainWindow::dowloadLastFirmwareUpdate()
245 checkForUpdatesState = CHECK_FIRMWARE | AUTOMATIC_DOWNLOAD | SHOW_DIALOG_WAIT;
246 checkForUpdates();
249 QString MainWindow::getCompanionUpdateBaseUrl()
251 return OPENTX_COMPANION_DOWNLOAD_URL[g.boundedOpenTxBranch()];
254 void MainWindow::checkForUpdates()
256 if (!checkForUpdatesState) {
257 closeUpdatesWaitDialog();
258 if (networkManager) {
259 networkManager->deleteLater();
260 networkManager = nullptr;
262 return;
265 if (checkForUpdatesState & SHOW_DIALOG_WAIT) {
266 checkForUpdatesState -= SHOW_DIALOG_WAIT;
267 downloadDialog_forWait = new downloadDialog(NULL, tr("Checking for updates"));
268 downloadDialog_forWait->show();
271 if (networkManager)
272 disconnect(networkManager, 0, this, 0);
273 else
274 networkManager = new QNetworkAccessManager(this);
276 QUrl url;
277 if (checkForUpdatesState & CHECK_COMPANION) {
278 checkForUpdatesState -= CHECK_COMPANION;
279 url.setUrl(QString("%1/%2").arg(getCompanionUpdateBaseUrl()).arg(COMPANION_STAMP));
280 connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForCompanionUpdateFinished);
281 qDebug() << "Checking for Companion update " << url.url();
283 else if (checkForUpdatesState & CHECK_FIRMWARE) {
284 checkForUpdatesState -= CHECK_FIRMWARE;
285 const QString stamp = getCurrentFirmware()->getStampUrl();
286 if (!stamp.isEmpty()) {
287 url.setUrl(stamp);
288 connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForFirmwareUpdateFinished);
289 qDebug() << "Checking for firmware update " << url.url();
292 if (!url.isValid()) {
293 checkForUpdates();
294 return;
297 QNetworkRequest request(url);
298 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
299 networkManager->get(request);
302 void MainWindow::onUpdatesError()
304 checkForUpdatesState = 0;
305 closeUpdatesWaitDialog();
306 QMessageBox::warning(this, CPN_STR_APP_NAME, tr("Unable to check for updates."));
309 void MainWindow::closeUpdatesWaitDialog()
311 if (downloadDialog_forWait) {
312 downloadDialog_forWait->close();
313 delete downloadDialog_forWait;
314 downloadDialog_forWait = NULL;
318 QString MainWindow::seekCodeString(const QByteArray & qba, const QString & label)
320 int posLabel = qba.indexOf(label);
321 if (posLabel < 0)
322 return QString::null;
323 int start = qba.indexOf("\"", posLabel + label.length());
324 if (start < 0)
325 return QString::null;
326 int end = qba.indexOf("\"", start + 1);
327 if (end < 0)
328 return QString::null;
329 return qba.mid(start + 1, end - start - 1);
332 void MainWindow::checkForCompanionUpdateFinished(QNetworkReply * reply)
334 QByteArray qba = reply->readAll();
335 reply->deleteLater();
336 QString version = seekCodeString(qba, "VERSION");
337 if (version.isNull())
338 return onUpdatesError();
340 int webVersion = version2index(version);
342 int ownVersion = version2index(VERSION);
344 if (ownVersion < webVersion) {
345 #if defined WIN32 || defined __APPLE__
346 int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("A new version of Companion is available (version %1)<br>"
347 "Would you like to download it?").arg(version) ,
348 QMessageBox::Yes | QMessageBox::No);
350 if (ret == QMessageBox::Yes) {
351 QDir dir(g.updatesDir());
352 QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), dir.absoluteFilePath(QString(COMPANION_INSTALLER).arg(version)), tr(COMPANION_FILEMASK));
354 if (!fileName.isEmpty()) {
355 g.updatesDir(QFileInfo(fileName).dir().absolutePath());
356 downloadDialog * dd = new downloadDialog(this, QString("%1/%2").arg(getCompanionUpdateBaseUrl()).arg(QString(COMPANION_INSTALLER).arg(version)), fileName);
357 installer_fileName = fileName;
358 connect(dd, SIGNAL(accepted()), this, SLOT(updateDownloaded()));
359 dd->exec();
362 #else
363 QMessageBox::warning(this, tr("New release available"), tr("A new release of Companion is available, please check the <a href='%1'>OpenTX website!</a>").arg(OPENTX_DOWNLOADS_PAGE_URL));
364 #endif
366 else {
367 if (downloadDialog_forWait && checkForUpdatesState==0) {
368 QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time."));
372 checkForUpdates();
375 void MainWindow::updateDownloaded()
377 int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr(COMPANION_INSTALL_QUESTION), QMessageBox::Yes | QMessageBox::No);
378 if (ret == QMessageBox::Yes) {
379 if (QDesktopServices::openUrl(QUrl::fromLocalFile(installer_fileName)))
380 QApplication::exit();
384 void MainWindow::firmwareDownloadAccepted()
386 QString errormsg;
387 QFile file(g.profile[g.id()].fwName());
388 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { //reading HEX TEXT file
389 QMessageBox::critical(this, CPN_STR_TTL_ERROR,
390 tr("Error opening file %1:\n%2.")
391 .arg(g.profile[g.id()].fwName())
392 .arg(file.errorString()));
393 return;
395 file.reset();
396 QTextStream inputStream(&file);
397 QString hline = inputStream.readLine();
398 if (hline.startsWith("ERROR:")) {
399 int errnum = hline.mid(6).toInt();
400 switch(errnum) {
401 case 1:
402 errormsg = tr("Not enough flash available on this board for all the selected options");
403 break;
404 case 2:
405 errormsg = tr("Compilation server temporary failure, try later");
406 break;
407 case 3:
408 errormsg = tr("Compilation server too busy, try later");
409 break;
410 case 4:
411 errormsg = tr("Compilation error");
412 break;
413 case 5:
414 errormsg = tr("Invalid firmware");
415 break;
416 case 6:
417 errormsg = tr("Invalid board");
418 break;
419 case 7:
420 errormsg = tr("Invalid language");
421 break;
422 default:
423 errormsg = tr("Unknown server failure, try later");
424 break;
426 file.close();
427 file.remove();
428 QMessageBox::critical(this, CPN_STR_TTL_ERROR, errormsg);
429 return;
431 file.close();
432 g.fwRev.set(Firmware::getCurrentVariant()->getId(), version2index(firmwareVersionString));
433 if (g.profile[g.id()].burnFirmware()) {
434 int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to write the firmware to the radio now ?"), QMessageBox::Yes | QMessageBox::No);
435 if (ret == QMessageBox::Yes) {
436 writeFlash(g.profile[g.id()].fwName());
441 void MainWindow::checkForFirmwareUpdateFinished(QNetworkReply * reply)
443 bool download = false;
444 bool ignore = false;
446 QByteArray qba = reply->readAll();
447 reply->deleteLater();
448 QString versionString = seekCodeString(qba, "VERSION");
449 QString dateString = seekCodeString(qba, "DATE");
450 if (versionString.isNull() || dateString.isNull())
451 return onUpdatesError();
453 long version = version2index(versionString);
454 if (version <= 0)
455 return onUpdatesError();
457 QString fullVersionString = QString("%1 (%2)").arg(versionString).arg(dateString);
459 if (checkForUpdatesState & AUTOMATIC_DOWNLOAD) {
460 checkForUpdatesState -= AUTOMATIC_DOWNLOAD;
461 download = true;
463 else {
464 int currentVersion = g.fwRev.get(Firmware::getCurrentVariant()->getId());
465 QString currentVersionString = index2version(currentVersion);
467 QMessageBox msgBox;
468 msgBox.setWindowTitle(CPN_STR_APP_NAME);
469 QSpacerItem * horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
470 QGridLayout * layout = (QGridLayout*)msgBox.layout();
471 layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
473 if (currentVersion == 0) {
474 QString rn = getCurrentFirmware()->getReleaseNotesUrl();
475 QAbstractButton *rnButton = NULL;
476 msgBox.setText(tr("Firmware %1 does not seem to have ever been downloaded.\nVersion %2 is available.\nDo you want to download it now?\n\nWe recommend you view the release notes using the button below to learn about any changes that may be important to you.")
477 .arg(Firmware::getCurrentVariant()->getId()).arg(fullVersionString));
478 QAbstractButton *YesButton = msgBox.addButton(trUtf8("Yes"), QMessageBox::YesRole);
479 msgBox.addButton(trUtf8("No"), QMessageBox::NoRole);
480 if (!rn.isEmpty()) {
481 rnButton = msgBox.addButton(trUtf8("Release Notes"), QMessageBox::ActionRole);
483 msgBox.setIcon(QMessageBox::Question);
484 msgBox.resize(0, 0);
485 msgBox.exec();
486 if (msgBox.clickedButton() == rnButton) {
487 ReleaseNotesFirmwareDialog * dialog = new ReleaseNotesFirmwareDialog(this, rn);
488 dialog->exec();
489 int ret2 = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to download version %1 now ?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No);
490 if (ret2 == QMessageBox::Yes)
491 download = true;
492 else
493 ignore = true;
495 else if (msgBox.clickedButton() == YesButton ) {
496 download = true;
498 else {
499 ignore = true;
502 else if (version > currentVersion) {
503 QString rn = getCurrentFirmware()->getReleaseNotesUrl();
504 QAbstractButton *rnButton = NULL;
505 msgBox.setText(tr("A new version of %1 firmware is available:\n - current is %2\n - newer is %3\n\nDo you want to download it now?\n\nWe recommend you view the release notes using the button below to learn about any changes that may be important to you.")
506 .arg(Firmware::getCurrentVariant()->getId()).arg(currentVersionString).arg(fullVersionString));
507 QAbstractButton *YesButton = msgBox.addButton(trUtf8("Yes"), QMessageBox::YesRole);
508 msgBox.addButton(trUtf8("No"), QMessageBox::NoRole);
509 if (!rn.isEmpty()) {
510 rnButton = msgBox.addButton(trUtf8("Release Notes"), QMessageBox::ActionRole);
512 msgBox.setIcon(QMessageBox::Question);
513 msgBox.resize(0,0);
514 msgBox.exec();
515 if( msgBox.clickedButton() == rnButton ) {
516 ReleaseNotesFirmwareDialog * dialog = new ReleaseNotesFirmwareDialog(this, rn);
517 dialog->exec();
518 int ret2 = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to download version %1 now ?").arg(fullVersionString),
519 QMessageBox::Yes | QMessageBox::No);
520 if (ret2 == QMessageBox::Yes) {
521 download = true;
523 else {
524 ignore = true;
527 else if (msgBox.clickedButton() == YesButton ) {
528 download = true;
530 else {
531 ignore = true;
534 else {
535 if (downloadDialog_forWait && checkForUpdatesState==0) {
536 QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time."));
541 if (ignore) {
542 int res = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Ignore this version %1?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No);
543 if (res==QMessageBox::Yes) {
544 g.fwRev.set(Firmware::getCurrentVariant()->getId(), version);
547 else if (download == true) {
548 firmwareVersionString = versionString;
549 startFirmwareDownload();
552 checkForUpdates();
555 void MainWindow::startFirmwareDownload()
557 QString url = Firmware::getCurrentVariant()->getFirmwareUrl();
558 qDebug() << "Downloading firmware" << url;
559 QString ext = url.mid(url.lastIndexOf("."));
560 QString defaultFilename = g.flashDir() + "/" + Firmware::getCurrentVariant()->getId();
561 if (g.profile[g.id()].renameFwFiles()) {
562 defaultFilename += "-" + firmwareVersionString;
564 defaultFilename += ext;
566 QString filename = QFileDialog::getSaveFileName(this, tr("Save As"), defaultFilename);
567 if (!filename.isEmpty()) {
568 g.profile[g.id()].fwName(filename);
569 g.flashDir(QFileInfo(filename).dir().absolutePath());
570 downloadDialog * dd = new downloadDialog(this, url, filename);
571 connect(dd, SIGNAL(accepted()), this, SLOT(firmwareDownloadAccepted()));
572 dd->exec();
576 void MainWindow::closeEvent(QCloseEvent *event)
578 g.mainWinGeo(saveGeometry());
579 g.mainWinState(saveState());
580 g.tabbedMdi(actTabbedWindows->isChecked());
581 QApplication::closeAllWindows();
582 mdiArea->closeAllSubWindows();
583 if (mdiArea->currentSubWindow()) {
584 event->ignore();
586 else {
587 event->accept();
591 void MainWindow::changeEvent(QEvent * e)
593 QMainWindow::changeEvent(e);
594 switch (e->type()) {
595 case QEvent::LanguageChange:
596 retranslateUi(true);
597 break;
598 default:
599 break;
603 void MainWindow::setLanguage(const QString & langString)
605 g.locale(langString);
606 Translations::installTranslators();
609 void MainWindow::onLanguageChanged(QAction * act)
611 QString lang = act->property("locale").toString();
612 if (!lang.isNull())
613 setLanguage(lang);
616 void MainWindow::setTheme(int index)
618 g.theme(index);
619 QMessageBox::information(this, CPN_STR_APP_NAME, tr("The new theme will be loaded the next time you start Companion."));
622 void MainWindow::onThemeChanged(QAction * act)
624 bool ok;
625 int id = act->property("themeId").toInt(&ok);
626 if (ok && id >= 0 && id < 5)
627 setTheme(id);
630 void MainWindow::setIconThemeSize(int index)
632 if (index != g.iconSize())
633 g.iconSize(index);
635 QSize size;
636 switch(g.iconSize()) {
637 case 0:
638 size=QSize(16,16);
639 break;
640 case 2:
641 size=QSize(32,32);
642 break;
643 case 3:
644 size=QSize(48,48);
645 break;
646 case 1:
647 default:
648 size=QSize(24,24);
649 break;
651 this->setIconSize(size);
654 void MainWindow::onIconSizeChanged(QAction * act)
656 bool ok;
657 int id = act->property("sizeId").toInt(&ok);
658 if (ok && id >= 0 && id < 4)
659 setIconThemeSize(id);
662 void MainWindow::setTabbedWindows(bool on)
664 mdiArea->setViewMode(on ? QMdiArea::TabbedView : QMdiArea::SubWindowView);
665 if (actTileWindows)
666 actTileWindows->setDisabled(on);
667 if (actCascadeWindows)
668 actCascadeWindows->setDisabled(on);
670 if (actTabbedWindows->isChecked() != on)
671 actTabbedWindows->setChecked(on);
674 void MainWindow::newFile()
676 MdiChild * child = createMdiChild();
677 child->newFile();
678 child->show();
681 void MainWindow::openDocURL()
683 QString link = "http://www.open-tx.org/documents.html";
684 QDesktopServices::openUrl(QUrl(link));
687 void MainWindow::openFile(const QString & fileName, bool updateLastUsedDir)
689 if (!fileName.isEmpty()) {
690 if (updateLastUsedDir) {
691 g.eepromDir(QFileInfo(fileName).dir().absolutePath());
694 QMdiSubWindow *existing = findMdiChild(fileName);
695 if (existing) {
696 mdiArea->setActiveSubWindow(existing);
697 return;
700 MdiChild *child = createMdiChild();
701 if (child->loadFile(fileName)) {
702 statusBar()->showMessage(tr("File loaded"), 2000);
703 child->show();
705 else {
706 child->closeFile(true);
711 void MainWindow::openFile()
713 QString fileName = QFileDialog::getOpenFileName(this, tr("Open Models and Settings file"), g.eepromDir(), EEPROM_FILES_FILTER);
714 openFile(fileName);
717 void MainWindow::save()
719 if (activeMdiChild() && activeMdiChild()->save()) {
720 statusBar()->showMessage(tr("File saved"), 2000);
724 void MainWindow::saveAs()
726 if (activeMdiChild() && activeMdiChild()->saveAs()) {
727 statusBar()->showMessage(tr("File saved"), 2000);
731 void MainWindow::saveAll()
733 foreach (QMdiSubWindow * window, mdiArea->subWindowList()) {
734 MdiChild * child;
735 if ((child = qobject_cast<MdiChild *>(window->widget())) && child->isWindowModified())
736 child->save();
740 void MainWindow::closeFile()
742 if (mdiArea->activeSubWindow())
743 mdiArea->activeSubWindow()->close();
746 void MainWindow::openRecentFile()
748 QAction *action = qobject_cast<QAction *>(sender());
749 if (action) {
750 QString fileName = action->data().toString();
751 openFile(fileName, false);
755 bool MainWindow::loadProfileId(const unsigned pid) // TODO Load all variables - Also HW!
757 if (pid >= MAX_PROFILES)
758 return false;
760 Firmware * newFw = Firmware::getFirmwareForId(g.profile[pid].fwType());
761 // warn if we're switching between incompatible board types and any files have been modified
762 if (!Boards::isBoardCompatible(Firmware::getCurrentVariant()->getBoard(), newFw->getBoard()) && anyChildrenDirty()) {
763 if (QMessageBox::question(this, CPN_STR_APP_NAME,
764 tr("There are unsaved file changes which you may lose when switching radio types.\n\nDo you wish to continue?"),
765 (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) != QMessageBox::Yes) {
766 updateProfilesActions();
767 return false;
771 // Set the new profile number
772 g.id(pid);
773 Firmware::setCurrentVariant(newFw);
774 emit firmwareChanged();
775 updateMenus();
776 return true;
779 void MainWindow::loadProfile()
781 QAction * action = qobject_cast<QAction *>(sender());
782 if (action) {
783 bool ok = false;
784 unsigned profnum = action->data().toUInt(&ok);
785 if (ok)
786 loadProfileId(profnum);
790 void MainWindow::appPrefs()
792 AppPreferencesDialog * dialog = new AppPreferencesDialog(this);
793 dialog->setMainWinHasDirtyChild(anyChildrenDirty());
794 connect(dialog, &AppPreferencesDialog::firmwareProfileAboutToChange, this, &MainWindow::saveAll);
795 connect(dialog, &AppPreferencesDialog::firmwareProfileChanged, this, &MainWindow::loadProfileId);
796 dialog->exec();
797 dialog->deleteLater();
800 void MainWindow::fwPrefs()
802 FirmwarePreferencesDialog * dialog = new FirmwarePreferencesDialog(this);
803 dialog->exec();
804 dialog->deleteLater();
807 void MainWindow::contributors()
809 CreditsDialog * dialog = new CreditsDialog(this);
810 dialog->exec();
811 dialog->deleteLater();
814 // Create a widget with a line edit and folder select button and handles all interactions. Features autosuggest
815 // path hints while typing, invalid paths shown in red. The label string is only for dialog title, not a QLabel.
816 // This should probably be moved some place more reusable, esp. the QFileSystemModel.
817 QWidget * folderSelectorWidget(QString * path, const QString & label, QWidget * parent)
819 static QFileSystemModel fileModel;
820 static bool init = false;
821 if (!init) {
822 init = true;
823 fileModel.setFilter(QDir::Dirs);
824 fileModel.setRootPath("/");
827 QWidget * fsw = new QWidget(parent);
828 QLineEdit * le = new QLineEdit(parent);
829 QCompleter * fsc = new QCompleter(fsw);
830 fsc->setModel(&fileModel);
831 //fsc->setCompletionMode(QCompleter::InlineCompletion);
832 le->setCompleter(fsc);
834 QToolButton * btn = new QToolButton(fsw);
835 btn->setIcon(CompanionIcon("open.png"));
836 QHBoxLayout * l = new QHBoxLayout(fsw);
837 l->setContentsMargins(0,0,0,0);
838 l->setSpacing(3);
839 l->addWidget(le);
840 l->addWidget(btn);
842 QObject::connect(btn, &QToolButton::clicked, [=]() {
843 QString dir = QFileDialog::getExistingDirectory(parent, label, le->text(), 0);
844 if (!dir.isEmpty()) {
845 le->setText(QDir::toNativeSeparators(dir));
846 le->setFocus();
850 QObject::connect(le, &QLineEdit::textChanged, [=](const QString & text) {
851 *path = text;
852 if (QFile::exists(text))
853 le->setStyleSheet("");
854 else
855 le->setStyleSheet("QLineEdit {color: red;}");
857 le->setText(QDir::toNativeSeparators(*path));
859 return fsw;
862 void MainWindow::sdsync()
864 const QString dlgTtl = tr("Synchronize SD");
865 const QIcon dlgIcn = CompanionIcon("sdsync.png");
866 const QString srcArw = CPN_STR_SW_INDICATOR_UP % " ";
867 const QString dstArw = CPN_STR_SW_INDICATOR_DN % " ";
868 QStringList errorMsgs;
870 // remember user-selectable options for duration of session
871 static QString sourcePath;
872 static QString destPath;
873 static int syncDirection = SyncProcess::SYNC_A2B_B2A;
874 static int compareType = SyncProcess::OVERWR_NEWER_IF_DIFF;
875 static int maxFileSize = 2 * 1024 * 1024; // Bytes
876 static bool dryRun = false;
878 if (sourcePath.isEmpty())
879 sourcePath = g.profile[g.id()].sdPath();
880 if (destPath.isEmpty())
881 destPath = findMassstoragePath("SOUNDS").replace(QRegExp("[/\\\\]?SOUNDS"), "");
883 if (sourcePath.isEmpty())
884 errorMsgs << tr("No local SD structure path configured!");
885 if (destPath.isEmpty())
886 errorMsgs << tr("No Radio or SD card detected!");
888 QDialog dlg(this);
889 dlg.setWindowTitle(dlgTtl % tr(" :: Options"));
890 dlg.setWindowIcon(dlgIcn);
891 dlg.setSizeGripEnabled(true);
892 dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowContextHelpButtonHint);
894 QLabel * lblSrc = new QLabel(tr("Local Folder:"), &dlg);
895 QWidget * wdgSrc = folderSelectorWidget(&sourcePath, lblSrc->text(), &dlg);
897 QLabel * lblDst = new QLabel(tr("Radio Folder:"), &dlg);
898 QWidget * wdgDst = folderSelectorWidget(&destPath, lblDst->text(), &dlg);
900 QLabel * lbDir = new QLabel(tr("Sync. Direction:"), &dlg);
901 QComboBox * syncDir = new QComboBox(&dlg);
902 syncDir->addItem(tr("%1%2 Both directions, to radio folder first").arg(dstArw, srcArw), SyncProcess::SYNC_A2B_B2A);
903 syncDir->addItem(tr("%1%2 Both directions, to local folder first").arg(srcArw, dstArw), SyncProcess::SYNC_B2A_A2B);
904 syncDir->addItem(tr(" %1 Only from local folder to radio folder").arg(dstArw), SyncProcess::SYNC_A2B);
905 syncDir->addItem(tr(" %1 Only from radio folder to local folder").arg(srcArw), SyncProcess::SYNC_B2A);
906 syncDir->setCurrentIndex(-1); // we set the default option later
908 QLabel * lbMode = new QLabel(tr("Existing Files:"), &dlg);
909 QComboBox * copyMode = new QComboBox(&dlg);
910 copyMode->setToolTip(tr("How to handle overwriting files which already exist in the destination folder."));
911 copyMode->addItem(tr("Copy only if newer and different (compare contents)"), SyncProcess::OVERWR_NEWER_IF_DIFF);
912 copyMode->addItem(tr("Copy only if newer (do not compare contents)"), SyncProcess::OVERWR_NEWER_ALWAYS);
913 copyMode->addItem(tr("Copy only if different (ignore file time stamps)"), SyncProcess::OVERWR_IF_DIFF);
914 copyMode->addItem(tr("Always copy (force overwite existing files)"), SyncProcess::OVERWR_ALWAYS);
916 QLabel * lbSize = new QLabel(tr("Max. File Size:"), &dlg);
917 QSpinBox * maxSize = new QSpinBox(&dlg);
918 maxSize->setRange(0, 100 * 1024);
919 maxSize->setAccelerated(true);
920 maxSize->setSpecialValueText(tr("Any size"));
921 maxSize->setToolTip(tr("Skip files larger than this size. Enter zero for unlimited."));
922 #if (QT_VERSION >= QT_VERSION_CHECK(5, 3, 0))
923 maxSize->setGroupSeparatorShown(true);
924 #endif
926 QCheckBox * testRun = new QCheckBox(tr("Test-run only"), &dlg);
927 testRun->setToolTip(tr("Run as normal but do not actually copy anything. Useful for verifying results before real sync."));
928 connect(testRun, &QCheckBox::toggled, [=](bool on) { dryRun = on; });
930 // layout to hold size spinbox and checkbox option(s)
931 QHBoxLayout * hlay1 = new QHBoxLayout();
932 hlay1->addWidget(maxSize, 1);
933 hlay1->addWidget(testRun);
935 // dialog OK/Cancel buttons
936 QDialogButtonBox * bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
938 // Create main layout and add everything
939 QGridLayout * dlgL = new QGridLayout(&dlg);
940 dlgL->setSizeConstraint(QLayout::SetFixedSize);
941 int row = 0;
942 if (errorMsgs.size()) {
943 QLabel * lblWarn = new QLabel(QString(errorMsgs.join('\n')), &dlg);
944 lblWarn->setStyleSheet("QLabel { color: red; }");
945 dlgL->addWidget(lblWarn, row++, 0, 1, 2);
947 dlgL->addWidget(lblSrc, row, 0);
948 dlgL->addWidget(wdgSrc, row++, 1);
949 dlgL->addWidget(lblDst, row, 0);
950 dlgL->addWidget(wdgDst, row++, 1);
951 dlgL->addWidget(lbDir, row, 0);
952 dlgL->addWidget(syncDir, row++, 1);
953 dlgL->addWidget(lbMode, row, 0);
954 dlgL->addWidget(copyMode, row++, 1);
955 dlgL->addWidget(lbSize, row, 0);
956 dlgL->addLayout(hlay1, row++, 1);
957 dlgL->addWidget(bb, row++, 0, 1, 2);
958 dlgL->setRowStretch(row, 1);
960 connect(copyMode, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=](int) {
961 compareType = copyMode->currentData().toInt();
964 // function to dis/enable the OVERWR_ALWAYS option depending on sync direction
965 connect(syncDir, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=](int) {
966 int dir = syncDir->currentData().toInt();
967 int idx = copyMode->findData(SyncProcess::OVERWR_ALWAYS);
968 int flg = (dir == SyncProcess::SYNC_A2B || dir == SyncProcess::SYNC_B2A) ? 33 : 0;
969 if (!flg && idx == copyMode->currentIndex())
970 copyMode->setCurrentIndex(copyMode->findData(SyncProcess::OVERWR_NEWER_IF_DIFF));
971 copyMode->setItemData(idx, flg, Qt::UserRole - 1);
972 syncDirection = dir;
975 // function to set magnitude of file size spinbox, KB or MB
976 connect(maxSize, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), [=](int value) {
977 int multi = maxSize->property("multi").isValid() ? maxSize->property("multi").toInt() : 0;
978 maxSize->blockSignals(true);
979 if (value >= 10 * 1024 && multi != 1024 * 1024) {
980 // KB -> MB
981 multi = 1024 * 1024;
982 maxSize->setValue(value / 1024);
983 maxSize->setMaximum(100);
984 maxSize->setSingleStep(1);
985 maxSize->setSuffix(tr(" MB"));
987 else if ((value < 10 && multi != 1024) || !multi) {
988 // MB -> KB
989 if (multi)
990 value *= 1024;
991 multi = 1024;
992 if (value == 9 * 1024)
993 value += 1024 - 32; // avoid large jump when stepping from 10MB to 10,208KB
994 maxSize->setMaximum(100 * 1024);
995 maxSize->setValue(value);
996 maxSize->setSingleStep(32);
997 maxSize->setSuffix(tr(" KB"));
999 maxSize->setProperty("multi", multi);
1000 maxSize->blockSignals(false);
1001 maxFileSize = value * multi;
1004 copyMode->setCurrentIndex(copyMode->findData(compareType));
1005 syncDir->setCurrentIndex(syncDir->findData(syncDirection));
1006 maxSize->setValue(maxFileSize / 1024);
1007 testRun->setChecked(dryRun);
1009 connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
1010 connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
1012 // to restart dialog on error/etc
1013 openDialog:
1015 // show the modal options dialog
1016 if (dlg.exec() == QDialog::Rejected)
1017 return;
1019 // validate
1020 errorMsgs.clear();
1021 if (sourcePath == destPath)
1022 errorMsgs << tr("Source and destination folders are the same!");
1023 if (sourcePath.isEmpty() || !QFile::exists(sourcePath))
1024 errorMsgs << tr("Source folder not found: %1").arg(sourcePath);
1025 if (destPath.isEmpty() || !QFile::exists(destPath))
1026 errorMsgs << tr("Destination folder not found: %1").arg(destPath);
1028 if (!errorMsgs.isEmpty()) {
1029 QMessageBox::warning(this, dlgTtl % tr(" :: Error"), errorMsgs.join('\n'));
1030 goto openDialog;
1033 // set up the progress dialog and the sync process worker
1034 ProgressDialog * progressDlg = new ProgressDialog(this, dlgTtl % tr(" :: Progress"), dlgIcn);
1035 progressDlg->setAttribute(Qt::WA_DeleteOnClose, true);
1036 ProgressWidget * progWidget = progressDlg->progress();
1037 SyncProcess * syncProcess = new SyncProcess(sourcePath, destPath, syncDirection, compareType, maxFileSize, dryRun);
1039 // move sync process to separate thread, we only use signals/slots from here on...
1040 QThread * syncThread = new QThread(this);
1041 syncProcess->moveToThread(syncThread);
1043 // ...and quite a few of them!
1044 connect(this, &MainWindow::startSync, syncProcess, &SyncProcess::run);
1045 connect(syncThread, &QThread::finished, syncProcess, &SyncProcess::deleteLater);
1046 connect(syncProcess, &SyncProcess::finished, syncThread, &QThread::quit);
1047 connect(syncProcess, &SyncProcess::destroyed, syncThread, &QThread::quit);
1048 connect(syncProcess, &SyncProcess::destroyed, syncThread, &QThread::deleteLater);
1049 connect(syncProcess, &SyncProcess::fileCountChanged, progWidget, &ProgressWidget::setMaximum);
1050 connect(syncProcess, &SyncProcess::progressStep, progWidget, &ProgressWidget::setValue);
1051 connect(syncProcess, &SyncProcess::progressMessage, progWidget, &ProgressWidget::addMessage);
1052 connect(syncProcess, &SyncProcess::statusMessage, progWidget, &ProgressWidget::setInfo);
1053 connect(syncProcess, &SyncProcess::started, progressDlg, &ProgressDialog::setProcessStarted);
1054 connect(syncProcess, &SyncProcess::finished, progressDlg, &ProgressDialog::setProcessStopped);
1055 connect(syncProcess, &SyncProcess::finished, [=]() { QApplication::alert(this); });
1056 connect(progressDlg, &ProgressDialog::rejected, syncProcess, &SyncProcess::stop);
1057 connect(progressDlg, &ProgressDialog::rejected, syncProcess, &SyncProcess::deleteLater);
1059 // go (finally)
1060 syncThread->start();
1061 emit startSync();
1064 void MainWindow::changelog()
1066 QString link = "http://www.open-tx.org";
1067 QDesktopServices::openUrl(QUrl(link));
1070 void MainWindow::customizeSplash()
1072 CustomizeSplashDialog * dialog = new CustomizeSplashDialog(this);
1073 dialog->exec();
1074 dialog->deleteLater();
1077 void MainWindow::writeEeprom()
1079 if (activeMdiChild()) activeMdiChild()->writeEeprom();
1082 void MainWindow::readEeprom()
1084 Board::Type board = getCurrentBoard();
1085 QString tempFile;
1086 if (IS_HORUS(board))
1087 tempFile = generateProcessUniqueTempFileName("temp.otx");
1088 else if (IS_ARM(board))
1089 tempFile = generateProcessUniqueTempFileName("temp.bin");
1090 else
1091 tempFile = generateProcessUniqueTempFileName("temp.hex");
1093 qDebug() << "MainWindow::readEeprom(): using temp file: " << tempFile;
1095 if (readEepromFromRadio(tempFile)) {
1096 MdiChild * child = createMdiChild();
1097 child->newFile(false);
1098 child->loadFile(tempFile, false);
1099 child->show();
1100 qunlink(tempFile);
1104 bool MainWindow::readFirmwareFromRadio(const QString & filename)
1106 ProgressDialog progressDialog(this, tr("Read Firmware from Radio"), CompanionIcon("read_flash.png"));
1107 bool result = readFirmware(filename, progressDialog.progress());
1108 if (!result && !progressDialog.isEmpty()) {
1109 progressDialog.exec();
1111 return result;
1114 bool MainWindow::readEepromFromRadio(const QString & filename)
1116 ProgressDialog progressDialog(this, tr("Read Models and Settings from Radio"), CompanionIcon("read_eeprom.png"));
1117 bool result = ::readEeprom(filename, progressDialog.progress());
1118 if (!result) {
1119 if (!progressDialog.isEmpty()) {
1120 progressDialog.exec();
1123 else {
1124 statusBar()->showMessage(tr("Models and Settings read"), 2000);
1126 return result;
1129 void MainWindow::writeBackup()
1131 if (IS_HORUS(getCurrentBoard())) {
1132 QMessageBox::information(this, CPN_STR_APP_NAME, tr("This function is not yet implemented"));
1133 return;
1134 // TODO implementation
1136 FlashEEpromDialog *cd = new FlashEEpromDialog(this);
1137 cd->exec();
1140 void MainWindow::writeFlash(QString fileToFlash)
1142 FlashFirmwareDialog * cd = new FlashFirmwareDialog(this);
1143 cd->exec();
1146 void MainWindow::readBackup()
1148 if (IS_HORUS(getCurrentBoard())) {
1149 QMessageBox::information(this, CPN_STR_APP_NAME, tr("This function is not yet implemented"));
1150 return;
1151 // TODO implementation
1153 QString fileName = QFileDialog::getSaveFileName(this, tr("Save Radio Backup to File"), g.eepromDir(), EXTERNAL_EEPROM_FILES_FILTER);
1154 if (!fileName.isEmpty()) {
1155 if (!readEepromFromRadio(fileName))
1156 return;
1160 void MainWindow::readFlash()
1162 QString fileName = QFileDialog::getSaveFileName(this,tr("Read Radio Firmware to File"), g.flashDir(), FLASH_FILES_FILTER);
1163 if (!fileName.isEmpty()) {
1164 readFirmwareFromRadio(fileName);
1168 void MainWindow::burnConfig()
1170 burnConfigDialog *bcd = new burnConfigDialog(this);
1171 bcd->exec();
1172 delete bcd;
1175 void MainWindow::burnList()
1177 burnConfigDialog bcd(this);
1178 bcd.listAvrdudeProgrammers();
1181 void MainWindow::burnFuses()
1183 FusesDialog *fd = new FusesDialog(this);
1184 fd->exec();
1185 delete fd;
1188 void MainWindow::compare()
1190 CompareDialog *fd = new CompareDialog(this,getCurrentFirmware());
1191 fd->setAttribute(Qt::WA_DeleteOnClose, true);
1192 fd->show();
1195 void MainWindow::logFile()
1197 LogsDialog *fd = new LogsDialog(this);
1198 fd->setWindowFlags(Qt::Window); //to show minimize an maximize buttons
1199 fd->setAttribute(Qt::WA_DeleteOnClose, true);
1200 fd->show();
1203 void MainWindow::about()
1205 QString aboutStr = "<center><img src=\":/images/companion-title.png\"></center><br/>";
1206 aboutStr.append(tr("OpenTX Home Page: <a href='%1'>%1</a>").arg("http://www.open-tx.org"));
1207 aboutStr.append("<br/><br/>");
1208 aboutStr.append(tr("The OpenTX Companion project was originally forked from <a href='%1'>eePe</a>").arg("http://code.google.com/p/eepe"));
1209 aboutStr.append("<br/><br/>");
1210 aboutStr.append(tr("If you've found this program useful, please support by <a href='%1'>donating</a>").arg(DONATE_STR));
1211 aboutStr.append("<br/><br/>");
1212 aboutStr.append(QString("Version %1, %2").arg(VERSION).arg(__DATE__));
1213 aboutStr.append("<br/><br/>");
1214 aboutStr.append(tr("Copyright OpenTX Team") + "<br/>&copy; 2011-2017<br/>");
1215 QMessageBox msgBox(this);
1216 msgBox.setWindowIcon(CompanionIcon("information.png"));
1217 msgBox.setWindowTitle(tr("About Companion"));
1218 msgBox.setText(aboutStr);
1219 msgBox.exec();
1222 void MainWindow::updateMenus()
1224 QMdiSubWindow * activeChild = mdiArea->activeSubWindow();
1226 newAct->setEnabled(true);
1227 openAct->setEnabled(true);
1228 saveAct->setEnabled(activeChild);
1229 saveAsAct->setEnabled(activeChild);
1230 closeAct->setEnabled(activeChild);
1231 compareAct->setEnabled(activeChild);
1232 writeEepromAct->setEnabled(activeChild);
1233 readEepromAct->setEnabled(true);
1234 writeBUToRadioAct->setEnabled(true);
1235 readBUToFileAct->setEnabled(true);
1236 editSplashAct->setDisabled(IS_HORUS(getCurrentBoard()));
1238 foreach (QAction * act, fileWindowActions) {
1239 if (!act)
1240 continue;
1241 if (fileMenu && fileMenu->actions().contains(act))
1242 fileMenu->removeAction(act);
1243 if (fileToolBar && fileToolBar->actions().contains(act)) {
1244 fileToolBar->removeAction(act);
1246 if (act->isSeparator() && act->parent() == this)
1247 delete act;
1249 fileWindowActions.clear();
1250 if (activeChild) {
1251 editMenu->clear();
1252 editMenu->addActions(activeMdiChild()->getEditActions());
1253 editMenu->addSeparator();
1254 editMenu->addActions(activeMdiChild()->getModelActions()); // maybe separate menu/toolbar?
1255 editMenu->setEnabled(true);
1257 editToolBar->clear();
1258 editToolBar->addActions(activeMdiChild()->getEditActions());
1259 editToolBar->setEnabled(true);
1260 if (activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV)) {
1261 // workaround for default split button appearance of action with menu :-/
1262 QToolButton * btn;
1263 if ((btn = qobject_cast<QToolButton *>(editToolBar->widgetForAction(activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV)))))
1264 btn->setPopupMode(QToolButton::InstantPopup);
1267 fileWindowActions = activeMdiChild()->getGeneralActions();
1268 QAction *sep = new QAction(this);
1269 sep->setSeparator(true);
1270 fileWindowActions.append(sep);
1271 fileMenu->insertActions(logsAct, fileWindowActions);
1272 fileToolBar->insertActions(logsAct, fileWindowActions);
1274 else {
1275 editToolBar->setDisabled(true);
1276 editMenu->setDisabled(true);
1279 foreach (QAction * act, windowsListActions->actions()) {
1280 if (act->property("window_ptr").canConvert<QMdiSubWindow *>() &&
1281 act->property("window_ptr").value<QMdiSubWindow *>() == activeChild) {
1282 act->setChecked(true);
1283 break;
1287 updateRecentFileActions();
1288 updateProfilesActions();
1289 setWindowTitle(tr("OpenTX Companion %1 - Radio: %2 - Profile: %3").arg(VERSION).arg(getCurrentFirmware()->getName()).arg(g.profile[g.id()].name()));
1292 MdiChild * MainWindow::createMdiChild()
1294 QMdiSubWindow * win = new QMdiSubWindow();
1295 MdiChild * child = new MdiChild(this, win);
1296 win->setAttribute(Qt::WA_DeleteOnClose);
1297 win->setWidget(child);
1298 mdiArea->addSubWindow(win);
1299 if (g.mdiWinGeo().size() < 10 && g.mdiWinGeo() == "maximized")
1300 win->showMaximized();
1302 connect(this, &MainWindow::firmwareChanged, child, &MdiChild::onFirmwareChanged);
1303 connect(child, &MdiChild::windowTitleChanged, this, &MainWindow::onSubwindowTitleChanged);
1304 connect(child, &MdiChild::modified, this, &MainWindow::onSubwindowModified);
1305 connect(child, &MdiChild::newStatusMessage, statusBar(), &QStatusBar::showMessage);
1306 connect(child, &MdiChild::destroyed, win, &QMdiSubWindow::close);
1307 connect(win, &QMdiSubWindow::destroyed, this, &MainWindow::updateWindowActions);
1309 updateWindowActions();
1310 return child;
1313 QAction * MainWindow::addAct(const QString & icon, const char *slot, const QKeySequence & shortcut, QObject *slotObj, const char * signal)
1315 QAction * newAction = new QAction( this );
1316 newAction->setMenuRole(QAction::NoRole);
1317 if (!icon.isEmpty())
1318 newAction->setIcon(CompanionIcon(icon));
1319 if (!shortcut.isEmpty())
1320 newAction->setShortcut(shortcut);
1321 if (slot) {
1322 if (!slotObj)
1323 slotObj = this;
1324 if (!signal)
1325 connect(newAction, SIGNAL(triggered()), slotObj, slot);
1326 else
1327 connect(newAction, signal, slotObj, slot);
1329 return newAction;
1332 QAction * MainWindow::addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, const char * propName, const QVariant & propValue, const QVariant & dfltValue, const QKeySequence & shortcut)
1334 QAction * act = aGroup->addAction(sName);
1335 act->setMenuRole(QAction::NoRole);
1336 act->setStatusTip(lName);
1337 act->setCheckable(true);
1338 if (!shortcut.isEmpty())
1339 act->setShortcut(shortcut);
1340 if (propName) {
1341 act->setProperty(propName, propValue);
1342 if (propValue == dfltValue)
1343 act->setChecked(true);
1345 return act;
1348 void MainWindow::trAct(QAction * act, const QString & text, const QString & descript)
1350 if (!text.isEmpty())
1351 act->setText(text);
1352 if (!descript.isEmpty())
1353 act->setStatusTip(descript);
1356 void MainWindow::retranslateUi(bool showMsg)
1358 trAct(newAct, tr("New"), tr("Create a new Models and Settings file"));
1359 trAct(openAct, tr("Open..."), tr("Open Models and Settings file"));
1360 trAct(saveAct, tr("Save"), tr("Save Models and Settings file"));
1361 trAct(saveAsAct, tr("Save As..."), tr("Save Models and Settings file"));
1362 trAct(closeAct, tr("Close"), tr("Close Models and Settings file"));
1363 trAct(exitAct, tr("Exit"), tr("Exit the application"));
1364 trAct(aboutAct, tr("About..."), tr("Show the application's About box"));
1366 trAct(recentFilesAct, tr("Recent Files"), tr("List of recently used files"));
1367 trAct(profilesMenuAct, tr("Radio Profiles"), tr("Create or Select Radio Profiles"));
1368 trAct(logsAct, tr("View Log File..."), tr("Open and view log file"));
1369 trAct(appPrefsAct, tr("Settings..."), tr("Edit Settings"));
1370 trAct(fwPrefsAct, tr("Download..."), tr("Download firmware and voice files"));
1371 trAct(checkForUpdatesAct, tr("Check for Updates..."), tr("Check OpenTX and Companion updates"));
1372 trAct(changelogAct, tr("Release notes..."), tr("Show release notes"));
1373 trAct(compareAct, tr("Compare Models..."), tr("Compare models"));
1374 trAct(editSplashAct, tr("Edit Radio Splash Image..."), tr("Edit the splash image of your Radio"));
1375 trAct(burnListAct, tr("List programmers..."), tr("List available programmers"));
1376 trAct(burnFusesAct, tr("Fuses..."), tr("Show fuses dialog"));
1377 trAct(readFlashAct, tr("Read Firmware from Radio"), tr("Read firmware from Radio"));
1378 trAct(writeFlashAct, tr("Write Firmware to Radio"), tr("Write firmware to Radio"));
1379 trAct(sdsyncAct, tr("Synchronize SD"), tr("SD card synchronization"));
1381 trAct(openDocURLAct, tr("Manuals and other Documents"), tr("Open the OpenTX document page in a web browser"));
1382 trAct(writeEepromAct, tr("Write Models and Settings To Radio"), tr("Write Models and Settings to Radio"));
1383 trAct(readEepromAct, tr("Read Models and Settings From Radio"), tr("Read Models and Settings from Radio"));
1384 trAct(burnConfigAct, tr("Configure Communications..."), tr("Configure software for communicating with the Radio"));
1385 trAct(writeBUToRadioAct, tr("Write Backup to Radio"), tr("Write Backup from file to Radio"));
1386 trAct(readBUToFileAct, tr("Backup Radio to File"), tr("Save a complete backup file of all settings and model data in the Radio"));
1387 trAct(contributorsAct, tr("Contributors..."), tr("A tribute to those who have contributed to OpenTX and Companion"));
1389 trAct(createProfileAct, tr("Add Radio Profile"), tr("Create a new Radio Settings Profile"));
1390 trAct(copyProfileAct, tr("Copy Current Radio Profile"), tr("Duplicate current Radio Settings Profile"));
1391 trAct(deleteProfileAct, tr("Delete Current Radio Profile..."), tr("Delete the current Radio Settings Profile"));
1393 trAct(actTabbedWindows, tr("Tabbed Windows"), tr("Use tabs to arrange open windows."));
1394 trAct(actTileWindows, tr("Tile Windows"), tr("Arrange open windows across all the available space."));
1395 trAct(actCascadeWindows, tr("Cascade Windows"), tr("Arrange all open windows in a stack."));
1396 trAct(actCloseAllWindows, tr("Close All Windows"), tr("Closes all open files (prompts to save if necessary."));
1398 editMenu->setTitle(tr("Edit"));
1399 fileMenu->setTitle(tr("File"));
1400 settingsMenu->setTitle(tr("Settings"));
1401 themeMenu->setTitle(tr("Set Icon Theme"));
1402 iconThemeSizeMenu->setTitle(tr("Set Icon Size"));
1403 burnMenu->setTitle(tr("Read/Write"));
1404 windowMenu->setTitle(tr("Window"));
1405 helpMenu->setTitle(tr("Help"));
1407 fileToolBar->setWindowTitle(tr("File"));
1408 editToolBar->setWindowTitle(tr("Edit"));
1409 burnToolBar->setWindowTitle(tr("Write"));
1410 helpToolBar->setWindowTitle(tr("Help"));
1412 showReadyStatus();
1414 if (showMsg)
1415 QMessageBox::information(this, CPN_STR_APP_NAME, tr("Some text will not be translated until the next time you start Companion. Please note that some translations may not be complete."));
1418 void MainWindow::createActions()
1420 newAct = addAct("new.png", SLOT(newFile()), QKeySequence::New);
1421 openAct = addAct("open.png", SLOT(openFile()), QKeySequence::Open);
1422 saveAct = addAct("save.png", SLOT(save()), QKeySequence::Save);
1423 saveAsAct = addAct("saveas.png", SLOT(saveAs()), tr("Ctrl+Shift+S")); // Windows doesn't have "native" save-as key, Lin/OSX both use this one anyway
1424 closeAct = addAct("clear.png", SLOT(closeFile()) /*, QKeySequence::Close*/); // setting/showing this shortcut interferes with the system one (Ctrl+W/Ctrl-F4)
1425 exitAct = addAct("exit.png", SLOT(closeAllWindows()), QKeySequence::Quit, qApp);
1427 logsAct = addAct("logs.png", SLOT(logFile()), tr("Ctrl+Alt+L"));
1428 appPrefsAct = addAct("apppreferences.png", SLOT(appPrefs()), QKeySequence::Preferences);
1429 fwPrefsAct = addAct("fwpreferences.png", SLOT(fwPrefs()), tr("Ctrl+Alt+D"));
1430 compareAct = addAct("compare.png", SLOT(compare()), tr("Ctrl+Alt+R"));
1431 sdsyncAct = addAct("sdsync.png", SLOT(sdsync()));
1433 editSplashAct = addAct("paintbrush.png", SLOT(customizeSplash()));
1434 burnListAct = addAct("list.png", SLOT(burnList()));
1435 burnFusesAct = addAct("fuses.png", SLOT(burnFuses()));
1436 readFlashAct = addAct("read_flash.png", SLOT(readFlash()));
1437 writeFlashAct = addAct("write_flash.png", SLOT(writeFlash()));
1438 writeEepromAct = addAct("write_eeprom.png", SLOT(writeEeprom()));
1439 readEepromAct = addAct("read_eeprom.png", SLOT(readEeprom()));
1440 burnConfigAct = addAct("configure.png", SLOT(burnConfig()));
1441 writeBUToRadioAct = addAct("write_eeprom_file.png", SLOT(writeBackup()));
1442 readBUToFileAct = addAct("read_eeprom_file.png", SLOT(readBackup()));
1444 createProfileAct = addAct("new.png", SLOT(createProfile()));
1445 copyProfileAct = addAct("copy.png", SLOT(copyProfile()));
1446 deleteProfileAct = addAct("clear.png", SLOT(deleteCurrentProfile()));
1448 actTabbedWindows = addAct("", SLOT(setTabbedWindows(bool)), 0, this, SIGNAL(triggered(bool)));
1449 actTileWindows = addAct("", SLOT(tileSubWindows()), 0, mdiArea);
1450 actCascadeWindows = addAct("", SLOT(cascadeSubWindows()), 0, mdiArea);
1451 actCloseAllWindows = addAct("", SLOT(closeAllSubWindows()), 0, mdiArea);
1453 checkForUpdatesAct = addAct("update.png", SLOT(doUpdates()));
1454 aboutAct = addAct("information.png", SLOT(about()));
1455 openDocURLAct = addAct("changelog.png", SLOT(openDocURL()));
1456 changelogAct = addAct("changelog.png", SLOT(changelog()));
1457 contributorsAct = addAct("contributors.png", SLOT(contributors()));
1459 // these two get assigned menus in createMenus()
1460 recentFilesAct = addAct("recentdocument.png");
1461 profilesMenuAct = addAct("profiles.png");
1463 exitAct->setMenuRole(QAction::QuitRole);
1464 aboutAct->setMenuRole(QAction::AboutRole);
1465 appPrefsAct->setMenuRole(QAction::PreferencesRole);
1466 contributorsAct->setMenuRole(QAction::ApplicationSpecificRole);
1467 openDocURLAct->setMenuRole(QAction::ApplicationSpecificRole);
1468 checkForUpdatesAct->setMenuRole(QAction::ApplicationSpecificRole);
1469 changelogAct->setMenuRole(QAction::ApplicationSpecificRole);
1471 actTabbedWindows->setCheckable(true);
1472 compareAct->setEnabled(false);
1475 void MainWindow::createMenus()
1477 fileMenu = menuBar()->addMenu("");
1478 fileMenu->addAction(newAct);
1479 fileMenu->addAction(openAct);
1480 fileMenu->addAction(saveAct);
1481 fileMenu->addAction(saveAsAct);
1482 fileMenu->addAction(closeAct);
1483 fileMenu->addAction(recentFilesAct);
1484 fileMenu->addSeparator();
1485 fileMenu->addAction(logsAct);
1486 fileMenu->addAction(fwPrefsAct);
1487 fileMenu->addAction(compareAct);
1488 fileMenu->addAction(sdsyncAct);
1489 fileMenu->addSeparator();
1490 fileMenu->addAction(exitAct);
1492 editMenu = menuBar()->addMenu("");
1494 settingsMenu = menuBar()->addMenu("");
1495 settingsMenu->addMenu(createLanguageMenu(settingsMenu));
1497 themeMenu = settingsMenu->addMenu("");
1498 QActionGroup * themeGroup = new QActionGroup(themeMenu);
1499 addActToGroup(themeGroup, tr("Classical"), tr("The classic companion9x icon theme"), "themeId", 0, g.theme());
1500 addActToGroup(themeGroup, tr("Yerico"), tr("Yellow round honey sweet icon theme"), "themeId", 1, g.theme());
1501 addActToGroup(themeGroup, tr("Monochrome"), tr("A monochrome black icon theme"), "themeId", 3, g.theme());
1502 addActToGroup(themeGroup, tr("MonoBlue"), tr("A monochrome blue icon theme"), "themeId", 4, g.theme());
1503 addActToGroup(themeGroup, tr("MonoWhite"), tr("A monochrome white icon theme"), "themeId", 2, g.theme());
1504 connect(themeGroup, &QActionGroup::triggered, this, &MainWindow::onThemeChanged);
1505 themeMenu->addActions(themeGroup->actions());
1507 iconThemeSizeMenu = settingsMenu->addMenu("");
1508 QActionGroup * szGroup = new QActionGroup(iconThemeSizeMenu);
1509 addActToGroup(szGroup, tr("Small"), tr("Use small toolbar icons"), "sizeId", 0, g.iconSize());
1510 addActToGroup(szGroup, tr("Normal"), tr("Use normal size toolbar icons"), "sizeId", 1, g.iconSize());
1511 addActToGroup(szGroup, tr("Big"), tr("Use big toolbar icons"), "sizeId", 2, g.iconSize());
1512 addActToGroup(szGroup, tr("Huge"), tr("Use huge toolbar icons"), "sizeId", 3, g.iconSize());
1513 connect(szGroup, &QActionGroup::triggered, this, &MainWindow::onIconSizeChanged);
1514 iconThemeSizeMenu->addActions(szGroup->actions());
1516 settingsMenu->addSeparator();
1517 settingsMenu->addAction(appPrefsAct);
1518 settingsMenu->addAction(profilesMenuAct);
1519 settingsMenu->addAction(editSplashAct);
1520 settingsMenu->addAction(burnConfigAct);
1522 burnMenu = menuBar()->addMenu("");
1523 burnMenu->addAction(writeEepromAct);
1524 burnMenu->addAction(readEepromAct);
1525 burnMenu->addSeparator();
1526 burnMenu->addAction(writeBUToRadioAct);
1527 burnMenu->addAction(readBUToFileAct);
1528 burnMenu->addSeparator();
1529 burnMenu->addAction(writeFlashAct);
1530 burnMenu->addAction(readFlashAct);
1531 burnMenu->addSeparator();
1532 burnMenu->addSeparator();
1533 if (!IS_ARM(getCurrentBoard())) {
1534 burnMenu->addAction(burnFusesAct);
1535 burnMenu->addAction(burnListAct);
1538 windowMenu = menuBar()->addMenu("");
1539 windowMenu->addAction(actTabbedWindows);
1540 windowMenu->addAction(actTileWindows);
1541 windowMenu->addAction(actCascadeWindows);
1542 windowMenu->addAction(actCloseAllWindows);
1543 windowMenu->addSeparator();
1545 helpMenu = menuBar()->addMenu("");
1546 helpMenu->addSeparator();
1547 helpMenu->addAction(checkForUpdatesAct);
1548 helpMenu->addSeparator();
1549 helpMenu->addAction(aboutAct);
1550 helpMenu->addAction(openDocURLAct);
1551 helpMenu->addSeparator();
1552 helpMenu->addAction(changelogAct);
1553 helpMenu->addSeparator();
1554 helpMenu->addAction(contributorsAct);
1556 recentFilesMenu = new QMenu(this);
1557 recentFilesMenu->setToolTipsVisible(true);
1558 for ( int i = 0; i < g.historySize(); ++i) {
1559 recentFileActs.append(recentFilesMenu->addAction(""));
1560 recentFileActs[i]->setVisible(false);
1561 connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile()));
1563 recentFilesAct->setMenu(recentFilesMenu);
1565 profilesMenu = new QMenu(this);
1566 QActionGroup *profilesGroup = new QActionGroup(this);
1567 for (int i=0; i < MAX_PROFILES; i++) {
1568 profileActs.append(profilesMenu->addAction(""));
1569 profileActs[i]->setVisible(false);
1570 profileActs[i]->setCheckable(true);
1571 connect(profileActs[i], SIGNAL(triggered()), this, SLOT(loadProfile()));
1572 profilesGroup->addAction(profileActs[i]);
1574 profilesMenu->addSeparator();
1575 profilesMenu->addAction(createProfileAct);
1576 profilesMenu->addAction(copyProfileAct);
1577 profilesMenu->addAction(deleteProfileAct);
1578 profilesMenuAct->setMenu(profilesMenu);
1581 void MainWindow::createToolBars()
1583 fileToolBar = addToolBar("");
1584 fileToolBar->setObjectName("File");
1585 fileToolBar->addAction(newAct);
1586 fileToolBar->addAction(openAct);
1587 fileToolBar->addAction(recentFilesAct);
1588 fileToolBar->addAction(saveAct);
1589 fileToolBar->addAction(closeAct);
1590 fileToolBar->addSeparator();
1591 fileToolBar->addAction(logsAct);
1592 fileToolBar->addAction(fwPrefsAct);
1593 fileToolBar->addSeparator();
1594 fileToolBar->addAction(appPrefsAct);
1595 fileToolBar->addAction(profilesMenuAct);
1596 fileToolBar->addAction(editSplashAct);
1597 fileToolBar->addAction(editSplashAct);
1598 fileToolBar->addSeparator();
1599 fileToolBar->addAction(compareAct);
1600 fileToolBar->addAction(sdsyncAct);
1602 // workaround for default split button appearance of action with menu :-/
1603 QToolButton * btn;
1604 if ((btn = qobject_cast<QToolButton *>(fileToolBar->widgetForAction(recentFilesAct))))
1605 btn->setPopupMode(QToolButton::InstantPopup);
1606 if ((btn = qobject_cast<QToolButton *>(fileToolBar->widgetForAction(profilesMenuAct))))
1607 btn->setPopupMode(QToolButton::InstantPopup);
1609 // gets populated later
1610 editToolBar = addToolBar("");
1611 editToolBar->setObjectName("Edit");
1613 burnToolBar = new QToolBar(this);
1614 addToolBar( Qt::LeftToolBarArea, burnToolBar );
1615 burnToolBar->setObjectName("Write");
1616 burnToolBar->addAction(writeEepromAct);
1617 burnToolBar->addAction(readEepromAct);
1618 burnToolBar->addSeparator();
1619 burnToolBar->addAction(writeBUToRadioAct);
1620 burnToolBar->addAction(readBUToFileAct);
1621 burnToolBar->addSeparator();
1622 burnToolBar->addAction(writeFlashAct);
1623 burnToolBar->addAction(readFlashAct);
1624 burnToolBar->addSeparator();
1625 burnToolBar->addAction(burnConfigAct);
1627 helpToolBar = addToolBar("");
1628 helpToolBar->setObjectName("Help");
1629 helpToolBar->addAction(checkForUpdatesAct);
1630 helpToolBar->addAction(aboutAct);
1633 QMenu * MainWindow::createLanguageMenu(QWidget * parent)
1635 QMenu * menu = new QMenu(tr("Set Menu Language"), parent);
1636 QActionGroup * actGroup = new QActionGroup(menu);
1637 QString lName;
1639 addActToGroup(actGroup, tr("System language"), tr("Use default system language."), "locale", QString(""), g.locale());
1640 foreach (const QString & lang, Translations::getAvailableTranslations()) {
1641 QLocale locale(lang);
1642 lName = locale.nativeLanguageName();
1643 addActToGroup(actGroup, lName.left(1).toUpper() % lName.mid(1), tr("Use %1 language (some translations may not be complete).").arg(lName), "locale", lang, g.locale());
1645 if (!actGroup->checkedAction())
1646 actGroup->actions().first()->setChecked(true);
1648 connect(actGroup, &QActionGroup::triggered, this, &MainWindow::onLanguageChanged);
1649 menu->addActions(actGroup->actions());
1650 return menu;
1653 void MainWindow::showReadyStatus()
1655 statusBar()->showMessage(tr("Ready"));
1658 MdiChild *MainWindow::activeMdiChild()
1660 if (QMdiSubWindow *activeSubWindow = mdiArea->activeSubWindow())
1661 return qobject_cast<MdiChild *>(activeSubWindow->widget());
1662 return 0;
1665 QMdiSubWindow *MainWindow::findMdiChild(const QString &fileName)
1667 QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
1669 foreach (QMdiSubWindow *window, mdiArea->subWindowList()) {
1670 MdiChild *mdiChild = qobject_cast<MdiChild *>(window->widget());
1671 if (mdiChild->currentFile() == canonicalFilePath)
1672 return window;
1674 return 0;
1677 bool MainWindow::anyChildrenDirty()
1679 foreach (QMdiSubWindow * window, mdiArea->subWindowList()) {
1680 MdiChild * child;
1681 if ((child = qobject_cast<MdiChild *>(window->widget())) && child->isWindowModified())
1682 return true;
1684 return false;
1687 void MainWindow::updateRecentFileActions()
1689 QStringList files = g.recentFiles();
1690 for (int i=0; i < recentFileActs.size(); i++) {
1691 if (i < files.size() && !files.at(i).isEmpty()) {
1692 recentFileActs[i]->setText(QFileInfo(files.at(i)).fileName());
1693 recentFileActs[i]->setData(files.at(i));
1694 recentFileActs[i]->setStatusTip(QDir::toNativeSeparators(files.at(i)));
1695 recentFileActs[i]->setToolTip(recentFileActs[i]->statusTip());
1696 recentFileActs[i]->setVisible(true);
1698 else {
1699 recentFileActs[i]->setVisible(false);
1704 void MainWindow::updateProfilesActions()
1706 for (int i=0; i < qMin(profileActs.size(), MAX_PROFILES); i++) {
1707 if (g.profile[i].existsOnDisk()) {
1708 QString text = tr("%2").arg(g.profile[i].name());
1709 profileActs[i]->setText(text);
1710 profileActs[i]->setData(i);
1711 profileActs[i]->setVisible(true);
1712 if (i == g.id())
1713 profileActs[i]->setChecked(true);
1715 else {
1716 profileActs[i]->setVisible(false);
1721 void MainWindow::updateWindowActions()
1723 if (!windowsListActions)
1724 return;
1726 foreach (QAction * act, windowsListActions->actions()) {
1727 windowsListActions->removeAction(act);
1728 if (windowMenu->actions().contains(act))
1729 windowMenu->removeAction(act);
1730 delete act;
1732 int count = 0;
1733 foreach (QMdiSubWindow * win, mdiArea->subWindowList()) {
1734 QString scut;
1735 if (++count < 10)
1736 scut = tr("Alt+%1").arg(count);
1737 QAction * act = addActToGroup(windowsListActions, "", "", "window_ptr", qVariantFromValue(win), QVariant(), scut);
1738 act->setChecked(win == mdiArea->activeSubWindow());
1739 updateWindowActionTitle(win, act);
1741 windowMenu->addActions(windowsListActions->actions());
1744 void MainWindow::updateWindowActionTitle(const QMdiSubWindow * win, QAction * act)
1746 MdiChild * child = qobject_cast<MdiChild *>(win->widget());
1747 if (!child)
1748 return;
1750 if (!act) {
1751 foreach (QAction * a, windowsListActions->actions()) {
1752 if (a->property("window_ptr").canConvert<QMdiSubWindow *>() &&
1753 a->property("window_ptr").value<QMdiSubWindow *>() == win) {
1754 act = a;
1755 break;
1759 if (!act)
1760 return;
1762 QString ttl = child->userFriendlyCurrentFile();
1763 if (child->isWindowModified())
1764 ttl.prepend("* ");
1765 act->setText(ttl);
1768 void MainWindow::onSubwindowTitleChanged()
1770 QMdiSubWindow * win = NULL;
1771 if ((win = qobject_cast<QMdiSubWindow *>(sender()->parent())))
1772 updateWindowActionTitle(win);
1775 void MainWindow::onSubwindowModified()
1777 onSubwindowTitleChanged();
1780 void MainWindow::onChangeWindowAction(QAction * act)
1782 if (!act->isChecked())
1783 return;
1785 QMdiSubWindow * win = NULL;
1786 if (act->property("window_ptr").canConvert<QMdiSubWindow *>())
1787 win = act->property("window_ptr").value<QMdiSubWindow *>();
1788 if (win)
1789 mdiArea->setActiveSubWindow(win);
1792 int MainWindow::newProfile(bool loadProfile)
1794 int i;
1795 for (i=0; i < MAX_PROFILES && g.profile[i].existsOnDisk(); i++)
1797 if (i == MAX_PROFILES) //Failed to find free slot
1798 return -1;
1800 g.profile[i].init(i);
1801 g.profile[i].name(tr("New Radio"));
1803 if (loadProfile) {
1804 if (loadProfileId(i))
1805 appPrefs();
1808 return i;
1811 void MainWindow::createProfile()
1813 newProfile(true);
1816 void MainWindow::copyProfile()
1818 int newId = newProfile(false);
1820 if (newId > -1) {
1821 g.profile[newId] = g.profile[g.id()];
1822 g.profile[newId].name(g.profile[newId].name() + tr(" - Copy"));
1823 if (loadProfileId(newId))
1824 appPrefs();
1828 void MainWindow::deleteProfile(const int pid)
1830 if (pid == g.id() && anyChildrenDirty()) {
1831 QMessageBox::warning(this, tr("Companion :: Open files warning"), tr("Please save or close modified file(s) before deleting the active profile."));
1832 return;
1834 if (pid == 0) {
1835 QMessageBox::warning(this, tr("Not possible to remove profile"), tr("The default profile can not be removed."));
1836 return;
1838 int ret = QMessageBox::question(this,
1839 tr("Confirm Delete Profile"),
1840 tr("Are you sure you wish to delete the \"%1\" radio profile? There is no way to undo this action!").arg(g.profile[pid].name()));
1841 if (ret != QMessageBox::Yes)
1842 return;
1844 g.profile[pid].remove();
1845 loadProfileId(0);
1848 void MainWindow::deleteCurrentProfile()
1850 deleteProfile(g.id());
1853 QString MainWindow::strippedName(const QString &fullFileName)
1855 return QFileInfo(fullFileName).fileName();
1858 void MainWindow::dragEnterEvent(QDragEnterEvent *event)
1860 if (event->mimeData()->hasFormat("text/uri-list"))
1861 event->acceptProposedAction();
1864 void MainWindow::dropEvent(QDropEvent *event)
1866 QList<QUrl> urls = event->mimeData()->urls();
1867 if (urls.isEmpty()) return;
1868 QString fileName = urls.first().toLocalFile();
1869 openFile(fileName);
1872 void MainWindow::autoClose()
1874 this->close();