diff --git a/companion/src/apppreferencesdialog.cpp b/companion/src/apppreferencesdialog.cpp index 871064cf4..4f3b8fc24 100644 --- a/companion/src/apppreferencesdialog.cpp +++ b/companion/src/apppreferencesdialog.cpp @@ -67,7 +67,8 @@ void AppPreferencesDialog::writeValues() g.autoCheckFw(ui->autoCheckFirmware->isChecked()); g.showSplash(ui->showSplash->isChecked()); g.simuSW(ui->simuSW->isChecked()); - g.useWizard(ui->modelWizard_CB->isChecked()); + g.removeModelSlots(ui->opt_removeBlankSlots->isChecked()); + g.newModelAction(ui->opt_newMdl_useWizard->isChecked() ? 1 : ui->opt_newMdl_useEditor->isChecked() ? 2 : 0); g.historySize(ui->historySize->value()); g.backLight(ui->backLightColor->currentIndex()); g.profile[g.id()].volumeGain(round(ui->volumeGain->value() * 10.0)); @@ -149,7 +150,10 @@ void AppPreferencesDialog::initSettings() } ui->simuSW->setChecked(g.simuSW()); - ui->modelWizard_CB->setChecked(g.useWizard()); + ui->opt_removeBlankSlots->setChecked(g.removeModelSlots()); + ui->opt_newMdl_useNone->setChecked(g.newModelAction() == 0); + ui->opt_newMdl_useWizard->setChecked(g.newModelAction() == 1); + ui->opt_newMdl_useEditor->setChecked(g.newModelAction() == 2); ui->libraryPath->setText(g.libDir()); ui->ge_lineedit->setText(g.gePath()); diff --git a/companion/src/apppreferencesdialog.ui b/companion/src/apppreferencesdialog.ui index b7f715dc9..23ad622d0 100644 --- a/companion/src/apppreferencesdialog.ui +++ b/companion/src/apppreferencesdialog.ui @@ -662,6 +662,13 @@ Mode 4: Application Settings + + + + Automatic Backup Folder + + + @@ -682,103 +689,6 @@ Mode 4: - - - - Enable automatic backup before writing firmware - - - - - - - Qt::Horizontal - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - - - - - Automatic check for OpenTX firmware updates - - - true - - - - - - - - 0 - 0 - - - - Splash Screen Library - - - - - - - - 0 - 0 - - - - Files to keep - - - - - - - Qt::Horizontal - - - - - - - Google Earth Executable - - - - - - - - 0 - 0 - - - - User Splash Screens - - - @@ -794,45 +704,37 @@ Mode 4: - 15 + 50 10 - - + + - Automatic Backup Folder + Use OpenTX firmware nightly builds + + + false - - - - Qt::Horizontal - - - - - - - Use model wizard when creating a new model - - - - - - - true + + + + + 350 + 0 + false - + @@ -852,23 +754,134 @@ Mode 4: - - - + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + Files to keep + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + Automatic check for OpenTX firmware updates + + + true + + + + + + + Select Folder + + + + + + + + 0 + 0 + + + + Splash Screen Library + + + + + + + Google Earth Executable + + + + + + + Qt::Vertical + + - 350 - 0 + 20 + 40 + + + + + + Enable automatic backup before writing firmware + + + + + + + true + false - - + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + - Select Folder + User Splash Screens + + + + + + + Use Companion nightly builds + + + false @@ -892,30 +905,67 @@ Mode 4: - + Select Folder - - - - Use OpenTX firmware nightly builds + + + + + 0 + 0 + - - false + + Action on New Model - - - - Use Companion nightly builds + + + + + + Use model wizard + + + btnGrp_newModelActions + + + + + + + Open model editor + + + btnGrp_newModelActions + + + + + + + Just create the model + + + btnGrp_newModelActions + + + + + + + + + <html><head/><body><p>This option maintains the behaviour from older OpenTx versions where empty model slots are preserved when a model is deleted or moved. </p><p>When this option is de-selected, the other models may be re-arranged to fill the gap left by the removed model.</p></body></html> - - false + + Remove empty model slots when deleting models (only applies for radios w/out categories) @@ -1137,7 +1187,6 @@ Mode 4: ge_pathButton historySize showSplash - modelWizard_CB autoCheckFirmware autoCheckCompanion backupPath @@ -1192,4 +1241,7 @@ Mode 4: + + + diff --git a/companion/src/companion.qrc b/companion/src/companion.qrc index 26c8f57d2..18b46f26a 100644 --- a/companion/src/companion.qrc +++ b/companion/src/companion.qrc @@ -620,6 +620,7 @@ themes/monoblue/16/contributors.png themes/monoblue/16/stop.png themes/monoblue/16/write_eeprom.png + themes/monoblue/16/sdsync.png themes/monoblue/24/paintbrush.png themes/monoblue/24/open.png themes/monoblue/24/edit.png @@ -663,6 +664,7 @@ themes/monoblue/24/contributors.png themes/monoblue/24/stop.png themes/monoblue/24/write_eeprom.png + themes/monoblue/24/sdsync.png themes/monoblue/32/paintbrush.png themes/monoblue/32/open.png themes/monoblue/32/edit.png @@ -706,6 +708,7 @@ themes/monoblue/32/contributors.png themes/monoblue/32/stop.png themes/monoblue/32/write_eeprom.png + themes/monoblue/32/sdsync.png themes/monoblue/48/paintbrush.png themes/monoblue/48/open.png themes/monoblue/48/edit.png @@ -749,6 +752,7 @@ themes/monoblue/48/contributors.png themes/monoblue/48/stop.png themes/monoblue/48/write_eeprom.png + themes/monoblue/48/sdsync.png themes/monowhite/16/paintbrush.png themes/monowhite/16/open.png themes/monowhite/16/edit.png @@ -792,6 +796,7 @@ themes/monowhite/16/contributors.png themes/monowhite/16/stop.png themes/monowhite/16/write_eeprom.png + themes/monowhite/16/sdsync.png themes/monowhite/24/paintbrush.png themes/monowhite/24/open.png themes/monowhite/24/edit.png @@ -835,6 +840,7 @@ themes/monowhite/24/contributors.png themes/monowhite/24/stop.png themes/monowhite/24/write_eeprom.png + themes/monowhite/24/sdsync.png themes/monowhite/32/paintbrush.png themes/monowhite/32/open.png themes/monowhite/32/edit.png @@ -878,6 +884,7 @@ themes/monowhite/32/contributors.png themes/monowhite/32/stop.png themes/monowhite/32/write_eeprom.png + themes/monowhite/32/sdsync.png themes/monowhite/48/paintbrush.png themes/monowhite/48/open.png themes/monowhite/48/edit.png @@ -921,6 +928,7 @@ themes/monowhite/48/contributors.png themes/monowhite/48/stop.png themes/monowhite/48/write_eeprom.png + themes/monowhite/48/sdsync.png themes/yerico/16/paintbrush.png themes/yerico/16/open.png themes/yerico/16/edit.png diff --git a/companion/src/comparedialog.cpp b/companion/src/comparedialog.cpp index 12e154572..1fc7a743d 100644 --- a/companion/src/comparedialog.cpp +++ b/companion/src/comparedialog.cpp @@ -20,42 +20,46 @@ #include "comparedialog.h" #include "ui_comparedialog.h" +#include "appdata.h" #include "helpers.h" +#include "modelslist.h" #include #include -class DragDropHeader { - public: - DragDropHeader(): - general_settings(false), - models_count(0) - { - } - bool general_settings; - uint8_t models_count; - uint8_t models[CPN_MAX_MODELS]; -}; +//class DragDropHeader { +// public: +// DragDropHeader(): +// general_settings(false), +// models_count(0) +// { +// } +// bool general_settings; +// uint8_t models_count; +// uint8_t models[CPN_MAX_MODELS]; +//}; CompareDialog::CompareDialog(QWidget * parent, Firmware * firmware): - QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), - multimodelprinter(firmware), - model1Valid(false), - model2Valid(false), + QDialog(parent, Qt::Window), + multimodelprinter(new MultiModelPrinter(firmware)), ui(new Ui::CompareDialog) { ui->setupUi(this); - setWindowIcon(CompanionIcon("compare.png")); setAcceptDrops(true); + if (!g.compareWinGeo().isEmpty()) { + restoreGeometry(g.compareWinGeo()); + } +} - //setDragDropOverwriteMode(true); - //setDropIndicatorShown(true); - // TODO scroll to top ... ui->textEdit->scrollToAnchor("1"); +CompareDialog::~CompareDialog() +{ + delete multimodelprinter; + delete ui; } void CompareDialog::dragMoveEvent(QDragMoveEvent * event) { - if (event->mimeData()->hasFormat("application/x-companion")) { + if (event->mimeData()->hasFormat("application/x-companion-modeldata")) { event->acceptProposedAction(); } else { @@ -65,8 +69,7 @@ void CompareDialog::dragMoveEvent(QDragMoveEvent * event) void CompareDialog::dragEnterEvent(QDragEnterEvent * event) { - // accept just text/uri-list mime format - if (event->mimeData()->hasFormat("application/x-companion")) { + if (event->mimeData()->hasFormat("application/x-companion-modeldata")) { event->acceptProposedAction(); } else { @@ -79,55 +82,85 @@ void CompareDialog::dragLeaveEvent(QDragLeaveEvent * event) event->accept(); } -bool CompareDialog::handleDroppedModel(const QMimeData * mimeData, ModelData & model, QLabel * label) +bool CompareDialog::handleMimeData(const QMimeData * mimeData) { - if (mimeData->hasFormat("application/x-companion")) { - QByteArray gmData = mimeData->data("application/x-companion"); - DragDropHeader * header = (DragDropHeader *)gmData.data(); - if (!header->general_settings) { - char * gData = gmData.data() + sizeof(DragDropHeader); - if (gData[0] == 'M') { - ModelData * modeltemp = (ModelData *)(gData + 1); - if (modeltemp->used) { - memcpy(&model, modeltemp, sizeof(ModelData)); - QString name(model.name); - if (!name.isEmpty()) - label->setText(name); - else - label->setText(tr("No name")); - return true; - } - } - } + QVector mList; + GeneralSettings gs; + if (!TreeModel::decodeMimeData(mimeData, &mList, &gs) || mList.isEmpty()) + return false; + for (int i=0; i < mList.size(); ++i) { + GMData data; + data.model = mList[i]; + data.gs = gs; + modelsList.append(data); } - return false; + return true; +} + +void CompareDialog::closeEvent(QCloseEvent * event) +{ + g.compareWinGeo(saveGeometry()); } void CompareDialog::dropEvent(QDropEvent *event) { - QLabel * child = qobject_cast(childAt(event->pos())); - if (!child) return; - if (child->objectName().contains("label_1")) { - model1Valid = handleDroppedModel(event->mimeData(), model1, ui->label_1); - } - else if (child->objectName().contains("label_2")) { - model2Valid = handleDroppedModel(event->mimeData(), model2, ui->label_2); - } - event->accept(); - if (model1Valid && model2Valid) { - multimodelprinter.setModel(0, model1); - multimodelprinter.setModel(1, model2); - ui->textEdit->setHtml(multimodelprinter.print(ui->textEdit->document())); + if (handleMimeData(event->mimeData())) { + event->accept(); + compare(); } } -void CompareDialog::closeEvent(QCloseEvent *event) +void CompareDialog::compare() { + QLayoutItem *child; + while ((child = ui->layout_modelNames->takeAt(0))) { + if (child->widget()) + delete child->widget(); + delete child; + } + + multimodelprinter->clearModels(); + ui->textEdit->clear(); + + for (int i=0; i < modelsList.size(); ++i) { + multimodelprinter->setModel(i, &modelsList[i].model, &modelsList[i].gs); + QString name(modelsList.at(i).model.name); + if (name.isEmpty()) + name = tr("Unnamed Model %1").arg(i+1); + + QWidget * hdr = new QWidget(this); + hdr->setLayout(new QHBoxLayout()); + hdr->layout()->setContentsMargins(0, 0, 0, 0); + hdr->layout()->setSpacing(2); + QToolButton * btn = new QToolButton(hdr); + btn->setIcon(CompanionIcon("clear.png")); + btn->setProperty("index", i); + btn->setFixedSize(18, 18); + btn->setToolTip(tr("Click to remove this model.")); + hdr->layout()->addWidget(btn); + QLabel * lbl = new QLabel(name, this); + lbl->setStyleSheet("font-weight: bold;"); + hdr->layout()->addWidget(lbl); + connect(btn, &QToolButton::clicked, this, &CompareDialog::removeModelBtnClicked); + + ui->layout_modelNames->addWidget(hdr); + } + if (modelsList.size()) + ui->textEdit->setHtml(multimodelprinter->print(ui->textEdit->document())); } -CompareDialog::~CompareDialog() +void CompareDialog::removeModel(int idx) { - delete ui; + if (idx < modelsList.size()) { + modelsList.remove(idx); + compare(); + } +} + +void CompareDialog::removeModelBtnClicked() +{ + if (sender() && sender()->property("index").isValid()) + removeModel(sender()->property("index").toInt()); } void CompareDialog::on_printButton_clicked() diff --git a/companion/src/comparedialog.h b/companion/src/comparedialog.h index ca6d2e0b6..03e709360 100644 --- a/companion/src/comparedialog.h +++ b/companion/src/comparedialog.h @@ -35,27 +35,32 @@ class CompareDialog : public QDialog public: CompareDialog(QWidget *parent, Firmware * firmware); ~CompareDialog(); - + protected: - MultiModelPrinter multimodelprinter; - ModelData model1; - ModelData model2; - bool model1Valid; - bool model2Valid; + struct GMData { + ModelData model; + GeneralSettings gs; + }; + + MultiModelPrinter * multimodelprinter; + QVector modelsList; + QMap modelsMap; Ui::CompareDialog * ui; - + protected slots: + void removeModelBtnClicked(); void on_printButton_clicked(); void on_printFileButton_clicked(); - + protected: - void closeEvent(QCloseEvent * event); - bool handleDroppedModel(const QMimeData * mimeData, ModelData & model, QLabel * label); - void dropEvent(QDropEvent *event); - void dragMoveEvent(QDragMoveEvent *event); - void dragEnterEvent(QDragEnterEvent *event); - void dragLeaveEvent(QDragLeaveEvent *event); - void printDiff(); + virtual void closeEvent(QCloseEvent * event); + virtual void dragMoveEvent(QDragMoveEvent *event); + virtual void dragEnterEvent(QDragEnterEvent *event); + virtual void dragLeaveEvent(QDragLeaveEvent *event); + virtual void dropEvent(QDropEvent *event); + bool handleMimeData(const QMimeData * mimeData); + void compare(); + void removeModel(int idx); }; #endif // _COMPAREDIALOG_H_ diff --git a/companion/src/comparedialog.ui b/companion/src/comparedialog.ui index d56701196..ba4d24c81 100644 --- a/companion/src/comparedialog.ui +++ b/companion/src/comparedialog.ui @@ -29,63 +29,22 @@ Compare Models - - - - - - - 75 - true - - - - true - - - color:green; - - - Drag first model here - - - Qt::AlignCenter - - - - - - - - 75 - true - - - - true - - - color:red; - - - Drag second model here - - - Qt::AlignCenter - - - - - - + true - + + + + + To compare models, drag and drop them anywhere in this window. + + + @@ -122,11 +81,28 @@ + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + - - - + closeButton diff --git a/companion/src/eeprominterface.cpp b/companion/src/eeprominterface.cpp index ccfe39d4d..d13b0e108 100644 --- a/companion/src/eeprominterface.cpp +++ b/companion/src/eeprominterface.cpp @@ -1502,6 +1502,7 @@ RadioData::RadioData() void ModelData::clear() { memset(this, 0, sizeof(ModelData)); + modelIndex = -1; // an invalid index, this is managed by the TreeView data model moduleData[0].channelsCount = 8; moduleData[1].channelsStart = 0; moduleData[1].channelsCount = 8; diff --git a/companion/src/mainwindow.cpp b/companion/src/mainwindow.cpp index 2544913c2..c7fb519e4 100644 --- a/companion/src/mainwindow.cpp +++ b/companion/src/mainwindow.cpp @@ -72,38 +72,35 @@ MainWindow::MainWindow(): downloadDialog_forWait(NULL), checkForUpdatesState(0), - fileMenu(NULL), - editMenu(NULL), - settingsMenu(NULL), - burnMenu(NULL), - helpMenu(NULL), - fileToolBar(NULL), - editToolBar(NULL), - burnToolBar(NULL), - helpToolBar(NULL) + windowsListActions(new QActionGroup(this)) { + // setUnifiedTitleAndToolBarOnMac(true); + this->setWindowIcon(QIcon(":/icon.png")); + QNetworkProxyFactory::setUseSystemConfiguration(true); + setAcceptDrops(true); + mdiArea = new QMdiArea(this); mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + mdiArea->setTabsClosable(true); + mdiArea->setTabsMovable(true); + mdiArea->setDocumentMode(true); + connect(mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::updateMenus); + setCentralWidget(mdiArea); - connect(mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(updateMenus())); - windowMapper = new QSignalMapper(this); - connect(windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*))); createActions(); createMenus(); createToolBars(); - createStatusBar(); + retranslateUi(); updateMenus(); - restoreState(g.mainWinState()); + setIconThemeSize(g.iconSize()); restoreGeometry(g.mainWinGeo()); + restoreState(g.mainWinState()); + setTabbedWindows(g.tabbedMdi()); - // setUnifiedTitleAndToolBarOnMac(true); - this->setWindowIcon(QIcon(":/icon.png")); - this->setIconSize(QSize(32, 32)); - QNetworkProxyFactory::setUseSystemConfiguration(true); - setAcceptDrops(true); + connect(windowsListActions, &QActionGroup::triggered, this, &MainWindow::onChangeWindowAction); // give time to the splash to disappear and main window to open before starting updates int updateDelay = 1000; @@ -129,7 +126,8 @@ MainWindow::MainWindow(): printfilename = strl[count]; } } - if (strl.count()>1) str = strl[1]; + if (strl.count() > 1) + str = strl[1]; if (!str.isEmpty()) { int fileType = getStorageType(str); @@ -153,6 +151,14 @@ MainWindow::MainWindow(): } } +MainWindow::~MainWindow() +{ + if (windowsListActions) { + delete windowsListActions; + windowsListActions = NULL; + } +} + void MainWindow::displayWarnings() { int warnId = g.warningId(); @@ -522,6 +528,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { g.mainWinGeo(saveGeometry()); g.mainWinState(saveState()); + g.tabbedMdi(actTabbedWindows->isChecked()); mdiArea->closeAllSubWindows(); if (mdiArea->currentSubWindow()) { event->ignore(); @@ -536,21 +543,13 @@ void MainWindow::changeEvent(QEvent * e) QMainWindow::changeEvent(e); switch (e->type()) { case QEvent::LanguageChange: - retranslateUi(); + retranslateUi(true); break; default: break; } } -void MainWindow::retranslateUi() -{ - createActions(); - createMenus(); - createToolBars(); - QMessageBox::information(this, tr("Companion"), tr("Some text will not be translated until the next time you start Companion. Please note that some translations may not be complete.")); -} - void MainWindow::setLanguage(const QString & langString) { g.locale(langString); @@ -580,8 +579,26 @@ void MainWindow::onThemeChanged(QAction * act) void MainWindow::setIconThemeSize(int index) { - g.iconSize(index); - QMessageBox::information(this, tr("Companion"), tr("The icon size will be used the next time you start Companion.")); + if (index != g.iconSize()) + g.iconSize(index); + + QSize size; + switch(g.iconSize()) { + case 0: + size=QSize(16,16); + break; + case 2: + size=QSize(32,32); + break; + case 3: + size=QSize(48,48); + break; + case 1: + default: + size=QSize(24,24); + break; + } + this->setIconSize(size); } void MainWindow::onIconSizeChanged(QAction * act) @@ -592,14 +609,22 @@ void MainWindow::onIconSizeChanged(QAction * act) setIconThemeSize(id); } +void MainWindow::setTabbedWindows(bool on) +{ + mdiArea->setViewMode(on ? QMdiArea::TabbedView : QMdiArea::SubWindowView); + if (actTileWindows) + actTileWindows->setDisabled(on); + if (actCascadeWindows) + actCascadeWindows->setDisabled(on); + + if (actTabbedWindows->isChecked() != on) + actTabbedWindows->setChecked(on); +} + void MainWindow::newFile() { MdiChild * child = createMdiChild(); child->newFile(); - - if (IS_HORUS(getCurrentBoard())) { - child->categoryAdd(); - } child->show(); } @@ -652,6 +677,12 @@ void MainWindow::saveAs() } } +void MainWindow::closeFile() +{ + if (mdiArea->activeSubWindow()) + mdiArea->activeSubWindow()->close(); +} + void MainWindow::openRecentFile() { QAction *action = qobject_cast(sender()); @@ -669,7 +700,7 @@ void MainWindow::loadProfileId(const unsigned pid) // TODO Load all variables - // Set the new profile number g.id(pid); Firmware::setCurrentVariant(Firmware::getFirmwareForId(g.profile[pid].fwType())); - emit FirmwareChanged(); + emit firmwareChanged(); updateMenus(); } @@ -754,47 +785,11 @@ void MainWindow::customizeSplash() dialog->deleteLater(); } -void MainWindow::cut() -{ - if (activeMdiChild()) { - activeMdiChild()->cut(); - updateMenus(); - } -} - -void MainWindow::copy() -{ - if (activeMdiChild()) { - activeMdiChild()->copy(); - updateMenus(); - } -} - -void MainWindow::paste() -{ - if (activeMdiChild()) activeMdiChild()->paste(); -} - void MainWindow::writeEeprom() { if (activeMdiChild()) activeMdiChild()->writeEeprom(); } -void MainWindow::simulate() -{ - if (activeMdiChild()) activeMdiChild()->radioSimulate(); -} - -void MainWindow::print() -{ - if (activeMdiChild()) activeMdiChild()->print(); -} - -void MainWindow::loadBackup() -{ - if (activeMdiChild()) activeMdiChild()->loadBackup(); -} - void MainWindow::readEeprom() { Board::Type board = getCurrentBoard(); @@ -937,28 +932,68 @@ void MainWindow::about() void MainWindow::updateMenus() { - bool hasMdiChild = (activeMdiChild() != 0); - bool hasModelSelected = (activeMdiChild() && activeMdiChild()->hasModelSelected()); + QMdiSubWindow * activeChild = mdiArea->activeSubWindow(); newAct->setEnabled(true); openAct->setEnabled(true); - saveAct->setEnabled(hasMdiChild); - saveAsAct->setEnabled(hasMdiChild); - cutAct->setEnabled(hasModelSelected); - copyAct->setEnabled(hasModelSelected); - pasteAct->setEnabled(hasMdiChild ? activeMdiChild()->hasPasteData() : false); - writeEepromAct->setEnabled(hasMdiChild); + saveAct->setEnabled(activeChild); + saveAsAct->setEnabled(activeChild); + closeAct->setEnabled(activeChild); + compareAct->setEnabled(activeChild); + writeEepromAct->setEnabled(activeChild); readEepromAct->setEnabled(true); - writeBackupToRadioAct->setEnabled(true); - readBackupToFileAct->setEnabled(true); - for (int i=0; isetEnabled(true); + writeBUToRadioAct->setEnabled(true); + readBUToFileAct->setEnabled(true); + + foreach (QAction * act, fileWindowActions) { + if (!act) + continue; + if (fileMenu && fileMenu->actions().contains(act)) + fileMenu->removeAction(act); + if (fileToolBar && fileToolBar->actions().contains(act)) { + fileToolBar->removeAction(act); + } + if (act->isSeparator() && act->parent() == this) + delete act; } - separatorAct->setVisible(hasMdiChild); - simulateAct->setEnabled(hasMdiChild); - printAct->setEnabled(hasModelSelected); - loadbackupAct->setEnabled(hasMdiChild); - compareAct->setEnabled(activeMdiChild()); + fileWindowActions.clear(); + if (activeChild) { + editMenu->clear(); + editMenu->addActions(activeMdiChild()->getEditActions()); + editMenu->addSeparator(); + editMenu->addActions(activeMdiChild()->getModelActions()); // maybe separate menu/toolbar? + editMenu->setEnabled(true); + + editToolBar->clear(); + editToolBar->addActions(activeMdiChild()->getEditActions()); + editToolBar->setEnabled(true); + if (activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV)) { + // workaround for default split button appearance of action with menu :-/ + QToolButton * btn; + if ((btn = qobject_cast(editToolBar->widgetForAction(activeMdiChild()->getAction(MdiChild::ACT_MDL_MOV))))) + btn->setPopupMode(QToolButton::InstantPopup); + } + + fileWindowActions = activeMdiChild()->getGeneralActions(); + QAction *sep = new QAction(this); + sep->setSeparator(true); + fileWindowActions.append(sep); + fileMenu->insertActions(logsAct, fileWindowActions); + fileToolBar->insertActions(logsAct, fileWindowActions); + } + else { + editToolBar->setDisabled(true); + editMenu->setDisabled(true); + } + + foreach (QAction * act, windowsListActions->actions()) { + if (act->property("window_ptr").canConvert() && + act->property("window_ptr").value() == activeChild) { + act->setChecked(true); + break; + } + } + updateRecentFileActions(); updateProfilesActions(); setWindowTitle(tr("OpenTX Companion %1 - Radio: %2 - Profile: %3").arg(VERSION).arg(getCurrentFirmware()->getName()).arg(g.profile[g.id()].name())); @@ -966,53 +1001,51 @@ void MainWindow::updateMenus() MdiChild * MainWindow::createMdiChild() { - MdiChild * child = new MdiChild(this); - mdiArea->addSubWindow(child); - if (!child->parentWidget()->isMaximized() && !child->parentWidget()->isMinimized()) { - child->parentWidget()->resize(400, 400); - } + QMdiSubWindow * win = new QMdiSubWindow(); + MdiChild * child = new MdiChild(this, win); + win->setAttribute(Qt::WA_DeleteOnClose); + win->setWidget(child); + mdiArea->addSubWindow(win); + if (g.mdiWinGeo().size() < 10 && g.mdiWinGeo() == "maximized") + win->showMaximized(); - connect(child, SIGNAL(copyAvailable(bool)),cutAct, SLOT(setEnabled(bool))); - connect(child, SIGNAL(copyAvailable(bool)),copyAct, SLOT(setEnabled(bool))); - connect(child, SIGNAL(copyAvailable(bool)),simulateAct, SLOT(setEnabled(bool))); - connect(child, SIGNAL(copyAvailable(bool)),printAct, SLOT(setEnabled(bool))); + connect(this, &MainWindow::firmwareChanged, child, &MdiChild::onFirmwareChanged); + connect(child, &MdiChild::windowTitleChanged, this, &MainWindow::onSubwindowTitleChanged); + connect(child, &MdiChild::modified, this, &MainWindow::onSubwindowModified); + connect(child, &MdiChild::newStatusMessage, statusBar(), &QStatusBar::showMessage); + connect(win, &QMdiSubWindow::destroyed, this, &MainWindow::updateWindowActions); + updateWindowActions(); return child; } -QAction * MainWindow::addAct(const QString & icon, const QString & sName, const QString & lName, enum QKeySequence::StandardKey shortcut, const char *slot, QObject *slotObj) -{ - return addAct(icon, sName, lName, QKeySequence(shortcut), slot, slotObj); -} - -QAction * MainWindow::addAct(const QString & icon, const QString & sName, const QString & lName, const QKeySequence & shortcut, const char *slot, QObject *slotObj) +QAction * MainWindow::addAct(const QString & icon, const char *slot, const QKeySequence & shortcut, QObject *slotObj, const char * signal) { QAction * newAction = new QAction( this ); + newAction->setMenuRole(QAction::NoRole); if (!icon.isEmpty()) newAction->setIcon(CompanionIcon(icon)); - if (!sName.isEmpty()) - newAction->setText(sName); - if (!lName.isEmpty()) - newAction->setStatusTip(lName); - newAction->setShortcut(shortcut); - if (slotObj == NULL) - slotObj = this; - connect(newAction, SIGNAL(triggered()), slotObj, slot); - actionsList.append(newAction); + if (!shortcut.isEmpty()) + newAction->setShortcut(shortcut); + if (slot) { + if (!slotObj) + slotObj = this; + if (!signal) + connect(newAction, SIGNAL(triggered()), slotObj, slot); + else + connect(newAction, signal, slotObj, slot); + } return newAction; } - -QAction * MainWindow::addAct(const QString & icon, const QString & sName, const QString & lName, const char *slot) -{ - return addAct(icon, sName, lName, QKeySequence::UnknownKey, slot); -} - -QAction * MainWindow::addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, const char * propName, const QVariant & propValue, const QVariant & dfltValue) +QAction * MainWindow::addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, const char * propName, const QVariant & propValue, const QVariant & dfltValue, const QKeySequence & shortcut) { QAction * act = aGroup->addAction(sName); + act->setMenuRole(QAction::NoRole); act->setStatusTip(lName); act->setCheckable(true); + if (!shortcut.isEmpty()) + act->setShortcut(shortcut); if (propName) { act->setProperty(propName, propValue); if (propValue == dfltValue) @@ -1021,77 +1054,292 @@ QAction * MainWindow::addActToGroup(QActionGroup * aGroup, const QString & sName return act; } +void MainWindow::trAct(QAction * act, const QString & text, const QString & descript) +{ + if (!text.isEmpty()) + act->setText(text); + if (!descript.isEmpty()) + act->setStatusTip(descript); +} + +void MainWindow::retranslateUi(bool showMsg) +{ + trAct(newAct, tr("New"), tr("Create a new Models and Settings file")); + trAct(openAct, tr("Open..."), tr("Open Models and Settings file")); + trAct(saveAct, tr("Save"), tr("Save Models and Settings file")); + trAct(saveAsAct, tr("Save As..."), tr("Save Models and Settings file")); + trAct(closeAct, tr("Close"), tr("Close Models and Settings file")); + trAct(exitAct, tr("Exit"), tr("Exit the application")); + trAct(aboutAct, tr("About..."), tr("Show the application's About box")); + + trAct(recentFilesAct, tr("Recent Files"), tr("List of recently used files")); + trAct(profilesMenuAct, tr("Radio Profiles"), tr("Create or Select Radio Profiles")); + trAct(logsAct, tr("View Log File..."), tr("Open and view log file")); + trAct(appPrefsAct, tr("Settings..."), tr("Edit Settings")); + trAct(fwPrefsAct, tr("Download..."), tr("Download firmware and voice files")); + trAct(checkForUpdatesAct, tr("Check for Updates..."), tr("Check OpenTX and Companion updates")); + trAct(changelogAct, tr("Companion Changes..."), tr("Show Companion change log")); + trAct(fwchangelogAct, tr("Firmware Changes..."), tr("Show firmware change log")); + trAct(compareAct, tr("Compare Models..."), tr("Compare models")); + trAct(editSplashAct, tr("Edit Radio Splash Image..."), tr("Edit the splash image of your Radio")); + trAct(burnListAct, tr("List programmers..."), tr("List available programmers")); + trAct(burnFusesAct, tr("Fuses..."), tr("Show fuses dialog")); + trAct(readFlashAct, tr("Read Firmware from Radio"), tr("Read firmware from Radio")); + trAct(writeFlashAct, tr("Write Firmware to Radio"), tr("Write firmware to Radio")); + trAct(sdsyncAct, tr("Synchronize SD"), tr("SD card synchronization")); + + trAct(openDocURLAct, tr("Manuals and other Documents"), tr("Open the OpenTX document page in a web browser")); + trAct(writeEepromAct, tr("Write Models and Settings To Radio"), tr("Write Models and Settings to Radio")); + trAct(readEepromAct, tr("Read Models and Settings From Radio"), tr("Read Models and Settings from Radio")); + trAct(burnConfigAct, tr("Configure Communications..."), tr("Configure software for communicating with the Radio")); + trAct(writeBUToRadioAct, tr("Write Backup to Radio"), tr("Write Backup from file to Radio")); + trAct(readBUToFileAct, tr("Backup Radio to File"), tr("Save a complete backup file of all settings and model data in the Radio")); + trAct(contributorsAct, tr("Contributors..."), tr("A tribute to those who have contributed to OpenTX and Companion")); + + trAct(createProfileAct, tr("Add Radio Profile"), tr("Create a new Radio Settings Profile")); + trAct(copyProfileAct, tr("Copy Current Radio Profile"), tr("Duplicate current Radio Settings Profile")); + trAct(deleteProfileAct, tr("Delete Current Radio Profile..."), tr("Delete the current Radio Settings Profile")); + + trAct(actTabbedWindows, tr("Tabbed Windows"), tr("Use tabs to arrange open windows.")); + trAct(actTileWindows, tr("Tile Windows"), tr("Arrange open windows across all the available space.")); + trAct(actCascadeWindows, tr("Cascade Windows"), tr("Arrange all open windows in a stack.")); + trAct(actCloseAllWindows, tr("Close All Windows"), tr("Closes all open files (prompts to save if necessary.")); + + editMenu->setTitle(tr("Edit")); + fileMenu->setTitle(tr("File")); + settingsMenu->setTitle(tr("Settings")); + themeMenu->setTitle(tr("Set Icon Theme")); + iconThemeSizeMenu->setTitle(tr("Set Icon Size")); + burnMenu->setTitle(tr("Read/Write")); + windowMenu->setTitle(tr("Window")); + helpMenu->setTitle(tr("Help")); + + fileToolBar->setWindowTitle(tr("File")); + editToolBar->setWindowTitle(tr("Edit")); + burnToolBar->setWindowTitle(tr("Write")); + helpToolBar->setWindowTitle(tr("Help")); + + showReadyStatus(); + + if (showMsg) + QMessageBox::information(this, tr("Companion"), tr("Some text will not be translated until the next time you start Companion. Please note that some translations may not be complete.")); +} + void MainWindow::createActions() { - foreach (QAction * act, actionsList) { - if (act) - act->deleteLater(); + newAct = addAct("new.png", SLOT(newFile()), QKeySequence::New); + openAct = addAct("open.png", SLOT(openFile()), QKeySequence::Open); + saveAct = addAct("save.png", SLOT(save()), QKeySequence::Save); + 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 + closeAct = addAct("clear.png", SLOT(closeFile()) /*, QKeySequence::Close*/); // setting/showing this shortcut interferes with the system one (Ctrl+W/Ctrl-F4) + exitAct = addAct("exit.png", SLOT(closeAllWindows()), QKeySequence::Quit, qApp); + + logsAct = addAct("logs.png", SLOT(logFile()), tr("Ctrl+Alt+L")); + appPrefsAct = addAct("apppreferences.png", SLOT(appPrefs()), QKeySequence::Preferences); + fwPrefsAct = addAct("fwpreferences.png", SLOT(fwPrefs()), tr("Ctrl+Alt+D")); + compareAct = addAct("compare.png", SLOT(compare()), tr("Ctrl+Alt+R")); + sdsyncAct = addAct("sdsync.png", SLOT(sdsync())); + + editSplashAct = addAct("paintbrush.png", SLOT(customizeSplash())); + burnListAct = addAct("list.png", SLOT(burnList())); + burnFusesAct = addAct("fuses.png", SLOT(burnFuses())); + readFlashAct = addAct("read_flash.png", SLOT(readFlash())); + writeFlashAct = addAct("write_flash.png", SLOT(writeFlash())); + writeEepromAct = addAct("write_eeprom.png", SLOT(writeEeprom())); + readEepromAct = addAct("read_eeprom.png", SLOT(readEeprom())); + burnConfigAct = addAct("configure.png", SLOT(burnConfig())); + writeBUToRadioAct = addAct("write_eeprom_file.png", SLOT(writeBackup())); + readBUToFileAct = addAct("read_eeprom_file.png", SLOT(readBackup())); + + createProfileAct = addAct("new.png", SLOT(createProfile())); + copyProfileAct = addAct("copy.png", SLOT(copyProfile())); + deleteProfileAct = addAct("clear.png", SLOT(deleteCurrentProfile())); + + actTabbedWindows = addAct("", SLOT(setTabbedWindows(bool)), 0, this, SIGNAL(triggered(bool))); + actTileWindows = addAct("", SLOT(tileSubWindows()), 0, mdiArea); + actCascadeWindows = addAct("", SLOT(cascadeSubWindows()), 0, mdiArea); + actCloseAllWindows = addAct("", SLOT(closeAllSubWindows()), 0, mdiArea); + + checkForUpdatesAct = addAct("update.png", SLOT(doUpdates())); + aboutAct = addAct("information.png", SLOT(about())); + openDocURLAct = addAct("changelog.png", SLOT(openDocURL())); + changelogAct = addAct("changelog.png", SLOT(changelog())); + fwchangelogAct = addAct("changelog.png", SLOT(fwchangelog())); + contributorsAct = addAct("contributors.png", SLOT(contributors())); + + // these two get assigned menus in createMenus() + recentFilesAct = addAct("recentdocument.png"); + profilesMenuAct = addAct("profiles.png"); + + exitAct->setMenuRole(QAction::QuitRole); + aboutAct->setMenuRole(QAction::AboutRole); + appPrefsAct->setMenuRole(QAction::PreferencesRole); + contributorsAct->setMenuRole(QAction::ApplicationSpecificRole); + openDocURLAct->setMenuRole(QAction::ApplicationSpecificRole); + checkForUpdatesAct->setMenuRole(QAction::ApplicationSpecificRole); + changelogAct->setMenuRole(QAction::ApplicationSpecificRole); + fwchangelogAct->setMenuRole(QAction::ApplicationSpecificRole); + + actTabbedWindows->setCheckable(true); + compareAct->setEnabled(false); +} + +void MainWindow::createMenus() +{ + fileMenu = menuBar()->addMenu(""); + fileMenu->addAction(newAct); + fileMenu->addAction(openAct); + fileMenu->addAction(saveAct); + fileMenu->addAction(saveAsAct); + fileMenu->addAction(closeAct); + fileMenu->addAction(recentFilesAct); + fileMenu->addSeparator(); + fileMenu->addAction(logsAct); + fileMenu->addAction(fwPrefsAct); + fileMenu->addAction(compareAct); + fileMenu->addAction(sdsyncAct); + fileMenu->addSeparator(); + fileMenu->addAction(exitAct); + + editMenu = menuBar()->addMenu(""); + + settingsMenu = menuBar()->addMenu(""); + settingsMenu->addMenu(createLanguageMenu(settingsMenu)); + + themeMenu = settingsMenu->addMenu(""); + QActionGroup * themeGroup = new QActionGroup(themeMenu); + addActToGroup(themeGroup, tr("Classical"), tr("The classic companion9x icon theme"), "themeId", 0, g.theme()); + addActToGroup(themeGroup, tr("Yerico"), tr("Yellow round honey sweet icon theme"), "themeId", 1, g.theme()); + addActToGroup(themeGroup, tr("Monochrome"), tr("A monochrome black icon theme"), "themeId", 3, g.theme()); + addActToGroup(themeGroup, tr("MonoBlue"), tr("A monochrome blue icon theme"), "themeId", 4, g.theme()); + addActToGroup(themeGroup, tr("MonoWhite"), tr("A monochrome white icon theme"), "themeId", 2, g.theme()); + connect(themeGroup, &QActionGroup::triggered, this, &MainWindow::onThemeChanged); + themeMenu->addActions(themeGroup->actions()); + + iconThemeSizeMenu = settingsMenu->addMenu(""); + QActionGroup * szGroup = new QActionGroup(iconThemeSizeMenu); + addActToGroup(szGroup, tr("Small"), tr("Use small toolbar icons"), "sizeId", 0, g.iconSize()); + addActToGroup(szGroup, tr("Normal"), tr("Use normal size toolbar icons"), "sizeId", 1, g.iconSize()); + addActToGroup(szGroup, tr("Big"), tr("Use big toolbar icons"), "sizeId", 2, g.iconSize()); + addActToGroup(szGroup, tr("Huge"), tr("Use huge toolbar icons"), "sizeId", 3, g.iconSize()); + connect(szGroup, &QActionGroup::triggered, this, &MainWindow::onIconSizeChanged); + iconThemeSizeMenu->addActions(szGroup->actions()); + + settingsMenu->addSeparator(); + settingsMenu->addAction(appPrefsAct); + settingsMenu->addAction(profilesMenuAct); + settingsMenu->addAction(editSplashAct); + settingsMenu->addAction(burnConfigAct); + + burnMenu = menuBar()->addMenu(""); + burnMenu->addAction(writeEepromAct); + burnMenu->addAction(readEepromAct); + burnMenu->addSeparator(); + burnMenu->addAction(writeBUToRadioAct); + burnMenu->addAction(readBUToFileAct); + burnMenu->addSeparator(); + burnMenu->addAction(writeFlashAct); + burnMenu->addAction(readFlashAct); + burnMenu->addSeparator(); + burnMenu->addSeparator(); + if (!IS_ARM(getCurrentBoard())) { + burnMenu->addAction(burnFusesAct); + burnMenu->addAction(burnListAct); } - actionsList.clear(); - separatorAct = new QAction(this); - separatorAct->setSeparator(true); - actionsList.append(separatorAct); + windowMenu = menuBar()->addMenu(""); + windowMenu->addAction(actTabbedWindows); + windowMenu->addAction(actTileWindows); + windowMenu->addAction(actCascadeWindows); + windowMenu->addAction(actCloseAllWindows); + windowMenu->addSeparator(); - for (int i = 0; i < MAX_RECENT; ++i) { - recentFileActs[i] = new QAction(this); + helpMenu = menuBar()->addMenu(""); + helpMenu->addSeparator(); + helpMenu->addAction(checkForUpdatesAct); + helpMenu->addSeparator(); + helpMenu->addAction(aboutAct); + helpMenu->addAction(openDocURLAct); + helpMenu->addSeparator(); + helpMenu->addAction(changelogAct); + helpMenu->addAction(fwchangelogAct); + helpMenu->addSeparator(); + helpMenu->addAction(contributorsAct); + + recentFilesMenu = new QMenu(this); + for ( int i = 0; i < g.historySize(); ++i) { + recentFileActs.append(recentFilesMenu->addAction("")); recentFileActs[i]->setVisible(false); connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile())); - actionsList.append(recentFileActs[i]); } - updateRecentFileActions(); + recentFilesAct->setMenu(recentFilesMenu); - QActionGroup *profilesAlignmentGroup = new QActionGroup(this); - for (int i=0; iaddAction("")); profileActs[i]->setVisible(false); profileActs[i]->setCheckable(true); connect(profileActs[i], SIGNAL(triggered()), this, SLOT(loadProfile())); - actionsList.append(profileActs[i]); + profilesGroup->addAction(profileActs[i]); } - updateProfilesActions(); + profilesMenu->addSeparator(); + profilesMenu->addAction(createProfileAct); + profilesMenu->addAction(copyProfileAct); + profilesMenu->addAction(deleteProfileAct); + profilesMenuAct->setMenu(profilesMenu); +} - newAct = addAct("new.png", tr("New"), tr("Create a new Models and Settings file"), QKeySequence::New, SLOT(newFile())); - openAct = addAct("open.png", tr("Open..."), tr("Open Models and Settings file"), QKeySequence::Open, SLOT(openFile())); - saveAct = addAct("save.png", tr("Save"), tr("Save Models and Settings file"), QKeySequence::Save, SLOT(save())); - saveAsAct = addAct("saveas.png", tr("Save As..."), tr("Save Models and Settings file"), QKeySequence::SaveAs, SLOT(saveAs())); - exitAct = addAct("exit.png", tr("Exit"), tr("Exit the application"), QKeySequence::Quit, SLOT(closeAllWindows()), qApp); - cutAct = addAct("cut.png", tr("Cut Model"), tr("Cut current model to the clipboard"), QKeySequence::Cut, SLOT(cut())); - copyAct = addAct("copy.png", tr("Copy Model"), tr("Copy current model to the clipboard"), QKeySequence::Copy, SLOT(copy())); - pasteAct = addAct("paste.png", tr("Paste Model"), tr("Paste model from clipboard"), QKeySequence::Paste, SLOT(paste())); +void MainWindow::createToolBars() +{ + fileToolBar = addToolBar(""); + fileToolBar->setObjectName("File"); + fileToolBar->addAction(newAct); + fileToolBar->addAction(openAct); + fileToolBar->addAction(recentFilesAct); + fileToolBar->addAction(saveAct); + fileToolBar->addAction(closeAct); + fileToolBar->addSeparator(); + fileToolBar->addAction(logsAct); + fileToolBar->addAction(fwPrefsAct); + fileToolBar->addSeparator(); + fileToolBar->addAction(appPrefsAct); + fileToolBar->addAction(profilesMenuAct); + fileToolBar->addAction(editSplashAct); + fileToolBar->addAction(editSplashAct); + fileToolBar->addSeparator(); + fileToolBar->addAction(compareAct); + fileToolBar->addAction(sdsyncAct); - aboutAct = addAct("information.png", tr("About..."), tr("Show the application's About box"), SLOT(about())); - printAct = addAct("print.png", tr("Print..."), tr("Print current model"), QKeySequence::Print, SLOT(print())); - simulateAct = addAct("simulate.png", tr("Simulate..."), tr("Simulate current model"), QKeySequence(tr("Alt+S")), SLOT(simulate())); - loadbackupAct = addAct("open.png", tr("Load Backup..."), tr("Load backup from file"), SLOT(loadBackup())); - logsAct = addAct("logs.png", tr("View Log File..."), tr("Open and view log file"), SLOT(logFile())); - appPrefsAct = addAct("apppreferences.png",tr("Settings..."), tr("Edit Settings"), SLOT(appPrefs())); - fwPrefsAct = addAct("fwpreferences.png", tr("Download..."), tr("Download firmware and voice files"), SLOT(fwPrefs())); - checkForUpdatesAct = addAct("update.png", tr("Check for Updates..."), tr("Check OpenTX and Companion updates"), SLOT(doUpdates())); - changelogAct = addAct("changelog.png", tr("Companion Changes..."), tr("Show Companion change log"), SLOT(changelog())); - fwchangelogAct = addAct("changelog.png", tr("Firmware Changes..."), tr("Show firmware change log"), SLOT(fwchangelog())); - compareAct = addAct("compare.png", tr("Compare Models..."), tr("Compare models"), SLOT(compare())); - editSplashAct = addAct("paintbrush.png", tr("Edit Radio Splash Image..."), tr("Edit the splash image of your Radio"), SLOT(customizeSplash())); - burnListAct = addAct("list.png", tr("List programmers..."), tr("List available programmers"), SLOT(burnList())); - burnFusesAct = addAct("fuses.png", tr("Fuses..."), tr("Show fuses dialog"), SLOT(burnFuses())); - readFlashAct = addAct("read_flash.png", tr("Read Firmware from Radio"),tr("Read firmware from Radio"), SLOT(readFlash())); - writeFlashAct = addAct("write_flash.png", tr("Write Firmware to Radio"), tr("Write firmware to Radio"), SLOT(writeFlash())); - openDocURLAct = addAct("", tr("Manuals and other Documents"), tr("Open the OpenTX document page in a web browser"), SLOT(openDocURL())); - writeEepromAct = addAct("write_eeprom.png", tr("Write Models and Settings To Radio"), tr("Write Models and Settings to Radio"), SLOT(writeEeprom())); - readEepromAct = addAct("read_eeprom.png", tr("Read Models and Settings From Radio"), tr("Read Models and Settings from Radio"), SLOT(readEeprom())); - burnConfigAct = addAct("configure.png", tr("Configure Communications..."), tr("Configure software for communicating with the Radio"), SLOT(burnConfig())); - writeBackupToRadioAct = addAct("write_eeprom_file.png", tr("Write Backup to Radio"), tr("Write Backup from file to Radio"), SLOT(writeBackup())); - readBackupToFileAct = addAct("read_eeprom_file.png", tr("Backup Radio to File"), tr("Save a complete backup file of all settings and model data in the Radio"), SLOT(readBackup())); - contributorsAct = addAct("contributors.png", tr("Contributors..."), tr("A tribute to those who have contributed to OpenTX and Companion"), SLOT(contributors())); - sdsyncAct = addAct("sdsync.png", tr("Synchronize SD"), tr("SD card synchronization"), SLOT(sdsync())); + // workaround for default split button appearance of action with menu :-/ + QToolButton * btn; + if ((btn = qobject_cast(fileToolBar->widgetForAction(recentFilesAct)))) + btn->setPopupMode(QToolButton::InstantPopup); + if ((btn = qobject_cast(fileToolBar->widgetForAction(profilesMenuAct)))) + btn->setPopupMode(QToolButton::InstantPopup); - createProfileAct = addAct("new.png", tr("Add Radio Profile"), tr("Create a new Radio Settings Profile"), SLOT(createProfile())); - copyProfileAct = addAct("copy.png", tr("Copy Current Radio Profile"), tr("Duplicate current Radio Settings Profile"), SLOT(copyProfile())); - deleteProfileAct = addAct("clear.png", tr("Delete Current Radio Profile"), tr("Delete the current Radio Settings Profile"), SLOT(deleteCurrentProfile())); + // gets populated later + editToolBar = addToolBar(""); + editToolBar->setObjectName("Edit"); - compareAct->setEnabled(false); - simulateAct->setEnabled(false); - printAct->setEnabled(false); + burnToolBar = new QToolBar(this); + addToolBar( Qt::LeftToolBarArea, burnToolBar ); + burnToolBar->setObjectName("Write"); + burnToolBar->addAction(writeEepromAct); + burnToolBar->addAction(readEepromAct); + burnToolBar->addSeparator(); + burnToolBar->addAction(writeBUToRadioAct); + burnToolBar->addAction(readBUToFileAct); + burnToolBar->addSeparator(); + burnToolBar->addAction(writeFlashAct); + burnToolBar->addAction(readFlashAct); + burnToolBar->addSeparator(); + burnToolBar->addAction(burnConfigAct); + + helpToolBar = addToolBar(""); + helpToolBar->setObjectName("Help"); + helpToolBar->addAction(checkForUpdatesAct); + helpToolBar->addAction(aboutAct); } QMenu * MainWindow::createLanguageMenu(QWidget * parent) @@ -1114,246 +1362,7 @@ QMenu * MainWindow::createLanguageMenu(QWidget * parent) return menu; } -void MainWindow::createMenus() -{ - menuBar()->clear(); - - if (fileMenu) { - fileMenu->deleteLater(); - } - fileMenu = menuBar()->addMenu(tr("File")); - fileMenu->addAction(newAct); - fileMenu->addAction(openAct); - fileMenu->addAction(saveAct); - fileMenu->addAction(saveAsAct); - - QMenu *recentFileMenu = new QMenu(tr("Recent Files"), fileMenu); - recentFileMenu->setIcon(CompanionIcon("recentdocument.png")); - for (int i=0; iaddAction(recentFileActs[i]); - } - - fileMenu->addMenu(recentFileMenu); - fileMenu->addSeparator(); - fileMenu->addAction(logsAct); - fileMenu->addAction(fwPrefsAct); - fileMenu->addSeparator(); - fileMenu->addAction(simulateAct); - fileMenu->addAction(printAct); - fileMenu->addAction(compareAct); - fileMenu->addAction(sdsyncAct); - fileMenu->addSeparator(); - fileMenu->addAction(exitAct); - - if (editMenu) { - editMenu->deleteLater(); - } - editMenu = menuBar()->addMenu(tr("Edit")); - editMenu->addAction(cutAct); - editMenu->addAction(copyAct); - editMenu->addAction(pasteAct); - - if (settingsMenu) { - settingsMenu->deleteLater(); - } - settingsMenu = menuBar()->addMenu(tr("Settings")); - - QMenu *themeMenu = new QMenu(tr("Set Icon Theme"), settingsMenu); - QActionGroup * themeGroup = new QActionGroup(themeMenu); - addActToGroup(themeGroup, tr("Classical"), tr("The classic companion9x icon theme"), "themeId", 0, g.theme()); - addActToGroup(themeGroup, tr("Yerico"), tr("Yellow round honey sweet icon theme"), "themeId", 1, g.theme()); - addActToGroup(themeGroup, tr("Monochrome"), tr("A monochrome black icon theme"), "themeId", 3, g.theme()); - addActToGroup(themeGroup, tr("MonoBlue"), tr("A monochrome blue icon theme"), "themeId", 4, g.theme()); - addActToGroup(themeGroup, tr("MonoWhite"), tr("A monochrome white icon theme"), "themeId", 2, g.theme()); - connect(themeGroup, &QActionGroup::triggered, this, &MainWindow::onThemeChanged); - themeMenu->addActions(themeGroup->actions()); - - QMenu *iconThemeSizeMenu = new QMenu(tr("Set Icon Size"), settingsMenu); - QActionGroup * szGroup = new QActionGroup(iconThemeSizeMenu); - addActToGroup(szGroup, tr("Small"), tr("Use small toolbar icons"), "sizeId", 0, g.iconSize()); - addActToGroup(szGroup, tr("Normal"), tr("Use normal size toolbar icons"), "sizeId", 1, g.iconSize()); - addActToGroup(szGroup, tr("Big"), tr("Use big toolbar icons"), "sizeId", 2, g.iconSize()); - addActToGroup(szGroup, tr("Huge"), tr("Use huge toolbar icons"), "sizeId", 3, g.iconSize()); - connect(szGroup, &QActionGroup::triggered, this, &MainWindow::onIconSizeChanged); - iconThemeSizeMenu->addActions(szGroup->actions()); - - settingsMenu->addMenu(createLanguageMenu(settingsMenu)); - settingsMenu->addMenu(themeMenu); - settingsMenu->addMenu(iconThemeSizeMenu); - settingsMenu->addSeparator(); - settingsMenu->addAction(appPrefsAct); - settingsMenu->addMenu(createProfilesMenu()); - settingsMenu->addAction(editSplashAct); - settingsMenu->addAction(burnConfigAct); - - if (burnMenu) { - burnMenu->deleteLater(); - } - burnMenu = menuBar()->addMenu(tr("Read/Write")); - burnMenu->addAction(writeEepromAct); - burnMenu->addAction(readEepromAct); - burnMenu->addSeparator(); - burnMenu->addAction(writeBackupToRadioAct); - burnMenu->addAction(readBackupToFileAct); - burnMenu->addSeparator(); - burnMenu->addAction(writeFlashAct); - burnMenu->addAction(readFlashAct); - burnMenu->addSeparator(); - burnMenu->addSeparator(); - if (!IS_ARM(getCurrentBoard())) { - burnMenu->addAction(burnFusesAct); - burnMenu->addAction(burnListAct); - } - - if (helpMenu) { - helpMenu->deleteLater(); - } - helpMenu = menuBar()->addMenu(tr("Help")); - helpMenu->addSeparator(); - helpMenu->addAction(checkForUpdatesAct); - helpMenu->addSeparator(); - helpMenu->addAction(aboutAct); - helpMenu->addAction(openDocURLAct); - helpMenu->addSeparator(); - helpMenu->addAction(changelogAct); - helpMenu->addAction(fwchangelogAct); - helpMenu->addSeparator(); - helpMenu->addAction(contributorsAct); -} - -QMenu *MainWindow::createRecentFileMenu() -{ - QMenu *recentFileMenu = new QMenu(this); - for ( int i = 0; i < MAX_RECENT; ++i) { - recentFileMenu->addAction(recentFileActs[i]); - } - return recentFileMenu; -} - -QMenu *MainWindow::createProfilesMenu() -{ - QMenu *profilesMenu=new QMenu(tr("Radio Profile"), this); - for (int i = 0; i < MAX_PROFILES; ++i) { - profilesMenu->addAction(profileActs[i]); - } - profilesMenu->addSeparator(); - - profilesMenu->addAction(createProfileAct); - profilesMenu->addAction(copyProfileAct); - profilesMenu->addAction(deleteProfileAct); - profilesMenu->setIcon(CompanionIcon("profiles.png")); - return profilesMenu; -} - -void MainWindow::createToolBars() -{ - QSize size; - switch(g.iconSize()) { - case 0: - size=QSize(16,16); - break; - case 1: - size=QSize(24,24); - break; - case 2: - size=QSize(32,32); - break; - case 3: - size=QSize(48,48); - break; - default: - size=QSize(24,24); - break; - } - - if (fileToolBar) { - removeToolBar(fileToolBar); - fileToolBar->deleteLater(); - } - - fileToolBar = addToolBar(tr("File")); - fileToolBar->setIconSize(size); - fileToolBar->setObjectName("File"); - fileToolBar->addAction(newAct); - fileToolBar->addAction(openAct); - - QToolButton * recentToolButton = new QToolButton; - recentToolButton->setPopupMode(QToolButton::InstantPopup); - recentToolButton->setFocusPolicy(Qt::NoFocus); - recentToolButton->setMenu(createRecentFileMenu()); - recentToolButton->setIcon(CompanionIcon("recentdocument.png")); - recentToolButton->setToolTip(tr("Recent Models+Settings")); - recentToolButton->setStatusTip(tr("Show recent Models+Settings documents")); - - fileToolBar->addWidget(recentToolButton); - fileToolBar->addAction(saveAct); - fileToolBar->addSeparator(); - fileToolBar->addAction(logsAct); - fileToolBar->addAction(fwPrefsAct); - fileToolBar->addSeparator(); - fileToolBar->addAction(appPrefsAct); - - QToolButton * profileButton = new QToolButton; - profileButton->setPopupMode(QToolButton::InstantPopup); - profileButton->setFocusPolicy(Qt::NoFocus); - profileButton->setMenu(createProfilesMenu()); - profileButton->setIcon(CompanionIcon("profiles.png")); - profileButton->setToolTip(tr("Radio Profile")); - profileButton->setStatusTip(tr("Show the list of radio profiles")); - - fileToolBar->addWidget(profileButton); - fileToolBar->addAction(editSplashAct); - fileToolBar->addSeparator(); - fileToolBar->addAction(simulateAct); - fileToolBar->addAction(printAct); - fileToolBar->addAction(compareAct); - fileToolBar->addAction(sdsyncAct); - - if (editToolBar) { - removeToolBar(editToolBar); - editToolBar->deleteLater(); - } - - editToolBar = addToolBar(tr("Edit")); - editToolBar->setIconSize(size); - editToolBar->setObjectName("Edit"); - editToolBar->addAction(cutAct); - editToolBar->addAction(copyAct); - editToolBar->addAction(pasteAct); - - if (burnToolBar) { - removeToolBar(burnToolBar); - burnToolBar->deleteLater(); - } - - burnToolBar = new QToolBar(tr("Write")); - addToolBar( Qt::LeftToolBarArea, burnToolBar ); - burnToolBar->setIconSize(size); - burnToolBar->setObjectName("Write"); - burnToolBar->addAction(writeEepromAct); - burnToolBar->addAction(readEepromAct); - burnToolBar->addSeparator(); - burnToolBar->addAction(writeBackupToRadioAct); - burnToolBar->addAction(readBackupToFileAct); - burnToolBar->addSeparator(); - burnToolBar->addAction(writeFlashAct); - burnToolBar->addAction(readFlashAct); - burnToolBar->addSeparator(); - burnToolBar->addAction(burnConfigAct); - - if (helpToolBar) { - removeToolBar(helpToolBar); - helpToolBar->deleteLater(); - } - - helpToolBar = addToolBar(tr("Help")); - helpToolBar->setIconSize(size); - helpToolBar->setObjectName("Help"); - helpToolBar->addAction(checkForUpdatesAct); - helpToolBar->addAction(aboutAct); -} - -void MainWindow::createStatusBar() +void MainWindow::showReadyStatus() { statusBar()->showMessage(tr("Ready")); } @@ -1377,24 +1386,18 @@ QMdiSubWindow *MainWindow::findMdiChild(const QString &fileName) return 0; } -void MainWindow::setActiveSubWindow(QWidget *window) -{ - if (!window) return; - mdiArea->setActiveSubWindow(qobject_cast(window)); -} - void MainWindow::updateRecentFileActions() { // Hide all document slots - for (int i=0 ; isetVisible(false); } // Fill slots with content and unhide them QStringList files = g.recentFiles(); - int numRecentFiles = qMin(files.size(), g.historySize()); + int numRecentFiles = qMin(recentFileActs.size(), qMin(files.size(), g.historySize())); - for (int i=0; isetText(text); @@ -1406,7 +1409,7 @@ void MainWindow::updateRecentFileActions() void MainWindow::updateProfilesActions() { - for (int i=0; isetText(text); @@ -1421,6 +1424,77 @@ void MainWindow::updateProfilesActions() } } +void MainWindow::updateWindowActions() +{ + if (!windowsListActions) + return; + + foreach (QAction * act, windowsListActions->actions()) { + windowsListActions->removeAction(act); + if (windowMenu->actions().contains(act)) + windowMenu->removeAction(act); + delete act; + } + int count = 0; + foreach (QMdiSubWindow * win, mdiArea->subWindowList()) { + QString scut; + if (++count < 10) + scut = tr("Alt+%1").arg(count); + QAction * act = addActToGroup(windowsListActions, "", "", "window_ptr", qVariantFromValue(win), QVariant(), scut); + act->setChecked(win == mdiArea->activeSubWindow()); + updateWindowActionTitle(win, act); + } + windowMenu->addActions(windowsListActions->actions()); +} + +void MainWindow::updateWindowActionTitle(const QMdiSubWindow * win, QAction * act) +{ + MdiChild * child = qobject_cast(win->widget()); + if (!child) + return; + + if (!act) { + foreach (QAction * a, windowsListActions->actions()) { + if (a->property("window_ptr").canConvert() && + a->property("window_ptr").value() == win) { + act = a; + break; + } + } + } + if (!act) + return; + + QString ttl = child->userFriendlyCurrentFile(); + if (child->isWindowModified()) + ttl.prepend("* "); + act->setText(ttl); +} + +void MainWindow::onSubwindowTitleChanged() +{ + QMdiSubWindow * win = NULL; + if ((win = qobject_cast(sender()->parent()))) + updateWindowActionTitle(win); +} + +void MainWindow::onSubwindowModified() +{ + onSubwindowTitleChanged(); +} + +void MainWindow::onChangeWindowAction(QAction * act) +{ + if (!act->isChecked()) + return; + + QMdiSubWindow * win = NULL; + if (act->property("window_ptr").canConvert()) + win = act->property("window_ptr").value(); + if (win) + mdiArea->setActiveSubWindow(win); +} + int MainWindow::newProfile(bool loadProfile) { int i; diff --git a/companion/src/mainwindow.h b/companion/src/mainwindow.h index cd51184d4..f71e454d3 100644 --- a/companion/src/mainwindow.h +++ b/companion/src/mainwindow.h @@ -27,8 +27,6 @@ #include "eeprominterface.h" #define SPLASH_TIME 5 -#define MAX_RECENT 15 -#define MAX_PROFILES 15 class MdiChild; QT_BEGIN_NAMESPACE @@ -52,9 +50,10 @@ class MainWindow : public QMainWindow public: MainWindow(); + ~MainWindow(); signals: - void FirmwareChanged(); + void firmwareChanged(); protected: QString getCompanionUpdateBaseUrl(); @@ -70,7 +69,7 @@ class MainWindow : public QMainWindow private slots: void openDocURL(); - void retranslateUi(); + void retranslateUi(bool showMsg = false); void setLanguage(const QString & langString); void onLanguageChanged(QAction * act); @@ -78,6 +77,12 @@ class MainWindow : public QMainWindow void onThemeChanged(QAction * act); void setIconThemeSize(int index); void onIconSizeChanged(QAction * act); + void setTabbedWindows(bool on); + void onChangeWindowAction(QAction * act); + void updateWindowActions(); + void updateWindowActionTitle(const QMdiSubWindow * win, QAction * act = NULL); + void onSubwindowTitleChanged(); + void onSubwindowModified(); void checkForUpdates(); void checkForFirmwareUpdate(); @@ -94,13 +99,11 @@ class MainWindow : public QMainWindow void openFile(); void save(); void saveAs(); - void cut(); + void closeFile(); void openRecentFile(); void loadProfileId(const unsigned pid); void loadProfile(); void logFile(); - void copy(); - void paste(); void writeEeprom(); void readEeprom(); void writeFlash(QString fileToFlash=""); @@ -110,7 +113,6 @@ class MainWindow : public QMainWindow void burnConfig(); void burnList(); void burnFuses(); - void simulate(); void contributors(); void sdsync(); void changelog(); @@ -118,8 +120,6 @@ class MainWindow : public QMainWindow void customizeSplash(); void about(); void compare(); - void print(); - void loadBackup(); void appPrefs(); void fwPrefs(); void updateMenus(); @@ -127,7 +127,6 @@ class MainWindow : public QMainWindow void copyProfile(); void deleteProfile(const int pid); void deleteCurrentProfile(); - void setActiveSubWindow(QWidget *window); void autoClose(); void closeUpdatesWaitDialog(); @@ -135,20 +134,17 @@ class MainWindow : public QMainWindow void openFile(const QString & fileName, bool updateLastUsedDir = false); private: - QAction * addAct(const QString &, const QString &, const QString &, enum QKeySequence::StandardKey, const char *, QObject *slotObj=NULL); - QAction * addAct(const QString &, const QString &, const QString &, const QKeySequence &, const char *, QObject *slotObj=NULL); - QAction * addAct(const QString &, const QString &, const QString &, const char *); - QAction * addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, - const char * propName = 0, const QVariant & propValue = QVariant(), const QVariant & dfltValue = QVariant()); + QAction * addAct(const QString & icon, const char * slot = NULL, const QKeySequence & shortcut = 0, QObject * slotObj = NULL, const char * signal = NULL); + QAction * addActToGroup(QActionGroup * aGroup, const QString & sName, const QString & lName, const char * propName = 0, + const QVariant & propValue = QVariant(), const QVariant & dfltValue = QVariant(), const QKeySequence & shortcut = 0); + void trAct(QAction * act, const QString & text, const QString & descript); QMenu * createLanguageMenu(QWidget * parent = Q_NULLPTR); - QMenu * createRecentFileMenu(); - QMenu * createProfilesMenu(); void createActions(); void createMenus(); void createToolBars(); - void createStatusBar(); + void showReadyStatus(); void updateRecentFileActions(); void updateProfilesActions(); @@ -172,13 +168,21 @@ class MainWindow : public QMainWindow QNetworkAccessManager *networkManager; - QVector actionsList; + QVector recentFileActs; + QVector profileActs; + QList fileWindowActions; QMenu *fileMenu; QMenu *editMenu; QMenu *settingsMenu; QMenu *burnMenu; QMenu *helpMenu; + QMenu *windowMenu; + QMenu *iconThemeSizeMenu; + QMenu *themeMenu; + QMenu *recentFilesMenu; + QMenu *profilesMenu; + QActionGroup * windowsListActions; QToolBar *fileToolBar; QToolBar *editToolBar; QToolBar *burnToolBar; @@ -187,6 +191,8 @@ class MainWindow : public QMainWindow QAction *openAct; QAction *saveAct; QAction *saveAsAct; + QAction *closeAct; + QAction *recentFilesAct; QAction *exitAct; QAction *appPrefsAct; QAction *fwPrefsAct; @@ -197,9 +203,6 @@ class MainWindow : public QMainWindow QAction *fwchangelogAct; QAction *compareAct; QAction *editSplashAct; - QAction *cutAct; - QAction *copyAct; - QAction *pasteAct; QAction *writeEepromAct; QAction *readEepromAct; QAction *burnConfigAct; @@ -207,20 +210,19 @@ class MainWindow : public QMainWindow QAction *burnFusesAct; QAction *writeFlashAct; QAction *readFlashAct; - QAction *writeBackupToRadioAct; - QAction *readBackupToFileAct; - QAction *simulateAct; - QAction *separatorAct; + QAction *writeBUToRadioAct; + QAction *readBUToFileAct; QAction *aboutAct; - QAction *printAct; - QAction *loadbackupAct; QAction *logsAct; - QAction *recentFileActs[MAX_RECENT]; - QAction *profileActs[MAX_PROFILES]; + QAction *profilesMenuAct; QAction *createProfileAct; QAction *copyProfileAct; QAction *deleteProfileAct; QAction *openDocURLAct; + QAction *actTabbedWindows; + QAction *actTileWindows; + QAction *actCascadeWindows; + QAction *actCloseAllWindows; }; #endif // _MAINWINDOW_H_ diff --git a/companion/src/mdichild.cpp b/companion/src/mdichild.cpp index ac94dbdde..65cb37039 100644 --- a/companion/src/mdichild.cpp +++ b/companion/src/mdichild.cpp @@ -20,7 +20,6 @@ #include "mdichild.h" #include "ui_mdichild.h" -#include "mainwindow.h" #include "modeledit/modeledit.h" #include "generaledit/generaledit.h" #include "burnconfigdialog.h" @@ -33,37 +32,78 @@ #include "storage.h" #include "radiointerface.h" -MdiChild::MdiChild(MainWindow * parent): - QWidget(), - parent(parent), +#include + +MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): + QWidget(parent, f), ui(new Ui::MdiChild), modelsListModel(NULL), + parentWindow(parentWin), + radioToolbar(NULL), + categoriesToolbar(NULL), + modelsToolbar(NULL), firmware(getCurrentFirmware()), + lastSelectedModel(-1), isUntitled(true), - fileChanged(false) + fileChanged(false), + showCatToolbar(true), + stateDataVersion(1) { ui->setupUi(this); setWindowIcon(CompanionIcon("open.png")); - ui->simulateButton->setIcon(CompanionIcon("simulate.png")); setAttribute(Qt::WA_DeleteOnClose); + setContextMenuPolicy(Qt::CustomContextMenu); + if (parentWindow) + parentWindow->setWindowIcon(windowIcon()); + + setupNavigation(); initModelsList(); - connect(parent, &MainWindow::FirmwareChanged, this, &MdiChild::onFirmwareChanged); - connect(ui->modelsList, &QTreeView::doubleClicked, this, &MdiChild::openModelEditWindow); - connect(ui->modelsList, &QTreeView::activated, this, &MdiChild::openModelEditWindow); - connect(ui->modelsList, &QTreeView::pressed, this, &MdiChild::onItemSelected); - connect(ui->modelsList, &QTreeView::customContextMenuRequested, this, &MdiChild::showModelsListContextMenu); - // connect(ui->modelsList, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), this, SLOT(onCurrentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *))); ui->modelsList->setContextMenuPolicy(Qt::CustomContextMenu); ui->modelsList->setSelectionBehavior(QAbstractItemView::SelectRows); ui->modelsList->setSelectionMode(QAbstractItemView::ExtendedSelection); - // ui->modelsList->setDragEnabled(true); - // ui->modelsList->setAcceptDrops(true); - // ui->modelsList->setDragDropOverwriteMode(true); - // ui->modelsList->setDropIndicatorShown(true); + ui->modelsList->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + ui->modelsList->setDragEnabled(true); + ui->modelsList->setAcceptDrops(true); + ui->modelsList->setDragDropOverwriteMode(false); + ui->modelsList->setDropIndicatorShown(true); + ui->modelsList->setDragDropMode(QAbstractItemView::DragDrop); + ui->modelsList->setStyle(new ItemViewProxyStyle(ui->modelsList->style())); + ui->modelsList->setStyleSheet("QTreeView::item {margin: 2px 0;}"); // a little more space for our drop indicators + + retranslateUi(); + + connect(this, &MdiChild::customContextMenuRequested, this, &MdiChild::showContextMenu); + connect(ui->modelsList, &QTreeView::activated, this, &MdiChild::onItemActivated); + connect(ui->modelsList, &QTreeView::customContextMenuRequested, this, &MdiChild::showModelsListContextMenu); + connect(ui->modelsList, &QTreeView::pressed, this, &MdiChild::onItemSelected); + connect(ui->modelsList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MdiChild::onCurrentItemChanged); if (!(isMaximized() || isMinimized())) { - adjustSize(); + QByteArray geo = g.mdiWinGeo(); + if (geo.isEmpty()) + adjustSize(); + else if (geo.size() < 10 && geo == "maximized") { + if (!parentWindow) // otherwise we let the MdiArea manage the window maximizing + setWindowState(windowState() ^ Qt::WindowMaximized); + } + else if (parentWindow) + parentWindow->restoreGeometry(geo); + else + restoreGeometry(geo); + } + if (!g.mdiWinState().isEmpty()) { + QByteArray state = g.mdiWinState(); + QDataStream stream(&state, QIODevice::ReadOnly); + quint16 ver; + stream >> ver; + if (ver <= stateDataVersion) { + bool visMdl, visGen; + stream >> showCatToolbar >> visMdl >> visGen; + categoriesToolbar->setVisible(showCatToolbar); + modelsToolbar->setVisible(visMdl); + radioToolbar->setVisible(visGen); + } } } @@ -72,80 +112,233 @@ MdiChild::~MdiChild() delete ui; } -void MdiChild::refresh(bool expand) +void MdiChild::closeEvent(QCloseEvent *event) { - modelsListModel->refresh(); - if (1 || expand) { - ui->modelsList->expandAll(); + if (!maybeSave()) { + event->ignore(); + return; } - ui->simulateButton->setEnabled(firmware->getCapability(Simulation)); - updateTitle(); -} - -void MdiChild::onItemSelected(QModelIndex idx) -{ - emit copyAvailable(isModel(idx) ? true : false); -} - -bool MdiChild::isModel(QModelIndex idx) -{ - return (modelsListModel->getModelIndex(idx) >= 0 ? true : false); -} - -bool MdiChild::isCategory(QModelIndex idx) -{ - if (isModel(idx)) { - return false; + if (!isMinimized()) { + QByteArray geo; + if (isMaximized()) + geo.append("maximized"); + else if (parentWindow) + geo = parentWindow->saveGeometry(); + else + geo = saveGeometry(); + g.mdiWinGeo(geo); } - else { - return (modelsListModel->getCategoryIndex(idx) >= 0); + + QByteArray state; + QDataStream stream(&state, QIODevice::WriteOnly); + stream << stateDataVersion + << (firmware->getCapability(Capability::HasModelCategories) ? categoriesToolbar->isVisible() : showCatToolbar) + << modelsToolbar->isVisible() + << radioToolbar->isVisible(); + g.mdiWinState(state); + + event->accept(); +} + +void MdiChild::resizeEvent(QResizeEvent * event) +{ + QWidget::resizeEvent(event); + adjustToolbarLayout(); +} + +QSize MdiChild::sizeHint() const +{ + QWidget * p; + if (parentWindow) + p = parentWindow->parentWidget(); + else + p = parentWidget(); + if (!p) + return QWidget::sizeHint(); + // use toolbar as a gauge for width, and take all the height availabe. + int w = qMax(ui->topToolbarLayout->sizeHint().width(), ui->botToolbarLayout->sizeHint().width()); + return QSize(w + 30, qMin(p->height(), 1000)); +} + +void MdiChild::changeEvent(QEvent * event) +{ + QWidget::changeEvent(event); + switch (event->type()) { + case QEvent::LanguageChange: + retranslateUi(); + break; + default: + break; } } -void MdiChild::confirmDelete() +QAction * MdiChild::addAct(Actions actId, const QString & icon, const char * slot, const QKeySequence & shortcut, QObject * slotObj) { - if (QMessageBox::warning(this, "Companion", tr("Delete selected models?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - deleteSelectedModels(); - } + QAction * newAction = new QAction(this); + newAction->setMenuRole(QAction::NoRole); + if (!icon.isEmpty()) + newAction->setIcon(CompanionIcon(icon)); + if (!shortcut.isEmpty()) + newAction->setShortcut(shortcut); + if (slotObj == NULL) + slotObj = this; + if (slot) + connect(newAction, SIGNAL(triggered()), slotObj, slot); + action.replace(actId, newAction); + return newAction; } -void MdiChild::deleteSelectedModels() +void MdiChild::setupNavigation() { - foreach (QModelIndex index, ui->modelsList->selectionModel()->selectedIndexes()) { - if (index.column() == 0) { - unsigned int modelIndex = modelsListModel->getModelIndex(index); - if (radioData.generalSettings.currModelIndex != modelIndex) { - qDebug() << "delete" << modelIndex; - radioData.models[modelIndex].clear(); - setModified(); - } - else { - QMessageBox::warning(this, "Companion", tr("Cannot delete default model."), QMessageBox::Ok); - } - } + foreach (QAction * act, action) { + if (act) + act->deleteLater(); } + action.clear(); + action.fill(NULL, ACT_ENUM_END); + + addAct(ACT_GEN_EDT, "edit.png", SLOT(generalEdit()), tr("Alt+Shift+E")); + addAct(ACT_GEN_CPY, "copy.png", SLOT(copyGeneralSettings()), tr("Ctrl+Alt+C")); + addAct(ACT_GEN_PST, "paste.png", SLOT(pasteGeneralSettings()), tr("Ctrl+Alt+V")); + addAct(ACT_GEN_SIM, "simulate.png", SLOT(radioSimulate()), tr("Alt+Shift+S")); + + addAct(ACT_ITM_EDT, "edit.png", SLOT(edit()), Qt::Key_Enter); + addAct(ACT_ITM_DEL, "clear.png", SLOT(confirmDelete()), QKeySequence::Delete); + + addAct(ACT_CAT_ADD, "add.png", SLOT(categoryAdd()), tr("Alt+C")); + //addAct(ACT_CAT_EDT, "edit.png", SLOT(edit()), Qt::Key_Enter); + //addAct(ACT_CAT_DEL, "clear.png", SLOT(confirmDelete()), QKeySequence::Delete); + action[ACT_CAT_SEP] = new QAction(this); + action[ACT_CAT_SEP]->setSeparator(true); + + addAct(ACT_MDL_ADD, "add.png", SLOT(modelAdd()), tr("Alt+A")); + addAct(ACT_MDL_RTR, "open.png", SLOT(loadBackup()), tr("Alt+R")); + addAct(ACT_MDL_WIZ, "wizard.png", SLOT(wizardEdit()), tr("Alt+W")); + + addAct(ACT_MDL_DFT, "currentmodel.png", SLOT(setDefault()), tr("Alt+U")); + addAct(ACT_MDL_PRT, "print.png", SLOT(print()), QKeySequence::Print); + addAct(ACT_MDL_SIM, "simulate.png", SLOT(modelSimulate()), tr("Alt+S")); + addAct(ACT_MDL_DUP, "duplicate.png", SLOT(modelDuplicate()), QKeySequence::Underline); + + addAct(ACT_MDL_CUT, "cut.png", SLOT(cut()), QKeySequence::Cut); + addAct(ACT_MDL_CPY, "copy.png", SLOT(copy()), QKeySequence::Copy); + addAct(ACT_MDL_PST, "paste.png", SLOT(paste()), QKeySequence::Paste); + addAct(ACT_MDL_INS, "list.png", SLOT(insert()), QKeySequence::Italic); + + addAct(ACT_MDL_MOV, "arrow-right.png"); + QMenu * catsMenu = new QMenu(this); + action[ACT_MDL_MOV]->setMenu(catsMenu); + + // set up the toolbars + + QToolButton * btn; + QSize tbIcnSz(16, 16); + QString tbCss = "QToolBar {border: 1px solid palette(midlight);}"; + + if (categoriesToolbar) + categoriesToolbar->deleteLater(); + categoriesToolbar = new QToolBar(this); + categoriesToolbar->setObjectName("TB_CATEGORIES"); + categoriesToolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + categoriesToolbar->setFloatable(false); + categoriesToolbar->setIconSize(tbIcnSz); + categoriesToolbar->setStyleSheet(tbCss); + categoriesToolbar->addAction(getAction(ACT_CAT_ADD)); + ui->topToolbarLayout->addWidget(categoriesToolbar); + + if (radioToolbar) + radioToolbar->deleteLater(); + radioToolbar = new QToolBar(this); + radioToolbar->setObjectName("TB_GENERAL"); + radioToolbar->setFloatable(false); + radioToolbar->setIconSize(tbIcnSz); + radioToolbar->setStyleSheet(tbCss); + radioToolbar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + radioToolbar->addActions(getGeneralActions()); + if ((btn = qobject_cast(radioToolbar->widgetForAction(action[ACT_GEN_EDT])))) { + btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + } + if ((btn = qobject_cast(radioToolbar->widgetForAction(action[ACT_GEN_SIM])))) { + btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + } + // add spacer to right-align the buttons + QWidget * sp = new QWidget(this); + sp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + radioToolbar->insertWidget(radioToolbar->actions().first(), sp); + ui->topToolbarLayout->addWidget(radioToolbar); + + if (modelsToolbar) + modelsToolbar->deleteLater(); + modelsToolbar = new QToolBar(this); + modelsToolbar->setObjectName("TB_MODELS"); + modelsToolbar->setFloatable(false); + modelsToolbar->setIconSize(tbIcnSz); + modelsToolbar->setStyleSheet(tbCss); + modelsToolbar->addActions(getEditActions(false)); + modelsToolbar->addSeparator(); + modelsToolbar->addActions(getModelActions()); + if ((btn = qobject_cast(modelsToolbar->widgetForAction(action[ACT_MDL_ADD])))) { + btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + } + if ((btn = qobject_cast(modelsToolbar->widgetForAction(action[ACT_MDL_MOV])))) { + btn->setPopupMode(QToolButton::InstantPopup); + } + ui->botToolbarLayout->addWidget(modelsToolbar); + + connect(categoriesToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); + connect(radioToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); + connect(modelsToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); } -void MdiChild::showModelsListContextMenu(const QPoint & pos) +void MdiChild::updateNavigation() { - QModelIndex modelIndex = ui->modelsList->indexAt(pos); - QPoint globalPos = ui->modelsList->mapToGlobal(pos); - QMenu contextMenu; - const QClipboard * clipboard = QApplication::clipboard(); - const QMimeData * mimeData = clipboard->mimeData(); - bool hasData = mimeData->hasFormat("application/x-companion"); - if (modelsListModel->getModelIndex(modelIndex) >= 0) { - contextMenu.addAction(CompanionIcon("edit.png"), tr("&Edit"), this, SLOT(modelEdit())); - contextMenu.addAction(CompanionIcon("open.png"), tr("&Restore from backup"), this, SLOT(loadBackup())); - contextMenu.addAction(CompanionIcon("wizard.png"), tr("&Model Wizard"), this, SLOT(wizardEdit())); - contextMenu.addSeparator(); - contextMenu.addAction(CompanionIcon("clear.png"), tr("&Delete"), this, SLOT(confirmDelete()), tr("Delete")); - contextMenu.addAction(CompanionIcon("copy.png"), tr("&Copy"), this, SLOT(copy()), tr("Ctrl+C")); - contextMenu.addAction(CompanionIcon("cut.png"), tr("&Cut"), this, SLOT(cut()), tr("Ctrl+X")); - contextMenu.addAction(CompanionIcon("paste.png"), tr("&Paste"), this, SLOT(paste()), tr("Ctrl+V"))->setEnabled(hasData); - contextMenu.addAction(CompanionIcon("duplicate.png"), tr("D&uplicate"), this, SLOT(modelDuplicate()), tr("Ctrl+U")); - if (radioData.categories.size() > 1) { - QMenu * catsMenu = contextMenu.addMenu(CompanionIcon("arrow-right.png"), tr("Move to Category")); + const int modelsSelected = countSelectedModels(); + const int catsSelected = countSelectedCats(); + const bool singleModelSelected = (modelsSelected == 1); + const bool hasModelSlotSelcted = (getCurrentModel() > -1); + const bool hasCats = firmware->getCapability(Capability::HasModelCategories); + const bool hasCatSelected = hasCats && modelsListModel->isCategoryType(getCurrentIndex()); + const int numOnClipbrd = modelsListModel->countModelsInMimeData(QApplication::clipboard()->mimeData()); + const QString modelsRemvTxt = tr("%n Model(s)", "As in \"Copy 3 Models\" or \"Cut 1 Model\" or \"Delete 3 Models\" action).", modelsSelected); + const QString modelsAddTxt = tr("%n Model(s)", "As in \"Paste 3 Models\" or \"Insert 1 Model.\"", numOnClipbrd); + const QString catsRemvTxt = tr("%n Category(ies)", "As in \"Delete 3 Categories\" or \"Delete 1 Category.\"", catsSelected); + static const QString noSelection = tr("Nothing selected"); + static const QString sp = " "; + static const QString ns; + + categoriesToolbar->setVisible(hasCats && showCatToolbar); + + action[ACT_GEN_PST]->setEnabled(hasClipboardData(1)); + + action[ACT_ITM_EDT]->setEnabled(singleModelSelected || catsSelected == 1); + action[ACT_ITM_EDT]->setText((hasCatSelected ? tr("Rename Category") : modelsSelected ? tr("Edit Model") : noSelection)); + action[ACT_ITM_DEL]->setEnabled(modelsSelected || catsSelected); + action[ACT_ITM_DEL]->setText((action[ACT_ITM_DEL]->isEnabled() ? tr("Delete") % " " % (hasCatSelected ? catsRemvTxt : modelsRemvTxt) : noSelection)); + + action[ACT_CAT_ADD]->setVisible(hasCats); + action[ACT_CAT_SEP]->setVisible(hasCats); +// action[ACT_CAT_EDT]->setVisible(hasCats); +// action[ACT_CAT_EDT]->setEnabled(catsSelected == 1); +// action[ACT_CAT_DEL]->setVisible(hasCats); +// action[ACT_CAT_DEL]->setEnabled(catsSelected); +// action[ACT_CAT_DEL]->setText((hasCatSelected ? tr("Delete") % " " % catsRemTxt : noSelection)); + + action[ACT_MDL_CUT]->setEnabled(modelsSelected); + action[ACT_MDL_CUT]->setText(tr("Cut") % (modelsSelected ? sp % modelsRemvTxt : ns)); + action[ACT_MDL_CPY]->setEnabled(modelsSelected); + action[ACT_MDL_CPY]->setText(tr("Copy") % (modelsSelected ? sp % modelsRemvTxt : ns)); + action[ACT_MDL_PST]->setEnabled(numOnClipbrd); + action[ACT_MDL_PST]->setText(tr("Paste") % (numOnClipbrd ? sp % modelsAddTxt : ns)); + action[ACT_MDL_INS]->setEnabled(numOnClipbrd && (hasModelSlotSelcted || catsSelected)); + action[ACT_MDL_INS]->setText(tr("Insert") % QString(action[ACT_MDL_INS]->isEnabled() ? sp % modelsAddTxt : ns)); + + if (hasCats && action[ACT_MDL_MOV]->menu()) { + action[ACT_MDL_MOV]->setVisible(true); + QModelIndex modelIndex = getCurrentIndex(); + QMenu * catsMenu = action[ACT_MDL_MOV]->menu(); + catsMenu->clear(); + if (modelsSelected && modelsListModel && radioData.categories.size() > 1) { + action[ACT_MDL_MOV]->setEnabled(true); for (unsigned i=0; i < radioData.categories.size(); ++i) { QAction * act = catsMenu->addAction(QString(radioData.categories[i].name), this, SLOT(onModelMoveToCategory())); act->setProperty("categoryId", i); @@ -157,68 +350,185 @@ void MdiChild::showModelsListContextMenu(const QPoint & pos) act->setEnabled(false); } } - contextMenu.addSeparator(); - contextMenu.addAction(CompanionIcon("currentmodel.png"), tr("&Use as default"), this, SLOT(setDefault())); - contextMenu.addSeparator(); - contextMenu.addAction(CompanionIcon("print.png"), tr("P&rint model"), this, SLOT(print()), QKeySequence(tr("Ctrl+P"))); - contextMenu.addSeparator(); - contextMenu.addAction(CompanionIcon("simulate.png"), tr("&Simulate model"), this, SLOT(modelSimulate()), tr("Alt+S")); - } - else if (IS_HORUS(firmware->getBoard())) { - if (modelsListModel->getCategoryIndex(modelIndex) >= 0) { - contextMenu.addAction(CompanionIcon("add.png"), tr("&Add model"), this, SLOT(modelAdd())); - contextMenu.addAction(CompanionIcon("paste.png"), tr("&Paste"), this, SLOT(paste()), tr("Ctrl+V"))->setEnabled(hasData); - contextMenu.addSeparator(); - contextMenu.addAction(CompanionIcon("rename.png"), tr("&Rename category"), this, SLOT(categoryRename())); - contextMenu.addAction(CompanionIcon("delete.png"), tr("&Delete category"), this, SLOT(categoryDelete())); - } else { - contextMenu.addAction(CompanionIcon("add.png"), tr("&Add category"), this, SLOT(categoryAdd())); + action[ACT_MDL_MOV]->setDisabled(true); } } else { - return; + action[ACT_MDL_MOV]->setVisible(false); } - contextMenu.exec(globalPos); + action[ACT_MDL_DUP]->setEnabled(singleModelSelected); + action[ACT_MDL_RTR]->setEnabled(singleModelSelected); + action[ACT_MDL_WIZ]->setEnabled(singleModelSelected); + action[ACT_MDL_DFT]->setEnabled(singleModelSelected && getCurrentModel() != (int)radioData.generalSettings.currModelIndex); + action[ACT_MDL_PRT]->setEnabled(singleModelSelected); + action[ACT_MDL_SIM]->setEnabled(singleModelSelected); } -void MdiChild::forceNewFilename(const QString & suffix, const QString & ext) +void MdiChild::retranslateUi() { - QFileInfo info(curFile); - curFile = info.path() + "/" + info.baseName() + suffix + ext; + action[ACT_GEN_EDT]->setText(tr("Edit Radio Settings")); + action[ACT_GEN_CPY]->setText(tr("Copy Radio Settings")); + action[ACT_GEN_PST]->setText(tr("Paste Radio Settings")); + action[ACT_GEN_SIM]->setText(tr("Simulate Radio")); + + action[ACT_CAT_ADD]->setText(tr("Add Category")); + action[ACT_CAT_ADD]->setIconText(tr("Category")); + //action[ACT_CAT_EDT]->setText(tr("Rename Category")); + + action[ACT_MDL_ADD]->setText(tr("Add Model")); + action[ACT_MDL_ADD]->setIconText(tr("Model")); + action[ACT_MDL_RTR]->setText(tr("Restore from Backup")); + action[ACT_MDL_WIZ]->setText(tr("Model Wizard")); + action[ACT_MDL_DFT]->setText(tr("Set as Default")); + action[ACT_MDL_PRT]->setText(tr("Print Model")); + action[ACT_MDL_SIM]->setText(tr("Simulate Model")); + action[ACT_MDL_DUP]->setText(tr("Duplicate Model")); + + action[ACT_MDL_MOV]->setText(tr("Move to Category")); + + categoriesToolbar->setWindowTitle(tr("Show Category Actions Toolbar")); + radioToolbar->setWindowTitle(tr("Show Radio Actions Toolbar")); + modelsToolbar->setWindowTitle(tr("Show Model Actions Toolbar")); } -void MdiChild::onFirmwareChanged() +QList MdiChild::getGeneralActions() { - Firmware * previous = firmware; - firmware = getCurrentFirmware(); - qDebug() << "onFirmwareChanged" << previous->getName() << "=>" << firmware->getName(); - if (previous->getBoard() != firmware->getBoard()) { - convertStorage(previous->getBoard(), firmware->getBoard()); + QList actGrp; + actGrp.append(getAction(ACT_GEN_SIM)); + actGrp.append(getAction(ACT_GEN_EDT)); + actGrp.append(getAction(ACT_GEN_CPY)); + actGrp.append(getAction(ACT_GEN_PST)); + return actGrp; +} + +QList MdiChild::getEditActions(bool incCatNew) +{ + QList actGrp; + if (incCatNew) { + actGrp.append(getAction(ACT_CAT_ADD)); + actGrp.append(getAction(ACT_CAT_SEP)); + } + actGrp.append(action[ACT_MDL_ADD]); + QAction * sep2 = new QAction(this); + sep2->setSeparator(true); + actGrp.append(sep2); + actGrp.append(getAction(ACT_ITM_EDT)); + actGrp.append(getAction(ACT_ITM_DEL)); + actGrp.append(getAction(ACT_MDL_CUT)); + actGrp.append(getAction(ACT_MDL_CPY)); + actGrp.append(getAction(ACT_MDL_PST)); + actGrp.append(getAction(ACT_MDL_INS)); + actGrp.append(getAction(ACT_MDL_DUP)); + actGrp.append(getAction(ACT_MDL_MOV)); + return actGrp; +} + +QList MdiChild::getModelActions() +{ + QList actGrp; + actGrp.append(getAction(ACT_MDL_RTR)); + actGrp.append(getAction(ACT_MDL_WIZ)); + actGrp.append(getAction(ACT_MDL_DFT)); + actGrp.append(getAction(ACT_MDL_PRT)); + actGrp.append(getAction(ACT_MDL_SIM)); + return actGrp; +} + +//QList MdiChild::getCategoryActions() +//{ +// QList actGrp; +// actGrp.append(getAction(ACT_CAT_ADD)); +// actGrp.append(getAction(ACT_CAT_EDT)); +// actGrp.append(getAction(ACT_CAT_DEL)); +// actGrp.append(getAction(ACT_CAT_SEP)); +// return actGrp; +//} + +QAction * MdiChild::getAction(const MdiChild::Actions type) +{ + if (type < ACT_ENUM_END) + return action[type]; + else + return NULL; +} + +void MdiChild::showModelsListContextMenu(const QPoint & pos) +{ + QModelIndex modelIndex = ui->modelsList->indexAt(pos); + QMenu contextMenu; + + updateNavigation(); + + if (firmware->getCapability(Capability::HasModelCategories)) { + contextMenu.addAction(action[ACT_CAT_ADD]); + if(modelsListModel->isCategoryType(modelIndex)) { + contextMenu.addAction(action[ACT_ITM_EDT]); + contextMenu.addAction(action[ACT_ITM_DEL]); + } + contextMenu.addSeparator(); + } + + if (modelsListModel->isModelType(modelIndex)) { + contextMenu.addActions(getEditActions()); + if (countSelectedModels() == 1) { + contextMenu.addSeparator(); + contextMenu.addActions(getModelActions()); + } + } + else { + contextMenu.addAction(action[ACT_MDL_ADD]); + if (hasClipboardData()) + contextMenu.addAction(action[ACT_MDL_PST]); + } + + if (!contextMenu.isEmpty()) + contextMenu.exec(ui->modelsList->mapToGlobal(pos)); +} + +void MdiChild::showContextMenu(const QPoint & pos) +{ + QMenu contextMenu; + if (firmware->getCapability(Capability::HasModelCategories)) + contextMenu.addAction(categoriesToolbar->toggleViewAction()); + contextMenu.addAction(modelsToolbar->toggleViewAction()); + contextMenu.addAction(radioToolbar->toggleViewAction()); + if (!contextMenu.isEmpty()) + contextMenu.exec(mapToGlobal(pos)); +} + +void MdiChild::adjustToolbarLayout() +{ + if (size().width() > ui->topToolbarLayout->sizeHint().width() + ui->botToolbarLayout->sizeHint().width() + 30) { + ui->botToolbarLayout->removeWidget(modelsToolbar); + ui->topToolbarLayout->insertWidget(1, modelsToolbar); + } + else { + ui->topToolbarLayout->removeWidget(modelsToolbar); + ui->botToolbarLayout->insertWidget(0, modelsToolbar); } } -void MdiChild::convertStorage(Board::Type from, Board::Type to) -{ - QMessageBox::warning(this, "Companion", tr("Models and settings will be automatically converted.\nIf that is not what you intended, please close the file\nand choose the correct radio type/profile before reopening it."), QMessageBox::Ok); - radioData.convert(from, to); - forceNewFilename("_converted", ".otx"); - initModelsList(); - fileChanged = true; - isUntitled = true; - documentWasModified(); -} +/* + * Data model +*/ void MdiChild::initModelsList() { Board::Type board = firmware->getBoard(); - delete modelsListModel; + if (modelsListModel) + delete modelsListModel; + modelsListModel = new TreeModel(&radioData, this); + connect(modelsListModel, &TreeModel::modelsDropped, this, &MdiChild::pasteModelData); + connect(modelsListModel, &TreeModel::modelsRemoved, this, &MdiChild::deleteModels); + connect(modelsListModel, &TreeModel::refreshRequested, this, &MdiChild::refresh); connect(modelsListModel, &QAbstractItemModel::dataChanged, this, &MdiChild::onDataChanged); + ui->modelsList->setModel(modelsListModel); - ui->modelsList->header()->setVisible(!IS_HORUS(board)); + ui->modelsList->header()->setVisible(!firmware->getCapability(Capability::HasModelCategories)); if (IS_HORUS(board)) { ui->modelsList->setIndentation(20); // ui->modelsList->resetIndentation(); // introduced in next Qt versions ... @@ -229,133 +539,150 @@ void MdiChild::initModelsList() refresh(); } -void MdiChild::cut() +void MdiChild::refresh() { - copy(); - deleteSelectedModels(); -} - -void MdiChild::copy() -{ - QByteArray gmData; - doCopy(&gmData); - - QMimeData * mimeData = new QMimeData; - mimeData->setData("application/x-companion", gmData); - - QClipboard * clipboard = QApplication::clipboard(); - clipboard->setMimeData(mimeData, QClipboard::Clipboard); -} - -void MdiChild::doCopy(QByteArray * gmData) -{ - foreach(QModelIndex index, ui->modelsList->selectionModel()->selectedIndexes()) { - if (index.column() == 0) { - int modelIndex = modelsListModel->getModelIndex(index); - if (modelIndex >= 0) { - gmData->append('M'); - gmData->append((char *) &radioData.models[modelIndex], sizeof(ModelData)); - } - } + bool expand = true; // TODO: restore user-preferred state + clearCutList(); + modelsListModel->refresh(); + if (expand) + ui->modelsList->expandAll(); + if (lastSelectedModel > -1) { + setSelectedModel(lastSelectedModel); + lastSelectedModel = -1; } - -// TODO to copy radio settings -// gmData->append('G'); -// gmData->append((char *) &radioData.generalSettings, sizeof(GeneralSettings)); + updateNavigation(); + updateTitle(); } -void MdiChild::doPaste(QByteArray * gmData, QModelIndex row) +void MdiChild::onItemActivated(const QModelIndex index) { - char * gData = gmData->data(); - bool modified = false; - int size = 0; - int modelIdx = modelsListModel->getModelIndex(row); - int categoryIdx = modelsListModel->getCategoryIndex(row); - int childNumber = modelsListModel->rowNumber(row); - - while (size < gmData->size()) { - char c = *gData++; - size++; - if (c == 'G') { - // General settings - int ret = QMessageBox::question(this, "Companion", tr("Do you want to overwrite radio general settings?"), - QMessageBox::Yes | QMessageBox::No); - if (ret == QMessageBox::Yes) { - radioData.generalSettings = *((GeneralSettings *)gData); - modified = 1; - } - gData += sizeof(GeneralSettings); - size += sizeof(GeneralSettings); - } - else if (c == 'M') { - if (firmware->getCapability(Models) == 0 || modelIdx < firmware->getCapability(Models)) { - // Model data - int ret = QMessageBox::Yes; - - if (modelIdx == -1) { - // This handles pasting onto a category label or pasting past the end - // of a category when pasting multiple models. - ModelData blank; - blank.used = false; - blank.category = categoryIdx; - radioData.models.push_back(blank); - modelIdx = radioData.models.size() - 1; - } - - if (!radioData.models[modelIdx].isEmpty()) { - ret = QMessageBox::question(this, "Companion", tr("You are pasting on an not empty model, are you sure?"), - QMessageBox::Yes | QMessageBox::No); - } - if (ret == QMessageBox::Yes) { - // Set the destination category, so a user can copy/paste across categories. - radioData.models[modelIdx] = *((ModelData *)gData); - radioData.models[modelIdx].category = categoryIdx; - strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str()); - modified = 1; - } - gData += sizeof(ModelData); - size += sizeof(ModelData); - // This gets the next row in the UI, which works for category-capable - // and non-category radio profiles. modelIndex will be -1, if there - // is no next row in the current category. - modelIdx = modelsListModel->getModelIndex(row.sibling(++childNumber, 0)); - } - } - else { - qWarning() << "paste error"; - break; - } + if (modelsListModel->isModelType(index)) { + int mIdx = modelsListModel->getModelIndex(index); + if (mIdx < 0 || mIdx >= (int)radioData.models.size()) + return; + if (radioData.models[mIdx].isEmpty()) + newModel(mIdx); + else + openModelEditWindow(mIdx); } - if (modified) { - setModified(); + else if (modelsListModel->isCategoryType(index)) { + ui->modelsList->edit(index); } } -void MdiChild::paste() +void MdiChild::onItemSelected(const QModelIndex &) { - if (hasPasteData()) { - const QClipboard * clipboard = QApplication::clipboard(); - const QMimeData * mimeData = clipboard->mimeData(); - QByteArray gmData = mimeData->data("application/x-companion"); - doPaste(&gmData, ui->modelsList->currentIndex()); + updateNavigation(); +} + +void MdiChild::onCurrentItemChanged(const QModelIndex &, const QModelIndex &) +{ + updateNavigation(); +} + +void MdiChild::onDataChanged(const QModelIndex & index) +{ + if (!modelsListModel->isCategoryType(index)) + return; + + int categoryIndex = modelsListModel->getCategoryIndex(index); + if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { + return; } + strcpy(radioData.categories[categoryIndex].name, modelsListModel->data(index, 0).toString().left(sizeof(CategoryData::name)-1).toStdString().c_str()); + + setModified(); } -bool MdiChild::hasPasteData() const +/* + * Get info from data model +*/ + +QModelIndex MdiChild::getCurrentIndex() const { - const QClipboard * clipboard = QApplication::clipboard(); - const QMimeData * mimeData = clipboard->mimeData(); - return mimeData->hasFormat("application/x-companion"); + return ui->modelsList->selectionModel()->currentIndex(); } -bool MdiChild::hasModelSelected() +int MdiChild::getCurrentCategory() const { - return isModel(ui->modelsList->currentIndex()); + return modelsListModel->getCategoryIndex(getCurrentIndex()); } +int MdiChild::countSelectedCats() const +{ + int ret = 0; + + foreach (QModelIndex index, ui->modelsList->selectionModel()->selectedRows()) { + if (index.isValid() && modelsListModel->isCategoryType(index)) + ++ret; + } + return ret; +} + +bool MdiChild::hasSelectedCat() +{ + return modelsListModel->isCategoryType(getCurrentIndex()); +} + +QVector MdiChild::getSelectedCategories() const +{ + QVector cats; + foreach (QModelIndex index, ui->modelsList->selectionModel()->selectedRows()) { + if (index.isValid() && modelsListModel->isCategoryType(index)) + cats.append(modelsListModel->getCategoryIndex(index)); + } + return cats; +} + +int MdiChild::getCurrentModel() const +{ + return modelsListModel->getModelIndex(getCurrentIndex()); +} + +int MdiChild::countSelectedModels() const +{ + int ret = 0; + + foreach (QModelIndex index, ui->modelsList->selectionModel()->selectedRows()) { + if (index.isValid() && modelsListModel->isModelType(index) && !radioData.models.at(modelsListModel->getModelIndex(index)).isEmpty()) + ++ret; + } + return ret; +} + +bool MdiChild::hasSelectedModel() +{ + return modelsListModel->isModelType(getCurrentIndex()); +} + +bool MdiChild::setSelectedModel(const int modelIndex) +{ + QModelIndex idx = modelsListModel->getIndexForModel(modelIndex); + if (idx.isValid()) { + ui->modelsList->scrollTo(idx); + ui->modelsList->setCurrentIndex(idx); + return true; + } + return false; +} + +QVector MdiChild::getSelectedModels() const +{ + QVector models; + foreach (QModelIndex index, ui->modelsList->selectionModel()->selectedRows()) { + if (index.isValid() && modelsListModel->isModelType(index)) + models.append(modelsListModel->getModelIndex(index)); + } + return models; +} + +/* + * Misc. internal event handlers +*/ + void MdiChild::updateTitle() { - QString title = userFriendlyCurrentFile() + "[*]" + " (" + firmware->getName() + QString(")"); + QString title = "[*]" + userFriendlyCurrentFile(); // + " (" + firmware->getName() + QString(")"); int availableEEpromSize = modelsListModel->getAvailableEEpromSize(); if (availableEEpromSize >= 0) { title += QString(" - %1 ").arg(availableEEpromSize) + tr("free bytes"); @@ -365,43 +692,374 @@ void MdiChild::updateTitle() void MdiChild::setModified() { - refresh(); fileChanged = true; - documentWasModified(); + refresh(); + setWindowModified(true); + emit modified(); } -void MdiChild::keyPressEvent(QKeyEvent * event) +void MdiChild::onFirmwareChanged() { - if (event->matches(QKeySequence::Delete)) { - deleteSelectedModels(); - } - else if (event->matches(QKeySequence::Cut)) { - cut(); - } - else if (event->matches(QKeySequence::Copy)) { - copy(); - } - else if (event->matches(QKeySequence::Paste)) { - paste(); - } - else if (event->matches(QKeySequence::Underline)) { - // TODO duplicate(); - } - else { - QWidget::keyPressEvent(event); // run the standard event in case we didn't catch an action + Firmware * previous = firmware; + firmware = getCurrentFirmware(); + //qDebug() << "onFirmwareChanged" << previous->getName() << "=>" << firmware->getName(); + if (previous->getBoard() != firmware->getBoard()) { + convertStorage(previous->getBoard(), firmware->getBoard()); + setModified(); } } -void MdiChild::on_simulateButton_clicked() +/* + * Categories CRUD +*/ + +void MdiChild::categoryAdd() { - radioSimulate(); + /*: Translators do NOT use accent for this, this is the default category name on Horus. */ + CategoryData category(qPrintable(tr("New category"))); + radioData.categories.push_back(category); + setModified(); + QModelIndex idx = modelsListModel->getIndexForCategory(radioData.categories.size()-1); + if (idx.isValid()) { + ui->modelsList->scrollTo(idx); + ui->modelsList->setCurrentIndex(idx); + ui->modelsList->edit(idx); + } } +// NOTE: this does not refresh the data/view, need to call setModified() at some point afterwards. +bool MdiChild::deleteCategory(int categoryIndex, QString * error) +{ + if (categoryIndex < 0) + categoryIndex = modelsListModel->getCategoryIndex(getCurrentIndex()); + if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { + if (error) + *error = tr("Category index out of range."); + return false; + } + if (radioData.categories.size() <= 1) { + if (error) + *error = tr("Cannot delete the last category."); + return false; + } + + QVector modelsToMove; + for (unsigned i=0; i < radioData.models.size(); i++) { + ModelData & model = radioData.models[i]; + if (model.used && model.category == categoryIndex) { + if (error) + *error = tr("This category is not empty!"); + return false; + } + else if (model.used && model.category > categoryIndex) { + modelsToMove.append(i); + } + } + + radioData.categories.erase(radioData.categories.begin() + categoryIndex); + foreach (const int i, modelsToMove) { + --radioData.models[i].category; + } + return true; +} + +void MdiChild::deleteSelectedCats() +{ + bool modified = false; + QString error; + QVector cats = getSelectedCategories(); + std::sort(cats.begin(), cats.end()); + std::reverse(cats.begin(), cats.end()); + foreach (const int cat, cats) { + error.clear(); + if (deleteCategory(cat, &error)) + modified = true; + else + showWarning(error); + } + if (modified) + setModified(); +} + +/* + * Models CRUD +*/ + void MdiChild::checkAndInitModel(int row) { - ModelData & model = radioData.models[row]; - if (model.isEmpty()) { - model.setDefaultValues(row, radioData.generalSettings); + if (row < (int)radioData.models.size() && radioData.models[row].isEmpty()) { + radioData.models[row].setDefaultValues(row, radioData.generalSettings); + setModified(); + } +} + +void MdiChild::findNewDefaultModel(const unsigned startAt) +{ + for (unsigned i = startAt; i < radioData.models.size(); ++i) { + if (!radioData.models[i].isEmpty()) { + radioData.setCurrentModel(i); + return; + } + } + if (startAt > 0) + findNewDefaultModel(0); // restart search from beginning + else + radioData.setCurrentModel(0); +} + +// NOTE: insertModelRows() does not update the TreeModel, only modifies radioData.models[] array by inserting row(s) of blank model(s). +// TreeModel::refresh() needs to be called at some point afterwards to sync the data. +// This invalidates any model indices stored previously. +bool MdiChild::insertModelRows(int atModelIdx, int count) +{ + const unsigned maxModels = firmware->getCapability(Models); + if (atModelIdx < 0) + return false; + + for (int i=0; i < count; ++i) { + //qDebug() << atRow << atRow.row() + i << modelIdx + i << maxModels << radioData.models.size(); + if (maxModels > 0 && radioData.models.size() >= maxModels) { + // trim the array, unless the last model slot is being used. + if (radioData.models[maxModels-1].isEmpty()) { + radioData.models.pop_back(); + } + else { + showWarning(tr("Cannot insert model, last model in list would be deleted.")); + return false; // TODO: perhaps something more elegant... + } + } + // add a placeholder model + radioData.models.insert(radioData.models.begin() + atModelIdx + i, ModelData()); + // adjust current model index if needed + if ((int)radioData.generalSettings.currModelIndex >= atModelIdx + i) + findNewDefaultModel(radioData.generalSettings.currModelIndex + 1); + } + return true; +} + +// Finds the first empty slot and inserts the model into it. In case of category-style models, will append to end of list. +// Return -1 if no slot was found, otherwise new array index. +// TreeModel::refresh() needs to be called at some point afterwards to sync the data. +int MdiChild::modelAppend(const ModelData model) +{ + int newIdx = -1; + int trySlot = 0; + // try to find the next empty slot + for ( ; trySlot < (int)radioData.models.size(); ++trySlot) { + if (radioData.models[trySlot].isEmpty()) { + newIdx = trySlot; + radioData.models[newIdx] = model; + break; + } + } + // if no empty slots then check if we can append it + if (newIdx < 0 && (firmware->getCapability(Models) == 0 || trySlot < firmware->getCapability(Models) - 1)) { + radioData.models.push_back(model); + newIdx = radioData.models.size() - 1; + } + return newIdx; +} + +int MdiChild::newModel(int modelIndex, int categoryIndex) +{ + if (modelIndex < 0) + modelIndex = modelAppend(ModelData()); + + if (modelIndex < 0) { + showWarning(tr("Cannot add model, could not find an available model slot.")); + return modelIndex; + } + + if (categoryIndex < 0) + categoryIndex = modelsListModel->getCategoryIndex(getCurrentIndex()); + //qDebug() << modelIndex << categoryIndex; + + bool isNewModel = radioData.models[modelIndex].isEmpty(); + checkAndInitModel(modelIndex); + if (isNewModel && firmware->getCapability(Capability::HasModelCategories) && categoryIndex > -1) { + radioData.models[modelIndex].category = categoryIndex; + strcpy(radioData.models[modelIndex].filename, radioData.getNextModelFilename().toStdString().c_str()); + strcpy(radioData.models[modelIndex].name, qPrintable(tr("New model"))); // TODO: Why not just use existing default model name? + } + // Only set the default model if we just added the first one. + if (modelIndex == 0) { + radioData.setCurrentModel(modelIndex); + } + setModified(); + setSelectedModel(modelIndex); + + if (g.newModelAction() == 1) + openModelWizard(modelIndex); + else if (g.newModelAction() == 2) + openModelEditWindow(modelIndex); + + return modelIndex; +} + + +// NOTE: deleteModelss() does not update the TreeModel, only modifies radioData.models[] array by clearing the model data. +// If (removeModelSlotsWhenDeleting == true) then removes array rows entirely (and pads w/blank model at the end if needed). +// TreeModel::refresh() needs to be called at some point afterwards to sync the data +// We delete using stored indexes because actual indexes may change during inserts/deletes. +// Obviously this only works before the stored indexes get updated in TreeModel::refresh(). +unsigned MdiChild::deleteModels(const QVector modelIndices) +{ + unsigned deletes = 0; + int idx; + + for (int i = (int)radioData.models.size() - 1; i > -1; --i) { + idx = radioData.models.at(i).modelIndex; + if (idx > -1 && modelIndices.contains(idx)) { + radioData.models[i].clear(); + if (g.removeModelSlots() || firmware->getCapability(Models) == 0) { + radioData.models.erase(radioData.models.begin() + i); + // append padding rows at the end if needed + if (firmware->getCapability(Models) > 0) + insertModelRows(radioData.models.size(), 1); + } + ++deletes; + // adjust current model index if needed + if ((int)radioData.generalSettings.currModelIndex >= idx) + findNewDefaultModel(qMax((int)radioData.generalSettings.currModelIndex - 1, 0)); + + } + //qDebug() << "i:" << i << "modelIndex:" << idx << "deletes:" << deletes; + } + + if (deletes) + setModified(); + + return deletes; +} + +bool MdiChild::deleteModel(const int modelIndex) +{ + QVector list = QVector() << modelIndex; + if (deleteModels(list) == 1) + return true; + else + return false; +} + +void MdiChild::deleteSelectedModels() +{ + deleteModels(getSelectedModels()); +} + +void MdiChild::moveModelsToCategory(const QVector models, const int toCategoryId) +{ + if (toCategoryId < 0 || !models.size()) + return; + + bool modified = false; + //QVector models = getSelectedModels(); + foreach(const int model, models) { + if (model < 0 || model >= (int)radioData.models.size()) + continue; + + if (radioData.models[model].category != toCategoryId) { + radioData.models[model].category = toCategoryId; + modified = true; + } + } + if (modified) + setModified(); +} + +void MdiChild::moveSelectedModelsToCat(const int toCategoryId) +{ + moveModelsToCategory(getSelectedModels(), toCategoryId); +} + +void MdiChild::pasteModelData(const QMimeData * mimeData, const QModelIndex row, bool insert, bool move) +{ + QVector modelsList; + if (!TreeModel::decodeMimeData(mimeData, &modelsList)) + return; + + bool modified = false; + int modelIdx = modelsListModel->getModelIndex(row); + int categoryIdx = modelsListModel->getCategoryIndex(row); + unsigned inserts = 0; + QVector deletesList; + + // Force DnD moves from other file windows to be copy actions because we don't want to delete our models. + bool hasOwnData = modelsListModel->hasOwnMimeData(mimeData); + move = (move && hasOwnData); + + //qDebug().nospace() << "row: " << row << "; ins: " << insert << "; mv: " << move << "; row modelIdx: " << modelIdx << "; row categoryIdx: " << categoryIdx << "; removeSlots: " << removeModelSlotsWhenDeleting; + + // Model data + for (int i=0; i < modelsList.size(); ++i) { + int origMdlIdx = hasOwnData ? modelsList.at(i).modelIndex : -1; // where is the modeul in *our* current array? + bool doMove = (origMdlIdx > -1 && origMdlIdx < (int)radioData.models.size() && (move || cutModels.contains(origMdlIdx))); // DnD-moved or clipboard cut + bool ok = true; + + if (modelIdx == -1 || (!insert && modelIdx >= (int)radioData.models.size())) { + // This handles pasting onto a category label or pasting past the end + // of a category when pasting multiple models. + modelIdx = modelAppend(modelsList[i]); + if (modelIdx < 0) { + ok = false; + showWarning(tr("Cannot paste model, out of available model slots.")); + } + } + else if (insert) { + ok = insertModelRows(modelIdx, 1); + if (ok) { + radioData.models[modelIdx] = modelsList[i]; + ++inserts; + } + } + else { // pasting on top of a slot + if (!radioData.models[modelIdx].isEmpty() && !deletesList.contains(modelIdx)) + ok = askQuestion(tr("You are replacing an existing model, are you sure?")) == QMessageBox::Yes; + if (ok) + radioData.models[modelIdx] = modelsList[i]; + } + + if (ok) { + // We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here) + // this is esp. important because otherwise we may delete this model during a move operation (eg. after a cut) + radioData.models[modelIdx].modelIndex = -modelIdx; + // Set the destination category, so a user can copy/paste across categories. + if (categoryIdx > -1) + radioData.models[modelIdx].category = categoryIdx; + strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str()); + lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected + modified = true; + if (doMove) { + deletesList.append(origMdlIdx); + removeModelFromCutList(origMdlIdx); + } + } + //qDebug().nospace() << "i: " << i << "; modelIdx:" << modelIdx << "; origMdlIdx: " << origMdlIdx << "; doMove: " << doMove << "; inserts:" << inserts << "; deletes: " << deletesList; + + ++modelIdx; + } + + if (deletesList.size()) { + deleteModels(deletesList); + } + if (modified) { + setModified(); + } +} + +/* + * General settings CRUD +*/ + +void MdiChild::pasteGeneralData(const QMimeData * mimeData) +{ + GeneralSettings gs; + bool hasGenSettings = false; + + if (!TreeModel::decodeMimeData(mimeData, NULL, &gs, &hasGenSettings)) + return; + + if (hasGenSettings && askQuestion(tr("Do you want to overwrite radio general settings?")) == QMessageBox::Yes) { + radioData.generalSettings = gs; setModified(); } } @@ -413,97 +1071,173 @@ void MdiChild::generalEdit() t->show(); } -void MdiChild::categoryAdd() +void MdiChild::copyGeneralSettings() { - /*: Translators do NOT use accent for this, this is the default category name on Horus. */ - CategoryData category(qPrintable(tr("New category"))); - radioData.categories.push_back(category); - setModified(); - emit copyAvailable(false); // workaround : nothing is selected after model creation + QMimeData * mimeData = modelsListModel->getGeneralMimeData(); + modelsListModel->getHeaderMimeData(mimeData); + QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); + updateNavigation(); } -void MdiChild::categoryRename() +void MdiChild::pasteGeneralSettings() { - QModelIndex modelIndex = ui->modelsList->currentIndex(); - int categoryIndex = modelsListModel->getCategoryIndex(modelIndex); - if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { - return; + if (hasClipboardData(1)) { + pasteGeneralData(QApplication::clipboard()->mimeData()); } - - // Not calling setModified() here because QAbstractItemView::edit will - // haandle it if the user actually makes a change - ui->modelsList->edit(modelIndex); } -void MdiChild::categoryDelete() -{ - int categoryIndex = modelsListModel->getCategoryIndex(ui->modelsList->currentIndex()); - if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { - return; - } +/* + * Action targets +*/ - for (unsigned i=0; igetModelsMimeData(ui->modelsList->selectionModel()->selectedRows()); + modelsListModel->getHeaderMimeData(mimeData); + QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); + + clearCutList(); // clear the list by default, populate afterwards, eg. in cut(). + updateNavigation(); +} + +void MdiChild::cut() +{ + copy(); + cutModels = getSelectedModels(); + modelsListModel->markItemsForCut(ui->modelsList->selectionModel()->selectedIndexes()); +} + +void MdiChild::removeModelFromCutList(const int modelIndex) +{ + int idx = cutModels.indexOf(modelIndex); + if (idx > -1) { + cutModels.remove(idx); + modelsListModel->markItemForCut(modelsListModel->getIndexForModel(modelIndex), false); + } +} + +void MdiChild::clearCutList() +{ + foreach (const int id, cutModels) { + removeModelFromCutList(id); + } +} + +// type = 0 for models (default), 1 for general radio data +bool MdiChild::hasClipboardData(const quint8 type) const +{ + if (!type) { + return modelsListModel->hasModelsMimeData(QApplication::clipboard()->mimeData()); + } + else { + return modelsListModel->hasGenralMimeData(QApplication::clipboard()->mimeData()); + } +} + +void MdiChild::paste() +{ + if (hasClipboardData()) { + pasteModelData(QApplication::clipboard()->mimeData(), getCurrentIndex()); + } +} + +void MdiChild::insert() +{ + if (hasClipboardData()) { + pasteModelData(QApplication::clipboard()->mimeData(), getCurrentIndex(), true); + } +} + +void MdiChild::edit() +{ + onItemActivated(getCurrentIndex()); +} + +void MdiChild::confirmDelete() +{ + if (hasSelectedModel()) { + if (!countSelectedModels() || askQuestion(tr("Delete %n selected model(s)?", 0, countSelectedModels())) == QMessageBox::Yes) { + deleteSelectedModels(); } } - - radioData.categories.erase(radioData.categories.begin() + categoryIndex); - for (unsigned i=0; i categoryIndex) { - model.category--; + else if (hasSelectedCat()) { + if (askQuestion(tr("Delete %n selected category(ies)?", 0, countSelectedCats())) == QMessageBox::Yes) { + deleteSelectedCats(); } } - - setModified(); -} - -void MdiChild::onDataChanged(const QModelIndex & index) -{ - int modelIndex = modelsListModel->getModelIndex(index); - if (modelIndex >= 0) { - return; - } - int categoryIndex = modelsListModel->getCategoryIndex(index); - if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { - return; - } - strcpy(radioData.categories[categoryIndex].name, modelsListModel->data(index, 0).toString().left(sizeof(CategoryData::name)-1).toStdString().c_str()); - fileChanged = true; - documentWasModified(); } void MdiChild::modelAdd() { - int categoryIndex = modelsListModel->getCategoryIndex(ui->modelsList->currentIndex()); - if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { + int modelIdx = -1; + // add to currently selected empty slot? + if (modelsListModel->isModelType(getCurrentIndex())) { + int mIdx = modelsListModel->getModelIndex(getCurrentIndex()); + if (mIdx > -1 && mIdx < (int)radioData.models.size() && radioData.models[mIdx].isEmpty()) { + modelIdx = mIdx; + } + } + newModel(modelIdx); +} + +void MdiChild::modelDuplicate() +{ + int srcModelIndex = getCurrentModel(); + if (srcModelIndex < 0) { return; } - ModelData model; - radioData.models.push_back(model); - int row = radioData.models.size() - 1; - checkAndInitModel(row); - radioData.models[row].category = categoryIndex; - strcpy(radioData.models[row].filename, radioData.getNextModelFilename().toStdString().c_str()); - strcpy(radioData.models[row].name, qPrintable(tr("New model"))); + int newIdx = modelAppend(ModelData(radioData.models[srcModelIndex])); + if (newIdx > -1) { + newModel(newIdx); + } + else { + showWarning(tr("Cannot duplicate model, could not find an available model slot.")); + } +} - // Only set the default model if we just added the first one. - if (row == 0) { - radioData.setCurrentModel(row); +void MdiChild::onModelMoveToCategory() +{ + if (!sender()) { + return; + } + bool ok = false; + int toCatId = sender()->property("categoryId").toInt(&ok); + if (ok && toCatId >= 0) { + moveSelectedModelsToCat(toCatId); } - setModified(); - emit copyAvailable(false); // workaround : nothing is selected after model creation } void MdiChild::modelEdit() { - int row = getCurrentModel(); + openModelEditWindow(getCurrentModel()); +} + +void MdiChild::wizardEdit() +{ + openModelWizard(getCurrentModel()); +} + +void MdiChild::openModelWizard(int row) +{ + if (row < 0 && (row = getCurrentModel()) < 0) + return; + + WizardDialog * wizard = new WizardDialog(radioData.generalSettings, row+1, radioData.models[row], this); + int res = wizard->exec(); + if (res == QDialog::Accepted && wizard->mix.complete /*TODO rather test the exec() result?*/) { + radioData.models[row] = wizard->mix; + radioData.fixModelFilenames(); + setModified(); + setSelectedModel(row); + } +} + +void MdiChild::openModelEditWindow(int row) +{ + if (row < 0 && (row = getCurrentModel()) < 0) + return; + QApplication::setOverrideCursor(Qt::WaitCursor); checkAndInitModel(row); ModelData & model = radioData.models[row]; @@ -517,42 +1251,24 @@ void MdiChild::modelEdit() t->show(); QApplication::restoreOverrideCursor(); gStopwatch.report("ModelEdit shown"); + } -void MdiChild::modelDuplicate() +void MdiChild::print(int model, const QString & filename) { - int srcModelIndex = getCurrentModel(); - if (srcModelIndex < 0) { - return; + // TODO + PrintDialog * pd = NULL; + + if (model>=0 && !filename.isEmpty()) { + pd = new PrintDialog(this, firmware, radioData.generalSettings, radioData.models[model], filename); + } + else { + pd = new PrintDialog(this, firmware, radioData.generalSettings, radioData.models[getCurrentModel()]); } - ModelData model(radioData.models[srcModelIndex]); - strcpy(model.filename, radioData.getNextModelFilename().toStdString().c_str()); - radioData.models.push_back(model); - setModified(); -} - -void MdiChild::modelChangeCategory(int toCategoryId) -{ - int row = getCurrentModel(); - if (row < 0 || row >= (int)radioData.models.size() || toCategoryId < 0) { - return; - } - if (radioData.models[row].category != toCategoryId) { - radioData.models[row].category = toCategoryId; - setModified(); - } -} - -void MdiChild::onModelMoveToCategory() -{ - if (!sender()) { - return; - } - bool ok = false; - int toCatId = sender()->property("categoryId").toInt(&ok); - if (ok && toCatId >= 0) { - modelChangeCategory(toCatId); + if (pd) { + pd->setAttribute(Qt::WA_DeleteOnClose, true); + pd->show(); } } @@ -561,35 +1277,17 @@ void MdiChild::setDefault() int row = getCurrentModel(); if (!radioData.models[row].isEmpty() && radioData.generalSettings.currModelIndex != (unsigned)row) { radioData.setCurrentModel(row); - setModified(); } } -void MdiChild::wizardEdit() +void MdiChild::radioSimulate() { - int row = getCurrentModel(); - checkAndInitModel(row); - WizardDialog * wizard = new WizardDialog(radioData.generalSettings, row+1, radioData.models[row], this); - wizard->exec(); - if (wizard->mix.complete /*TODO rather test the exec() result?*/) { - radioData.models[row] = wizard->mix; - radioData.fixModelFilenames(); - setModified(); - } + startSimulation(this, radioData, -1); } -void MdiChild::openModelEditWindow() +void MdiChild::modelSimulate() { - int row = getCurrentModel(); - if (row >= 0) { - ModelData & model = radioData.models[row]; - if (model.isEmpty() && g.useWizard()) { - wizardEdit(); - } - else { - modelEdit(); - } - } + startSimulation(this, radioData, getCurrentModel()); } void MdiChild::newFile() @@ -598,6 +1296,10 @@ void MdiChild::newFile() isUntitled = true; curFile = QString("document%1.otx").arg(sequenceNumber++); updateTitle(); + + if (firmware->getCapability(Capability::HasModelCategories)) { + categoryAdd(); + } } bool MdiChild::loadFile(const QString & filename, bool resetCurrentFile) @@ -613,13 +1315,16 @@ bool MdiChild::loadFile(const QString & filename, bool resetCurrentFile) // TODO ShowEepromWarnings(this, tr("Warning"), warning); } - refresh(true); if (resetCurrentFile) { setCurrentFile(filename); } if (!storage.isBoardCompatible(getCurrentBoard())) { convertStorage(storage.getBoard(), getCurrentBoard()); + setModified(); + } + else { + refresh(); } return true; @@ -680,34 +1385,11 @@ bool MdiChild::saveFile(const QString & filename, bool setCurrent) return true; } -QString MdiChild::userFriendlyCurrentFile() const -{ - return QFileInfo(curFile).fileName(); -} - -void MdiChild::closeEvent(QCloseEvent *event) -{ - if (maybeSave()) { - event->accept(); - } - else { - event->ignore(); - } -} - -void MdiChild::documentWasModified() -{ - setWindowModified(fileChanged); -} - bool MdiChild::maybeSave() { if (fileChanged) { - QMessageBox::StandardButton ret; - ret = QMessageBox::warning(this, tr("Companion"), - tr("%1 has been modified.\n" - "Do you want to save your changes?").arg(userFriendlyCurrentFile()), - QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + int ret = askQuestion(tr("%1 has been modified.\nDo you want to save your changes?").arg(userFriendlyCurrentFile()), + QMessageBox::Save, QMessageBox::Discard, QMessageBox::Cancel | QMessageBox::Default); if (ret == QMessageBox::Save) return save(); @@ -717,6 +1399,16 @@ bool MdiChild::maybeSave() return true; } +QString MdiChild::currentFile() const +{ + return curFile; +} + +QString MdiChild::userFriendlyCurrentFile() const +{ + return QFileInfo(curFile).fileName(); +} + void MdiChild::setCurrentFile(const QString & fileName) { curFile = QFileInfo(fileName).canonicalFilePath(); @@ -733,6 +1425,34 @@ void MdiChild::setCurrentFile(const QString & fileName) g.recentFiles(files); } +void MdiChild::forceNewFilename(const QString & suffix, const QString & ext) +{ + QFileInfo info(curFile); + curFile = info.path() + "/" + info.baseName() + suffix + ext; +} + + +void MdiChild::convertStorage(Board::Type from, Board::Type to) +{ + showWarning(tr("Models and settings will be automatically converted.\nIf that is not what you intended, please close the file\nand choose the correct radio type/profile before reopening it.")); + radioData.convert(from, to); + forceNewFilename("_converted", ".otx"); + initModelsList(); + fileChanged = true; + isUntitled = true; +} + +void MdiChild::showWarning(const QString & msg) +{ + if (!msg.isEmpty()) + QMessageBox::warning(this, "Companion", msg); +} + +int MdiChild::askQuestion(const QString & msg, int button0, int button1, int button2) +{ + return QMessageBox::question(this, tr("Companion"), msg, button0, button1, button2); +} + void MdiChild::writeEeprom() // write to Tx { Board::Type board = getCurrentBoard(); @@ -745,7 +1465,7 @@ void MdiChild::writeEeprom() // write to Tx return; } if (saveFile(radioPath, false)) { - parent->statusBar()->showMessage(tr("Models and Settings written"), 2000); + emit newStatusMessage(tr("Models and Settings written"), 2000); } else { qDebug() << "MdiChild::writeEeprom(): saveFile error"; @@ -763,49 +1483,6 @@ void MdiChild::writeEeprom() // write to Tx } } -void MdiChild::on_radioSettings_clicked() -{ - generalEdit(); -} - -void MdiChild::radioSimulate() -{ - startSimulation(this, radioData, -1); -} - -void MdiChild::modelSimulate() -{ - startSimulation(this, radioData, getCurrentModel()); -} - -void MdiChild::print(int model, const QString & filename) -{ - // TODO - PrintDialog * pd = NULL; - - if (model>=0 && !filename.isEmpty()) { - pd = new PrintDialog(this, firmware, radioData.generalSettings, radioData.models[model], filename); - } - else { - pd = new PrintDialog(this, firmware, radioData.generalSettings, radioData.models[getCurrentModel()]); - } - - if (pd) { - pd->setAttribute(Qt::WA_DeleteOnClose, true); - pd->show(); - } -} - -int MdiChild::getCurrentModel() const -{ - return modelsListModel->getModelIndex(ui->modelsList->currentIndex()); -} - -int MdiChild::getCurrentCategory() const -{ - return modelsListModel->getCategoryIndex(ui->modelsList->currentIndex()); -} - bool MdiChild::loadBackup() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open backup Models and Settings file"), g.eepromDir(), tr(EEPROM_FILES_FILTER)); diff --git a/companion/src/mdichild.h b/companion/src/mdichild.h index 7ab23c6a2..2aa20ae1f 100644 --- a/companion/src/mdichild.h +++ b/companion/src/mdichild.h @@ -23,9 +23,14 @@ #include "eeprominterface.h" #include "modelslist.h" -#include -class MainWindow; +#include +#include +#include +#include +#include + +class QToolBar; namespace Ui { class MdiChild; @@ -33,93 +38,198 @@ class MdiChild; class MdiChild : public QWidget { - // friend class ModelsListWidget; - - Q_OBJECT + Q_OBJECT public: - MdiChild(MainWindow * parent); + enum Actions { + ACT_GEN_EDT, // radio general settings + ACT_GEN_CPY, + ACT_GEN_PST, + ACT_GEN_SIM, + ACT_ITM_EDT, // edit model/rename category + ACT_ITM_DEL, // delete model or cat + ACT_CAT_ADD, // category actions... + //ACT_CAT_EDT, // not sure these are needed... + //ACT_CAT_DEL, // the ACT_ITM_* actions do the same thing + ACT_CAT_SEP, // convenience separator shown/hidden with category actions + ACT_MDL_ADD, // model actions... + ACT_MDL_CPY, + ACT_MDL_CUT, + ACT_MDL_PST, + ACT_MDL_DUP, + ACT_MDL_INS, + ACT_MDL_MOV, + ACT_MDL_RTR, // ResToRe backup + ACT_MDL_WIZ, + ACT_MDL_DFT, // set as DeFaulT + ACT_MDL_PRT, // print + ACT_MDL_SIM, + ACT_ENUM_END + }; + + MdiChild(QWidget *parent = Q_NULLPTR, QWidget * parentWin = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); ~MdiChild(); + QString currentFile() const; + QString userFriendlyCurrentFile() const; + QVector getSelectedCategories() const; + QVector getSelectedModels() const; + QList getGeneralActions(); + QList getEditActions(bool incCatNew = true); + QList getModelActions(); + //QList getCategoryActions(); + QAction * getAction(const Actions type); + + public slots: void newFile(); bool loadFile(const QString & fileName, bool resetCurrentFile=true); bool save(); bool saveAs(bool isNew=false); bool saveFile(const QString & fileName, bool setCurrent=true); - bool hasModelSelected(); - bool hasPasteData() const; - QString userFriendlyCurrentFile() const; - QString currentFile() const { return curFile; } - int getCurrentModel() const; - int getCurrentCategory() const; - void refresh(bool expand=false); - void keyPressEvent(QKeyEvent * event); - - signals: - void copyAvailable(bool val); - - protected: - void convertStorage(Board::Type from, Board::Type to); - void forceNewFilename(const QString & suffix, const QString & ext); - void closeEvent(QCloseEvent * event); - - protected slots: - void documentWasModified(); - void on_simulateButton_clicked(); - void onModelMoveToCategory(); - void on_radioSettings_clicked(); - void setDefault(); + void writeEeprom(); + void print(int model=-1, const QString & filename=""); void onFirmwareChanged(); - public slots: + signals: + void modified(); + void newStatusMessage(const QString & msg, const int duration); + + protected: + virtual void changeEvent(QEvent * event); + virtual void closeEvent(QCloseEvent * event); + virtual void resizeEvent(QResizeEvent * event); + virtual QSize sizeHint() const; + + private slots: + void setupNavigation(); + void updateNavigation(); + void updateTitle(); + void setModified(); + void retranslateUi(); void showModelsListContextMenu(const QPoint & pos); - void checkAndInitModel(int row); - void generalEdit(); - void categoryAdd(); - void categoryRename(); - void categoryDelete(); - void modelAdd(); - void modelEdit(); - void modelDuplicate(); - void modelChangeCategory(int toCategoryId); - void wizardEdit(); - void openModelEditWindow(); - bool loadBackup(); - void confirmDelete(); - void deleteSelectedModels(); + void showContextMenu(const QPoint & pos); + void adjustToolbarLayout(); + + void initModelsList(); + void refresh(); + void onItemActivated(const QModelIndex index); + void onItemSelected(const QModelIndex &); + void onCurrentItemChanged(const QModelIndex &, const QModelIndex &); void onDataChanged(const QModelIndex & index); - void cut(); + void generalEdit(); + void copyGeneralSettings(); + void pasteGeneralSettings(); + void copy(); + void cut(); void paste(); - void writeEeprom(); + void insert(); + void edit(); + void confirmDelete(); + void categoryAdd(); + void modelAdd(); + void modelEdit(); + void wizardEdit(); + void modelDuplicate(); + void onModelMoveToCategory(); + + void openModelWizard(int row = -1); + void openModelEditWindow(int row = -1); + + void setDefault(); void modelSimulate(); void radioSimulate(); - void print(int model=-1, const QString & filename=""); - void setModified(); - void updateTitle(); - void onItemSelected(QModelIndex); - bool isModel(QModelIndex); - bool isCategory(QModelIndex); + bool loadBackup(); + + void pasteModelData(const QMimeData * mimeData, const QModelIndex row, bool insert = false, bool move = false); + void pasteGeneralData(const QMimeData * mimeData); private: + QAction *addAct(Actions actId, const QString & icon, const char * slot = 0, const QKeySequence & shortcut = 0, QObject * slotObj = NULL); + + QModelIndex getCurrentIndex() const; + int getCurrentModel() const; + int countSelectedModels() const; + bool hasSelectedModel(); + bool setSelectedModel(const int modelIndex); + int getCurrentCategory() const; + int countSelectedCats() const; + bool hasSelectedCat(); + + bool deleteCategory(int categoryIndex = -1, QString * error = NULL); + void deleteSelectedCats(); + + void checkAndInitModel(int row); + void findNewDefaultModel(const unsigned startAt = 0); + bool insertModelRows(int atModelIdx, int count); + int modelAppend(const ModelData model); + int newModel(int modelIndex = -1, int categoryIndex = -1); + unsigned deleteModels(const QVector modelIndices); + bool deleteModel(const int modelIndex); + void deleteSelectedModels(); + void moveModelsToCategory(const QVector models, const int toCategoryId); + void moveSelectedModelsToCat(const int toCategoryId); + + void clearCutList(); + void removeModelFromCutList(const int modelIndex); + bool hasClipboardData(const quint8 type = 0) const; + bool maybeSave(); void setCurrentFile(const QString & fileName); - void doCopy(QByteArray * gmData); - void doPaste(QByteArray * gmData, QModelIndex row); - void initModelsList(); + void forceNewFilename(const QString & suffix, const QString & ext); + void convertStorage(Board::Type from, Board::Type to); + void showWarning(const QString & msg); + int askQuestion(const QString & msg, int button0 = QMessageBox::Yes, int button1 = QMessageBox::No | QMessageBox::Default, int button2 = 0); - MainWindow * parent; Ui::MdiChild * ui; TreeModel * modelsListModel; + QWidget * parentWindow; QString curFile; + QVector cutModels; + QVector action; + QToolBar * radioToolbar; + QToolBar * categoriesToolbar; + QToolBar * modelsToolbar; Firmware * firmware; RadioData radioData; + int lastSelectedModel; bool isUntitled; bool fileChanged; + bool showCatToolbar; + const quint16 stateDataVersion; +}; + +// This will draw the drop indicator across all columns of a model View (vs. in just one column), and lets us make the indicator more obvious. +class ItemViewProxyStyle: public QProxyStyle +{ + public: + ItemViewProxyStyle(QStyle * style = 0) : QProxyStyle(style) {} + + void drawPrimitive(PrimitiveElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget = 0) const + { + if (element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull()) { + painter->save(); + // set a wider stroke + QPen pen(painter->pen()); + pen.setWidthF(2.5f); + painter->setPen(pen); + // adjust the rectangle size of the indicator to encompass the whole row + QStyleOption opt(*option); + opt.rect.setLeft(0); + if (widget) + opt.rect.setRight(widget->width()); + // call default handler and exit + QProxyStyle::drawPrimitive(element, &opt, painter, widget); + painter->restore(); + } + else { + QProxyStyle::drawPrimitive(element, option, painter, widget); + } + } }; #endif // _MDICHILD_H_ diff --git a/companion/src/mdichild.ui b/companion/src/mdichild.ui index 0b8a6c1b3..d4b48cfa7 100644 --- a/companion/src/mdichild.ui +++ b/companion/src/mdichild.ui @@ -6,35 +6,37 @@ 0 0 - 468 - 414 + 545 + 575 - - - - + + + 5 + - - - Radio settings + + + 2 - + - + + + 2 + + - - - Simulate Tx + + + true - - - + diff --git a/companion/src/modelslist.cpp b/companion/src/modelslist.cpp index 19ffff11a..98ef3dbeb 100644 --- a/companion/src/modelslist.cpp +++ b/companion/src/modelslist.cpp @@ -24,16 +24,17 @@ TreeItem::TreeItem(const QVector & itemData): itemData(itemData), parentItem(NULL), categoryIndex(-1), - modelIndex(-1) + modelIndex(-1), + flags(0) { } TreeItem::TreeItem(TreeItem * parent, int categoryIndex, int modelIndex): - itemData(parent->columnCount()), - parentItem(parent), - categoryIndex(categoryIndex), - modelIndex(modelIndex) + TreeItem(QVector(parent->columnCount())) { + setParent(parent); + setCategoryIndex(categoryIndex); + setModelIndex(modelIndex); } TreeItem::~TreeItem() @@ -69,16 +70,16 @@ QVariant TreeItem::data(int column) const return itemData.value(column); } -TreeItem * TreeItem::appendChild(int categoryIndex, int modelIndex) +TreeItem *TreeItem::insertChild(const int row, int categoryIndex, int modelIndex) { TreeItem * item = new TreeItem(this, categoryIndex, modelIndex); - childItems.insert(childItems.size(), item); + childItems.insert(row, item); return item; } -TreeItem * TreeItem::parent() +TreeItem * TreeItem::appendChild(int categoryIndex, int modelIndex) { - return parentItem; + return insertChild(childItems.size(), categoryIndex, modelIndex); } bool TreeItem::removeChildren(int position, int count) @@ -92,6 +93,14 @@ bool TreeItem::removeChildren(int position, int count) return true; } +bool TreeItem::insertChildren(int row, int count) +{ + for (int i=0; i < count; ++i) { + insertChild(row + i, -1, -1); + } + return true; +} + bool TreeItem::setData(int column, const QVariant & value) { if (column < 0 || column >= itemData.size()) @@ -101,6 +110,29 @@ bool TreeItem::setData(int column, const QVariant & value) return true; } +void TreeItem::setFlag(const quint16 & flag, const bool on) +{ + if (on) + flags |= flag; + else + flags &= ~flag; +} + +bool TreeItem::isCategory() const +{ + return (modelIndex < 0 && categoryIndex > -1); +} + +bool TreeItem::isModel() const +{ + return (modelIndex > -1); +} + + +/* + * TreeModel +*/ + TreeModel::TreeModel(RadioData * radioData, QObject * parent): QAbstractItemModel(parent), radioData(radioData), @@ -108,14 +140,20 @@ TreeModel::TreeModel(RadioData * radioData, QObject * parent): { Board::Type board = getCurrentBoard(); QVector labels; - if (!IS_HORUS(board)) + if (!getCurrentFirmware()->getCapability(Capability::HasModelCategories)) labels << tr("Index"); labels << tr("Name"); if (!(IS_HORUS(board) || IS_SKY9X(board))) { labels << tr("Size"); } rootItem = new TreeItem(labels); + // uniqueId and version for drag/drop operations (see encodeHeaderData()) + mimeHeaderData.instanceId = QUuid::createUuid(); + mimeHeaderData.dataVersion = 1; + refresh(); + //connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TreeModel::onRowsAboutToBeRemoved); + connect(this, &QAbstractItemModel::rowsRemoved, this, &TreeModel::onRowsRemoved); } TreeModel::~TreeModel() @@ -133,39 +171,38 @@ QVariant TreeModel::data(const QModelIndex & index, int role) const if (!index.isValid()) return QVariant(); - if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::FontRole) { - return QVariant(); - } - TreeItem * item = getItem(index); - if (role == Qt::FontRole) { - if (item->getModelIndex() == (int)radioData->generalSettings.currModelIndex) { - QFont font; - font.setBold(true); - return font; - } + if (role == Qt::DisplayRole || role == Qt::EditRole) { + return item->data(index.column()); } - return item->data(index.column()); + + if (role == Qt::FontRole && item->getModelIndex() == (int)radioData->generalSettings.currModelIndex) { + QFont font; + font.setBold(true); + return font; + } + + if (role == Qt::ForegroundRole && (item->getFlags() & TreeItem::MarkedForCut)) { + return QPalette().brush(QPalette::Disabled, QPalette::Text); + } + + return QVariant(); } Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { - if (!index.isValid()) - return 0; + Qt::ItemFlags f = QAbstractItemModel::flags(index); - return Qt::ItemIsEditable | QAbstractItemModel::flags(index); -} - -TreeItem * TreeModel::getItem(const QModelIndex & index) const -{ if (index.isValid()) { - TreeItem * item = static_cast(index.internalPointer()); - if (item) { - return item; - } + if (getItem(index)->isCategory()) + f |= Qt::ItemIsEditable; + else + f |= Qt::ItemIsDragEnabled; // TODO drag/drop categories } - return rootItem; + f |= Qt::ItemIsDropEnabled; + + return f; } QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -206,8 +243,10 @@ QModelIndex TreeModel::parent(const QModelIndex & index) const bool TreeModel::removeRows(int position, int rows, const QModelIndex & parent) { TreeItem * parentItem = getItem(parent); - bool success = true; + if (!parentItem) + return false; + bool success = true; if (position >= 0 && rows > 0) { beginRemoveRows(parent, position, position + rows - 1); success = parentItem->removeChildren(position, rows); @@ -217,6 +256,23 @@ bool TreeModel::removeRows(int position, int rows, const QModelIndex & parent) return success; } +/* unused but possibly useful in future +bool TreeModel::insertRows(int row, int count, const QModelIndex & parent) +{ + TreeItem * parentItem = getItem(parent); + if (!parentItem) + return false; + + bool success = true; + if (row >= 0 && count > 0) { + beginInsertRows(parent, row, row + count - 1); + success = parentItem->insertChildren(row, count); + endInsertRows(); + } + + return success; +} */ + int TreeModel::rowCount(const QModelIndex &parent) const { TreeItem * parentItem = getItem(parent); @@ -241,21 +297,361 @@ bool TreeModel::setData(const QModelIndex & index, const QVariant & value, int r return result; } +QStringList TreeModel::mimeTypes() const +{ + QStringList types; + types << "application/x-companion-modeldata"; + types << "application/x-companion-generaldata"; + //type << "application/x-companion-radiodata-header"; // supported but not advertised, must be in conjunction with one of the above + return types; +} + +Qt::DropActions TreeModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::DropActions TreeModel::supportedDragActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +// This method encodes all the data on default drag operation, including general radio settings. This is useful for eg. Compare dialog/model printer. +QMimeData * TreeModel::mimeData(const QModelIndexList & indexes) const +{ + QMimeData * mimeData = new QMimeData(); + getModelsMimeData(indexes, mimeData); + getGeneralMimeData(mimeData); + getHeaderMimeData(mimeData); + return mimeData; +} + +bool TreeModel::canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const +{ + Q_UNUSED(action); + //qDebug() << action << row << column << parent.row(); + + // we do not accept dropped general settings right now (user must copy/paste those) + if (hasHeaderMimeData(data) && hasModelsMimeData(data) && (row > -1 || parent.isValid())) + return true; + + return false; +} + +bool TreeModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) +{ + if (!canDropMimeData(data, action, row, column, parent)) + return false; + + if (action == Qt::IgnoreAction) + return true; + + QModelIndex idx; + bool isInsert = false; + if (row > -1) { + // dropped between rows (insert) + isInsert = true; + idx = index(row, column, parent); + } + else if (parent.isValid()) { + // was dropped on a row (overwrite) + idx = parent; + } + else { + // dropped who knows where, (shouldn't be here though due check in canDropMimeData()) + return false; + } + //qDebug() << action << row << column << parent.row() << idx << idx.row() << hasOwnMimeData(data); + + // Force drops from other file windows to be copy actions because we don't want to delete our models. + if (action == Qt::MoveAction && !hasOwnMimeData(data)) + action = Qt::CopyAction; + + // canDropMimeData() only accepts models + emit modelsDropped(data, idx, isInsert, (action == Qt::MoveAction)); + + return true; +} + +QMimeData *TreeModel::getModelsMimeData(const QModelIndexList & indexes, QMimeData * mimeData) const +{ + if (!mimeData) + mimeData = new QMimeData(); + QByteArray mData; + encodeModelsData(indexes, &mData); + mimeData->setData("application/x-companion-modeldata", mData); + return mimeData; +} + +QMimeData *TreeModel::getGeneralMimeData(QMimeData * mimeData) const +{ + if (!mimeData) + mimeData = new QMimeData(); + QByteArray mData; + encodeGeneralData(&mData); + mimeData->setData("application/x-companion-generaldata", mData); + return mimeData; +} + +QMimeData *TreeModel::getHeaderMimeData(QMimeData * mimeData) const +{ + if (!mimeData) + mimeData = new QMimeData(); + QByteArray mData; + encodeHeaderData(&mData); + mimeData->setData("application/x-companion-radiodata-header", mData); + return mimeData; +} + +QUuid TreeModel::getMimeDataSourceId(const QMimeData * mimeData) const +{ + MimeHeaderData header; + decodeHeaderData(mimeData, &header); + return header.instanceId; +} + +bool TreeModel::hasSupportedMimeData(const QMimeData * mimeData) const +{ + foreach (const QString & mtype, mimeTypes()) { + if (mimeData->hasFormat(mtype)) + return true; + } + return false; +} + +bool TreeModel::hasModelsMimeData(const QMimeData * mimeData) const +{ + return mimeData->hasFormat("application/x-companion-modeldata"); +} + +bool TreeModel::hasGenralMimeData(const QMimeData * mimeData) const +{ + return mimeData->hasFormat("application/x-companion-generaldata"); +} + +bool TreeModel::hasHeaderMimeData(const QMimeData * mimeData) const +{ + return mimeData->hasFormat("application/x-companion-radiodata-header"); +} + +// returns true if mime data origin was this data model (vs. from another file window) +bool TreeModel::hasOwnMimeData(const QMimeData * mimeData) const +{ + return (getMimeDataSourceId(mimeData) == mimeHeaderData.instanceId); +} + +void TreeModel::encodeModelsData(const QModelIndexList & indexes, QByteArray * data) const +{ + foreach (const QModelIndex &index, indexes) { + if (index.isValid() && index.column() == 0) { + if (!getItem(index)->isCategory()) { // TODO: encode categoreis also + data->append('M'); + data->append((char *)&radioData->models[getModelIndex(index)], sizeof(ModelData)); + } + } + } +} + +void TreeModel::encodeGeneralData(QByteArray * data) const +{ + data->append('G'); + data->append((char *)&radioData->generalSettings, sizeof(GeneralSettings)); +} + +void TreeModel::encodeHeaderData(QByteArray * data) const +{ + // We use a unique ID representing this TreeModel instance (a unique file). + // This can be used eg. to detect cross-file drop operations. + QDataStream stream(data, QIODevice::WriteOnly); + stream << mimeHeaderData.dataVersion; + stream << mimeHeaderData.instanceId; +} + +// static +bool TreeModel::decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * header) +{ + if (header && mimeData->hasFormat("application/x-companion-radiodata-header")) { + QByteArray data = mimeData->data("application/x-companion-radiodata-header"); + QDataStream stream(&data, QIODevice::ReadOnly); + stream >> header->dataVersion >> header->instanceId; + return true; + } + return false; +} + +// static +bool TreeModel::decodeMimeData(const QMimeData * mimeData, QVector * models, GeneralSettings * gs, bool * hasGenSet) +{ + bool ret = false; + char * gData; + + if (hasGenSet) + *hasGenSet = false; + + if (models && mimeData->hasFormat("application/x-companion-modeldata")) { + QByteArray mdlData = mimeData->data("application/x-companion-modeldata"); + gData = mdlData.data(); + int size = 0; + while (size < mdlData.size()) { + char c = *gData++; + if (c != 'M') + break; + ModelData model(*((ModelData *)gData)); + models->append(model); + gData += sizeof(ModelData); + size += sizeof(ModelData) + 1; + ret = true; + } + } + + // General settings + if (gs && mimeData->hasFormat("application/x-companion-generaldata")) { + QByteArray genData = mimeData->data("application/x-companion-generaldata"); + gData = genData.data(); + char c = *gData++; + if (c == 'G') { + *gs = *((GeneralSettings *)gData); + ret = true; + if (hasGenSet) + *hasGenSet = true; + } + } + + return ret; +} + +// static +int TreeModel::countModelsInMimeData(const QMimeData * mimeData) +{ + int ret = 0; + if (mimeData->hasFormat("application/x-companion-modeldata")) { + QByteArray mdlData = mimeData->data("application/x-companion-modeldata"); + ret = mdlData.size() / (sizeof(ModelData) + 1); + } + return ret; +} + + +TreeItem * TreeModel::getItem(const QModelIndex & index) const +{ + if (index.isValid()) { + TreeItem * item = static_cast(index.internalPointer()); + if (item) { + return item; + } + } + return rootItem; +} + +// recursive +QModelIndex TreeModel::getIndexForModel(const int modelIndex, QModelIndex parent) +{ + for (int i=0; i < rowCount(parent); ++i) { + QModelIndex idx = index(i, 0, parent); + if (hasChildren(idx) && (idx = getIndexForModel(modelIndex, idx)).isValid()) + return idx; + if (getItem(idx)->getModelIndex() == modelIndex) + return idx; + } + return QModelIndex(); +} + +QModelIndex TreeModel::getIndexForCategory(const int categoryIndex) +{ + for (int i=0; i < rowCount(); ++i) { + if (getItem(index(i, 0))->getCategoryIndex() == categoryIndex) + return index(i, 0); + } + return QModelIndex(); +} + +int TreeModel::getAvailableEEpromSize() +{ + return availableEEpromSize; +} + +int TreeModel::getModelIndex(const QModelIndex & index) const +{ + return getItem(index)->getModelIndex(); +} + +int TreeModel::getCategoryIndex(const QModelIndex & index) const +{ + return getItem(index)->getCategoryIndex(); +} + +int TreeModel::rowNumber(const QModelIndex & index) const +{ + return getItem(index)->childNumber(); +} + +bool TreeModel::isCategoryType(const QModelIndex & index) const +{ + return index.isValid() && getItem(index)->isCategory(); +} + +bool TreeModel::isModelType(const QModelIndex & index) const +{ + return index.isValid() && getItem(index)->isModel(); +} + +void TreeModel::markItemForCut(const QModelIndex & index, bool on) +{ + if (index.isValid() && index.column() == 0) + getItem(index)->setFlag(TreeItem::MarkedForCut, on); +} + +void TreeModel::markItemsForCut(const QModelIndexList & indexes, bool on) +{ + foreach (const QModelIndex &index, indexes) + markItemForCut(index, on); +} + +// onRowsAboutToBeRemoved could be a way to deal with models being drag-drop moved to another window/file. +// TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes +// but not the actual models from the RadioData::models array. +// BUT this also runs when moving rows within our own tree, and if there is an error during the move, +// or the user cancels the operation, removeRows() is still called automatically somewhere inside QAbstractItemModel(). +// If a solution could be found to this problem then we could enable DnD-moving models between file windows. +/* +void TreeModel::onRowsAboutToBeRemoved(const QModelIndex & parent, int first, int last) +{ + qDebug() << parent << first << last; + QVector modelIndices; + for (int i=first; i <= last; ++i) { + modelIndices << getItem(index(i, 0, parent))->getModelIndex(); + } + if (modelIndices.size()) + emit modelsRemoved(modelIndices); +} +*/ + +void TreeModel::onRowsRemoved(const QModelIndex & parent, int first, int last) +{ + // This is a workaround to deal with models being DnD moved to another window/file or if user cancels a DnD move within our own. + // TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes but not our actual models. See notes above. + //qDebug() << parent << first << last; + emit refreshRequested(); // request refresh from View because it may have it's own ideas +} + void TreeModel::refresh() { EEPROMInterface * eepromInterface = getCurrentEEpromInterface(); Board::Type board = eepromInterface->getBoard(); + TreeItem * defaultCategoryItem = NULL; + bool hasCategories = getCurrentFirmware()->getCapability(Capability::HasModelCategories); + bool hasEepromSizeData = (rootItem->columnCount() > 2); - if (!IS_SKY9X(board) && !IS_HORUS(board)) { + if (hasEepromSizeData) { availableEEpromSize = Boards::getEEpromSize(board) - 64; // let's consider fat availableEEpromSize -= 16 * ((eepromInterface->getSize(radioData->generalSettings) + 14) / 15); } + this->blockSignals(true); // make sure onRowsRemoved is not triggered removeRows(0, rowCount()); + this->blockSignals(false); - TreeItem * defaultCategoryItem = NULL; - - if (IS_HORUS(board)) { + if (hasCategories) { for (unsigned i = 0; i < radioData->categories.size(); i++) { TreeItem * current = rootItem->appendChild(i, -1); current->setData(0, QString(radioData->categories[i].name)); @@ -266,7 +662,10 @@ void TreeModel::refresh() ModelData & model = radioData->models[i]; int currentColumn = 0; TreeItem * current = NULL; - if (IS_HORUS(board)) { + + model.modelIndex = i; + + if (hasCategories) { if (!model.isEmpty()) { TreeItem * categoryItem; // TODO category should be set to -1 if not Horus @@ -297,7 +696,7 @@ void TreeModel::refresh() else modelName = QString().sprintf("Model%02d", i+1); current->setData(currentColumn++, modelName); - if (!IS_SKY9X(board) && !IS_HORUS(board)) { + if (hasEepromSizeData) { int size = eepromInterface->getSize(model); current->setData(currentColumn++, QString().sprintf("%5d", size)); size = 16 * ((size + 14) / 15); @@ -310,190 +709,7 @@ void TreeModel::refresh() } } - if (!IS_SKY9X(board) && !IS_HORUS(board)) { + if (hasEepromSizeData) { availableEEpromSize = (availableEEpromSize / 16) * 15; } } - -#if 0 -ModelsListWidget::ModelsListWidget(QWidget * parent): - QTreeView(parent), - radioData(NULL) -{ - setColumnWidth(0, 50); - setColumnWidth(2, 100); - - active_highlight_color = palette().color(QPalette::Active, QPalette::Highlight); -} - -void ModelsListWidget::mousePressEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - dragStartPosition = event->pos(); - - QTreeView::mousePressEvent(event); -} - -void ModelsListWidget::mouseMoveEvent(QMouseEvent *event) -{ - if (!(event->buttons() & Qt::LeftButton)) - return; - - if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) - return; - - QDrag * drag = new QDrag(this); - - QByteArray gmData; - doCopy(&gmData); - - QMimeData * mimeData = new QMimeData; - mimeData->setData("application/x-companion", gmData); - - drag->setMimeData(mimeData); - - //Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction); - - //if(dropAction==Qt::MoveAction) - - // QTreeWidget::mouseMoveEvent(event); -} - -void ModelsListWidget::saveSelection() -{ - /*currentSelection.current_item = currentItem(); - for (int i=0; igetCapability(Models)+1; ++i) { - currentSelection.selected[i] = selectionModel()->isSelected(model()->index(i, 0)); - }*/ -} - -void ModelsListWidget::restoreSelection() -{ - /*setCurrentItem(currentSelection.current_item); - for (int i=0; igetCapability(Models)+1; ++i) { - selectionModel()->select(model()->index(i, 0), currentSelection.selected[i] ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); - }*/ -} - -void ModelsListWidget::dragEnterEvent(QDragEnterEvent *event) -{ - if (event->mimeData()->hasFormat("application/x-companion")) { - event->acceptProposedAction(); - saveSelection(); - } -} - -void ModelsListWidget::dragLeaveEvent(QDragLeaveEvent *) -{ - restoreSelection(); -} - -void ModelsListWidget::dragMoveEvent(QDragMoveEvent *event) -{ - int row = indexAt(event->pos()).row(); - const QMimeData * mimeData = event->mimeData(); - if (mimeData->hasFormat("application/x-companion")) { - QByteArray gmData = mimeData->data("application/x-companion"); - event->acceptProposedAction(); - clearSelection(); - DragDropHeader * header = (DragDropHeader *)gmData.data(); - if (row >= 0) { - if (header->general_settings) - selectionModel()->select(model()->index(0, 0), QItemSelectionModel::Select); - for (int i=row, end=std::min(getCurrentFirmware()->getCapability(Models)+1, row+header->models_count); iselect(model()->index(i, 0), QItemSelectionModel::Select); - } - } -} - -void ModelsListWidget::dropEvent(QDropEvent *event) -{ - int row = this->indexAt(event->pos()).row(); - if (row < 0) - return; - - // QMessageBox::warning(this, tr("Companion"), tr("Index :%1").arg(row)); - const QMimeData * mimeData = event->mimeData(); - - if (mimeData->hasFormat("application/x-companion")) { - QByteArray gmData = mimeData->data("application/x-companion"); - if (event->source() && event->dropAction() == Qt::MoveAction) - ((ModelsListWidget*)event->source())->doCut(&gmData); - doPaste(&gmData, row); - clearSelection(); - // setCurrentItem(topLevelItem(row)); - DragDropHeader * header = (DragDropHeader *)gmData.data(); - if (header->general_settings) - selectionModel()->select(model()->index(0, 0), QItemSelectionModel::Select); - for (int i=row, end=std::min(getCurrentFirmware()->getCapability(Models)+1, row+header->models_count); iselect(model()->index(i, 0), QItemSelectionModel::Select); - } - event->acceptProposedAction(); -} - -#ifndef WIN32 -void ModelsListWidget::focusInEvent ( QFocusEvent * event ) -{ - QTreeView::focusInEvent(event); - QPalette palette = this->palette(); - palette.setColor(QPalette::Active, QPalette::Highlight, active_highlight_color); - palette.setColor(QPalette::Inactive, QPalette::Highlight, active_highlight_color); - setPalette(palette); -} - -void ModelsListWidget::focusOutEvent ( QFocusEvent * event ) -{ - QTreeView::focusOutEvent(event); - QPalette palette = this->palette(); - palette.setColor(QPalette::Active, QPalette::Highlight, palette.color(QPalette::Active, QPalette::Midlight)); - palette.setColor(QPalette::Inactive, QPalette::Highlight, palette.color(QPalette::Active, QPalette::Midlight)); - setPalette(palette); -} -#endif - -void ModelsListWidget::doCut(QByteArray * gmData) -{ - bool modified = false; - DragDropHeader * header = (DragDropHeader *)gmData->data(); - for (int i=0; imodels_count; i++) { - if (radioData->generalSettings.currModelIndex != (unsigned int)header->models[i]-1) { - radioData->models[header->models[i]-1].clear(); - modified=true; - } - } - if (modified) { - ((MdiChild *)parent())->setModified(); - } -} - -void ModelsListWidget::duplicate() -{ - int i = this->currentRow(); - if (i && igetCapability(Models)) { - ModelData * model = &radioData->models[i-1]; - while (igetCapability(Models)) { - if (radioData->models[i].isEmpty()) { - radioData->models[i] = *model; - strcpy(radioData->models[i].filename, qPrintable(radioData->getNextModelFilename())); - ((MdiChild *)parent())->setModified(); - break; - } - i++; - } - if (i==getCurrentFirmware()->getCapability(Models)) { - QMessageBox::warning(this, "Companion", tr("No free slot available, cannot duplicate"), QMessageBox::Ok); - } - } -} - -void ModelsListWidget::onCurrentItemChanged(QTreeWidgetItem * current, QTreeWidgetItem *) -{ - /*int index = indexOfTopLevelItem(current); - if (!isVisible()) - ((MdiChild*)parent())->viableModelSelected(false); - else if (index<1) - ((MdiChild*)parent())->viableModelSelected(false); - else - ((MdiChild*)parent())->viableModelSelected(!radioData->models[currentRow()-1].isEmpty()); */ -} -#endif diff --git a/companion/src/modelslist.h b/companion/src/modelslist.h index 46668dd0d..499e96a6b 100644 --- a/companion/src/modelslist.h +++ b/companion/src/modelslist.h @@ -22,17 +22,15 @@ #define _MODELSLIST_H_ #include "eeprominterface.h" -#include - -struct CurrentSelection -{ - QTreeWidgetItem * current_item; - bool selected[CPN_MAX_MODELS+1]; -}; +#include +#include +#include class TreeItem { public: + enum TreeItemFlags { MarkedForCut = 0x01 }; + explicit TreeItem(const QVector & itemData); explicit TreeItem(TreeItem * parent, int categoryIndex, int modelIndex); ~TreeItem(); @@ -42,14 +40,26 @@ class TreeItem int columnCount() const; QVariant data(int column) const; TreeItem * appendChild(int categoryIndex, int modelIndex); - TreeItem * parent(); + TreeItem * insertChild(const int row, int categoryIndex, int modelIndex); bool removeChildren(int position, int count); + bool insertChildren(int row, int count); int childNumber() const; bool setData(int column, const QVariant &value); + TreeItem * parent() { return parentItem; } + void setParent(TreeItem * p) { parentItem = p; } int getModelIndex() const { return modelIndex; } + void setModelIndex(int value) { modelIndex = value; } int getCategoryIndex() const { return categoryIndex; } + void setCategoryIndex(int value) { categoryIndex = value; } + + quint16 getFlags() const { return flags; } + void setFlags(const quint16 & value) { flags = value; } + void setFlag(const quint16 & flag, const bool on = true); + + bool isCategory() const; + bool isModel() const; private: QList childItems; @@ -57,6 +67,7 @@ class TreeItem TreeItem * parentItem; int categoryIndex; int modelIndex; + quint16 flags; }; @@ -65,47 +76,81 @@ class TreeModel : public QAbstractItemModel Q_OBJECT public: + struct MimeHeaderData { + QUuid instanceId; + quint16 dataVersion; + }; + TreeModel(RadioData * radioData, QObject *parent = 0); virtual ~TreeModel(); - QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; - QModelIndex index(int row, int column, - const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + virtual Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; - int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual bool removeRows(int position, int rows, const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE; + //virtual bool insertRows(int row, int count, const QModelIndex & parent) Q_DECL_OVERRIDE; - Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; - bool setData(const QModelIndex &index, const QVariant &value, - int role = Qt::EditRole) Q_DECL_OVERRIDE; + virtual QStringList mimeTypes() const Q_DECL_OVERRIDE; + virtual Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; + virtual Qt::DropActions supportedDragActions() const Q_DECL_OVERRIDE; + virtual QMimeData * mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; + virtual bool canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const Q_DECL_OVERRIDE; + virtual bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; - bool removeRows(int position, int rows, - const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE; + void encodeModelsData(const QModelIndexList & indexes, QByteArray * data) const; + void encodeGeneralData(QByteArray * data) const; + void encodeHeaderData(QByteArray * data) const; + QMimeData * getModelsMimeData(const QModelIndexList & indexes, QMimeData * mimeData = NULL) const; + QMimeData * getGeneralMimeData(QMimeData * mimeData = NULL) const; + QMimeData * getHeaderMimeData(QMimeData * mimeData = NULL) const; + QUuid getMimeDataSourceId(const QMimeData * mimeData) const; + bool hasSupportedMimeData(const QMimeData * mimeData) const; + bool hasModelsMimeData(const QMimeData * mimeData) const; + bool hasGenralMimeData(const QMimeData * mimeData) const; + bool hasHeaderMimeData(const QMimeData * mimeData) const; + bool hasOwnMimeData(const QMimeData * mimeData) const; + static bool decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * header); + static bool decodeMimeData(const QMimeData * mimeData, QVector * models = NULL, GeneralSettings * gs = NULL, bool * hasGenSet = NULL); + static int countModelsInMimeData(const QMimeData * mimeData); + + QModelIndex getIndexForModel(const int modelIndex, QModelIndex parent = QModelIndex()); + QModelIndex getIndexForCategory(const int categoryIndex); + int getAvailableEEpromSize(); + int getModelIndex(const QModelIndex & index) const; + int getCategoryIndex(const QModelIndex & index) const; + int rowNumber(const QModelIndex & index = QModelIndex()) const; + bool isCategoryType(const QModelIndex & index) const; + bool isModelType(const QModelIndex & index) const; + + public slots: + void markItemForCut(const QModelIndex & index, bool on = true); + void markItemsForCut(const QModelIndexList & indexes, bool on = true); void refresh(); - int getAvailableEEpromSize() { return availableEEpromSize; } + signals: + void modelsDropped(const QMimeData * mimeData, const QModelIndex toRowIdx, const bool insert, const bool move); + void modelsRemoved(const QVector modelIndices); + void refreshRequested(); - int getModelIndex(const QModelIndex & index) const { - return getItem(index)->getModelIndex(); - } + private slots: + //void onRowsAboutToBeRemoved(const QModelIndex & parent, int first, int last); + void onRowsRemoved(const QModelIndex & parent, int first, int last); - int getCategoryIndex(const QModelIndex & index) const { - return getItem(index)->getCategoryIndex(); - } - - int rowNumber(const QModelIndex & index = QModelIndex()) const { - return getItem(index)->childNumber(); - } private: TreeItem * getItem(const QModelIndex & index) const; + TreeItem * rootItem; RadioData * radioData; int availableEEpromSize; + MimeHeaderData mimeHeaderData; }; #endif // _MODELSLIST_H_ diff --git a/companion/src/multimodelprinter.cpp b/companion/src/multimodelprinter.cpp index 858664609..73df83002 100644 --- a/companion/src/multimodelprinter.cpp +++ b/companion/src/multimodelprinter.cpp @@ -121,13 +121,10 @@ MultiModelPrinter::MultiModelPrinter(Firmware * firmware): MultiModelPrinter::~MultiModelPrinter() { - for(int i=0; i < modelPrinterMap.size(); i++) { - if (modelPrinterMap.value(i).second) - delete modelPrinterMap.value(i).second; - } + clearModels(); } -void MultiModelPrinter::setModel(int idx, const ModelData & model, const GeneralSettings & generalSettings) +void MultiModelPrinter::setModel(int idx, const ModelData * model, const GeneralSettings * generalSettings) { if (modelPrinterMap.contains(idx) && modelPrinterMap.value(idx).second) { // free existing model printer @@ -135,13 +132,22 @@ void MultiModelPrinter::setModel(int idx, const ModelData & model, const General modelPrinterMap[idx].second = NULL; } - QPair pair(&model, new ModelPrinter(firmware, generalSettings, model)); + QPair pair(model, new ModelPrinter(firmware, *generalSettings, *model)); modelPrinterMap.insert(idx, pair); // QMap.insert will replace any existing key } -void MultiModelPrinter::setModel(int idx, const ModelData & model) +void MultiModelPrinter::setModel(int idx, const ModelData * model) { - setModel(idx, model, defaultSettings); + setModel(idx, model, &defaultSettings); +} + +void MultiModelPrinter::clearModels() +{ + for(int i=0; i < modelPrinterMap.size(); i++) { + if (modelPrinterMap.value(i).second) + delete modelPrinterMap.value(i).second; + } + modelPrinterMap.clear(); } QString MultiModelPrinter::print(QTextDocument * document) diff --git a/companion/src/multimodelprinter.h b/companion/src/multimodelprinter.h index 97332691a..a896f9d6e 100644 --- a/companion/src/multimodelprinter.h +++ b/companion/src/multimodelprinter.h @@ -34,8 +34,9 @@ class MultiModelPrinter: public QObject MultiModelPrinter(Firmware * firmware); virtual ~MultiModelPrinter(); - void setModel(int idx, const ModelData & model, const GeneralSettings & generalSettings); - void setModel(int idx, const ModelData & model); + void setModel(int idx, const ModelData * model, const GeneralSettings * generalSettings); + void setModel(int idx, const ModelData * model); + void clearModels(); QString print(QTextDocument * document); protected: diff --git a/companion/src/printdialog.cpp b/companion/src/printdialog.cpp index 6f403dfb9..8ea8d0525 100644 --- a/companion/src/printdialog.cpp +++ b/companion/src/printdialog.cpp @@ -36,7 +36,7 @@ PrintDialog::PrintDialog(QWidget *parent, Firmware * firmware, GeneralSettings & ui->setupUi(this); setWindowIcon(CompanionIcon("print.png")); setWindowTitle(model.name); - multimodelprinter.setModel(0, model, generalSettings); + multimodelprinter.setModel(0, &model, &generalSettings); ui->textEdit->setHtml(multimodelprinter.print(ui->textEdit->document())); if (!printfilename.isEmpty()) { printToFile(); diff --git a/companion/src/radiodata.h b/companion/src/radiodata.h index 7a099220f..d04dbabd4 100644 --- a/companion/src/radiodata.h +++ b/companion/src/radiodata.h @@ -1021,6 +1021,8 @@ class ModelData { int category; char name[15+1]; char filename[16+1]; + int modelIndex; // Companion only, temporary index position managed by data model. + TimerData timers[CPN_MAX_TIMERS]; bool noGlobalFunctions; bool thrTrim; // Enable Throttle Trim diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 0c9ae6dfe..e6feb194c 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -434,6 +434,9 @@ QStringList AppData::simuDbgFilters() { return _simuDbgFilters; } QByteArray AppData::mainWinGeo() { return _mainWinGeo; } QByteArray AppData::mainWinState() { return _mainWinState; } QByteArray AppData::modelEditGeo() { return _modelEditGeo; } +QByteArray AppData::mdiWinGeo() { return _mdiWinGeo; } +QByteArray AppData::mdiWinState() { return _mdiWinState; } +QByteArray AppData::compareWinGeo(){ return _compareWinGeo; } QString AppData::armMcu() { return _armMcu; } QString AppData::avrArguments() { return _avrArguments; } @@ -464,8 +467,10 @@ bool AppData::snapToClpbrd() { return _snapToClpbrd; } bool AppData::autoCheckApp() { return _autoCheckApp; } bool AppData::autoCheckFw() { return _autoCheckFw; } bool AppData::simuSW() { return _simuSW; } -bool AppData::useWizard() { return _useWizard; } +bool AppData::tabbedMdi() { return _tabbedMdi; } +bool AppData::removeModelSlots() { return _remvModelSlots; } +int AppData::newModelAction() { return _newModelAction; } int AppData::backLight() { return _backLight; } int AppData::embedSplashes() { return _embedSplashes; } int AppData::fwServerFails() { return _fwServerFails; } @@ -486,6 +491,9 @@ void AppData::simuDbgFilters (const QStringList x) { store(x, _simuDbgFilters, void AppData::mainWinGeo (const QByteArray x) { store(x, _mainWinGeo, "mainWindowGeometry" );} void AppData::mainWinState (const QByteArray x) { store(x, _mainWinState, "mainWindowState" );} void AppData::modelEditGeo (const QByteArray x) { store(x, _modelEditGeo, "modelEditGeometry" );} +void AppData::mdiWinGeo (const QByteArray x) { store(x, _mdiWinGeo, "mdiWinGeo" );} +void AppData::mdiWinState (const QByteArray x) { store(x, _mdiWinState, "mdiWinState" );} +void AppData::compareWinGeo (const QByteArray x) { store(x, _compareWinGeo, "compareWinGeo" );} void AppData::armMcu (const QString x) { store(x, _armMcu, "arm_mcu" );} void AppData::avrArguments (const QString x) { store(x, _avrArguments, "avr_arguments" );} @@ -516,8 +524,10 @@ void AppData::snapToClpbrd (const bool x) { store(x, _snapToClpbrd, void AppData::autoCheckApp (const bool x) { store(x, _autoCheckApp, "startup_check_companion" );} void AppData::autoCheckFw (const bool x) { store(x, _autoCheckFw, "startup_check_fw" );} void AppData::simuSW (const bool x) { store(x, _simuSW, "simuSW" );} -void AppData::useWizard (const bool x) { store(x, _useWizard, "useWizard" );} +void AppData::tabbedMdi (const bool x) { store(x, _tabbedMdi, "tabbedMdi" );} +void AppData::removeModelSlots(const bool x) { store(x, _remvModelSlots, "removeModelSlots" );} +void AppData::newModelAction (const int x) { store(x, _newModelAction, "newModelAction" );} void AppData::backLight (const int x) { store(x, _backLight, "backLight" );} void AppData::embedSplashes (const int x) { store(x, _embedSplashes, "embedded_splashes" );} void AppData::fwServerFails (const int x) { store(x, _fwServerFails, "fwserver" );} @@ -569,6 +579,9 @@ void AppData::init() getset( _mainWinGeo, "mainWindowGeometry" ,"" ); getset( _mainWinState, "mainWindowState" ,"" ); getset( _modelEditGeo, "modelEditGeometry" ,"" ); + getset( _mdiWinGeo, "mdiWinGeo" ,"" ); + getset( _mdiWinState, "mdiWinState" ,"" ); + getset( _compareWinGeo, "compareWinGeo" ,"" ); getset( _armMcu, "arm_mcu" ,"at91sam3s4-9x" ); getset( _avrArguments, "avr_arguments" ,"" ); @@ -607,8 +620,10 @@ void AppData::init() getset( _autoCheckApp, "startup_check_companion" ,true ); getset( _autoCheckFw, "startup_check_fw" ,true ); getset( _simuSW, "simuSW" ,false ); - getset( _useWizard, "useWizard" ,true ); + getset( _tabbedMdi, "tabbedMdi" ,false ); + getset( _remvModelSlots, "removeModelSlots" ,true ); + getset( _newModelAction, "newModelAction" ,1 ); getset( _backLight, "backLight" ,0 ); getset( _embedSplashes, "embedded_splashes" ,0 ); getset( _fwServerFails, "fwserver" ,0 ); @@ -636,7 +651,7 @@ QMap AppData::getActiveProfiles() bool AppData::importSettings(QSettings & toSettings) { - QSettings * fromSettings; + QSettings * fromSettings = NULL; QSettings settings21("OpenTX", "Companion 2.1"); QSettings settings20("OpenTX", "Companion 2.0"); @@ -651,8 +666,6 @@ bool AppData::importSettings(QSettings & toSettings) fromSettings = &settings16; else if (settings9x.contains("default_mode")) fromSettings = &settings9x; - else - return false; // do not copy these settings QStringList excludeKeys = QStringList() << "compilation-server"; @@ -664,17 +677,19 @@ bool AppData::importSettings(QSettings & toSettings) #endif // import settings - foreach (const QString & key, fromSettings->allKeys()) { - if (fromSettings->value(key).isValid() && !excludeKeys.contains(key)) { - toSettings.setValue(key, fromSettings->value(key)); + if (fromSettings) { + foreach (const QString & key, fromSettings->allKeys()) { + if (fromSettings->value(key).isValid() && !excludeKeys.contains(key)) { + toSettings.setValue(key, fromSettings->value(key)); + } } } // Additional adjustments for companion9x settings - if (fromSettings->applicationName() == "companion9x") { + if (fromSettings && fromSettings->applicationName() == "companion9x") { // Store old values in new locations autoCheckApp(toSettings.value("startup_check_companion9x", true).toBool()); - useWizard(toSettings.value("wizardEnable", true).toBool()); + toSettings.setValue("useWizard", toSettings.value("wizardEnable", true)); // Convert and store the firmware type QString fwType = toSettings.value("firmware", "").toString(); @@ -717,5 +732,14 @@ bool AppData::importSettings(QSettings & toSettings) id( 0 ); } - return true; + // convert old settings to new + + if (toSettings.contains("useWizard")) { + if (!toSettings.contains("newModelAction")) { + newModelAction(toSettings.value("useWizard").toBool() ? 1 : 2); + } + toSettings.remove("useWizard"); + } + + return fromSettings ? true : false; } diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index f7d3d0cd4..2f85e50e8 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -255,6 +255,9 @@ class AppData: protected CompStoreObj QByteArray _mainWinGeo; QByteArray _mainWinState; QByteArray _modelEditGeo; + QByteArray _mdiWinGeo; + QByteArray _mdiWinState; + QByteArray _compareWinGeo; QString _armMcu; QString _avrArguments; @@ -285,8 +288,10 @@ class AppData: protected CompStoreObj bool _autoCheckApp; bool _autoCheckFw; bool _simuSW; - bool _useWizard; + bool _tabbedMdi; + bool _remvModelSlots; + int _newModelAction; // 0=no action; 1=model wizard; 2=model edit int _backLight; int _embedSplashes; int _fwServerFails; @@ -309,6 +314,9 @@ class AppData: protected CompStoreObj QByteArray mainWinGeo(); QByteArray mainWinState(); QByteArray modelEditGeo(); + QByteArray mdiWinGeo(); + QByteArray mdiWinState(); + QByteArray compareWinGeo(); QString armMcu(); QString avrArguments(); @@ -340,8 +348,10 @@ class AppData: protected CompStoreObj bool autoCheckApp(); bool autoCheckFw(); bool simuSW(); - bool useWizard(); + bool tabbedMdi(); + bool removeModelSlots(); + int newModelAction(); int backLight(); int embedSplashes(); int fwServerFails(); @@ -362,6 +372,9 @@ class AppData: protected CompStoreObj void mainWinGeo (const QByteArray); void mainWinState (const QByteArray); void modelEditGeo (const QByteArray); + void mdiWinGeo (const QByteArray); + void mdiWinState (const QByteArray); + void compareWinGeo (const QByteArray); void armMcu (const QString); void avrArguments (const QString); @@ -393,8 +406,10 @@ class AppData: protected CompStoreObj void autoCheckApp (const bool); void autoCheckFw (const bool); void simuSW (const bool); - void useWizard (const bool); + void tabbedMdi (const bool); + void removeModelSlots(const bool); + void newModelAction (const int); void backLight (const int); void embedSplashes (const int); void fwServerFails (const int); diff --git a/companion/src/themes/monoblue/16/sdsync.png b/companion/src/themes/monoblue/16/sdsync.png new file mode 100644 index 000000000..47fc3ba81 Binary files /dev/null and b/companion/src/themes/monoblue/16/sdsync.png differ diff --git a/companion/src/themes/monoblue/24/sdsync.png b/companion/src/themes/monoblue/24/sdsync.png new file mode 100644 index 000000000..87bc0debf Binary files /dev/null and b/companion/src/themes/monoblue/24/sdsync.png differ diff --git a/companion/src/themes/monoblue/32/sdsync.png b/companion/src/themes/monoblue/32/sdsync.png new file mode 100644 index 000000000..bbc3697ce Binary files /dev/null and b/companion/src/themes/monoblue/32/sdsync.png differ diff --git a/companion/src/themes/monoblue/48/sdsync.png b/companion/src/themes/monoblue/48/sdsync.png new file mode 100644 index 000000000..451087d1d Binary files /dev/null and b/companion/src/themes/monoblue/48/sdsync.png differ diff --git a/companion/src/themes/monowhite/16/sdsync.png b/companion/src/themes/monowhite/16/sdsync.png new file mode 100644 index 000000000..019c24125 Binary files /dev/null and b/companion/src/themes/monowhite/16/sdsync.png differ diff --git a/companion/src/themes/monowhite/24/sdsync.png b/companion/src/themes/monowhite/24/sdsync.png new file mode 100644 index 000000000..9dcb93ece Binary files /dev/null and b/companion/src/themes/monowhite/24/sdsync.png differ diff --git a/companion/src/themes/monowhite/32/sdsync.png b/companion/src/themes/monowhite/32/sdsync.png new file mode 100644 index 000000000..f2362e59a Binary files /dev/null and b/companion/src/themes/monowhite/32/sdsync.png differ diff --git a/companion/src/themes/monowhite/48/sdsync.png b/companion/src/themes/monowhite/48/sdsync.png new file mode 100644 index 000000000..fbc1e8ce7 Binary files /dev/null and b/companion/src/themes/monowhite/48/sdsync.png differ diff --git a/companion/src/translations/companion_en.ts b/companion/src/translations/companion_en.ts index a1b639e8b..86d33fe45 100644 --- a/companion/src/translations/companion_en.ts +++ b/companion/src/translations/companion_en.ts @@ -4,27 +4,27 @@ AileronsPage - + No No - + Yes, controlled by a single channel - + Yes, controlled by two channels - + <br>First Aileron Channel: - + Second Aileron Channel: @@ -32,27 +32,27 @@ AirbrakesPage - + No No - + Yes, controlled by a single channel - + Yes, controlled by two channels - + <br>First Airbrake Channel: - + Second Airbrake Channel: @@ -70,90 +70,90 @@ - + Profile Name - + Radio Type - + Menu Language - + Voice Language - + Set voice language. May be different from firmware language - + Build Options - + Splash Screen - + Other Settings - + SD Structure path - - + + The profile specific folder, if set, will override general Backup folder - + Backup folder - + If set it will override the application general setting - + if set, will override general backup enable - - + + Enable automatic backup before writing firmware - + General Settings - + Default Stick Mode - + Mode selection: Mode 1: @@ -176,331 +176,326 @@ Mode 4: - + Mode 1 (RUD ELE THR AIL) - + Mode 2 (RUD THR ELE AIL) - + Mode 3 (AIL ELE THR RUD) - + Mode 4 (AIL THR ELE RUD) - + Default Channel Order - + <html><head/><body><p>Channel order</p><p><br/></p><p>Defines the order of the default mixes created on a new model.</p></body></html> - + R E T A - + R E A T - + R T E A - + R T A E - + R A E T - + R A T E - + E R T A - + E R A T - + E T R A - + E T A R - + E A R T - + E A T R - + T R E A - + T R A E - + T E R A - + T E A R - + T A R E - + T A E R - + A R E T - + A R T E - + A E R T - + A E T R - + A T R E - + A T E R - + Append version number to FW file name - + Offer to write FW to Tx after download - + Clear Image - + Select Image - - Remove Profile - - - - - - - - + + + + + Select Folder - + Application Settings - + Automatic check for Companion updates - + Show splash screen when Companion starts - + Automatic check for OpenTX firmware updates - + Splash Screen Library - + Files to keep - + Google Earth Executable - + User Splash Screens - + Automatic Backup Folder - + Use model wizard when creating a new model - + Only show user splash images - + Show user and companion splash images - + Select Executable - + Use OpenTX firmware nightly builds - + Use Companion nightly builds - + Simulator Settings - + Calibrate - + Blue - + Green - + Red - + Orange - + Yellow - + Simulator capture folder - + Only capture to clipboard - + Enable - + Joystick - + Simulator BackLight - + Remember simulator switch values - + Simulator Volume Gain @@ -510,13 +505,13 @@ Mode 4: - + Select your snapshot folder - - + + No joysticks found @@ -536,43 +531,33 @@ Mode 4: - + Select your library folder - - + + Select your Models and Settings backup folder - + Select Google Earth executable - + Select the folder replicating your SD structure - - Not possible to remove profile - - - - - The default profile can not be removed. - - - - + Open Image to load - + Images (%1) @@ -656,42 +641,42 @@ Mode 4: - - Drag first model here + + To compare models, drag and drop them anywhere in this window. - - Drag second model here - - - - + Close - + Print - + Print to file - - No name + + Unnamed Model %1 - + + Click to remove this model. + + + + Print Document - + Select PDF output file @@ -699,7 +684,7 @@ Mode 4: ConclusionPage - + OK, I understand. @@ -1040,22 +1025,22 @@ Mode 4: CyclicPage - + 90 - + 120 - + 120x - + 140 @@ -1134,12 +1119,12 @@ To <b>remove a remembered entry</b> from the filter list, first cho - + <html><head><style>kbd {background-color: palette(alternate-base); font-size: large; white-space: nowrap;}</style></head><body><p>The filter supports two syntax types: basic matching with common wildcards as well as full Perl-style (<code>pcre</code>) Regular Expressions.</p><p>By default a filter will only show lines which match (<b>inclusive</b>). To make an <b>exclusive</b> filter which removes matching lines, prefix the filter expression with a <kbd>!</kbd> (exclamation mark).</p><p>To use <b>Regular Expressions</b> (RegEx), prefix the filter text with a <kbd>/</kbd> (slash) or <kbd>^</kbd> (up caret). <ul><li>Put the <kbd>/</kbd> or <kbd>^</kbd> after the exclusive <kbd>!</kbd> indicator if you're using one.</li><li>By default the match is case-sensitive. To make it insensitive, add the typical <kbd>/i</kbd> (slash i) operator at the end of your RegEx.</li><li>If you use a caret (^) to denote a RegEx, it will become part of the Reg. Ex. (that is, matches from start of line).</li><li>If the RegEx is invalid, the filter edit field should show a red border and you will not be able to enable the filter.</li><li>A useful resource for testing REs (with a full reference) can be found at <a href="http://www.regexr.com/">http://www.regexr.com/</a></li></ul></p><p>To use <b>basic matching</b> just type any text.<ul><li>Wildcards: <kbd>*</kbd> (asterisk) matches zero or more of any character(s), and <kbd>?</kbd> (question mark) matches any single character.</li><li>The match is always case-insensitive.</li><li>The match always starts from the beginning of a log line. To ignore characters at the start, use a leading <kbd>*</kbd> wildcard.</li><li>A trailing <kbd>*</kbd> is always implied (that is, matches anything to the end of the log line). To avoid this, use a RegEx.</li><li>You can match literal wildcard characters by prefixing them with a <kbd>\</kbd> (backslash) character (eg. "foo\*bar" matches "foo*bar").</li></ul></p><p>After <b>editing text</b>, press ENTER or TAB key (or click anywhere outside the box) to update the filter.</p><p>To <b>remove an entry</b> from the filter selector list, first choose it, and while in the line editor press <kbd>Shift-Delete</kbd> (or <kbd>Shift-Backspace</kbd>) key combination. The default filters cannot be removed. Up to 50 filters are stored.</p></body></html> - + Debug Console Filter Help @@ -1147,90 +1132,90 @@ To <b>remove a remembered entry</b> from the filter list, first cho EepromInterface - + Possible causes for this: - + - Eeprom is from a newer version of OpenTX - + - Eeprom is not from OpenTX - + - Eeprom is not from Th9X - + - Eeprom is not from Gruvin9X - + - Eeprom is not from ErSky9X - + - Eeprom is not from Er9X - + - Eeprom size is invalid - + - Eeprom file system is invalid - + - Eeprom is from a unknown board - + - Eeprom is from the wrong board - + - Eeprom backup not supported - + - Something that couldn't be guessed, sorry - + Warning: - - + + - Your radio probably uses a wrong firmware, eeprom size is 4096 but only the first 2048 are used - + - Your eeprom is from an old version of OpenTX, upgrading! You should 'save as' to keep the old file as a backup. - + Warnings! @@ -1238,12 +1223,12 @@ To <b>remove a remembered entry</b> from the filter list, first cho ElevonsPage - + <br>First Elevon Channel: - + Second Elevon Channel: @@ -1387,22 +1372,22 @@ If blank then the input is considered to be "ON" all the time. FblPage - + Throttle Channel: - + Yaw Channel: - + Pitch Channel: - + Roll Channel: @@ -1454,27 +1439,27 @@ May be different from firmware language FlapsPage - + No No - + Yes, controlled by a single channel - + Yes, controlled by two channels - + <br>First Flap Channel: - + Second Flap Channel: @@ -1983,17 +1968,17 @@ Please use ZADIG to properly install the driver. FlybarSelectionPage - + Has Flybar - + Flybarless - + Flybar: @@ -2956,17 +2941,17 @@ Are you sure ? GyroPage - + No No - + Yes, controled by a switch - + Yes, controlled by a pot @@ -3350,22 +3335,22 @@ Are you sure ? HeliPage - + Throttle Channel: - + Yaw Channel: - + Pitch Channel: - + Roll Channel: @@ -3756,122 +3741,122 @@ The columns for altitude "GAlt" and for speed "GSpd" are opt MainWindow - - + + File loaded - + Display previous warning again at startup ? - + Checking for updates - + Unable to check for updates. - + A new version of Companion is available (version %1)<br>Would you like to download it? - - + + Save As - + Executable (*.exe) - + New release available - + A new release of Companion is available, please check the OpenTX website! - - + + No updates available at this time. - + Would you like to launch the installer? - - + + Error - + Error opening file %1: %2. - + Not enough flash available on this board for all the selected options - + Compilation server temporary failure, try later - + Compilation server too busy, try later - + Compilation error - + Invalid firmware - + Invalid board - + Invalid language - + Unknown server failure, try later - + Do you want to write the firmware to the radio now ? - + Firmware %1 does not seem to have ever been downloaded. Release %2 is available. Do you want to download it now? @@ -3880,31 +3865,31 @@ We recommend you view the release notes using the button below to learn about an - - + + Yes - - + + No No - - + + Release Notes - - + + Do you want to download release %1 now ? - + A new version of %1 firmware is available: - current is %2 - newer is %3 @@ -3915,625 +3900,624 @@ We recommend you view the release notes using the button below to learn about an - + Ignore this release %1? - - - + + Companion - + The new theme will be loaded the next time you start Companion. - - The icon size will be used the next time you start Companion. - - - - - + + Open Models and Settings file - - + + File saved - - + + Synchronize SD - + Firmware updates - + Current firmware does not provide release notes informations. - - + + Read Firmware from Radio - - + + Read Models and Settings from Radio - + Models and Settings read - - + + This function is not yet implemented - + Save Radio Backup to File - + Read Radio Firmware to File - + OpenTX Home Page: <a href='%1'>%1</a> - + The OpenTX Companion project was originally forked from <a href='%1'>eePe</a> - + If you've found this program useful, please support by <a href='%1'>donating</a> - + Copyright OpenTX Team - + About Companion - + OpenTX Companion %1 - Radio: %2 - Profile: %3 - + New - + Create a new Models and Settings file - + Open... - + Save - - + + Save Models and Settings file - + Save As... - + Exit - + Exit the application - - Cut Model + + Create a new Radio Settings Profile - - Cut current model to the clipboard + + Copy Current Radio Profile - - Copy Model + + Duplicate current Radio Settings Profile - - Copy current model to the clipboard + + Delete Current Radio Profile - - Paste Model + + Delete the current Radio Settings Profile - - Paste model from clipboard - - - - + Classical - + The classic companion9x icon theme - + Yerico - + Yellow round honey sweet icon theme - + Monochrome - + A monochrome black icon theme - + MonoWhite - + A monochrome white icon theme - + + Windows + + + + + Tabbed Windows + + + + + Tile Windows + + + + + Cascade Windows + + + + + Close All Windows + + + + + - Copy + + + + + Not possible to remove profile + + + + + The default profile can not be removed. + + + + + Confirm Delete Profile + + + + + Are you sure you wish to delete the "%1" radio profile? There is no way to undo this action! + + + + MonoBlue - + A monochrome blue icon theme - + Small - + Use small toolbar icons - + Normal - + Use normal size toolbar icons - + Big - + Use big toolbar icons - + Huge - + Use huge toolbar icons - + System language - + About... - + Show the application's About box - - Print... - - - - - Print current model - - - - - Simulate... - - - - - Simulate current model - - - - - Alt+S - - - - - Load Backup... - - - - - Load backup from file - - - - + View Log File... - + Open and view log file - + Settings... - + Edit Settings - + Download... - + Download firmware and voice files - + Check for Updates... - + Check OpenTX and Companion updates - + Companion Changes... - + Show Companion change log - + Firmware Changes... - + Show firmware change log - + Compare Models... - + Compare models - + Edit Radio Splash Image... - + Edit the splash image of your Radio - + List programmers... - + List available programmers - + Fuses... - + Show fuses dialog - + Read firmware from Radio - + Write Firmware to Radio - + Write firmware to Radio - + Add Radio Profile - - Create a new Radio Setting Profile - - - - + Manuals and other Documents - + Open the OpenTX document page in a web browser - + Write Models and Settings To Radio - + Write Models and Settings to Radio - + Read Models and Settings From Radio - + Configure Communications... - + Configure software for communicating with the Radio - + Write Backup to Radio - + Write Backup from file to Radio - + Backup Radio to File - + Save a complete backup file of all settings and model data in the Radio - + Contributors... - + A tribute to those who have contributed to OpenTX and Companion - + SD card synchronization - + Use default system language. - + Use %1 language (some translations may not be complete). - + Recent Files - + Set Menu Language - + Set Icon Theme - + Set Icon Size - - + + File - + Some text will not be translated until the next time you start Companion. Please note that some translations may not be complete. - - + + Edit - + Settings - + Read/Write - - + + Help - - + + Radio Profile - + Recent Models+Settings - + Show recent Models+Settings documents - + Show the list of radio profiles - + Write - + Ready - + %2 - + New Radio @@ -4541,254 +4525,372 @@ We recommend you view the release notes using the button below to learn about an MdiChild - - Radio settings - - - - - Simulate Tx - - - - - Delete selected models? - - - - - Cannot delete default model. - - - - - &Edit - - - - - &Restore from backup - - - - - &Model Wizard - - - - - &Delete - - - - - Delete - - - - - &Copy - - - - - Ctrl+C - - - - - &Cut - - - - - Ctrl+X - - - - - - &Paste - - - - - - Ctrl+V - - - - - D&uplicate - - - - - Ctrl+U - - - - - Move to Category - - - - - &Use as default - - - - - P&rint model - - - - - Ctrl+P - - - - - &Simulate model - - - - + Alt+S - - &Add model - - - - - &Rename category - - - - - &Delete category - - - - - &Add category - - - - + Models and settings will be automatically converted. If that is not what you intended, please close the file and choose the correct radio type/profile before reopening it. + + + Delete %n selected model(s)? + + Delete %n selected model? + Delete %n selected models? + + - + + Edit Radio Settings + + + + + Alt+Shift+E + + + + + Copy Radio Settings + + + + + Ctrl+Alt+C + + + + + Paste Radio Settings + + + + + Ctrl+Alt+V + + + + + Simulate Radio + + + + + Alt+Shift+S + + + + + Alt+C + + + + + Alt+R + + + + + Alt+A + + + + + Alt+W + + + + + Duplicate Model + + + + + Alt+U + + + + + Print Model + + + + + + Model + + + + + Cannot insert model, last model in list would be deleted. + + + + + Cannot paste model, out of available model slots. + + + + + You are replacing an existing model, are you sure? + + + + Do you want to overwrite radio general settings? - - You are pasting on an not empty model, are you sure? - - - - + free bytes - - + + New category + Translators do NOT use accent for this, this is the default category name on Horus. + + + + Companion - + This category is not empty! - + New model - + + Cannot add model, could not find an available model slot. + + + + + Cut + + + + + Copy + + + + + Paste + + + + + Insert + + + + + Rename Category + + + + + %n Model(s) + As in "Copy 3 Models" or "Cut 1 Model" or "Delete 3 Models" action). + + %n Model + %n Models + + + + + %n Model(s) + As in "Paste 3 Models" or "Insert 1 Model." + + %n Model + %n Models + + + + + %n Category(ies) + As in "Delete 3 Categories" or "Delete 1 Category." + + %n Category + %n Categories + + + + + Nothing selected + + + + + empty model slot + + + + + Edit Model + + + + + Delete + + + + + Add Category + + + + + + Category + + + + + Add Model + + + + + Restore from Backup + + + + + Model Wizard + + + + + Set as Default + + + + + Simulate Model + + + + + Move to Category + + + + + Radio + + + + + Category index out of range. + + + + + Cannot delete the last category. + + + + + Delete %n selected category(ies)? + + Delete %n selected category? + Delete %n selected categories? + + + + + Cannot duplicate model, could not find an available model slot. + + + + Editing model %1: - - - - - - - + + + + + + + Error - - - - + + + + Save As - + %1 has been modified. Do you want to save your changes? - + Unable to find Horus radio SD card! - + Models and Settings written - + Cannot write temporary file! - + Open backup Models and Settings file - + Unable to find file %1! - + Error opening file %1: %2. - + Error reading file %1: %2. - + Invalid binary backup File %1 - + Warning @@ -5152,52 +5254,52 @@ If blank then the mix is considered to be "ON" all the time. - + Setup - + Heli - + Flight Modes - + Inputs - + Mixes - + Outputs - + Curves - + Logical Switches - + Special Functions - + Telemetry @@ -5539,14 +5641,6 @@ If blank then the mix is considered to be "ON" all the time. - - ModelsListWidget - - - No free slot available, cannot duplicate - - - Module @@ -5794,256 +5888,256 @@ If blank then the mix is considered to be "ON" all the time. MultiModelPrinter - + General Model Settings - + Name: - + EEprom Size: - + Timer%1: - + Module%1: - + Module: - + Trainer port: - + Throttle Trim: - + Trim Increment: - + Center Beep: - + Helicopter Setup - + Swash Type: - + Swash Ring: - + Input - + Weight - + Long. cyc - + Lateral cyc - + Collective - + Flight modes - - + + Flight mode - + Switch - + Fade IN - + Fade OUT - - + + FM%1 - - + + GV%1 - + RE%1 - + Limits - + Channel - + Name - - + + Offset - + Min - + Max - + Invert - + Global Variables - + Inputs - + Mixers - + CV%1 - + Curves - + L%1 - + Logical Switches - + SF%1 - + Special Functions - + Telemetry Settings - + Analogs - + Unit - + Scale - + A%1 - + RSSI Alarms - + Low Alarm - + Critical Alarm @@ -6062,7 +6156,7 @@ If blank then the mix is considered to be "ON" all the time. - C2500 frequency fine tune + CC2500 frequency fine tune @@ -6094,22 +6188,22 @@ If blank then the mix is considered to be "ON" all the time. MultirotorPage - + Throttle Channel: - + Yaw Channel: - + Pitch Channel: - + Roll Channel: @@ -6117,17 +6211,17 @@ If blank then the mix is considered to be "ON" all the time. OptionsPage - + Throttle Cut - + Throttle Timer - + Flight Timer @@ -6225,7 +6319,7 @@ If blank then the mix is considered to be "ON" all the time. - + Unknown @@ -6269,7 +6363,7 @@ If blank then the mix is considered to be "ON" all the time. - + V @@ -6399,7 +6493,7 @@ If blank then the mix is considered to be "ON" all the time. - + s @@ -6426,14 +6520,14 @@ If blank then the mix is considered to be "ON" all the time. - + REa - + REb @@ -6482,21 +6576,21 @@ If blank then the mix is considered to be "ON" all the time. - + Timer1 - + Timer2 - + Timer3 @@ -6738,7 +6832,7 @@ If blank then the mix is considered to be "ON" all the time. - + ---- @@ -6886,14 +6980,14 @@ If blank then the mix is considered to be "ON" all the time. - + OFF - + ON @@ -6914,351 +7008,352 @@ If blank then the mix is considered to be "ON" all the time. - + One - + + ??? - + Diff(%1) - + Expo(%1) - + Function(%1) - + Curve(%1) - + !Curve(%1) - + --- - + a>x - + a<x - + |a|>x - + |a|<x - + AND - + OR - + XOR - + a=b - + a!=b - + a>b - + a<b - + a>=b - + a<=b - + d>=x - + |d|>=x - + a=x - + a~x - + Timer - + Sticky - + Edge - + Override %1 - + Trainer - + Trainer RUD - + Trainer ELE - + Trainer THR - + Trainer AIL - + Instant Trim - + Play Sound - + Haptic - + Reset - + Set Timer %1 - + Vario - + Play Track - + Play Both - + Play Value - + Play Script - + SD Logs - + Volume - + Backlight - + Screenshot - + Background Music - + Background Music Pause - + Adjust GV%1 - + SetFailsafe Int. Module - + SetFailsafe Ext. Module - + RangeCheck Int. Module - + RangeCheck Ext. Module - + Bind Int. Module - + Bind Ext. Module - + Flight - + Telemetry - + Rotary Encoder - - - + + + <font color=red><b>Inconsistent parameter</b></font> - + Value - + Decr: - + Incr: - + played once, not during startup - + repeat(%1s) - + DISABLED - + INV - + NOR @@ -8252,6 +8347,7 @@ If blank then the mix is considered to be "ON" all the time. + Warning @@ -8262,6 +8358,7 @@ If blank then the mix is considered to be "ON" all the time. + Uknown error during Simulator startup. @@ -8273,18 +8370,18 @@ If blank then the mix is considered to be "ON" all the time. - + Data Load Error - + Error occurred while starting simulator. - - + + @@ -8292,12 +8389,12 @@ If blank then the mix is considered to be "ON" all the time. - + No SD directory configured! - + No Radio connected! @@ -8372,7 +8469,7 @@ If blank then the mix is considered to be "ON" all the time. - + Models Translators do NOT use accent for this, this is the default category name on Horus. @@ -8405,12 +8502,12 @@ If blank then the mix is considered to be "ON" all the time. - + Positive - + Negative @@ -8442,77 +8539,89 @@ If blank then the mix is considered to be "ON" all the time. - + Select a data file - + Select Data Directory - + Select SD Card Image Folder + Available profiles: + ID: + Name: + Available radios: + WARNING: couldn't initialize SDL: %1 + ERROR: No simulator libraries available. + Radio profile ID or Name to use for simulator. + Radio type to simulate (usually defined in profile). + show this help text + Usage: simulator [OPTION]... [EEPROM.BIN FILE OR DATA FOLDER] + Options: + ERROR: Couldn't start simulator, missing radio/profile/data file/folder. Profile ID: [%1]; Radio ID: [%2]; @@ -8520,6 +8629,7 @@ Data File: [%3] + ERROR: Radio profile or simulator firmware not found. Profile ID: [%1]; Radio ID: [%2] @@ -8804,7 +8914,12 @@ The OpenTX Team. RadioKnobWidget - + + <p>Value (input): <b>%1</b></p> + + + + Right-double-click to reset to center. @@ -8936,17 +9051,17 @@ s RudderPage - + No No - + Yes - + <br>Rudder Channel: @@ -9128,7 +9243,7 @@ If this is checked the throttle will be reversed. Idle will be forward, trim wi SimpleTailPage - + Elevator Channel: @@ -9136,419 +9251,205 @@ If this is checked the throttle will be reversed. Idle will be forward, trim wi SimulatedUIWidget - - Rotary encoder click - - - - - Take Screenshot - - - - + + screenshot Simulator LCD screenshot file name prefix + + + ENTER + + + + + PG-UP + + + + + PG-DN + + + + + DEL + + + + + BKSP + + + + + ESC + + + + + INS + + + + + <font size=+3>+</font> + + + + + <font size=+3>-</font> + + + + + <font size=+3>&larr;</font> + + + + + <font size=+3>&rarr;</font> + + + + + <font size=+3>&uarr;</font> + + + + + <font size=+3>&darr;</font> + + + + + <font size=+3>&#x2686;</font> + + + + + <font size=+3>&#x21b6;</font> + + + + + <font size=+3>&#x21b7;</font> + + + + + <font size=+3>&#x21c6;</font> + + + + + + <font size=+3>&#x21d3;</font> + + + + + <font size=+3>&#x21d1;</font> + + + + + <font size=+3>&#x21d5;</font> + + + + + <img src='qrc:/images/simulator/icons/svg/mouse.svg' width=20 height=18 /> + + + + + <img src='qrc:/images/simulator/icons/svg/arrow_click.svg' width=18 height=18 /> + + + + + <pre>[ MENU ]</pre> + + + + + <pre>[ PAGE ]</pre> + + + + + <pre>[ EXIT ]</pre> + + + + + <pre>[ ENT ]</pre> + + + + + <pre>[ UP ]</pre> + + + + + <pre>[ DN ]</pre> + + + + + <pre>[ <font size=+2>+</font> ]</pre> + + + + + <pre>[ <font size=+2>-</font> ]</pre> + + + + + <pre>[ PgUp ]</pre> + + + + + <pre>[ PgDn ]</pre> + + + + + <pre>[ MDL ]</pre> + + + + + <pre>[ RTN ]</pre> + + + + + <pre>[ SYS ]</pre> + + + + + <pre>[ TELE ]</pre> + + + + + <font size=+3>&#x2261;</font> + + SimulatedUIWidget9X - - UP/PG-UP - - - - - [ UP ] - - - - - DN/PG-DN - - - - - [ DN ] - - - - - RIGHT/+ - - - - - [ + ] - - - - - LEFT/- - - - - - [ - ] - - - - - ENTER/MOUSE-MID - - - - - [ MENU ] - - - - - DEL/BKSP/ESC - - - - - [ EXIT ] - - - - - WHEEL/PAD SCRL - - - - - [ UP ]/[ DN ] or Rotary Sel. - - - - - SimulatedUIWidgetX12 - - - PG-UP - - - - - [ PgUp ] - - - - - PG-DN - - - - - [ PgDn ] - - - - - UP - - - - - [ MDL ] - - - - - DN/DEL/BKSP - - - - - [ RTN ] - - - - - LEFT - - - - - [ SYS ] - - - - - RIGHT - - - - - [ TELE ] - - - - - WHEEL/PAD SCRL - - - - - Rotary Selector - - - - - -/X - - - - - Rotary UP - - - - - +/C - - - - - Rotary DOWN - - - - - ENTER/MOUSE-MID - - - - - Selector Press - - - - - SimulatedUIWidgetX7 - - - Taranis X7 Simulator - - - - - PG-UP - - - - - [ PAGE ] - - - - - PG-DN - - - - - [ MENU ] - - - - - DEL/BKSP/ESC - - - - - [ EXIT ] - - - - - WHEEL/PAD SCRL - - - - - Rotary Selector - - - - - -/UP - - - - - Rotary UP - - - - - +/DN - - - - - Rotary DOWN - - - - - ENTER/MOUSE-MID - - - - - Selector Press - - - - - SimulatedUIWidgetX9 - - - PG-UP - - - - - [ MENU ] - - - - - PG-DN - - - - - [ PAGE ] - - - - - DEL/BKSP/ESC - - - - - [ EXIT ] - - - - - +/UP - - - - - [ + ] - - - - - -/DN - - - - - [ - ] - - - - - ENTER/MOUSE-MID - - - - - [ ENT ] - - - - - WHEEL/PAD SCRL - - - - - [ + ]/[ - ] - - - - - SimulatedUIWidgetX9E - - - PG-UP - - - - - [ MENU ] - - - - - PG-DN - - - - - [ PAGE ] - - - - - DEL/BKSP/ESC - - - - - [ EXIT ] - - - - - WHEEL/PAD SCRL - - - - - Rotary Selector - - - - - -/UP - - - - - Rotary UP - - - - - +/DN - - - - - Rotary DOWN - - - - - ENTER/MOUSE-MID - - - - - Selector Press + + + | @@ -9705,67 +9606,88 @@ If this is checked the throttle will be reversed. Idle will be forward, trim wi + ERROR: Failed to create simulator interface, possibly missing or bad library. - + + Alt+T - + + Radio Outputs - + + F2 - + + Telemetry Simulator - + + F4 - + + Trainer Simulator - + + F5 - + + Debug Output - + + F6 - - Simulator Controls: + + + <b>Simulator Controls:</b> - - <tr><th>Key/Mouse</td><th>Action</td></tr> + + + <tr><th>Key/Mouse</th><th>Action</th></tr> + note: must match html layout of each table row (keyTemplate). - + + + <tr><td><kbd>%1</kbd></td><td>%2</td></tr> + note: must match html layout of help text table header. + + + + + Simulator Help @@ -9885,7 +9807,7 @@ The default is configured in the chosen Radio Profile. - + All files (*.*) @@ -9898,78 +9820,102 @@ The default is configured in the chosen Radio Profile. - + + Radio Simulator (%1) - + + Could not determine startup data source. - + + Could not load data, possibly wrong format. - + + Data Load Error - + + Invalid startup data provided. Plese specify a proper file/path. - + + Simulator Startup Error - + + Error saving data: could open file for writing: '%1' - + + Error saving data: could not get data from simulator interface. - + + An unexpected error occurred while attempting to save radio data to file '%1'. - + + Data Save Error - + + Warning - + + Cannot open joystick, joystick disabled - - Firmware %1 error: %2 + + + Radio firmware error: %1 + + + + + + - Flight Mode %1 (#%2) SliderWidget - + Right-double-click to reset to center. + + + <p>Value (input): <b>%1</b></p> + + SplashLibraryDialog @@ -10040,17 +9986,17 @@ The default is configured in the chosen Radio Profile. TailPage - + Rudder Channel: - + Elevator Channel: - + Only one channel still available!<br>You probably should configure your model without using the wizard. @@ -10058,22 +10004,22 @@ The default is configured in the chosen Radio Profile. TailSelectionPage - + Elevator and Rudder - + Only Elevator - + V-tail - + Tail Type: @@ -11131,17 +11077,20 @@ hh:mm:ss - + + Log File - + + LOG Files (*.csv) - + + ERROR - invalid file @@ -11366,17 +11315,17 @@ hh:mm:ss ThrottlePage - + Yes - + No No - + <br>Throttle Channel: @@ -11560,17 +11509,17 @@ hh:mm:ss TreeModel - + Index - + Name - + Size @@ -11578,12 +11527,12 @@ hh:mm:ss VTailPage - + First Tail Channel: - + Second Tail Channel: @@ -11591,42 +11540,50 @@ hh:mm:ss VirtualJoystickWidget - + + Hld Y - + + Hold Vertical stick position. - + + Fix Y - + + Prevent Vertical movement of stick. - + + Fix X - + + Prevent Horizontal movement of stick. - + + Hld X - + + Hold Horizontal stick position. @@ -11634,12 +11591,12 @@ hh:mm:ss WingtypeSelectionPage - + Standard Wing - + Flying Wing / Deltawing @@ -11921,37 +11878,37 @@ hh:mm:ss WizardPrinter - + Plane - + Multicopter - + Helicopter - + Model Name: - + Model Type: - + Options: - + Channel %1: diff --git a/companion/src/wizarddata.cpp b/companion/src/wizarddata.cpp index 1fc337a02..9e9d2c2ec 100644 --- a/companion/src/wizarddata.cpp +++ b/companion/src/wizarddata.cpp @@ -44,7 +44,7 @@ WizMix::WizMix(const GeneralSettings & settings, unsigned int modelId, const Mod originalModelData(modelData), vehicle(NOVEHICLE) { - strcpy(name, " "); + strncpy(name, originalModelData.name, WIZ_MODEL_NAME_LENGTH); } diff --git a/companion/src/wizarddialog.cpp b/companion/src/wizarddialog.cpp index 9d83e97d1..e681cfc6e 100644 --- a/companion/src/wizarddialog.cpp +++ b/companion/src/wizarddialog.cpp @@ -295,6 +295,11 @@ ModelSelectionPage::ModelSelectionPage(WizardDialog *dlg, QString image, QString l->addWidget(helicopterRB); } +void ModelSelectionPage::initializePage() +{ + nameLineEdit->setText(wizDlg->mix.name); +} + bool ModelSelectionPage::validatePage() { //Filter and insert model name in mix data diff --git a/companion/src/wizarddialog.h b/companion/src/wizarddialog.h index 7f6988033..32a7c4b49 100644 --- a/companion/src/wizarddialog.h +++ b/companion/src/wizarddialog.h @@ -73,6 +73,7 @@ private: QRadioButton *planeRB; QRadioButton *multirotorRB; QRadioButton *helicopterRB; + void initializePage(); bool validatePage(); int nextId() const; };