Make TX volatge for simu more flexible (#7124)
[opentx.git] / companion / src / mainwindow.cpp
blobe795594cb5939596d642b73ff7a84b7f72070f97
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 "dialogs/filesyncdialog.h"
53 #include <QtGui>
54 #include <QFileInfo>
55 #include <QDesktopServices>
56 #include <QMessageBox>
57 #include <QNetworkAccessManager>
58 #include <QNetworkProxyFactory>
59 #include <QNetworkReply>
60 #include <QNetworkRequest>
62 // update check flags
63 #define CHECK_COMPANION 1
64 #define CHECK_FIRMWARE 2
65 #define INTERACTIVE_DOWNLOAD 4
66 #define AUTOMATIC_DOWNLOAD 8
68 #define OPENTX_DOWNLOADS_PAGE_URL QStringLiteral("http://www.open-tx.org/downloads")
69 #define DONATE_STR QStringLiteral("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QUZ48K4SEXDP2")
71 #ifdef Q_OS_MACOS
72 #define COMPANION_STAMP QStringLiteral("companion-macosx.stamp")
73 #define COMPANION_INSTALLER QStringLiteral("macosx/opentx-companion-%1.dmg")
74 #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Diskimage (*.dmg)")
75 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to open the disk image to install the new version?")
76 #elif defined(Q_OS_WIN)
77 #define COMPANION_STAMP QStringLiteral("companion-windows.stamp")
78 #define COMPANION_INSTALLER QStringLiteral("windows/companion-windows-%1.exe")
79 #define COMPANION_FILEMASK QT_TRANSLATE_NOOP("MainWindow", "Executable (*.exe)")
80 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?")
81 #else
82 #define COMPANION_STAMP QStringLiteral("companion-linux.stamp")
83 #define COMPANION_INSTALLER "" // no automated updates for linux
84 #define COMPANION_FILEMASK QStringLiteral("*.*")
85 #define COMPANION_INSTALL_QUESTION QT_TRANSLATE_NOOP("MainWindow", "Would you like to launch the installer?")
86 #endif
88 MainWindow::MainWindow():
89 downloadDialog_forWait(nullptr),
90 checkForUpdatesState(0),
91 networkManager(nullptr),
92 windowsListActions(new QActionGroup(this))
94 // setUnifiedTitleAndToolBarOnMac(true);
95 this->setWindowIcon(QIcon(":/icon.png"));
96 QNetworkProxyFactory::setUseSystemConfiguration(true);
97 setAcceptDrops(true);
99 mdiArea = new QMdiArea(this);
100 mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
101 mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
102 mdiArea->setTabsClosable(true);
103 mdiArea->setTabsMovable(true);
104 mdiArea->setDocumentMode(true);
105 connect(mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::updateMenus);
107 setCentralWidget(mdiArea);
109 createActions();
110 createMenus();
111 createToolBars();
112 retranslateUi();
114 initWindowOptions();
116 connect(windowsListActions, &QActionGroup::triggered, this, &MainWindow::onChangeWindowAction);
117 connect(&g, &AppData::currentProfileChanged, this, &MainWindow::onCurrentProfileChanged);
119 // give time to the splash to disappear and main window to open before starting updates
120 int updateDelay = 1000;
121 bool showSplash = g.showSplash();
122 if (showSplash) {
123 updateDelay += (SPLASH_TIME*1000);
126 if (g.isFirstUse()) {
127 g.warningId(g.warningId() | AppMessages::MSG_WELCOME);
128 QTimer::singleShot(updateDelay-500, this, SLOT(appPrefs())); // must be shown before warnings dialog but after splash
130 else {
131 if (!g.previousVersion().isEmpty())
132 g.warningId(g.warningId() | AppMessages::MSG_UPGRADED);
133 if (checkProfileRadioExists(g.sessionId()))
134 QTimer::singleShot(updateDelay, this, SLOT(doAutoUpdates()));
135 else
136 g.warningId(g.warningId() | AppMessages::MSG_NO_RADIO_TYPE);
138 QTimer::singleShot(updateDelay, this, SLOT(displayWarnings()));
140 QStringList strl = QApplication::arguments();
141 QString str;
142 QString printfilename;
143 int printing = strl.contains("--print");
144 int model = -1;
145 int count = 0;
146 foreach(QString arg, strl) {
147 count++;
148 if (arg.contains("--model")) {
149 model = strl[count].toInt() - 1;
151 if (arg.contains("--filename")) {
152 printfilename = strl[count];
155 if (strl.count() > 1)
156 str = strl[1];
157 if (!str.isEmpty()) {
158 int fileType = getStorageType(str);
160 if (fileType==STORAGE_TYPE_EEPE || fileType==STORAGE_TYPE_EEPM || fileType==STORAGE_TYPE_BIN || fileType==STORAGE_TYPE_OTX) {
161 MdiChild * child = createMdiChild();
162 if (child->loadFile(str)) {
163 if (!(printing && model >= 0 && (getCurrentFirmware()->getCapability(Models) == 0 || model<getCurrentFirmware()->getCapability(Models)) && !printfilename.isEmpty())) {
164 statusBar()->showMessage(tr("File loaded"), 2000);
165 child->show();
167 else {
168 child->show();
169 child->print(model, printfilename);
170 child->close();
173 else {
174 child->closeFile(true);
178 if (printing) {
179 QTimer::singleShot(0, this, SLOT(autoClose()));
183 MainWindow::~MainWindow()
185 if (windowsListActions) {
186 delete windowsListActions;
187 windowsListActions = nullptr;
191 void MainWindow::initWindowOptions()
193 updateMenus();
194 setIconThemeSize(g.iconSize());
195 restoreGeometry(g.mainWinGeo());
196 restoreState(g.mainWinState());
197 setTabbedWindows(g.tabbedMdi());
200 void MainWindow::displayWarnings()
202 static int shownMsgs = 0;
203 const int showMsgs = g.warningId();
204 int msgId = 0;
205 for (int i = 1; i < AppMessages::MSG_ENUM_END; i <<= 1) {
206 if ((showMsgs & i) && !(shownMsgs & i)) {
207 msgId = i;
208 break;
211 if (!msgId)
212 return;
213 AppMessages::displayMessage(msgId, this);
214 shownMsgs |= msgId;
215 if (shownMsgs != showMsgs)
216 displayWarnings(); // in case more warnings need showing
219 void MainWindow::doAutoUpdates()
221 if (g.autoCheckApp())
222 checkForUpdatesState |= CHECK_COMPANION;
223 if (g.autoCheckFw())
224 checkForUpdatesState |= CHECK_FIRMWARE;
225 checkForUpdates();
228 void MainWindow::doUpdates()
230 checkForUpdatesState = CHECK_COMPANION | CHECK_FIRMWARE | INTERACTIVE_DOWNLOAD;
231 checkForUpdates();
234 void MainWindow::checkForFirmwareUpdate()
236 checkForUpdatesState = CHECK_FIRMWARE | INTERACTIVE_DOWNLOAD;
237 checkForUpdates();
240 void MainWindow::dowloadLastFirmwareUpdate()
242 checkForUpdatesState = CHECK_FIRMWARE | AUTOMATIC_DOWNLOAD | INTERACTIVE_DOWNLOAD;
243 checkForUpdates();
246 QString MainWindow::getCompanionUpdateBaseUrl() const
248 return g.openTxCurrentDownloadBranchUrl() % QStringLiteral("companion/");
251 void MainWindow::checkForUpdates()
253 if (!(checkForUpdatesState & (CHECK_COMPANION | CHECK_FIRMWARE))) {
254 if (networkManager) {
255 networkManager->deleteLater();
256 networkManager = nullptr;
258 checkForUpdatesState = 0;
259 return;
262 if (networkManager)
263 disconnect(networkManager, 0, this, 0);
264 else
265 networkManager = new QNetworkAccessManager(this);
267 QUrl url;
268 if (checkForUpdatesState & CHECK_COMPANION) {
269 checkForUpdatesState -= CHECK_COMPANION;
270 if (checkForUpdatesState & INTERACTIVE_DOWNLOAD)
271 openUpdatesWaitDialog();
272 url.setUrl(getCompanionUpdateBaseUrl() % COMPANION_STAMP);
273 connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForCompanionUpdateFinished);
274 qDebug() << "Checking for Companion update " << url.url();
276 else if (checkForUpdatesState & CHECK_FIRMWARE) {
277 checkForUpdatesState -= CHECK_FIRMWARE;
278 const QString stamp = getCurrentFirmware()->getStampUrl();
279 if (!stamp.isEmpty()) {
280 if (checkForUpdatesState & INTERACTIVE_DOWNLOAD)
281 openUpdatesWaitDialog();
282 url.setUrl(stamp);
283 connect(networkManager, &QNetworkAccessManager::finished, this, &MainWindow::checkForFirmwareUpdateFinished);
284 qDebug() << "Checking for firmware update " << url.url();
287 if (!url.isValid()) {
288 checkForUpdates();
289 return;
292 QNetworkRequest request(url);
293 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
294 QNetworkReply * repl = networkManager->get(request);
295 if (downloadDialog_forWait)
296 connect(downloadDialog_forWait, &DownloadDialog::rejected, repl, &QNetworkReply::abort);
299 void MainWindow::onUpdatesError(const QString &err)
301 QMessageBox::warning(this, CPN_STR_APP_NAME, err);
302 checkForUpdates();
305 void MainWindow::openUpdatesWaitDialog()
307 if (!downloadDialog_forWait) {
308 downloadDialog_forWait = new DownloadDialog(nullptr, tr("Checking for updates"));
309 downloadDialog_forWait->show();
313 void MainWindow::closeUpdatesWaitDialog()
315 if (downloadDialog_forWait) {
316 downloadDialog_forWait->close();
317 delete downloadDialog_forWait;
318 downloadDialog_forWait = nullptr;
322 QString MainWindow::seekCodeString(const QByteArray & qba, const QString & label) const
324 int posLabel = qba.indexOf(label);
325 if (posLabel < 0)
326 return QString();
327 int start = qba.indexOf("\"", posLabel + label.length());
328 if (start < 0)
329 return QString();
330 int end = qba.indexOf("\"", start + 1);
331 if (end < 0)
332 return QString();
333 return qba.mid(start + 1, end - start - 1);
336 void MainWindow::checkForCompanionUpdateFinished(QNetworkReply * reply)
338 QByteArray qba = reply->readAll();
339 reply->deleteLater();
340 closeUpdatesWaitDialog();
342 QString version = seekCodeString(qba, "VERSION");
343 const QString errorString = seekCodeString(qba, "ERROR");
345 if (errorString == "NO_RC")
346 return onUpdatesError(tr("No Companion release candidates are currently being served for this version, please switch release channel"));
347 else if (errorString == "NO_NIGHTLY")
348 return onUpdatesError(tr("No nightly Companion builds are currently being served for this version, please switch release channel"));
349 else if (errorString == "NO_RELEASE")
350 return onUpdatesError(tr("No Companion release builds are currently being served for this version, please switch release channel"));
352 if (version.isNull())
353 return onUpdatesError(tr("Companion update check failed, new version information not found."));
355 int webVersion = version2index(version);
357 int ownVersion = version2index(VERSION);
359 if (ownVersion < webVersion) {
360 #if defined WIN32 || defined __APPLE__
361 int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr("A new version of Companion is available (version %1)<br>"
362 "Would you like to download it?").arg(version) ,
363 QMessageBox::Yes | QMessageBox::No);
365 if (ret == QMessageBox::Yes) {
366 QDir dir(g.updatesDir());
367 QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), dir.absoluteFilePath(QString(COMPANION_INSTALLER).arg(version)), tr(COMPANION_FILEMASK));
369 if (!fileName.isEmpty()) {
370 g.updatesDir(QFileInfo(fileName).dir().absolutePath());
371 DownloadDialog * dd = new DownloadDialog(this, getCompanionUpdateBaseUrl() % QString(COMPANION_INSTALLER).arg(version), fileName);
372 installer_fileName = fileName;
373 connect(dd, SIGNAL(accepted()), this, SLOT(updateDownloaded()));
374 dd->exec();
377 #else
378 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));
379 #endif
381 else {
382 if (checkForUpdatesState == INTERACTIVE_DOWNLOAD) {
383 QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time."));
387 checkForUpdates();
390 void MainWindow::updateDownloaded()
392 int ret = QMessageBox::question(this, CPN_STR_APP_NAME, tr(COMPANION_INSTALL_QUESTION), QMessageBox::Yes | QMessageBox::No);
393 if (ret == QMessageBox::Yes) {
394 if (QDesktopServices::openUrl(QUrl::fromLocalFile(installer_fileName)))
395 QApplication::exit();
399 void MainWindow::firmwareDownloadAccepted()
401 QString errormsg;
402 QFile file(g.profile[g.id()].fwName());
403 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { //reading HEX TEXT file
404 QMessageBox::critical(this, CPN_STR_TTL_ERROR,
405 tr("Error opening file %1:\n%2.")
406 .arg(g.profile[g.id()].fwName())
407 .arg(file.errorString()));
408 return;
410 file.reset();
411 QTextStream inputStream(&file);
412 QString hline = inputStream.readLine();
413 if (hline.startsWith("ERROR:")) {
414 int errnum = hline.mid(6).toInt();
415 switch(errnum) {
416 case 1:
417 errormsg = tr("Not enough flash available on this board for all the selected options");
418 break;
419 case 2:
420 errormsg = tr("Compilation server temporary failure, try later");
421 break;
422 case 3:
423 errormsg = tr("Compilation server too busy, try later");
424 break;
425 case 4:
426 errormsg = tr("Compilation error");
427 break;
428 case 5:
429 errormsg = tr("Invalid firmware");
430 break;
431 case 6:
432 errormsg = tr("Invalid board");
433 break;
434 case 7:
435 errormsg = tr("Invalid language");
436 break;
437 default:
438 errormsg = tr("Unknown server failure, try later");
439 break;
441 file.close();
442 file.remove();
443 QMessageBox::critical(this, CPN_STR_TTL_ERROR, errormsg);
444 return;
446 file.close();
447 g.fwRev.set(Firmware::getCurrentVariant()->getId(), version2index(firmwareVersionString));
448 if (g.profile[g.id()].burnFirmware()) {
449 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);
450 if (ret == QMessageBox::Yes) {
451 writeFlash(g.profile[g.id()].fwName());
454 emit firmwareDownloadCompleted();
457 void MainWindow::checkForFirmwareUpdateFinished(QNetworkReply * reply)
459 bool download = false;
460 bool ignore = false;
462 const QByteArray qba = reply->readAll();
463 reply->deleteLater();
464 closeUpdatesWaitDialog();
466 const QString versionString = seekCodeString(qba, "VERSION");
467 const QString dateString = seekCodeString(qba, "DATE");
468 const QString errorString = seekCodeString(qba, "ERROR");
469 const QString blockedRadios = seekCodeString(qba, "BLOCK");
470 long version;
472 if (errorString == "NO_RC")
473 return onUpdatesError(tr("No firmware release candidates are currently being served for this version, please switch release channel"));
474 else if (errorString == "NO_NIGHTLY")
475 return onUpdatesError(tr("No firmware nightly builds are currently being served for this version, please switch release channel"));
476 else if (errorString == "NO_RELEASE")
477 return onUpdatesError(tr("No firmware release builds are currently being served for this version, please switch release channel"));
478 else if (errorString == "MOVE_TO_RC") {
479 QMessageBox msgbox;
480 msgbox.setIcon(QMessageBox::Question);
481 msgbox.setText(tr("Release candidate builds are now available for this version, would you like to switch to using them?"));
482 msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
483 msgbox.setDefaultButton(QMessageBox::Yes);
485 if(msgbox.exec() == QMessageBox::Yes) {
486 g.OpenTxBranch(AppData::DownloadBranchType(AppData::BRANCH_RC_TESTING));
487 return onUpdatesError(tr("Channel changed to RC, please restart the download process"));
490 else if (errorString == "MOVE_TO_RELEASE") {
491 QMessageBox msgbox;
492 msgbox.setIcon(QMessageBox::Question);
493 msgbox.setText(tr("Official release builds are now available for this version, would you like to switch to using them?"));
494 msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
495 msgbox.setDefaultButton(QMessageBox::Yes);
497 if(msgbox.exec() == QMessageBox::Yes) {
498 g.OpenTxBranch(AppData::DownloadBranchType(AppData::BRANCH_RELEASE_STABLE));
499 return onUpdatesError(tr("Channel changed to Release, please restart the download process"));
503 QString variant = Firmware::getCurrentVariant()->getId();
504 QStringList splitId = variant.split("-");
505 if (blockedRadios.contains(splitId.value(1)))
506 return onUpdatesError(tr("This radio (%1) is not currently available in this firmware release channel").arg(getCurrentFirmware()->getName()));
508 if (versionString.isNull() || dateString.isNull() || (version = version2index(versionString)) <= 0)
509 return onUpdatesError(tr("Firmware update check failed, new version information not found or invalid."));
511 QString fullVersionString = QString("%1 (%2)").arg(versionString).arg(dateString);
513 if (checkForUpdatesState & AUTOMATIC_DOWNLOAD) {
514 checkForUpdatesState -= AUTOMATIC_DOWNLOAD;
515 download = true;
517 else {
518 int currentVersion = g.fwRev.get(Firmware::getCurrentVariant()->getId());
519 QString currentVersionString = index2version(currentVersion);
521 QMessageBox msgBox;
522 msgBox.setWindowTitle(CPN_STR_APP_NAME);
523 QSpacerItem * horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
524 QGridLayout * layout = (QGridLayout*)msgBox.layout();
525 layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
527 if (currentVersion == 0) {
528 QString rn = getCurrentFirmware()->getReleaseNotesUrl();
529 QAbstractButton *rnButton = nullptr;
530 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.")
531 .arg(Firmware::getCurrentVariant()->getId()).arg(fullVersionString));
532 QAbstractButton *YesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
533 msgBox.addButton(tr("No"), QMessageBox::NoRole);
534 if (!rn.isEmpty()) {
535 rnButton = msgBox.addButton(tr("Release Notes"), QMessageBox::ActionRole);
537 msgBox.setIcon(QMessageBox::Question);
538 msgBox.resize(0, 0);
539 msgBox.exec();
540 if (msgBox.clickedButton() == rnButton) {
541 ReleaseNotesFirmwareDialog * dialog = new ReleaseNotesFirmwareDialog(this, rn);
542 dialog->exec();
543 int ret2 = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to download version %1 now ?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No);
544 if (ret2 == QMessageBox::Yes)
545 download = true;
546 else
547 ignore = true;
549 else if (msgBox.clickedButton() == YesButton ) {
550 download = true;
552 else {
553 ignore = true;
556 else if (version > currentVersion) {
557 QString rn = getCurrentFirmware()->getReleaseNotesUrl();
558 QAbstractButton *rnButton = nullptr;
559 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.")
560 .arg(Firmware::getCurrentVariant()->getId()).arg(currentVersionString).arg(fullVersionString));
561 QAbstractButton *YesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
562 msgBox.addButton(tr("No"), QMessageBox::NoRole);
563 if (!rn.isEmpty()) {
564 rnButton = msgBox.addButton(tr("Release Notes"), QMessageBox::ActionRole);
566 msgBox.setIcon(QMessageBox::Question);
567 msgBox.resize(0,0);
568 msgBox.exec();
569 if( msgBox.clickedButton() == rnButton ) {
570 ReleaseNotesFirmwareDialog * dialog = new ReleaseNotesFirmwareDialog(this, rn);
571 dialog->exec();
572 int ret2 = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Do you want to download version %1 now ?").arg(fullVersionString),
573 QMessageBox::Yes | QMessageBox::No);
574 if (ret2 == QMessageBox::Yes) {
575 download = true;
577 else {
578 ignore = true;
581 else if (msgBox.clickedButton() == YesButton ) {
582 download = true;
584 else {
585 ignore = true;
588 else {
589 if (checkForUpdatesState == INTERACTIVE_DOWNLOAD) {
590 QMessageBox::information(this, CPN_STR_APP_NAME, tr("No updates available at this time."));
595 if (ignore) {
596 int res = QMessageBox::question(this, CPN_STR_APP_NAME, tr("Ignore this version %1?").arg(fullVersionString), QMessageBox::Yes | QMessageBox::No);
597 if (res==QMessageBox::Yes) {
598 g.fwRev.set(Firmware::getCurrentVariant()->getId(), version);
601 else if (download == true) {
602 firmwareVersionString = versionString;
603 startFirmwareDownload();
606 checkForUpdates();
609 void MainWindow::startFirmwareDownload()
611 QString url = Firmware::getCurrentVariant()->getFirmwareUrl();
612 qDebug() << "Downloading firmware" << url;
613 QString ext = url.mid(url.lastIndexOf("."));
614 QString defaultFilename = g.flashDir() + "/" + Firmware::getCurrentVariant()->getId();
615 if (g.profile[g.id()].renameFwFiles()) {
616 defaultFilename += "-" + firmwareVersionString;
618 defaultFilename += ext;
620 QString filename = QFileDialog::getSaveFileName(this, tr("Save As"), defaultFilename);
621 if (!filename.isEmpty()) {
622 g.profile[g.id()].fwName(filename);
623 g.flashDir(QFileInfo(filename).dir().absolutePath());
624 DownloadDialog * dd = new DownloadDialog(this, url, filename);
625 connect(dd, SIGNAL(accepted()), this, SLOT(firmwareDownloadAccepted()));
626 dd->exec();
630 void MainWindow::closeEvent(QCloseEvent *event)
632 g.mainWinGeo(saveGeometry());
633 g.mainWinState(saveState());
634 g.tabbedMdi(actTabbedWindows->isChecked());
635 QApplication::closeAllWindows();
636 mdiArea->closeAllSubWindows();
637 if (mdiArea->currentSubWindow()) {
638 event->ignore();
640 else {
641 event->accept();
645 void MainWindow::changeEvent(QEvent * e)
647 QMainWindow::changeEvent(e);
648 switch (e->type()) {
649 case QEvent::LanguageChange:
650 retranslateUi(true);
651 break;
652 default:
653 break;
657 void MainWindow::setLanguage(const QString & langString)
659 g.locale(langString);
660 Translations::installTranslators();
663 void MainWindow::onLanguageChanged(QAction * act)
665 QString lang = act->property("locale").toString();
666 if (!lang.isNull())
667 setLanguage(lang);
670 void MainWindow::setTheme(int index)
672 g.theme(index);
673 QMessageBox::information(this, CPN_STR_APP_NAME, tr("The new theme will be loaded the next time you start Companion."));
676 void MainWindow::onThemeChanged(QAction * act)
678 bool ok;
679 int id = act->property("themeId").toInt(&ok);
680 if (ok && id >= 0 && id < 5)
681 setTheme(id);
684 void MainWindow::setIconThemeSize(int index)
686 if (index != g.iconSize())
687 g.iconSize(index);
689 QSize size;
690 switch(g.iconSize()) {
691 case 0:
692 size=QSize(16,16);
693 break;
694 case 2:
695 size=QSize(32,32);
696 break;
697 case 3:
698 size=QSize(48,48);
699 break;
700 case 1:
701 default:
702 size=QSize(24,24);
703 break;
705 this->setIconSize(size);
708 void MainWindow::onIconSizeChanged(QAction * act)
710 bool ok;
711 int id = act->property("sizeId").toInt(&ok);
712 if (ok && id >= 0 && id < 4)
713 setIconThemeSize(id);
716 void MainWindow::setTabbedWindows(bool on)
718 mdiArea->setViewMode(on ? QMdiArea::TabbedView : QMdiArea::SubWindowView);
719 if (actTileWindows)
720 actTileWindows->setDisabled(on);
721 if (actCascadeWindows)
722 actCascadeWindows->setDisabled(on);
724 if (actTabbedWindows->isChecked() != on)
725 actTabbedWindows->setChecked(on);
728 void MainWindow::newFile()
730 MdiChild * child = createMdiChild();
731 child->newFile();
732 child->show();
735 void MainWindow::openDocURL()
737 QString link = "http://www.open-tx.org/documents.html";
738 QDesktopServices::openUrl(QUrl(link));
741 void MainWindow::openFile(const QString & fileName, bool updateLastUsedDir)
743 if (!fileName.isEmpty()) {
744 if (updateLastUsedDir) {
745 g.eepromDir(QFileInfo(fileName).dir().absolutePath());
748 QMdiSubWindow *existing = findMdiChild(fileName);
749 if (existing) {
750 mdiArea->setActiveSubWindow(existing);
751 return;
754 MdiChild *child = createMdiChild();
755 if (child->loadFile(fileName)) {
756 statusBar()->showMessage(tr("File loaded"), 2000);
757 child->show();
759 else {
760 child->closeFile(true);
765 void MainWindow::openFile()
767 QString fileName = QFileDialog::getOpenFileName(this, tr("Open Models and Settings file"), g.eepromDir(), EEPROM_FILES_FILTER);
768 openFile(fileName);
771 void MainWindow::save()
773 if (activeMdiChild() && activeMdiChild()->save()) {
774 statusBar()->showMessage(tr("File saved"), 2000);
778 void MainWindow::saveAs()
780 if (activeMdiChild() && activeMdiChild()->saveAs()) {
781 statusBar()->showMessage(tr("File saved"), 2000);
785 void MainWindow::saveAll()
787 foreach (QMdiSubWindow * window, mdiArea->subWindowList()) {
788 MdiChild * child;
789 if ((child = qobject_cast<MdiChild *>(window->widget())) && child->isWindowModified())
790 child->save();
794 void MainWindow::closeFile()
796 if (mdiArea->activeSubWindow())
797 mdiArea->activeSubWindow()->close();
800 void MainWindow::openRecentFile()
802 QAction *action = qobject_cast<QAction *>(sender());
803 if (action) {
804 QString fileName = action->data().toString();
805 openFile(fileName, false);
809 bool MainWindow::checkProfileRadioExists(int profId)
811 const QString profType = g.getProfile(profId).fwType();
812 return (Firmware::getFirmwareForId(profType)->getFirmwareBase()->getId() == profType.section('-', 0, 1));
815 bool MainWindow::loadProfileId(const unsigned pid) // TODO Load all variables - Also HW!
817 if (pid >= MAX_PROFILES)
818 return false;
820 Firmware * newFw = Firmware::getFirmwareForId(g.getProfile(pid).fwType());
821 // warn if the selected profile doesn't exist
822 if (!checkProfileRadioExists(pid))
823 AppMessages::displayMessage(AppMessages::MSG_NO_RADIO_TYPE, this);
824 // warn if we're switching between incompatible board types and any files have been modified
825 if (!Boards::isBoardCompatible(Firmware::getCurrentVariant()->getBoard(), newFw->getBoard()) && anyChildrenDirty()) {
826 if (QMessageBox::question(this, CPN_STR_APP_NAME,
827 tr("There are unsaved file changes which you may lose when switching radio types.\n\nDo you wish to continue?"),
828 (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) != QMessageBox::Yes) {
829 updateProfilesActions();
830 return false;
834 // Set the new profile number
835 g.id(pid);
836 return true;
839 void MainWindow::loadProfile()
841 QAction * action = qobject_cast<QAction *>(sender());
842 if (action) {
843 bool ok = false;
844 unsigned profnum = action->data().toUInt(&ok);
845 if (ok)
846 loadProfileId(profnum);
850 void MainWindow::appPrefs()
852 AppPreferencesDialog * dialog = new AppPreferencesDialog(this);
853 dialog->setMainWinHasDirtyChild(anyChildrenDirty());
854 connect(dialog, &AppPreferencesDialog::firmwareProfileAboutToChange, this, &MainWindow::saveAll);
855 connect(dialog, &AppPreferencesDialog::firmwareProfileChanged, this, &MainWindow::onCurrentProfileChanged);
856 dialog->exec();
857 dialog->deleteLater();
860 void MainWindow::fwPrefs()
862 FirmwarePreferencesDialog * dialog = new FirmwarePreferencesDialog(this);
863 dialog->exec();
864 dialog->deleteLater();
867 void MainWindow::contributors()
869 CreditsDialog * dialog = new CreditsDialog(this);
870 dialog->exec();
871 dialog->deleteLater();
874 void MainWindow::sdsync()
876 // remember user-selectable options for duration of session TODO: save to settings
877 static SyncProcess::SyncOptions syncOpts;
878 static bool showExtraOptions = false;
879 QStringList errorMsgs;
881 if (syncOpts.folderA.isEmpty())
882 syncOpts.folderA = g.profile[g.id()].sdPath();
883 if (syncOpts.folderB.isEmpty())
884 syncOpts.folderB = findMassstoragePath("SOUNDS", true);
886 if (syncOpts.folderA.isEmpty())
887 errorMsgs << tr("No local SD structure path configured!");
888 if (syncOpts.folderB.isEmpty())
889 errorMsgs << tr("No Radio or SD card detected!");
891 QPointer<FileSyncDialog> dlg = new FileSyncDialog(this, syncOpts);
892 dlg->setAttribute(Qt::WA_DeleteOnClose, true);
893 dlg->setWindowFlags(dlg->windowFlags() | Qt::WindowStaysOnTopHint);
894 dlg->setWindowTitle(tr("Synchronize SD"));
895 dlg->setWindowIcon(CompanionIcon("sdsync.png"));
896 dlg->setFolderNameA(tr("Local Folder"));
897 dlg->setFolderNameB(tr("Radio Folder"));
898 dlg->toggleExtraOptions(showExtraOptions);
899 if (errorMsgs.size())
900 dlg->setStatusText(errorMsgs.join('\n'), QtWarningMsg);
901 dlg->show();
903 connect(dlg.data(), &FileSyncDialog::finished, [=](int) {
904 if (!dlg.isNull()) {
905 syncOpts = dlg->syncOptions();
906 showExtraOptions = dlg->extraOptionsVisible();
911 void MainWindow::changelog()
913 QString link = "http://www.open-tx.org";
914 QDesktopServices::openUrl(QUrl(link));
917 void MainWindow::customizeSplash()
919 CustomizeSplashDialog * dialog = new CustomizeSplashDialog(this);
920 dialog->exec();
921 dialog->deleteLater();
924 void MainWindow::writeEeprom()
926 if (activeMdiChild()) activeMdiChild()->writeEeprom();
929 void MainWindow::readEeprom()
931 Board::Type board = getCurrentBoard();
932 QString tempFile;
933 if (IS_HORUS(board))
934 tempFile = generateProcessUniqueTempFileName("temp.otx");
935 else if (IS_ARM(board))
936 tempFile = generateProcessUniqueTempFileName("temp.bin");
937 else
938 tempFile = generateProcessUniqueTempFileName("temp.hex");
940 qDebug() << "MainWindow::readEeprom(): using temp file: " << tempFile;
942 if (readEepromFromRadio(tempFile)) {
943 MdiChild * child = createMdiChild();
944 child->newFile(false);
945 child->loadFile(tempFile, false);
946 child->show();
947 qunlink(tempFile);
951 bool MainWindow::readFirmwareFromRadio(const QString & filename)
953 ProgressDialog progressDialog(this, tr("Read Firmware from Radio"), CompanionIcon("read_flash.png"));
954 bool result = readFirmware(filename, progressDialog.progress());
955 if (!result && !progressDialog.isEmpty()) {
956 progressDialog.exec();
958 return result;
961 bool MainWindow::readEepromFromRadio(const QString & filename)
963 ProgressDialog progressDialog(this, tr("Read Models and Settings from Radio"), CompanionIcon("read_eeprom.png"));
964 bool result = ::readEeprom(filename, progressDialog.progress());
965 if (!result) {
966 if (!progressDialog.isEmpty()) {
967 progressDialog.exec();
970 else {
971 statusBar()->showMessage(tr("Models and Settings read"), 2000);
973 return result;
976 void MainWindow::writeBackup()
978 if (IS_HORUS(getCurrentBoard())) {
979 QMessageBox::information(this, CPN_STR_APP_NAME, tr("This function is not yet implemented"));
980 return;
981 // TODO implementation
983 FlashEEpromDialog *cd = new FlashEEpromDialog(this);
984 cd->exec();
987 void MainWindow::writeFlash(QString fileToFlash)
989 FlashFirmwareDialog * cd = new FlashFirmwareDialog(this);
990 cd->exec();
993 void MainWindow::readBackup()
995 if (IS_HORUS(getCurrentBoard())) {
996 QMessageBox::information(this, CPN_STR_APP_NAME, tr("This function is not yet implemented"));
997 return;
998 // TODO implementation
1000 QString fileName = QFileDialog::getSaveFileName(this, tr("Save Radio Backup to File"), g.eepromDir(), EXTERNAL_EEPROM_FILES_FILTER);
1001 if (!fileName.isEmpty()) {
1002 if (!readEepromFromRadio(fileName))
1003 return;
1007 void MainWindow::readFlash()
1009 QString fileName = QFileDialog::getSaveFileName(this,tr("Read Radio Firmware to File"), g.flashDir(), FLASH_FILES_FILTER);
1010 if (!fileName.isEmpty()) {
1011 readFirmwareFromRadio(fileName);
1015 void MainWindow::burnConfig()
1017 burnConfigDialog *bcd = new burnConfigDialog(this);
1018 bcd->exec();
1019 delete bcd;
1022 void MainWindow::burnList()
1024 burnConfigDialog bcd(this);
1025 bcd.listAvrdudeProgrammers();
1028 void MainWindow::burnFuses()
1030 FusesDialog *fd = new FusesDialog(this);
1031 fd->exec();
1032 delete fd;
1035 void MainWindow::compare()
1037 CompareDialog *fd = new CompareDialog(this,getCurrentFirmware());
1038 fd->setAttribute(Qt::WA_DeleteOnClose, true);
1039 fd->show();
1042 void MainWindow::logFile()
1044 LogsDialog *fd = new LogsDialog(this);
1045 fd->setWindowFlags(Qt::Window); //to show minimize an maximize buttons
1046 fd->setAttribute(Qt::WA_DeleteOnClose, true);
1047 fd->show();
1050 void MainWindow::about()
1052 QString aboutStr = "<center><img src=\":/images/companion-title.png\"></center><br/>";
1053 aboutStr.append(tr("OpenTX Home Page: <a href='%1'>%1</a>").arg("http://www.open-tx.org"));
1054 aboutStr.append("<br/><br/>");
1055 aboutStr.append(tr("The OpenTX Companion project was originally forked from <a href='%1'>eePe</a>").arg("http://code.google.com/p/eepe"));
1056 aboutStr.append("<br/><br/>");
1057 aboutStr.append(tr("If you've found this program useful, please support by <a href='%1'>donating</a>").arg(DONATE_STR));
1058 aboutStr.append("<br/><br/>");
1059 aboutStr.append(QString("Version %1, %2").arg(VERSION).arg(__DATE__));
1060 aboutStr.append("<br/><br/>");
1061 aboutStr.append(tr("Copyright OpenTX Team") + "<br/>&copy; 2011-2019<br/>");
1062 QMessageBox msgBox(this);
1063 msgBox.setWindowIcon(CompanionIcon("information.png"));
1064 msgBox.setWindowTitle(tr("About Companion"));
1065 msgBox.setText(aboutStr);
1066 msgBox.exec();
1069 void MainWindow::updateMenus()
1071 QMdiSubWindow * activeChild = mdiArea->activeSubWindow();
1073 newAct->setEnabled(true);
1074 openAct->setEnabled(true);
1075 saveAct->setEnabled(activeChild);
1076 saveAsAct->setEnabled(activeChild);
1077 closeAct->setEnabled(activeChild);
1078 compareAct->setEnabled(activeChild);
1079 writeEepromAct->setEnabled(activeChild);
1080 readEepromAct->setEnabled(true);
1081 writeBUToRadioAct->setEnabled(true);
1082 readBUToFileAct->setEnabled(true);
1083 editSplashAct->setDisabled(IS_HORUS(getCurrentBoard()));
1085 foreach (QAction * act, fileWindowActions) {
1086 if (!act)
1087 continue;
1088 if (fileMenu && fileMenu->actions().contains(act))
1089 fileMenu->removeAction(act);
1090 if (fileToolBar && fileToolBar->actions().contains(act)) {
1091 fileToolBar->removeAction(act);
1093 if (act->isSeparator() && act->parent() == this)
1094 delete act;
1096 fileWindowActions.clear();
1097 if (activeChild) {
1098 editMenu->clear();
1099 editMenu->addActions(activeMdiChild()->getEditActions());
1100 editMenu->addSeparator();
1101 editMenu->addActions(activeMdiChild()->getModelActions()); // maybe separate menu/toolbar?
1102 editMenu->setEnabled(true);
1104 editToolBar->clear();
1105 editToolBar->addActions(activeMdiChild()->getEditActions());
1106 editToolBar->setEnabled(true);
1107 if (activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV)) {
1108 // workaround for default split button appearance of action with menu :-/
1109 QToolButton * btn;
1110 if ((btn = qobject_cast<QToolButton *>(editToolBar->widgetForAction(activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV)))))
1111 btn->setPopupMode(QToolButton::InstantPopup);
1114 fileWindowActions = activeMdiChild()->getGeneralActions();
1115 QAction *sep = new QAction(this);
1116 sep->setSeparator(true);
1117 fileWindowActions.append(sep);
1118 fileMenu->insertActions(logsAct, fileWindowActions);
1119 fileToolBar->insertActions(logsAct, fileWindowActions);
1121 else {
1122 editToolBar->setDisabled(true);
1123 editMenu->setDisabled(true);
1126 foreach (QAction * act, windowsListActions->actions()) {
1127 if (act->property("window_ptr").canConvert<QMdiSubWindow *>() &&
1128 act->property("window_ptr").value<QMdiSubWindow *>() == activeChild) {
1129 act->setChecked(true);
1130 break;
1134 updateRecentFileActions();
1135 updateProfilesActions();
1136 setWindowTitle(tr("OpenTX Companion %1 - Radio: %2 - Profile: %3").arg(VERSION).arg(getCurrentFirmware()->getName()).arg(g.profile[g.id()].name()));
1139 MdiChild * MainWindow::createMdiChild()
1141 QMdiSubWindow * win = new QMdiSubWindow();
1142 MdiChild * child = new MdiChild(this, win);
1143 win->setAttribute(Qt::WA_DeleteOnClose);
1144 win->setWidget(child);
1145 mdiArea->addSubWindow(win);
1146 if (g.mdiWinGeo().size() < 10 && g.mdiWinGeo() == "maximized")
1147 win->showMaximized();
1149 connect(this, &MainWindow::firmwareChanged, child, &MdiChild::onFirmwareChanged);
1150 connect(child, &MdiChild::windowTitleChanged, this, &MainWindow::onSubwindowTitleChanged);
1151 connect(child, &MdiChild::modified, this, &MainWindow::onSubwindowModified);
1152 connect(child, &MdiChild::newStatusMessage, statusBar(), &QStatusBar::showMessage);
1153 connect(child, &MdiChild::destroyed, win, &QMdiSubWindow::close);
1154 connect(win, &QMdiSubWindow::destroyed, this, &MainWindow::updateWindowActions);
1156 updateWindowActions();
1157 return child;
1160 QAction * MainWindow::addAct(const QString & icon, const char *slot, const QKeySequence & shortcut, QObject *slotObj, const char * signal)
1162 QAction * newAction = new QAction( this );
1163 newAction->setMenuRole(QAction::NoRole);
1164 if (!icon.isEmpty())
1165 newAction->setIcon(CompanionIcon(icon));
1166 if (!shortcut.isEmpty())
1167 newAction->setShortcut(shortcut);
1168 if (slot) {
1169 if (!slotObj)
1170 slotObj = this;
1171 if (!signal)
1172 connect(newAction, SIGNAL(triggered()), slotObj, slot);
1173 else
1174 connect(newAction, signal, slotObj, slot);
1176 return newAction;
1179 QAction * MainWindow::addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, const char * propName, const QVariant & propValue, const QVariant & dfltValue, const QKeySequence & shortcut)
1181 QAction * act = aGroup->addAction(sName);
1182 act->setMenuRole(QAction::NoRole);
1183 act->setStatusTip(lName);
1184 act->setCheckable(true);
1185 if (!shortcut.isEmpty())
1186 act->setShortcut(shortcut);
1187 if (propName) {
1188 act->setProperty(propName, propValue);
1189 if (propValue == dfltValue)
1190 act->setChecked(true);
1192 return act;
1195 void MainWindow::trAct(QAction * act, const QString & text, const QString & descript)
1197 if (!text.isEmpty())
1198 act->setText(text);
1199 if (!descript.isEmpty())
1200 act->setStatusTip(descript);
1203 void MainWindow::retranslateUi(bool showMsg)
1205 trAct(newAct, tr("New"), tr("Create a new Models and Settings file"));
1206 trAct(openAct, tr("Open..."), tr("Open Models and Settings file"));
1207 trAct(saveAct, tr("Save"), tr("Save Models and Settings file"));
1208 trAct(saveAsAct, tr("Save As..."), tr("Save Models and Settings file"));
1209 trAct(closeAct, tr("Close"), tr("Close Models and Settings file"));
1210 trAct(exitAct, tr("Exit"), tr("Exit the application"));
1211 trAct(aboutAct, tr("About..."), tr("Show the application's About box"));
1213 trAct(recentFilesAct, tr("Recent Files"), tr("List of recently used files"));
1214 trAct(profilesMenuAct, tr("Radio Profiles"), tr("Create or Select Radio Profiles"));
1215 trAct(logsAct, tr("View Log File..."), tr("Open and view log file"));
1216 trAct(appPrefsAct, tr("Settings..."), tr("Edit Settings"));
1217 trAct(fwPrefsAct, tr("Download..."), tr("Download firmware and voice files"));
1218 trAct(checkForUpdatesAct, tr("Check for Updates..."), tr("Check OpenTX and Companion updates"));
1219 trAct(changelogAct, tr("Release notes..."), tr("Show release notes"));
1220 trAct(compareAct, tr("Compare Models..."), tr("Compare models"));
1221 trAct(editSplashAct, tr("Edit Radio Splash Image..."), tr("Edit the splash image of your Radio"));
1222 trAct(burnListAct, tr("List programmers..."), tr("List available programmers"));
1223 trAct(burnFusesAct, tr("Fuses..."), tr("Show fuses dialog"));
1224 trAct(readFlashAct, tr("Read Firmware from Radio"), tr("Read firmware from Radio"));
1225 trAct(writeFlashAct, tr("Write Firmware to Radio"), tr("Write firmware to Radio"));
1226 trAct(sdsyncAct, tr("Synchronize SD"), tr("SD card synchronization"));
1228 trAct(openDocURLAct, tr("Manuals and other Documents"), tr("Open the OpenTX document page in a web browser"));
1229 trAct(writeEepromAct, tr("Write Models and Settings To Radio"), tr("Write Models and Settings to Radio"));
1230 trAct(readEepromAct, tr("Read Models and Settings From Radio"), tr("Read Models and Settings from Radio"));
1231 trAct(burnConfigAct, tr("Configure Communications..."), tr("Configure software for communicating with the Radio"));
1232 trAct(writeBUToRadioAct, tr("Write Backup to Radio"), tr("Write Backup from file to Radio"));
1233 trAct(readBUToFileAct, tr("Backup Radio to File"), tr("Save a complete backup file of all settings and model data in the Radio"));
1234 trAct(contributorsAct, tr("Contributors..."), tr("A tribute to those who have contributed to OpenTX and Companion"));
1236 trAct(createProfileAct, tr("Add Radio Profile"), tr("Create a new Radio Settings Profile"));
1237 trAct(copyProfileAct, tr("Copy Current Radio Profile"), tr("Duplicate current Radio Settings Profile"));
1238 trAct(deleteProfileAct, tr("Delete Current Radio Profile..."), tr("Delete the current Radio Settings Profile"));
1240 trAct(exportSettingsAct, tr("Export Application Settings.."), tr("Save all the current %1 and Simulator settings (including radio profiles) to a file.").arg(CPN_STR_APP_NAME));
1241 trAct(importSettingsAct, tr("Import Application Settings.."), tr("Load %1 and Simulator settings from a prevously exported settings file.").arg(CPN_STR_APP_NAME));
1243 trAct(actTabbedWindows, tr("Tabbed Windows"), tr("Use tabs to arrange open windows."));
1244 trAct(actTileWindows, tr("Tile Windows"), tr("Arrange open windows across all the available space."));
1245 trAct(actCascadeWindows, tr("Cascade Windows"), tr("Arrange all open windows in a stack."));
1246 trAct(actCloseAllWindows, tr("Close All Windows"), tr("Closes all open files (prompts to save if necessary."));
1248 editMenu->setTitle(tr("Edit"));
1249 fileMenu->setTitle(tr("File"));
1250 settingsMenu->setTitle(tr("Settings"));
1251 themeMenu->setTitle(tr("Set Icon Theme"));
1252 iconThemeSizeMenu->setTitle(tr("Set Icon Size"));
1253 burnMenu->setTitle(tr("Read/Write"));
1254 windowMenu->setTitle(tr("Window"));
1255 helpMenu->setTitle(tr("Help"));
1257 fileToolBar->setWindowTitle(tr("File"));
1258 editToolBar->setWindowTitle(tr("Edit"));
1259 burnToolBar->setWindowTitle(tr("Write"));
1260 helpToolBar->setWindowTitle(tr("Help"));
1262 showReadyStatus();
1264 if (showMsg)
1265 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."));
1268 void MainWindow::createActions()
1270 newAct = addAct("new.png", SLOT(newFile()), QKeySequence::New);
1271 openAct = addAct("open.png", SLOT(openFile()), QKeySequence::Open);
1272 saveAct = addAct("save.png", SLOT(save()), QKeySequence::Save);
1273 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
1274 closeAct = addAct("clear.png", SLOT(closeFile()) /*, QKeySequence::Close*/); // setting/showing this shortcut interferes with the system one (Ctrl+W/Ctrl-F4)
1275 exitAct = addAct("exit.png", SLOT(closeAllWindows()), QKeySequence::Quit, qApp);
1277 logsAct = addAct("logs.png", SLOT(logFile()), tr("Ctrl+Alt+L"));
1278 appPrefsAct = addAct("apppreferences.png", SLOT(appPrefs()), QKeySequence::Preferences);
1279 fwPrefsAct = addAct("fwpreferences.png", SLOT(fwPrefs()), tr("Ctrl+Alt+D"));
1280 compareAct = addAct("compare.png", SLOT(compare()), tr("Ctrl+Alt+R"));
1281 sdsyncAct = addAct("sdsync.png", SLOT(sdsync()));
1283 editSplashAct = addAct("paintbrush.png", SLOT(customizeSplash()));
1284 burnListAct = addAct("list.png", SLOT(burnList()));
1285 burnFusesAct = addAct("fuses.png", SLOT(burnFuses()));
1286 readFlashAct = addAct("read_flash.png", SLOT(readFlash()));
1287 writeFlashAct = addAct("write_flash.png", SLOT(writeFlash()));
1288 writeEepromAct = addAct("write_eeprom.png", SLOT(writeEeprom()));
1289 readEepromAct = addAct("read_eeprom.png", SLOT(readEeprom()));
1290 burnConfigAct = addAct("configure.png", SLOT(burnConfig()));
1291 writeBUToRadioAct = addAct("write_eeprom_file.png", SLOT(writeBackup()));
1292 readBUToFileAct = addAct("read_eeprom_file.png", SLOT(readBackup()));
1294 createProfileAct = addAct("new.png", SLOT(createProfile()));
1295 copyProfileAct = addAct("copy.png", SLOT(copyProfile()));
1296 deleteProfileAct = addAct("clear.png", SLOT(deleteCurrentProfile()));
1298 exportSettingsAct = addAct("saveas.png", SLOT(exportSettings()));
1299 importSettingsAct = addAct("open.png", SLOT(importSettings()));
1301 actTabbedWindows = addAct("", SLOT(setTabbedWindows(bool)), 0, this, SIGNAL(triggered(bool)));
1302 actTileWindows = addAct("", SLOT(tileSubWindows()), 0, mdiArea);
1303 actCascadeWindows = addAct("", SLOT(cascadeSubWindows()), 0, mdiArea);
1304 actCloseAllWindows = addAct("", SLOT(closeAllSubWindows()), 0, mdiArea);
1306 checkForUpdatesAct = addAct("update.png", SLOT(doUpdates()));
1307 aboutAct = addAct("information.png", SLOT(about()));
1308 openDocURLAct = addAct("changelog.png", SLOT(openDocURL()));
1309 changelogAct = addAct("changelog.png", SLOT(changelog()));
1310 contributorsAct = addAct("contributors.png", SLOT(contributors()));
1312 // these two get assigned menus in createMenus()
1313 recentFilesAct = addAct("recentdocument.png");
1314 profilesMenuAct = addAct("profiles.png");
1316 exitAct->setMenuRole(QAction::QuitRole);
1317 aboutAct->setMenuRole(QAction::AboutRole);
1318 appPrefsAct->setMenuRole(QAction::PreferencesRole);
1319 contributorsAct->setMenuRole(QAction::ApplicationSpecificRole);
1320 openDocURLAct->setMenuRole(QAction::ApplicationSpecificRole);
1321 checkForUpdatesAct->setMenuRole(QAction::ApplicationSpecificRole);
1322 changelogAct->setMenuRole(QAction::ApplicationSpecificRole);
1324 actTabbedWindows->setCheckable(true);
1325 compareAct->setEnabled(false);
1328 void MainWindow::createMenus()
1330 fileMenu = menuBar()->addMenu("");
1331 fileMenu->addAction(newAct);
1332 fileMenu->addAction(openAct);
1333 fileMenu->addAction(saveAct);
1334 fileMenu->addAction(saveAsAct);
1335 fileMenu->addAction(closeAct);
1336 fileMenu->addAction(recentFilesAct);
1337 fileMenu->addSeparator();
1338 fileMenu->addAction(logsAct);
1339 fileMenu->addAction(fwPrefsAct);
1340 fileMenu->addAction(compareAct);
1341 fileMenu->addAction(sdsyncAct);
1342 fileMenu->addSeparator();
1343 fileMenu->addAction(exitAct);
1345 editMenu = menuBar()->addMenu("");
1347 settingsMenu = menuBar()->addMenu("");
1348 settingsMenu->addMenu(createLanguageMenu(settingsMenu));
1350 themeMenu = settingsMenu->addMenu("");
1351 QActionGroup * themeGroup = new QActionGroup(themeMenu);
1352 addActToGroup(themeGroup, tr("Classical"), tr("The classic companion9x icon theme"), "themeId", 0, g.theme());
1353 addActToGroup(themeGroup, tr("Yerico"), tr("Yellow round honey sweet icon theme"), "themeId", 1, g.theme());
1354 addActToGroup(themeGroup, tr("Monochrome"), tr("A monochrome black icon theme"), "themeId", 3, g.theme());
1355 addActToGroup(themeGroup, tr("MonoBlue"), tr("A monochrome blue icon theme"), "themeId", 4, g.theme());
1356 addActToGroup(themeGroup, tr("MonoWhite"), tr("A monochrome white icon theme"), "themeId", 2, g.theme());
1357 connect(themeGroup, &QActionGroup::triggered, this, &MainWindow::onThemeChanged);
1358 themeMenu->addActions(themeGroup->actions());
1360 iconThemeSizeMenu = settingsMenu->addMenu("");
1361 QActionGroup * szGroup = new QActionGroup(iconThemeSizeMenu);
1362 addActToGroup(szGroup, tr("Small"), tr("Use small toolbar icons"), "sizeId", 0, g.iconSize());
1363 addActToGroup(szGroup, tr("Normal"), tr("Use normal size toolbar icons"), "sizeId", 1, g.iconSize());
1364 addActToGroup(szGroup, tr("Big"), tr("Use big toolbar icons"), "sizeId", 2, g.iconSize());
1365 addActToGroup(szGroup, tr("Huge"), tr("Use huge toolbar icons"), "sizeId", 3, g.iconSize());
1366 connect(szGroup, &QActionGroup::triggered, this, &MainWindow::onIconSizeChanged);
1367 iconThemeSizeMenu->addActions(szGroup->actions());
1369 settingsMenu->addSeparator();
1370 settingsMenu->addAction(appPrefsAct);
1371 settingsMenu->addAction(profilesMenuAct);
1372 settingsMenu->addAction(editSplashAct);
1373 settingsMenu->addAction(burnConfigAct);
1374 settingsMenu->addSeparator();
1375 settingsMenu->addAction(exportSettingsAct);
1376 settingsMenu->addAction(importSettingsAct);
1378 burnMenu = menuBar()->addMenu("");
1379 burnMenu->addAction(writeEepromAct);
1380 burnMenu->addAction(readEepromAct);
1381 burnMenu->addSeparator();
1382 burnMenu->addAction(writeBUToRadioAct);
1383 burnMenu->addAction(readBUToFileAct);
1384 burnMenu->addSeparator();
1385 burnMenu->addAction(writeFlashAct);
1386 burnMenu->addAction(readFlashAct);
1387 burnMenu->addSeparator();
1388 burnMenu->addSeparator();
1389 if (!IS_ARM(getCurrentBoard())) {
1390 burnMenu->addAction(burnFusesAct);
1391 burnMenu->addAction(burnListAct);
1394 windowMenu = menuBar()->addMenu("");
1395 windowMenu->addAction(actTabbedWindows);
1396 windowMenu->addAction(actTileWindows);
1397 windowMenu->addAction(actCascadeWindows);
1398 windowMenu->addAction(actCloseAllWindows);
1399 windowMenu->addSeparator();
1401 helpMenu = menuBar()->addMenu("");
1402 helpMenu->addSeparator();
1403 helpMenu->addAction(checkForUpdatesAct);
1404 helpMenu->addSeparator();
1405 helpMenu->addAction(aboutAct);
1406 helpMenu->addAction(openDocURLAct);
1407 helpMenu->addSeparator();
1408 helpMenu->addAction(changelogAct);
1409 helpMenu->addSeparator();
1410 helpMenu->addAction(contributorsAct);
1412 recentFilesMenu = new QMenu(this);
1413 recentFilesMenu->setToolTipsVisible(true);
1414 for ( int i = 0; i < g.historySize(); ++i) {
1415 recentFileActs.append(recentFilesMenu->addAction(""));
1416 recentFileActs[i]->setVisible(false);
1417 connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile()));
1419 recentFilesAct->setMenu(recentFilesMenu);
1421 profilesMenu = new QMenu(this);
1422 QActionGroup *profilesGroup = new QActionGroup(this);
1423 for (int i=0; i < MAX_PROFILES; i++) {
1424 profileActs.append(profilesMenu->addAction(""));
1425 profileActs[i]->setVisible(false);
1426 profileActs[i]->setCheckable(true);
1427 connect(profileActs[i], SIGNAL(triggered()), this, SLOT(loadProfile()));
1428 profilesGroup->addAction(profileActs[i]);
1430 profilesMenu->addSeparator();
1431 profilesMenu->addAction(createProfileAct);
1432 profilesMenu->addAction(copyProfileAct);
1433 profilesMenu->addAction(deleteProfileAct);
1434 profilesMenuAct->setMenu(profilesMenu);
1437 void MainWindow::createToolBars()
1439 fileToolBar = addToolBar("");
1440 fileToolBar->setObjectName("File");
1441 fileToolBar->addAction(newAct);
1442 fileToolBar->addAction(openAct);
1443 fileToolBar->addAction(recentFilesAct);
1444 fileToolBar->addAction(saveAct);
1445 fileToolBar->addAction(closeAct);
1446 fileToolBar->addSeparator();
1447 fileToolBar->addAction(logsAct);
1448 fileToolBar->addAction(fwPrefsAct);
1449 fileToolBar->addSeparator();
1450 fileToolBar->addAction(appPrefsAct);
1451 fileToolBar->addAction(profilesMenuAct);
1452 fileToolBar->addAction(editSplashAct);
1453 fileToolBar->addAction(editSplashAct);
1454 fileToolBar->addSeparator();
1455 fileToolBar->addAction(compareAct);
1456 fileToolBar->addAction(sdsyncAct);
1458 // workaround for default split button appearance of action with menu :-/
1459 QToolButton * btn;
1460 if ((btn = qobject_cast<QToolButton *>(fileToolBar->widgetForAction(recentFilesAct))))
1461 btn->setPopupMode(QToolButton::InstantPopup);
1462 if ((btn = qobject_cast<QToolButton *>(fileToolBar->widgetForAction(profilesMenuAct))))
1463 btn->setPopupMode(QToolButton::InstantPopup);
1465 // gets populated later
1466 editToolBar = addToolBar("");
1467 editToolBar->setObjectName("Edit");
1469 burnToolBar = new QToolBar(this);
1470 addToolBar( Qt::LeftToolBarArea, burnToolBar );
1471 burnToolBar->setObjectName("Write");
1472 burnToolBar->addAction(writeEepromAct);
1473 burnToolBar->addAction(readEepromAct);
1474 burnToolBar->addSeparator();
1475 burnToolBar->addAction(writeBUToRadioAct);
1476 burnToolBar->addAction(readBUToFileAct);
1477 burnToolBar->addSeparator();
1478 burnToolBar->addAction(writeFlashAct);
1479 burnToolBar->addAction(readFlashAct);
1480 burnToolBar->addSeparator();
1481 burnToolBar->addAction(burnConfigAct);
1483 helpToolBar = addToolBar("");
1484 helpToolBar->setObjectName("Help");
1485 helpToolBar->addAction(checkForUpdatesAct);
1486 helpToolBar->addAction(aboutAct);
1489 QMenu * MainWindow::createLanguageMenu(QWidget * parent)
1491 QMenu * menu = new QMenu(tr("Set Menu Language"), parent);
1492 QActionGroup * actGroup = new QActionGroup(menu);
1493 QString lName;
1495 addActToGroup(actGroup, tr("System language"), tr("Use default system language."), "locale", QString(""), g.locale());
1496 foreach (const QString & lang, Translations::getAvailableTranslations()) {
1497 QLocale locale(lang);
1498 lName = locale.nativeLanguageName();
1499 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());
1501 if (!actGroup->checkedAction())
1502 actGroup->actions().first()->setChecked(true);
1504 connect(actGroup, &QActionGroup::triggered, this, &MainWindow::onLanguageChanged);
1505 menu->addActions(actGroup->actions());
1506 return menu;
1509 void MainWindow::showReadyStatus()
1511 statusBar()->showMessage(tr("Ready"));
1514 MdiChild *MainWindow::activeMdiChild()
1516 if (QMdiSubWindow *activeSubWindow = mdiArea->activeSubWindow())
1517 return qobject_cast<MdiChild *>(activeSubWindow->widget());
1518 return 0;
1521 QMdiSubWindow *MainWindow::findMdiChild(const QString &fileName)
1523 QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
1525 foreach (QMdiSubWindow *window, mdiArea->subWindowList()) {
1526 MdiChild *mdiChild = qobject_cast<MdiChild *>(window->widget());
1527 if (mdiChild->currentFile() == canonicalFilePath)
1528 return window;
1530 return 0;
1533 bool MainWindow::anyChildrenDirty()
1535 foreach (QMdiSubWindow * window, mdiArea->subWindowList()) {
1536 MdiChild * child;
1537 if ((child = qobject_cast<MdiChild *>(window->widget())) && child->isWindowModified())
1538 return true;
1540 return false;
1543 void MainWindow::updateRecentFileActions()
1545 QStringList files = g.recentFiles();
1546 for (int i=0; i < recentFileActs.size(); i++) {
1547 if (i < files.size() && !files.at(i).isEmpty()) {
1548 recentFileActs[i]->setText(QFileInfo(files.at(i)).fileName());
1549 recentFileActs[i]->setData(files.at(i));
1550 recentFileActs[i]->setStatusTip(QDir::toNativeSeparators(files.at(i)));
1551 recentFileActs[i]->setToolTip(recentFileActs[i]->statusTip());
1552 recentFileActs[i]->setVisible(true);
1554 else {
1555 recentFileActs[i]->setVisible(false);
1560 void MainWindow::updateProfilesActions()
1562 for (int i=0; i < qMin(profileActs.size(), MAX_PROFILES); i++) {
1563 if (g.profile[i].existsOnDisk()) {
1564 QString text = tr("%2").arg(g.profile[i].name());
1565 profileActs[i]->setText(text);
1566 profileActs[i]->setData(i);
1567 profileActs[i]->setVisible(true);
1568 if (i == g.id())
1569 profileActs[i]->setChecked(true);
1571 else {
1572 profileActs[i]->setVisible(false);
1577 void MainWindow::updateWindowActions()
1579 if (!windowsListActions)
1580 return;
1582 foreach (QAction * act, windowsListActions->actions()) {
1583 windowsListActions->removeAction(act);
1584 if (windowMenu->actions().contains(act))
1585 windowMenu->removeAction(act);
1586 delete act;
1588 int count = 0;
1589 foreach (QMdiSubWindow * win, mdiArea->subWindowList()) {
1590 QString scut;
1591 if (++count < 10)
1592 scut = tr("Alt+%1").arg(count);
1593 QAction * act = addActToGroup(windowsListActions, "", "", "window_ptr", qVariantFromValue(win), QVariant(), scut);
1594 act->setChecked(win == mdiArea->activeSubWindow());
1595 updateWindowActionTitle(win, act);
1597 windowMenu->addActions(windowsListActions->actions());
1600 void MainWindow::updateWindowActionTitle(const QMdiSubWindow * win, QAction * act)
1602 MdiChild * child = qobject_cast<MdiChild *>(win->widget());
1603 if (!child)
1604 return;
1606 if (!act) {
1607 foreach (QAction * a, windowsListActions->actions()) {
1608 if (a->property("window_ptr").canConvert<QMdiSubWindow *>() &&
1609 a->property("window_ptr").value<QMdiSubWindow *>() == win) {
1610 act = a;
1611 break;
1615 if (!act)
1616 return;
1618 QString ttl = child->userFriendlyCurrentFile();
1619 if (child->isWindowModified())
1620 ttl.prepend("* ");
1621 act->setText(ttl);
1624 void MainWindow::onSubwindowTitleChanged()
1626 QMdiSubWindow * win = nullptr;
1627 if ((win = qobject_cast<QMdiSubWindow *>(sender()->parent())))
1628 updateWindowActionTitle(win);
1631 void MainWindow::onSubwindowModified()
1633 onSubwindowTitleChanged();
1636 void MainWindow::onChangeWindowAction(QAction * act)
1638 if (!act->isChecked())
1639 return;
1641 QMdiSubWindow * win = nullptr;
1642 if (act->property("window_ptr").canConvert<QMdiSubWindow *>())
1643 win = act->property("window_ptr").value<QMdiSubWindow *>();
1644 if (win)
1645 mdiArea->setActiveSubWindow(win);
1648 void MainWindow::onCurrentProfileChanged()
1650 Firmware::setCurrentVariant(Firmware::getFirmwareForId(g.currentProfile().fwType()));
1651 emit firmwareChanged();
1652 updateMenus();
1655 int MainWindow::newProfile(bool loadProfile)
1657 int i;
1658 for (i=0; i < MAX_PROFILES && g.profile[i].existsOnDisk(); i++)
1660 if (i == MAX_PROFILES) //Failed to find free slot
1661 return -1;
1663 g.profile[i].init();
1664 g.profile[i].name(tr("New Radio"));
1665 g.profile[i].fwType(Firmware::getDefaultVariant()->getId());
1667 if (loadProfile) {
1668 if (loadProfileId(i))
1669 appPrefs();
1672 return i;
1675 void MainWindow::createProfile()
1677 newProfile(true);
1680 void MainWindow::copyProfile()
1682 int newId = newProfile(false);
1684 if (newId > -1) {
1685 g.profile[newId] = g.profile[g.id()];
1686 g.profile[newId].name(g.profile[newId].name() + tr(" - Copy"));
1687 if (loadProfileId(newId))
1688 appPrefs();
1692 void MainWindow::deleteProfile(const int pid)
1694 if (pid == g.id() && anyChildrenDirty()) {
1695 QMessageBox::warning(this, tr("Companion :: Open files warning"), tr("Please save or close modified file(s) before deleting the active profile."));
1696 return;
1698 if (pid == 0) {
1699 QMessageBox::warning(this, tr("Not possible to remove profile"), tr("The default profile can not be removed."));
1700 return;
1702 int ret = QMessageBox::question(this,
1703 tr("Confirm Delete Profile"),
1704 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()));
1705 if (ret != QMessageBox::Yes)
1706 return;
1708 g.getProfile(pid).resetAll();
1709 loadProfileId(0);
1712 void MainWindow::deleteCurrentProfile()
1714 deleteProfile(g.id());
1717 void MainWindow::exportSettings()
1719 Helpers::exportAppSettings();
1722 void MainWindow::importSettings()
1724 if (anyChildrenDirty()) {
1725 QMessageBox::warning(this, CPN_STR_APP_NAME, tr("Please save or close all modified files before importing settings"));
1726 return;
1728 QString resultMsg = tr("<html>" \
1729 "<p>%1 and Simulator settings can be imported (restored) from a previosly saved export (backup) file. " \
1730 "This will replace current settings with any settings found in the file.</p>" \
1731 "<p>An automatic backup of the current settings will be attempted. But if the current settings are useful then it is recommended that you make a manual backup first.</p>" \
1732 "<p>For best results when importing settings, <b>close any other %1 windows you may have open, and make sure the standalone Simulator application is not running.</p>" \
1733 "<p>Do you wish to continue?</p>" \
1734 "</html>").arg(CPN_STR_APP_NAME);
1736 int ret = QMessageBox::question(this, tr("Confirm Settings Import"), resultMsg);
1737 if (ret != QMessageBox::Yes)
1738 return;
1740 QString impFile = CPN_SETTINGS_BACKUP_DIR;
1741 impFile = QFileDialog::getOpenFileName(this, tr("Select %1:").arg(CPN_STR_APP_SETTINGS_FILES), impFile, CPN_STR_APP_SETTINGS_FILTER);
1742 if (impFile.isEmpty() || !QFileInfo(impFile).isReadable() || QFileInfo(impFile).isExecutable())
1743 return;
1745 // Try a backup first
1746 QString expFile = CPN_SETTINGS_INI_PATH.arg(tr("backup") % " " % QDateTime::currentDateTime().toString("dd-MMM-yy HH-mm"));
1747 if (!g.exportSettingsToFile(expFile, resultMsg)) {
1748 resultMsg.append("\n" % tr("Press the 'Ignore' button to continue anyway."));
1749 if (QMessageBox::warning(this, CPN_STR_APP_NAME, resultMsg, QMessageBox::Cancel, QMessageBox::Ignore) == QMessageBox::Cancel)
1750 return;
1751 expFile.clear();
1753 const QString prevLoc = g.locale();
1755 // Do the import
1756 QSettings fromSettings(impFile, QSettings::IniFormat);
1757 if (!g.importSettings(&fromSettings)) {
1758 QMessageBox::critical(this, CPN_STR_APP_NAME, tr("The settings could not be imported."), QMessageBox::Ok, 0);
1759 return;
1761 resultMsg = tr("<html>" \
1762 "<p>New settings have been imported from:<br> %1.</p>" \
1763 "<p>%2 will now re-initialize.</p>" \
1764 "<p>Note that you may need to close and restart %2 before some settings like language and icon theme take effect.</p>" \
1765 ).arg(impFile).arg(CPN_STR_APP_NAME);
1767 if (!expFile.isEmpty())
1768 resultMsg.append(tr("<p>The previous settings were backed up to:<br> %1</p>").arg(expFile));
1769 resultMsg.append("</html>");
1770 QMessageBox::information(this, CPN_STR_APP_NAME, resultMsg);
1772 g.init();
1773 initWindowOptions();
1774 if (prevLoc != g.locale())
1775 Translations::installTranslators();
1776 onCurrentProfileChanged();
1779 QString MainWindow::strippedName(const QString &fullFileName)
1781 return QFileInfo(fullFileName).fileName();
1784 void MainWindow::dragEnterEvent(QDragEnterEvent *event)
1786 if (event->mimeData()->hasFormat("text/uri-list"))
1787 event->acceptProposedAction();
1790 void MainWindow::dropEvent(QDropEvent *event)
1792 QList<QUrl> urls = event->mimeData()->urls();
1793 if (urls.isEmpty()) return;
1794 QString fileName = urls.first().toLocalFile();
1795 openFile(fileName);
1798 void MainWindow::autoClose()
1800 this->close();