From d864a67c3b7efadd69e45d442c025df8f71e0aa2 Mon Sep 17 00:00:00 2001 From: Max Paperno Date: Mon, 10 Apr 2017 13:29:41 -0400 Subject: [PATCH] [Companion] Restore model drag/drop/compare, many UI improvements to main and sub-windows. (#4769) * [Companion] Refactor MdiChild & TreeModel, restore drag/drop, and more: * Add full drag-and-drop functions to the models list, including model copy, move, and insert actions (also between windows); * Add general radio settings copy/paste across file windows; * Full MIME support with distinct types for various data (eg. models vs. general settings), also add new type for metadata (header); * Copied/cut models can now be inserted in any spot (similar to drop-insert); * Deleted/cut models can now be removed entirely (including the empty slot) for all radio types (any models after the removed one move up, like for Horus) (may need to make this an option); * Empty model slots can now also be removed (any models after the removed slot move up) (but total # of slots is still maintained); * Default model can be deleted (new default is picked); * Completely revamp the actions system (copy/paste/edit/etc) to make it fully functional: * Actions exist in the window, not just context menu, therefore all keyboard shortcuts actually work, w/out any duplicated code; * Provides access from MainWindow in a comprehensive manner: all actions are available and properly updated based on selections in current editor window, w/out awkward event monitoring; * More actions available from context menu, and somewhat reorganized; * Also show all actions on mini toolbars right in the MDI window (possibly make optional later); * Updated English localization file to account for plurals usage in new action item names (other languages need to be updated); * Keyboard navigation in the model list now works/practical; * MDI window uses all available height by default; * Newly added models/categories are selected and highlighted; Category names are immediately editable; Model editor is/can be launched if wizard is not (need to make option); * MainWindow updates: * Edit toolbar and menu now showing all available edit actions, with proper context; * File menu properly updates with all "general" radio actions: edit/copy/paste settings and simulate radio; * Added Windows menu showing list of open windows (eg. to switch when full screen) and tabbed window option (need to make persistent setting); * Immediate update of icon size preference; * Do not delete toolbars when re-translating UI (they don't lose assigned position); * [Companion] Restore model compare functionality by using new MIME data exchange system; Add ability to compare any number of models at a time; Multi-model printer now shows custom hardware settings for each dropped model (even from different radios). * [Companion] Add missing monoblue & monowhite theme sdsync icons. * [Companion] Model wizard starts with default model name populated. * [Companion] Properly re-translate UI instead of recreating all the actions/etc.; Tell Mac specifically where to put our actions (seems to make it happier too). * [Companion] Further improve handling of Window menu items/detection of sub-window changes. * [Companion] Add file-close action. * [Companion] Add user options and save preferences: * Option to use wizard, open editor, or do nothing on new model creation; * Option to remove empty model slots when deleting/moving models (in non-Horus radios); * Options to hide toolbars in document windows (r-click on TBs); * Save/restore size of document windows, and maximized/tabbed state; * Save/restore size/position of model compare window; * Add shortcuts for model compare, FW download, log viewer; * [Companion] Disable being able to "delete" empty model slots (more awkward than helpful). --- companion/src/apppreferencesdialog.cpp | 8 +- companion/src/apppreferencesdialog.ui | 336 +-- companion/src/companion.qrc | 8 + companion/src/comparedialog.cpp | 149 +- companion/src/comparedialog.h | 35 +- companion/src/comparedialog.ui | 82 +- companion/src/eeprominterface.cpp | 1 + companion/src/mainwindow.cpp | 922 +++---- companion/src/mainwindow.h | 64 +- companion/src/mdichild.cpp | 1549 ++++++++---- companion/src/mdichild.h | 222 +- companion/src/mdichild.ui | 36 +- companion/src/modelslist.cpp | 664 +++-- companion/src/modelslist.h | 109 +- companion/src/multimodelprinter.cpp | 22 +- companion/src/multimodelprinter.h | 5 +- companion/src/printdialog.cpp | 2 +- companion/src/radiodata.h | 2 + companion/src/storage/appdata.cpp | 48 +- companion/src/storage/appdata.h | 21 +- companion/src/themes/monoblue/16/sdsync.png | Bin 0 -> 427 bytes companion/src/themes/monoblue/24/sdsync.png | Bin 0 -> 572 bytes companion/src/themes/monoblue/32/sdsync.png | Bin 0 -> 714 bytes companion/src/themes/monoblue/48/sdsync.png | Bin 0 -> 809 bytes companion/src/themes/monowhite/16/sdsync.png | Bin 0 -> 729 bytes companion/src/themes/monowhite/24/sdsync.png | Bin 0 -> 1189 bytes companion/src/themes/monowhite/32/sdsync.png | Bin 0 -> 1605 bytes companion/src/themes/monowhite/48/sdsync.png | Bin 0 -> 2298 bytes companion/src/translations/companion_en.ts | 2385 +++++++++--------- companion/src/wizarddata.cpp | 2 +- companion/src/wizarddialog.cpp | 5 + companion/src/wizarddialog.h | 1 + 32 files changed, 3947 insertions(+), 2731 deletions(-) create mode 100644 companion/src/themes/monoblue/16/sdsync.png create mode 100644 companion/src/themes/monoblue/24/sdsync.png create mode 100644 companion/src/themes/monoblue/32/sdsync.png create mode 100644 companion/src/themes/monoblue/48/sdsync.png create mode 100644 companion/src/themes/monowhite/16/sdsync.png create mode 100644 companion/src/themes/monowhite/24/sdsync.png create mode 100644 companion/src/themes/monowhite/32/sdsync.png create mode 100644 companion/src/themes/monowhite/48/sdsync.png 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 0000000000000000000000000000000000000000..47fc3ba810a2a7047c57ea663d582cffe60f9726 GIT binary patch literal 427 zcmV;c0aX5pP)(`&prKAKjKYsjviJzZSZ}sZ4r$B0e7;FF&78@=Y8p;{Dx@vl;tBZpSw9LwKuz&mZ zs~3m{@}D7VU}B&UU=2i|b3eR({rMY+{qyIa_w(l;eagkf_6SIG0EK>_Ck}@P) zPR7KV7&jC21t6}(pp%Ick5J;`CaYlpU%+5|0L$VG(Md>TVsOy17@#5bJLJ^dRA~za zjW_vn+jH+d-?``S`c2dHEph~W3m%fwqThcK!=!joWSW=Sw>qY|2+EHzm4RiC#`bAH zh*Vbsw)_of0dzq3KtKi+fH+8WHy2rMQIpB|xh#j|;n27aheHR0fiZ|i<;T%zyloic zPAC*yqm7-;%PAA&fST7J>xsnDYNc|RlBD2zKEIQqe^TCVpQS*sK{5jNzVn&%uV&HT zrYVhHPnT=8`^mSaJx%^25xK9bx1Tb{RG*SX8BBM;o*;e&R;<_nlxbri$g(D9UP(+< zX-bNgEk)SeWH|+IpOtHt+;-Me45%VEfnz~A<@hsj0aSrH-*T=XlkzJluL+h{g=*cx z8r6x;#B*g)SNP5e-$l9=6f1nwsvAkbrP(RAW*38Omz7&R%8|rN9pr5N=7DExlq`47 zOu#|TuDBQXWEN(EV7%!i_wSTE( zwvd$ExLscuX(pSzfH0`*)QgHjaI-V?wFbXvT~ANuziHPW0t^7%F}$`oi*mUQ9Xbol$bjL3?V|a9 z$Gx-neL+azHrVO|Uv`Wy03-%!PrxGZ9zDAmz}vKkNqeLsU@#&&8~>=)DxdJ-^z_oz z^~GVKu&{#Tg9GWnT(E9Bz3KP+BP8$zOk|3I(3i{0ui9&COBLHb(xg0h&(0XXnohe^*9eIK22ksfg|P z`g-ad9{vmGTsAj5Il0V(g-WPrA^<9AaQOulb0SM*LMa!i2@p6+a2f0?fZGvyMsaB{ zBB=N6sMJ0r2t#m`;AYI-(1`g@U1boKsTYx3%!*u0Ett0$K|GmmjJvX>VayfEW%XAC zybIlDJRx$SS{hA%# wk?OmEg3HYKLBd^SQ(C6~FnRJN*{uKr0E?OgN0g~zy8r+H07*qoM6N<$g3`cCpa1{> literal 0 HcmV?d00001 diff --git a/companion/src/themes/monoblue/48/sdsync.png b/companion/src/themes/monoblue/48/sdsync.png new file mode 100644 index 0000000000000000000000000000000000000000..451087d1d7a803265b1a79f1026aa7d07fa14900 GIT binary patch literal 809 zcmV+^1J?YBP) zif(mJARVAUas#VV#-`{2WZ%FUvpF|#f$o+M5J1_2OwOLc0s?eY-%FoUf_}6p=~<3Z zJ%F%8Q`Gl~@0axQJXeP-UQ3`Q&=ROl!1K-r@Y6H8`_!t+5?YeySIeCdAmCGIpLY0h ze?p6Z$V!v|flr`y8_oZKA ziW{bPj(~@2Rf2b%*~a=M=MJ^&YXlE38!<+x7(-5s=TYvUePqVz1bW*5`8B--O00Kw zyKir@?DrE~2Fx?Daeu7=HAy&0M`Fq2_=|$oP(koP9UZ-S{wR2jefXa)*f*d5*yzJn zMLO!hr6tf3XbBv+_!5(h$G@H%%JK2Lzxl=D9wRCjWu5w(Brw)*9v`2UmSDsMr#mem zmJVVf1Rwy7N(^@bM>&_Q4Nw|=aga6u;CoCBkZu_tQR3Fu((BnSGFQJMc+AmfB`UR3 zOqt++tQ|)1Y|`WkBrXMph3Hx`Tmi;l8zJCY9nCD@zr!;VhH&4ta2E??Ht^&%f)e0y zo|IJXRYc;5>3i4$en_j8)FmNBfU8+21+}h{AR$G7)BUsnyavyf~tr1x^rrXnav_ zi)67^Besy>z}t*H-MbeuNU-2ReJ(8eKYJAw8Ic%Uz@v_HY4@UTUgRr5(Q5;8iv#jh zfh9kRn#hH+wVy{&MJX{Qb(1aLX+3AyR4$|g9yqMRiqz>nv`eyym-K>crRYQG#$NA$ z!|>f1Rb}F7u>t;@V+QO*phtk4IFZ?iB}FGR9T%0L@-S-}yxKfZD8B;6N<@}mBq=MZ npQlc^+Jo+vz<*BQj{pMzdreH5Sv*Dw00000NkvXXu0mjf)*5N1 literal 0 HcmV?d00001 diff --git a/companion/src/themes/monowhite/16/sdsync.png b/companion/src/themes/monowhite/16/sdsync.png new file mode 100644 index 0000000000000000000000000000000000000000..019c24125f867d910b9eb8b937a25d7cb02d7d44 GIT binary patch literal 729 zcmV;~0w(>5P))zWr2`Mui{<63jFx$-7t>&@SN6@&sdGysJU) zDV53vv)L>gM~B1VY(Ae4gYORrGT>naoPx-c$z+xt%E7@wC6!8@pk^|eYPnoKJv%$= zZES3OKwr!$7|Blfe7?JCwR&-9XXlGZB>L6uc4rO`59i@L1@Wpvp>TG2dHK4*V7R%x zy*&@G>bX9pQt4cwP^be?L{Zd2r_;Hj(P$)*NaS>7Wkn74xL&U}AQ!`NbQw`7*ab0E z&W{*(I!EGQFd!j1E0IVjxm-Sn=Vr5c;&eI*s45RuED#7Nk|bv@3Lpl#k06W1@)AjM zI2@~(H4q4V0~-OY0ikaoz^#b@e!u_0`uh4Km&^4E%g^OB;Js!^!1jEzyv|6oqX=zEZySw{BCX+pDG#cVdIl$;T zold^Cw)S|)B4M}NS4}3967O!e`wp1QLMB)>kx3;#5u~@8v=T@u5*lf1f zFbf?FJQ0A;=QGd-?!Yum+9q)E+-kLo_xJZL_&*d1eFu|;S6S@$`!iVJzZUSp!Qn`u zP+W^fqi?~fLen(AR;xAQ@wgc714uyc?d`oY8jZJso6i9I`KN7e8*!0fQJ%sqJnZ#) zQyeV}!~C5%lBAI%n1f?@txfoJ1X!9FUFZK;WpwW2%&|k@Zvh4Xp_?~s(-=2%00000 LNkvXXu0mjfrw>h2 literal 0 HcmV?d00001 diff --git a/companion/src/themes/monowhite/24/sdsync.png b/companion/src/themes/monowhite/24/sdsync.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcb93ece724d53871a9cc1bbbf72c29650a29dd GIT binary patch literal 1189 zcmV;W1X}xvP)Ek2|+)bL%#C zdXia`W@cVBJ(kNKB#D`rUH&1O2oi{>^p}DVLW)6xhSEPlhPV(prBsl>;?A3hZsuiw zs0An5oUU!oo$mI-+uP?^@9XWxa_fN)@3ZIqzMtp&eSUq%uq>MpDGUS;As_|>39*Gp z9svRH1)t!Gbik261*m}MIGu?>eg?RKKL7_fAMr0mQ9if?Tmy1bQ&ZoDJR9ymng{GD zI6gl98`Pfw6MlQnPeLc)!4VL7TX%Q2(c|&>*f{0nxM#RY1x|=ZO%(GsXLr zl$4ZDEf&ki2L}h%$;nBB%jI%A91fM;Zcq37{W76Y=#xsNc8Nst;PmuV5eNh%4Gj%h zWo2d0=jG*nw7$OnGkA{Z6wyK?EGjBelQJ|kbhoLg>83y+2%1c$otGhJXJ_KAtu1G6 zZf^GR@v&1bmn%WU3=a>R$w60F*ITuZ+B<;711h zn3k5NhGPriA%wocI(0Zl%o0S{ST0u-PKMXR6VBVCNlXX)$K(hSzMkc5O!!73B{Bzm zgM~z?R9aA~iS6y}8nIaHDlRU*1{-s9bo5|uZZ0h`G0}^B$;rvu+M4}B2V4b7D2!AU zWXs6NxIaBT-F0|)s6b*Fl}e=`J4*7fw6xT|ySw`}91HgL_L3GB7N~z(E;<2?1yy=_ z`t|<){yZ``olaM4YpWmXOF20?S@rexSKV&6$80vQjf{+#xz2S0GT_F4#h7ohWT0ec zXNOvy#m>LPZyoSWOG}G2ys5BbTXS>scX;;+^$m~;RMES|#l`QTU!nYlc*8oFjPKK; zjg5`l&|d^T$jr=K;fYa+-p0np&tU(mprBy8t*z}o^lsoy;AS)j(HboQ9c#lwPHkUZ zU7f+(2L=Z2jE;`7g@uLrv$L}=$Ye4mDJiM4udnY{ZidBIromv4?CO$|k{j9C*-xLh zxKMbtrlv-Z3VMUc$=2K3tDTvd(R1TEJ_jJ8#kS<+8JUG z(TYa+_T)FE1~^g>l#G^*&_(haeaCR#sM;xpHNA-e{M; zh`aK{F)sV?OFzIRI`+ScuzwcukcT@7pOSuAZ5G#EuR(FP;cNJF7bqRo%|@=u`zHAFE{kd%U=f@mm~7RBNgm()~D(Z-s^ z8X;*_G#WKkqce`q-tWkL!;9#2T%Z>Y_r1HF?K|h5%bCq)yZ^IEY+1yi@t@n|v!+3N#Hnvb%S$R*V(@lv) zA|rZxXy=CB$!IhZHtaBNkjZ2#VzF3`SRtMsi`SdL_~xS++rUW z7k9Y6zJ5G2GxOlUz`)zt+1a;#etuqH?oAfLT6JV((-U?2lS6_^k3YoO_iM~@y6+PVJz{z(9D2yQXKV*1U_6>SC4K(CqnPv0h! ziQjAF=RhXv*RQqKL_0~N%%o!e8zkagMm>ZS@0tL-X2gUcp4V2w&!dT%CCvQ4aPM~R z0t;9GER9qu)dNr;d3bmz2L}gLfq{X0hKGlv*}S%>sOSTQLLpJ9R5QuR$swmtpY~r} zUDeRutqNet|GY_+O643vvB=xo+mZeWWC*b*U?4}hNdd@ZgsjNb)pg0y(NP7^%C%ap z6w#u?-e8*rU@2Hm`9C8gBQhu`$ejtWc>VhIU)kI~kfOG>c9O>cODvRN#wh1AaMD>W zd3kxCZ|aK8QZT~nYf7Kw$nNg$XBdkG9R^+S@$vbtwY7DWPqWZo-O$kRG%6}8AM>Z! z#YL8qfjsl`^S_~WLqkIpqMv{~NI2pt+AIa%xpODi!NEZW6V7F2Wqr%E+XLn&J3BkS zNJvNs;5Sf6B1cGJu@m@xA9N7p#jM3`1%#FqjFd`(*u})e6vNk)Y}B9-+zkn#p`nia zhVt_AVF>sGbHTp4y1HvsRaJj9H8tJC|5K1gkTYxwLry3dn2#~$R15efB_&xjN7hq; z`Oo;AAaAy`w4|Ire?A=8_$)p?J`H0>2uqYfcajH8>;ef(Pft&Da&od9*3;9YX1?}q zYir9)Oic8rac5^|F)V>dOeWlTS`)yQnvh)LiTJ;OoA8Km;`s67t_1}Jak!S%2thC5 z+~VS*DK|Ga3b$tcu4KZ84fQGE65N_)0>l;M(H)w(Chn)yuV2R~% zdwctjX=!Oc&CJZKoH=vm3G%}$svD!DqYqxWa>bM1fa`ko?%liX44hgX0~^(owqk%Z zcum$o0$T9$@_L49=+vc4mqNhYGbJVEJ=_5nWp3ZTeV=K+0&-heSn$}20DLLE){u&W z5sD*hTni#!E0&g)f)H|Hu*7~r1#G-^>(=ijB_%B^bg7&V2S7g*);9b@#g4B`*S&zq zml5;x^UZER>%)8hoCMITa?x&!DP0MmxNY5t% z9d58%fJt-i+__Y14-!`StS?e5MGUU%<(DsCK1K@vo@q{i*7NT~Zll-nHlAdh@DVEb zHnL~g3}~L=;+^*)x7qW27apbWJWLs|P3vFC{|YbwnJS#{d}To200000NkvXXu0mjf D2=NR0 literal 0 HcmV?d00001 diff --git a/companion/src/themes/monowhite/48/sdsync.png b/companion/src/themes/monowhite/48/sdsync.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc1e8ce7558e77193d86a24a8cd1f15bd18dc0d GIT binary patch literal 2298 zcmV)t3{vxK!*)PQZY~{R%^QHa={F5v1oN;r01uMGha+8j0BLBh;+`WNrtn6Z zzX7VEc^&+AC%KbpL}GxA0|ySwaCCI^G?`3twl;nGbWe>&qaN(DWHOlr$Xr=jS(}lO zar@-SlU25D8)BqCqBucv0rE%Mx^?TGii(OF%P5ZG;^KQ+t@ca$O&20rFZl)#UP``> zG!5u;6uzUQquVkDL#NZ#!EeUVZ%-pRP$nspQQBXYqI79bm+12$t*x!eE-o$;8Bak$ zK^>Z&_$_eX@Z`ypCbQWrhiA*k%2Mu`78a~HXZMgFDKawB)6dUu9OFNI`t-5H#Ka`J zM7PrO>D87PVA)2qY15`vyztAHFVDDs{dyiuYiP;H1-d5%IR6X@3D8dSPL3aicE!on zt5?ILqociWK4;FH2>3`Po(8r7TZ33iX%dI);>C*t*p1xW+{YmN<7?Nhg*Z4k2q{l4 zmzzpTN?LI4?Cd22W@f$3*~^v^K&0S(nY18i;_TV8w?XKiUcP*(15Mf?J$v@73f9h_IC0_$ zX;?{qe}4V?^;dDu(l4z23Y5aEXlG~F!DUG)DOzEz&J5WC7;r4W!9`FwmCW6p#jjTg@R8)T9)(o=)Qu}wuMU^7LmJH_=GYTHyTV9 zddsv>kg#~Q;(Wt~4Z%n_2mJC91prj)1H#8Mekg1$9n}9)#MmyX% z@RbZkE`<#)#-N0f4?e&|sQs$g*w`PLD{**8P%tW<84MMD=FAxh;h;vYaZz%3Nto~6 zy*qMP8{k?;2kO2g@$vBs;^N}8ocTKmT$K|a18@^Kf#f0(#{e7ye+ZXNoU&X=IiJ`X z4>p+!Q0F-4kahsDV%CrVWG?NCA0Tkp+_`fD-Q3(fIy*Z%N=r+17cN}*Hzg(I)oA4F zew&zpfPjq+4Gm>{qjBWO5yUbPMDwWWh>MAd`3=x$Q(q0r)2B}>ckI~l3-E>!X~@-_ z$jz`7RUbZl_#5^eP!z__YNCl`sh);LF;EYXot^z1XtJlrJ$Ue74)nsmll$Os2U_6e z<)s8r_XU_=E`AtvaE6;VZ|Wdu+hDj;o5(zXS+bsGx)DvI0E@-K7h20 zgYYP10T;xcVAZdoIhwj|-MVwoef>r2`DiSzFb>NC(mw99?9s-xi{1$d3A3y;Jo-I* z_WVhg<>cQI=-@ts&TK065eblnRaseCjpW-es3zE42Sx-7fk5u!wd2Q+SBux~-o3jP zI=HD^Es>|Er>}%Y-fgU~keLWwOG`8#U_vjA-M@doi_0+H&?auN);03At8%)?b;PsU0v;KjWIDXaWXXKQ4rh>{{H^52@@uGpj&A)8hbG> zq}MKpuO)z^GV}BE+lN~RWR!1_R`Yort`mr&fxgep%`NEfV5+F7s70+-X9DjzpnFG; z9<8*-M?sE|1P=&e_U_&Lt*v!nSTV44>Cy}Ua}Cz+J%9eZ3o^|=wY9Zv1Q+BgmC8=c z_weCEOs~~c5AazH3WdTrS}~v}ss(zC1X<+D#ful4L@uEZ3k$ndQ&WRNRj~B9V#Nwq z@dLqaFhgOgEv8!0geMbYnuVYal(S8s@Jp5~c@3I>>ttcyzI|$I{tp3MI%Y38IH+95 z9S@=px4|YP$;rtpp<35AHZ~dwDhvIHq&Yk-lrfQ?c39T0d z)(hu0t0#+__(R?PEMTFRY&cpTJ$h86)oKO*-MDdMEcCoy&CShz0I>yBi7AiyY-?+) zs->kxO%U36`SRroAW3(3x0{kplTZ|?68A0Pbb^nU^j0L}hU U&8)w2`Tzg`07*qoM6N<$g4Cl`00000 literal 0 HcmV?d00001 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; };