diff --git a/companion/src/CMakeLists.txt b/companion/src/CMakeLists.txt index 20a6c0ea9..3629d51b7 100644 --- a/companion/src/CMakeLists.txt +++ b/companion/src/CMakeLists.txt @@ -166,6 +166,7 @@ set(common_SRCS eeprominterface.cpp helpers.cpp radiodata.cpp + radiodataconversionstate.cpp translations.cpp firmwares/er9x/er9xeeprom.cpp firmwares/er9x/er9xinterface.cpp @@ -207,6 +208,7 @@ add_subdirectory(generaledit) add_subdirectory(simulation) add_subdirectory(storage) add_subdirectory(thirdparty/qcustomplot) +add_subdirectory(thirdparty/maxlibqt/src/widgets) ############# Companion ############### @@ -305,7 +307,7 @@ qt5_wrap_cpp(companion_SRCS ${companion_MOC_HDRS}) add_executable(${COMPANION_NAME} MACOSX_BUNDLE ${WIN_EXECUTABLE_TYPE} ${companion_SRCS} ${icon_RC}) qt5_use_modules(${COMPANION_NAME} Core Widgets Network) -target_link_libraries(${COMPANION_NAME} PRIVATE generaledit modeledit simulation ${CPN_COMMON_LIB} qcustomplot shared storage ${PTHREAD_LIBRARY} ${SDL_LIBRARY} ${WIN_LINK_LIBRARIES}) +target_link_libraries(${COMPANION_NAME} PRIVATE generaledit modeledit simulation ${CPN_COMMON_LIB} maxLibQtWidgets qcustomplot shared storage ${PTHREAD_LIBRARY} ${SDL_LIBRARY} ${WIN_LINK_LIBRARIES}) PrintTargetReport("${COMPANION_NAME}") @@ -528,9 +530,9 @@ IF(APPLE) get_filename_component(AVRDUDECONF_ABSOLUTE_PATH ${AVRDUDE_CONF} REALPATH) install(PROGRAMS ${DFU_UTIL_ABSOLUTE_PATH} ${AVRDUDE_ABSOLUTE_PATH} DESTINATION ${companion_res_dir} COMPONENT Runtime) install(FILES ${AVRDUDECONF_ABSOLUTE_PATH} DESTINATION ${companion_res_dir} COMPONENT Runtime) - + set(bundle_tools_path "\${CMAKE_INSTALL_PREFIX}/${companion_res_dir}/dfu-util;\${CMAKE_INSTALL_PREFIX}/${companion_res_dir}/avrdude") - + # Include depencies (adding frameworks, fixing the embbeded libraries) # I get write errors without setting BU_CHMOD_BUNDLE_ITEMS even though it is diff --git a/companion/src/apppreferencesdialog.cpp b/companion/src/apppreferencesdialog.cpp index 5b5d4fb3a..17411667b 100644 --- a/companion/src/apppreferencesdialog.cpp +++ b/companion/src/apppreferencesdialog.cpp @@ -23,6 +23,7 @@ #include "mainwindow.h" #include "appdata.h" #include "helpers.h" +#include "storage.h" #if defined(JOYSTICKS) #include "joystick.h" #include "joystickdialog.h" @@ -30,17 +31,17 @@ AppPreferencesDialog::AppPreferencesDialog(QWidget * parent) : QDialog(parent), + updateLock(false), + mainWinHasDirtyChild(false), ui(new Ui::AppPreferencesDialog) { ui->setupUi(this); - updateLock=false; setWindowIcon(CompanionIcon("apppreferences.png")); initSettings(); connect(ui->downloadVerCB, SIGNAL(currentIndexChanged(int)), this, SLOT(baseFirmwareChanged())); connect(ui->opt_appDebugLog, &QCheckBox::toggled, this, &AppPreferencesDialog::toggleAppLogSettings); connect(ui->opt_fwTraceLog, &QCheckBox::toggled, this, &AppPreferencesDialog::toggleAppLogSettings); - connect(this, SIGNAL(accepted()), this, SLOT(writeValues())); #if !defined(JOYSTICKS) ui->joystickCB->hide(); @@ -58,7 +59,12 @@ AppPreferencesDialog::~AppPreferencesDialog() delete ui; } -void AppPreferencesDialog::writeValues() +void AppPreferencesDialog::setMainWinHasDirtyChild(bool value) +{ + mainWinHasDirtyChild = value; +} + +void AppPreferencesDialog::accept() { g.autoCheckApp(ui->autoCheckCompanion->isChecked()); g.OpenTxBranch(DownloadBranchType(ui->OpenTxBranch->currentIndex())); @@ -102,15 +108,42 @@ void AppPreferencesDialog::writeValues() else g.profile[g.id()].name(ui->profileNameLE->text()); + bool fwchange = false; + Firmware * newFw = getFirmwareVariant(); // If a new fw type has been choosen, several things need to reset - Firmware::setCurrentVariant(getFirmwareVariant()); - QString id = Firmware::getCurrentVariant()->getId(); - if (g.profile[g.id()].fwType() != id) { + if (Firmware::getCurrentVariant()->getId() != newFw->getId()) { + // check if we're going to be converting to a new radio type and there are unsaved files in the main window + if (mainWinHasDirtyChild && !Boards::isBoardCompatible(Firmware::getCurrentVariant()->getBoard(), newFw->getBoard())) { + QString q = tr("

You cannot switch Radio Type or change Build Options while there are unsaved file changes. What do you wish to do?

"); + int resp = QMessageBox::question(this, windowTitle(), q, (QMessageBox::SaveAll | QMessageBox::Reset | QMessageBox::Cancel), QMessageBox::Cancel); + if (resp == QMessageBox::SaveAll) { + // signal main window to save files, need to do this before the fw actually changes + emit firmwareProfileAboutToChange(); + } + else if (resp == QMessageBox::Reset) { + // bail out early before saving the radio type & firmware options + QDialog::accept(); + return; + } + else { + // we do not accept the dialog close + return; + } + } + Firmware::setCurrentVariant(newFw); g.profile[g.id()].fwName(""); g.profile[g.id()].initFwVariables(); - g.profile[g.id()].fwType(id); - emit firmwareProfileChanged(g.id()); + g.profile[g.id()].fwType(newFw->getId()); + fwchange = true; } + + QDialog::accept(); + + if (fwchange) + emit firmwareProfileChanged(g.id()); // important to do this after the accepted() signal } void AppPreferencesDialog::on_snapshotPathButton_clicked() diff --git a/companion/src/apppreferencesdialog.h b/companion/src/apppreferencesdialog.h index 82bb84a56..22949e990 100644 --- a/companion/src/apppreferencesdialog.h +++ b/companion/src/apppreferencesdialog.h @@ -38,14 +38,21 @@ class AppPreferencesDialog : public QDialog public: explicit AppPreferencesDialog(QWidget * parent = 0); ~AppPreferencesDialog(); + Joystick * joystick; + public slots: + void accept() Q_DECL_OVERRIDE; + void setMainWinHasDirtyChild(bool value); + signals: void firmwareProfileChanged(int profId); + void firmwareProfileAboutToChange(bool saveFiles = true); private: QList optionsCheckBoxes; bool updateLock; + bool mainWinHasDirtyChild; void showVoice(bool); void showVoice(); void hideVoice(); @@ -64,7 +71,6 @@ class AppPreferencesDialog : public QDialog void firmwareOptionChanged(bool state); void toggleAppLogSettings(); - void writeValues(); void on_libraryPathButton_clicked(); void on_snapshotPathButton_clicked(); void on_snapshotClipboardCKB_clicked(); diff --git a/companion/src/boards.cpp b/companion/src/boards.cpp index 00bc06385..f0d0b11ba 100644 --- a/companion/src/boards.cpp +++ b/companion/src/boards.cpp @@ -53,6 +53,32 @@ void Boards::setBoardType(const Type & board) m_boardType = BOARD_UNKNOWN; } +uint32_t Boards::getFourCC(Type board) +{ + switch (board) { + case BOARD_X12S: + return 0x3478746F; + case BOARD_X10: + return 0x3778746F; + case BOARD_TARANIS_X7: + return 0x3678746F; + case BOARD_TARANIS_X9E: + return 0x3578746F; + case BOARD_TARANIS_X9D: + case BOARD_TARANIS_X9DP: + return 0x3378746F; + case BOARD_SKY9X: + case BOARD_AR9X: + case BOARD_9XRPRO: + return 0x3278746F; + case BOARD_MEGA2560: + case BOARD_GRUVIN9X: + return 0x3178746F; + default: + return 0; + } +} + const int Boards::getEEpromSize(Board::Type board) { switch (board) { @@ -177,6 +203,12 @@ const int Boards::getCapability(Board::Type board, Board::Capability capability) else return 3; + case FactoryInstalledPots: + if (IS_TARANIS_X9(board)) + return 2; + else + return getCapability(board, Pots); + case Sliders: if (IS_HORUS_X12S(board) || IS_TARANIS_X9E(board)) return 4; @@ -191,6 +223,15 @@ const int Boards::getCapability(Board::Type board, Board::Capability capability) else return 0; + case MaxAnalogs: + return getCapability(board, Board::Sticks) + getCapability(board, Board::Pots) + getCapability(board, Board::Sliders) + getCapability(board, Board::MouseAnalogs); + + case MultiposPots: + return IS_HORUS_OR_TARANIS(board) ? 3 : 0; + + case MultiposPotsPositions: + return IS_HORUS_OR_TARANIS(board) ? 6 : 0; + case Switches: if (IS_TARANIS_X9E(board)) return 18; @@ -242,6 +283,89 @@ const QString Boards::getAxisName(int index) return QObject::tr("Unknown"); } +const QString Boards::getAnalogInputName(Board::Type board, unsigned index) +{ + if ((int)index < getBoardCapability(board, Board::Sticks)) { + const QString sticks[] = { + QObject::tr("Rud"), + QObject::tr("Ele"), + QObject::tr("Thr"), + QObject::tr("Ail") + }; + return sticks[index]; + } + + index -= getCapability(board, Board::Sticks); + + if (IS_9X(board) || IS_2560(board) || IS_SKY9X(board)) { + const QString pots[] = { + "P1", + "P2", + "P3" + }; + if (index < DIM(pots)) + return pots[index]; + } + else if (IS_TARANIS_X9E(board)) { + const QString pots[] = { + "F1", + "F2", + "F3", + "F4", + "S1", + "S2", + "LS", + "RS" + }; + if (index < DIM(pots)) + return pots[index]; + } + else if (IS_TARANIS(board)) { + const QString pots[] = { + "S1", + "S2", + "S3", + "LS", + "RS" + }; + if (index < DIM(pots)) + return pots[index]; + } + else if (IS_HORUS_X12S(board)) { + const QString pots[] = { + "S1", + "6P", + "S2", + "L1", + "L2", + "LS", + "RS", + "JSx", + "JSy" + }; + if (index < DIM(pots)) + return pots[index]; + } + else if (IS_HORUS_X10(board)) { + const QString pots[] = { + "S1", + "6P", + "S2", + "LS", + "RS" + }; + if (index < DIM(pots)) + return pots[index]; + } + + return "???"; +} + +const bool Boards::isBoardCompatible(Type board1, Type board2) +{ + return (getFourCC(board1) == getFourCC(board2)); +} + /* Currently unused const QString Boards::getBoardName(Board::Type board) diff --git a/companion/src/boards.h b/companion/src/boards.h index 7ce3a4213..f454a15d1 100644 --- a/companion/src/boards.h +++ b/companion/src/boards.h @@ -107,8 +107,12 @@ namespace Board { enum Capability { Sticks, Pots, + FactoryInstalledPots, Sliders, MouseAnalogs, + MaxAnalogs, + MultiposPots, + MultiposPotsPositions, Switches, SwitchPositions, FactoryInstalledSwitches, @@ -146,16 +150,22 @@ class Boards void setBoardType(const Board::Type & board); Board::Type getBoardType() const { return m_boardType; } - const int getEEpromSize() { return getEEpromSize(m_boardType); } - const int getFlashSize() { return getFlashSize(m_boardType); } - const Board::SwitchInfo getSwitchInfo(unsigned index) { return getSwitchInfo(m_boardType, index); } - const int getCapability(Board::Capability capability) { return getCapability(m_boardType, capability); } + const uint32_t getFourCC() const { return getFourCC(m_boardType); } + const int getEEpromSize() const { return getEEpromSize(m_boardType); } + const int getFlashSize() const { return getFlashSize(m_boardType); } + const Board::SwitchInfo getSwitchInfo(unsigned index) const { return getSwitchInfo(m_boardType, index); } + const int getCapability(Board::Capability capability) const { return getCapability(m_boardType, capability); } + const QString getAnalogInputName(unsigned index) const { return getAnalogInputName(m_boardType, index); } + const bool isBoardCompatible(Board::Type board2) const { return isBoardCompatible(m_boardType, board2); } + static uint32_t getFourCC(Board::Type board); static const int getEEpromSize(Board::Type board); static const int getFlashSize(Board::Type board); static const Board::SwitchInfo getSwitchInfo(Board::Type board, unsigned index); static const int getCapability(Board::Type board, Board::Capability capability); static const QString getAxisName(int index); + static const QString getAnalogInputName(Board::Type board, unsigned index); + static const bool isBoardCompatible(Board::Type board1, Board::Type board2); protected: @@ -178,6 +188,7 @@ class Boards #define IS_TARANIS_PLUS(board) (board==Board::BOARD_TARANIS_X9DP || board==Board::BOARD_TARANIS_X9E) #define IS_TARANIS_X9E(board) (board==Board::BOARD_TARANIS_X9E) #define IS_TARANIS(board) (IS_TARANIS_X9(board) || IS_TARANIS_X7(board)) +#define IS_TARANIS_NOT_X9E(board) (IS_TARANIS(board) && !IS_TARANIS_X9E(board)) #define IS_HORUS_X12S(board) (board==Board::BOARD_X12S) #define IS_HORUS_X10(board) (board==Board::BOARD_X10) #define IS_HORUS(board) (IS_HORUS_X12S(board) || IS_HORUS_X10(board)) diff --git a/companion/src/eeprominterface.cpp b/companion/src/eeprominterface.cpp index 154d8cbc6..d949f99fb 100644 --- a/companion/src/eeprominterface.cpp +++ b/companion/src/eeprominterface.cpp @@ -151,6 +151,11 @@ int TimToVal(float value) return (temp-129); } + +/* + * SensorData + */ + void SensorData::updateUnit() { if (type == TELEM_TYPE_CALCULATED) { @@ -215,13 +220,10 @@ QString SensorData::unitString() const } } -bool RawSource::isTimeBased() const -{ - if (IS_ARM(getCurrentBoard())) - return (type == SOURCE_TYPE_SPECIAL && index > 0); - else - return (type==SOURCE_TYPE_TELEMETRY && (index==TELEMETRY_SOURCE_TX_TIME || index==TELEMETRY_SOURCE_TIMER1 || index==TELEMETRY_SOURCE_TIMER2 || index==TELEMETRY_SOURCE_TIMER3)); -} + +/* + * RawSourceRange + */ float RawSourceRange::getValue(int value) { @@ -231,6 +233,11 @@ float RawSourceRange::getValue(int value) return min + float(value) * step; } + +/* + * RawSource + */ + RawSourceRange RawSource::getRange(const ModelData * model, const GeneralSettings & settings, unsigned int flags) const { RawSourceRange result; @@ -490,8 +497,12 @@ RawSourceRange RawSource::getRange(const ModelData * model, const GeneralSetting return result; } -QString RawSource::toString(const ModelData * model, const GeneralSettings * const generalSettings) const +QString RawSource::toString(const ModelData * model, const GeneralSettings * const generalSettings, Board::Type board) const { + if (board == Board::BOARD_UNKNOWN) { + board = getCurrentBoard(); + } + static const QString trims[] = { QObject::tr("TrmR"), QObject::tr("TrmE"), QObject::tr("TrmT"), QObject::tr("TrmA"), QObject::tr("Trm5"), QObject::tr("Trm6") }; @@ -518,12 +529,15 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con static const QString rotary[] = { QObject::tr("REa"), QObject::tr("REb") }; if (index<0) { - return QObject::tr("----"); + return QObject::tr("???"); } QString result; int genAryIdx = 0; switch (type) { + case SOURCE_TYPE_NONE: + return QObject::tr("----"); + case SOURCE_TYPE_VIRTUAL_INPUT: { const char * name = NULL; @@ -545,7 +559,7 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con result = QString(generalSettings->stickName[genAryIdx]); } if (result.isEmpty()) - result = getCurrentFirmware()->getAnalogInputName(index);; + result = Boards::getAnalogInputName(board, index); return result; case SOURCE_TYPE_TRIM: @@ -559,7 +573,7 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con if (generalSettings) result = QString(generalSettings->switchName[index]); if (result.isEmpty()) - result = getSwitchInfo(getCurrentBoard(), index).name; + result = Boards::getSwitchInfo(board, index).name; return result; case SOURCE_TYPE_CUSTOM_SWITCH: @@ -583,7 +597,7 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con return CHECK_IN_ARRAY(special, index); case SOURCE_TYPE_TELEMETRY: - if (IS_ARM(getCurrentBoard())) { + if (IS_ARM(board)) { div_t qr = div(index, 3); result = getElementName(QCoreApplication::translate("Telemetry", "TELE"), qr.quot+1, model ? model->sensorData[qr.quot].label : NULL); if (qr.rem) @@ -603,13 +617,16 @@ QString RawSource::toString(const ModelData * model, const GeneralSettings * con } default: - return QObject::tr("----"); + return QObject::tr("???"); } } -bool RawSource::isStick(int * stickIndex) const +bool RawSource::isStick(int * stickIndex, Board::Type board) const { - if (type == SOURCE_TYPE_STICK && index < getBoardCapability(getCurrentBoard(), Board::Sticks)) { + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + if (type == SOURCE_TYPE_STICK && index < Boards::getCapability(board, Board::Sticks)) { if (stickIndex) *stickIndex = index; return true; @@ -617,30 +634,103 @@ bool RawSource::isStick(int * stickIndex) const return false; } -bool RawSource::isPot(int * potsIndex) const +bool RawSource::isPot(int * potsIndex, Board::Type board) const { + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + Boards b(board); if (type == SOURCE_TYPE_STICK && - index >= getBoardCapability(getCurrentBoard(), Board::Sticks) && - index < getBoardCapability(getCurrentBoard(), Board::Sticks) + getBoardCapability(getCurrentBoard(), Board::Pots)) { + index >= b.getCapability(Board::Sticks) && + index < b.getCapability(Board::Sticks) + b.getCapability(Board::Pots)) { if (potsIndex) - *potsIndex = index - getBoardCapability(getCurrentBoard(), Board::Sticks); + *potsIndex = index - b.getCapability(Board::Sticks); return true; } return false; } -bool RawSource::isSlider(int * sliderIndex) const +bool RawSource::isSlider(int * sliderIndex, Board::Type board) const { + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + Boards b(board); if (type == SOURCE_TYPE_STICK && - index >= getBoardCapability(getCurrentBoard(), Board::Sticks) + getBoardCapability(getCurrentBoard(), Board::Pots) && - index < getBoardCapability(getCurrentBoard(), Board::Sticks) + getBoardCapability(getCurrentBoard(), Board::Pots) + getBoardCapability(getCurrentBoard(), Board::Sliders)) { + index >= b.getCapability(Board::Sticks) + b.getCapability(Board::Pots) && + index < b.getCapability(Board::Sticks) + b.getCapability(Board::Pots) + b.getCapability(Board::Sliders)) { if (sliderIndex) - *sliderIndex = index - getBoardCapability(getCurrentBoard(), Board::Sticks) - getBoardCapability(getCurrentBoard(), Board::Pots); + *sliderIndex = index - b.getCapability(Board::Sticks) - b.getCapability(Board::Pots); return true; } return false; } +bool RawSource::isTimeBased(Board::Type board) const +{ + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + if (IS_ARM(board)) + return (type == SOURCE_TYPE_SPECIAL && index > 0); + else + return (type==SOURCE_TYPE_TELEMETRY && (index==TELEMETRY_SOURCE_TX_TIME || index==TELEMETRY_SOURCE_TIMER1 || index==TELEMETRY_SOURCE_TIMER2 || index==TELEMETRY_SOURCE_TIMER3)); +} + +bool RawSource::isAvailable(const ModelData * const model, const GeneralSettings * const gs, Board::Type board) +{ + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + Boards b(board); + + if (type == SOURCE_TYPE_STICK && index >= b.getCapability(Board::MaxAnalogs)) + return false; + + if (type == SOURCE_TYPE_SWITCH && index >= b.getCapability(Board::Switches)) + return false; + + if (model) { + if (type == SOURCE_TYPE_VIRTUAL_INPUT && !model->isInputValid(index)) + return false; + + if (type == SOURCE_TYPE_CUSTOM_SWITCH && model->logicalSw[index].isEmpty()) + return false; + + if (type == SOURCE_TYPE_TELEMETRY) { + if (IS_ARM(board) && !model->sensorData[div(index, 3).quot].isAvailable()) { + return false; + } + else if (!IS_ARM(board)) { + Firmware * fw = getCurrentFirmware(); + if (type == (int)TELEMETRY_SOURCE_TX_TIME && !fw->getCapability(RtcTime)) + return false; + + if (type == (int)TELEMETRY_SOURCE_SWR && !fw->getCapability(SportTelemetry)) + return false; + + if (type == (int)TELEMETRY_SOURCE_TIMER3 && fw->getCapability(Timers) < 3) + return false; + } + } + } + + if (gs) { + int gsIdx = 0; + if (type == SOURCE_TYPE_STICK && ((isPot(&gsIdx) && !gs->isPotAvailable(gsIdx)) || (isSlider(&gsIdx) && !gs->isSliderAvailable(gsIdx)))) + return false; + + if (type == SOURCE_TYPE_SWITCH && IS_HORUS_OR_TARANIS(board) && !gs->switchSourceAllowedTaranis(index)) + return false; + } + + if (type == SOURCE_TYPE_TRIM && index >= b.getCapability(Board::NumTrims)) + return false; + + return true; +} + + /* * RawSwitch */ @@ -704,13 +794,13 @@ QString RawSwitch::toString(Board::Type board, const GeneralSettings * const gen return getElementName(QCoreApplication::translate("Logic Switch", "L"), index, NULL, true); case SWITCH_TYPE_MULTIPOS_POT: - if (!getCurrentFirmware()->getCapability(MultiposPotsPositions)) + if (!Boards::getCapability(board, Board::MultiposPotsPositions)) return QObject::tr("???"); - qr = div(index - 1, getCurrentFirmware()->getCapability(MultiposPotsPositions)); + qr = div(index - 1, Boards::getCapability(board, Board::MultiposPotsPositions)); if (generalSettings && qr.quot < (int)DIM(generalSettings->potConfig)) swName = QString(generalSettings->potName[qr.quot]); if (swName.isEmpty()) - swName = getCurrentFirmware()->getAnalogInputName(qr.quot + getBoardCapability(board, Board::Sticks));; + swName = Boards::getAnalogInputName(board, qr.quot + Boards::getCapability(board, Board::Sticks)); return swName + "_" + QString::number(qr.rem + 1); case SWITCH_TYPE_TRIM: @@ -749,6 +839,36 @@ QString RawSwitch::toString(Board::Type board, const GeneralSettings * const gen } } +bool RawSwitch::isAvailable(const ModelData * const model, const GeneralSettings * const gs, Board::Type board) +{ + if (board == Board::BOARD_UNKNOWN) + board = getCurrentBoard(); + + Boards b(board); + + if (type == SWITCH_TYPE_SWITCH && abs(index) > b.getCapability(Board::SwitchPositions)) + return false; + + if (type == SWITCH_TYPE_TRIM && abs(index) > b.getCapability(Board::NumTrimSwitches)) + return false; + + if (gs) { + if (type == SWITCH_TYPE_SWITCH && IS_HORUS_OR_TARANIS(board) && !gs->switchPositionAllowedTaranis(abs(index))) + return false; + + if (type == SWITCH_TYPE_MULTIPOS_POT) { + int pot = div(abs(index) - 1, b.getCapability(Board::MultiposPotsPositions)).quot; + if (!gs->isPotAvailable(pot) || gs->potConfig[pot] != Board::POT_MULTIPOS_SWITCH) + return false; + } + } + + if (model && !model->isAvailable(*this)) + return false; + + return true; +} + /* * CurveReference @@ -887,6 +1007,11 @@ bool CustomFunctionData::isEmpty() const return (swtch.type == SWITCH_TYPE_NONE); } +QString CustomFunctionData::toString(int index, bool globalContext) const +{ + return getElementName((globalContext ? QObject::tr("GF") : QObject::tr("SF")), index+1, 0, true); +} + QString CustomFunctionData::funcToString(const ModelData * model) const { if (func >= FuncOverrideCH1 && func <= FuncOverrideCH32) @@ -1209,10 +1334,7 @@ GeneralSettings::GeneralSettings() vBatMax = -40; //8V } - - for (int i=0; i= CPN_MAX_STICKS) @@ -1949,6 +2079,11 @@ void FlightModeData::clear(const int phase) } } +QString FlightModeData::toString(int index) const +{ + return getElementName(QObject::tr("FM"), index, name, true); +} + QString GVarData::unitToString() const { switch (unit) { diff --git a/companion/src/eeprominterface.h b/companion/src/eeprominterface.h index 31f897d50..76148d2b3 100644 --- a/companion/src/eeprominterface.h +++ b/companion/src/eeprominterface.h @@ -134,8 +134,6 @@ enum Capability { EnhancedCurves, HasFasOffset, HasMahPersistent, - MultiposPots, - MultiposPotsPositions, SimulatorVariant, MavlinkTelemetry, HasInputDiff, diff --git a/companion/src/firmwares/opentx/opentxinterface.cpp b/companion/src/firmwares/opentx/opentxinterface.cpp index eefb659e3..f1f069062 100644 --- a/companion/src/firmwares/opentx/opentxinterface.cpp +++ b/companion/src/firmwares/opentx/opentxinterface.cpp @@ -141,7 +141,7 @@ bool OpenTxEepromInterface::saveToByteArray(const T & src, QByteArray & data, ui // manager.Dump(); manager.Export(raw); data.resize(8); - *((uint32_t*)&data.data()[0]) = StorageFormat::getFourCC(board); + *((uint32_t*)&data.data()[0]) = Boards::getFourCC(board); data[4] = version; data[5] = 'M'; *((uint16_t*)&data.data()[6]) = raw.size(); @@ -164,12 +164,12 @@ template bool OpenTxEepromInterface::loadFromByteArray(T & dest, const QByteArray & data) { uint32_t fourcc = *((uint32_t*)&data.data()[0]); - if (StorageFormat::getFourCC(board) != fourcc) { + if (Boards::getFourCC(board) != fourcc) { if (IS_HORUS(board) && fourcc == 0x3178396F) { - qDebug() << QString().sprintf("%s: Deprecated fourcc used %x vs %x", getName(), fourcc, StorageFormat::getFourCC(board)); + qDebug() << QString().sprintf("%s: Deprecated fourcc used %x vs %x", getName(), fourcc, Boards::getFourCC(board)); } else { - qDebug() << QString().sprintf("%s: Wrong fourcc %x vs %x", getName(), fourcc, StorageFormat::getFourCC(board)); + qDebug() << QString().sprintf("%s: Wrong fourcc %x vs %x", getName(), fourcc, Boards::getFourCC(board)); return false; } } @@ -679,10 +679,6 @@ int OpenTxFirmware::getCapability(::Capability capability) return (IS_STOCK(board) ? false : true); case HasMahPersistent: return (IS_ARM(board) ? true : false); - case MultiposPots: - return IS_HORUS_OR_TARANIS(board) ? 3 : 0; - case MultiposPotsPositions: - return IS_HORUS_OR_TARANIS(board) ? 6 : 0; case SimulatorVariant: if (board == BOARD_STOCK) return SIMU_STOCK_VARIANTS; @@ -714,76 +710,7 @@ int OpenTxFirmware::getCapability(::Capability capability) QString OpenTxFirmware::getAnalogInputName(unsigned int index) { - if ((int)index < getBoardCapability(board, Board::Sticks)) { - const QString sticks[] = { - QObject::tr("Rud"), - QObject::tr("Ele"), - QObject::tr("Thr"), - QObject::tr("Ail") - }; - return sticks[index]; - } - - index -= getBoardCapability(board, Board::Sticks); - - if (IS_9X(board) || IS_2560(board) || IS_SKY9X(board)) { - const QString pots[] = { - QObject::tr("P1"), - QObject::tr("P2"), - QObject::tr("P3") - }; - return CHECK_IN_ARRAY(pots, index); - } - else if (IS_TARANIS_X9E(board)) { - const QString pots[] = { - QObject::tr("F1"), - QObject::tr("F2"), - QObject::tr("F3"), - QObject::tr("F4"), - QObject::tr("S1"), - QObject::tr("S2"), - QObject::tr("LS"), - QObject::tr("RS") - }; - return CHECK_IN_ARRAY(pots, index); - } - else if (IS_TARANIS(board)) { - const QString pots[] = { - QObject::tr("S1"), - QObject::tr("S2"), - QObject::tr("S3"), - QObject::tr("LS"), - QObject::tr("RS") - }; - return CHECK_IN_ARRAY(pots, index); - } - else if (IS_HORUS_X12S(board)) { - const QString pots[] = { - QObject::tr("S1"), - QObject::tr("6P"), - QObject::tr("S2"), - QObject::tr("L1"), - QObject::tr("L2"), - QObject::tr("LS"), - QObject::tr("RS"), - QObject::tr("JSx"), - QObject::tr("JSy") - }; - return CHECK_IN_ARRAY(pots, index); - } - else if (IS_HORUS_X10(board)) { - const QString pots[] = { - QObject::tr("S1"), - QObject::tr("6P"), - QObject::tr("S2"), - QObject::tr("LS"), - QObject::tr("RS") - }; - return CHECK_IN_ARRAY(pots, index); - } - else { - return "???"; - } + return Boards::getAnalogInputName(board, index); } QTime OpenTxFirmware::getMaxTimerStart() diff --git a/companion/src/helpers.cpp b/companion/src/helpers.cpp index b775a3eff..a77a0e45b 100644 --- a/companion/src/helpers.cpp +++ b/companion/src/helpers.cpp @@ -405,30 +405,16 @@ void Helpers::getFileComboBoxValue(QComboBox * b, char * dest, int length) } void Helpers::addRawSourceItems(QStandardItemModel * itemModel, const RawSourceType & type, int count, const GeneralSettings * const generalSettings, - const ModelData * const model, const int start, const QList exclude) + const ModelData * const model, const int start) { for (int i = start; i < start + count; i++) { - if (exclude.contains(i)) - continue; - RawSource src = RawSource(type, i); - if (model) { - if (type == SOURCE_TYPE_VIRTUAL_INPUT && !model->isInputValid(i)) - continue; - } - if (generalSettings) { - int genAryIdx = 0; - if (type == SOURCE_TYPE_STICK && ((src.isPot(&genAryIdx) && !generalSettings->isPotAvailable(genAryIdx)) || (src.isSlider(&genAryIdx) && !generalSettings->isSliderAvailable(genAryIdx)))) - continue; - } + if (!src.isAvailable(model, generalSettings, getCurrentBoard())) + continue; QStandardItem * modelItem = new QStandardItem(src.toString(model, generalSettings)); modelItem->setData(src.toValue(), Qt::UserRole); itemModel->appendRow(modelItem); - - if (type == SOURCE_TYPE_SWITCH && generalSettings && IS_HORUS_OR_TARANIS(getCurrentBoard()) && !generalSettings->switchSourceAllowedTaranis(i)) { - modelItem->setData(0, Qt::UserRole - 1); // trick to disable an item - } } } @@ -478,31 +464,16 @@ QStandardItemModel * Helpers::getRawSourceItemModel(const GeneralSettings * cons } if (flags & POPULATE_TELEMETRY) { + int count = 0; if (IS_ARM(board.getBoardType())) { addRawSourceItems(itemModel, SOURCE_TYPE_SPECIAL, 5, generalSettings, model); - - if (model) { - QList exclude; - for (int i=0; i < CPN_MAX_SENSORS * 3; ++i) { - //this conditon must be false if we populate Global Functions where model = 0 - if (!model->sensorData[div(i, 3).quot].isAvailable()) - exclude << i; - } - if (exclude.size() < CPN_MAX_SENSORS * 3) - addRawSourceItems(itemModel, SOURCE_TYPE_TELEMETRY, CPN_MAX_SENSORS * 3, generalSettings, model, 0, exclude); - } + count = CPN_MAX_SENSORS * 3; } else { - QList exclude; - if (!fw->getCapability(RtcTime)) - exclude << TELEMETRY_SOURCE_TX_TIME; - if (!fw->getCapability(SportTelemetry)) - exclude << TELEMETRY_SOURCE_SWR; - if (!IS_ARM(board.getBoardType())) - exclude << TELEMETRY_SOURCE_TIMER3; - int count = ((flags & POPULATE_TELEMETRYEXT) ? TELEMETRY_SOURCES_STATUS_COUNT : TELEMETRY_SOURCES_COUNT); - addRawSourceItems(itemModel, SOURCE_TYPE_TELEMETRY, count, generalSettings, model, 0, exclude); + count = ((flags & POPULATE_TELEMETRYEXT) ? TELEMETRY_SOURCES_STATUS_COUNT : TELEMETRY_SOURCES_COUNT); } + if (model && count) + addRawSourceItems(itemModel, SOURCE_TYPE_TELEMETRY, count, generalSettings, model); } if (flags & POPULATE_GVARS) { diff --git a/companion/src/helpers.h b/companion/src/helpers.h index 18554287c..cfdd9fe84 100644 --- a/companion/src/helpers.h +++ b/companion/src/helpers.h @@ -130,7 +130,7 @@ class CurveGroup : public QObject { namespace Helpers { void addRawSourceItems(QStandardItemModel * itemModel, const RawSourceType & type, int count, const GeneralSettings * const generalSettings = NULL, - const ModelData * const model = NULL, const int start = 0, const QList exclude = QList()); + const ModelData * const model = NULL, const int start = 0); QStandardItemModel * getRawSourceItemModel(const GeneralSettings * const generalSettings = NULL, const ModelData * const model = NULL, unsigned int flags = 0); void populateGvarUseCB(QComboBox *b, unsigned int phase); diff --git a/companion/src/mainwindow.cpp b/companion/src/mainwindow.cpp index 4a6064d83..3e02af58d 100644 --- a/companion/src/mainwindow.cpp +++ b/companion/src/mainwindow.cpp @@ -163,6 +163,9 @@ MainWindow::MainWindow(): child->close(); } } + else { + child->closeFile(true); + } } } if (printing) { @@ -685,6 +688,9 @@ void MainWindow::openFile(const QString & fileName, bool updateLastUsedDir) statusBar()->showMessage(tr("File loaded"), 2000); child->show(); } + else { + child->closeFile(true); + } } } @@ -708,6 +714,15 @@ void MainWindow::saveAs() } } +void MainWindow::saveAll() +{ + foreach (QMdiSubWindow * window, mdiArea->subWindowList()) { + MdiChild * child; + if ((child = qobject_cast(window->widget())) && child->isWindowModified()) + child->save(); + } +} + void MainWindow::closeFile() { if (mdiArea->activeSubWindow()) @@ -723,16 +738,28 @@ void MainWindow::openRecentFile() } } -void MainWindow::loadProfileId(const unsigned pid) // TODO Load all variables - Also HW! +bool MainWindow::loadProfileId(const unsigned pid) // TODO Load all variables - Also HW! { if (pid >= MAX_PROFILES) - return; + return false; + + Firmware * newFw = Firmware::getFirmwareForId(g.profile[pid].fwType()); + // warn if we're switching between incompatible board types and any files have been modified + if (!Boards::isBoardCompatible(Firmware::getCurrentVariant()->getBoard(), newFw->getBoard()) && anyChildrenDirty()) { + if (QMessageBox::question(this, tr("Companion"), + tr("There are unsaved file changes which you may lose when switching radio types.\n\nDo you wish to continue?"), + (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) != QMessageBox::Yes) { + updateProfilesActions(); + return false; + } + } // Set the new profile number g.id(pid); - Firmware::setCurrentVariant(Firmware::getFirmwareForId(g.profile[pid].fwType())); + Firmware::setCurrentVariant(newFw); emit firmwareChanged(); updateMenus(); + return true; } void MainWindow::loadProfile() @@ -749,6 +776,8 @@ void MainWindow::loadProfile() void MainWindow::appPrefs() { AppPreferencesDialog * dialog = new AppPreferencesDialog(this); + dialog->setMainWinHasDirtyChild(anyChildrenDirty()); + connect(dialog, &AppPreferencesDialog::firmwareProfileAboutToChange, this, &MainWindow::saveAll); connect(dialog, &AppPreferencesDialog::firmwareProfileChanged, this, &MainWindow::loadProfileId); dialog->exec(); dialog->deleteLater(); @@ -1275,6 +1304,7 @@ MdiChild * MainWindow::createMdiChild() connect(child, &MdiChild::windowTitleChanged, this, &MainWindow::onSubwindowTitleChanged); connect(child, &MdiChild::modified, this, &MainWindow::onSubwindowModified); connect(child, &MdiChild::newStatusMessage, statusBar(), &QStatusBar::showMessage); + connect(child, &MdiChild::destroyed, win, &QMdiSubWindow::close); connect(win, &QMdiSubWindow::destroyed, this, &MainWindow::updateWindowActions); updateWindowActions(); @@ -1648,6 +1678,16 @@ QMdiSubWindow *MainWindow::findMdiChild(const QString &fileName) return 0; } +bool MainWindow::anyChildrenDirty() +{ + foreach (QMdiSubWindow * window, mdiArea->subWindowList()) { + MdiChild * child; + if ((child = qobject_cast(window->widget())) && child->isWindowModified()) + return true; + } + return false; +} + void MainWindow::updateRecentFileActions() { // Hide all document slots @@ -1769,9 +1809,8 @@ int MainWindow::newProfile(bool loadProfile) g.profile[i].name(tr("New Radio")); if (loadProfile) { - g.id(i); - loadProfileId(i); - appPrefs(); + if (loadProfileId(i)) + appPrefs(); } return i; @@ -1788,17 +1827,20 @@ void MainWindow::copyProfile() if (newId > -1) { g.profile[newId] = g.profile[g.id()]; - g.id(newId); g.profile[newId].name(g.profile[newId].name() + tr(" - Copy")); - loadProfileId(newId); - appPrefs(); + if (loadProfileId(newId)) + appPrefs(); } } void MainWindow::deleteProfile(const int pid) { + if (pid == g.id() && anyChildrenDirty()) { + QMessageBox::warning(this, tr("Companion :: Open files warning"), tr("Please save or close modified file(s) before deleting the active profile.")); + return; + } if (pid == 0) { - QMessageBox::information(this, tr("Not possible to remove profile"), tr("The default profile can not be removed.")); + QMessageBox::warning(this, tr("Not possible to remove profile"), tr("The default profile can not be removed.")); return; } int ret = QMessageBox::question(this, diff --git a/companion/src/mainwindow.h b/companion/src/mainwindow.h index 860c21a12..dad7f5a91 100644 --- a/companion/src/mainwindow.h +++ b/companion/src/mainwindow.h @@ -100,9 +100,10 @@ class MainWindow : public QMainWindow void openFile(); void save(); void saveAs(); + void saveAll(); void closeFile(); void openRecentFile(); - void loadProfileId(const unsigned pid); + bool loadProfileId(const unsigned pid); void loadProfile(); void logFile(); void writeEeprom(); @@ -155,6 +156,7 @@ class MainWindow : public QMainWindow MdiChild * createMdiChild(); MdiChild * activeMdiChild(); QMdiSubWindow * findMdiChild(const QString & fileName); + bool anyChildrenDirty(); bool readEepromFromRadio(const QString & filename); bool readFirmwareFromRadio(const QString & filename); diff --git a/companion/src/mdichild.cpp b/companion/src/mdichild.cpp index e377e0b8a..563c2130e 100644 --- a/companion/src/mdichild.cpp +++ b/companion/src/mdichild.cpp @@ -31,8 +31,10 @@ #include "flashfirmwaredialog.h" #include "storage.h" #include "radiointerface.h" +#include "radiodataconversionstate.h" #include +#include MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): QWidget(parent, f), @@ -46,6 +48,7 @@ MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): lastSelectedModel(-1), isUntitled(true), showCatToolbar(true), + forceCloseFlag(false), stateDataVersion(1) { ui->setupUi(this); @@ -113,7 +116,7 @@ MdiChild::~MdiChild() void MdiChild::closeEvent(QCloseEvent *event) { - if (!maybeSave()) { + if (!maybeSave() && !forceCloseFlag) { event->ignore(); return; } @@ -702,8 +705,11 @@ void MdiChild::onFirmwareChanged() Firmware * previous = firmware; firmware = getCurrentFirmware(); //qDebug() << "onFirmwareChanged" << previous->getName() << "=>" << firmware->getName(); - if (StorageFormat::getFourCC(previous->getBoard()) != StorageFormat::getFourCC(firmware->getBoard())) { - convertStorage(previous->getBoard(), firmware->getBoard()); + if (!Boards::isBoardCompatible(previous->getBoard(), firmware->getBoard())) { + if (!convertStorage(previous->getBoard(), firmware->getBoard())) { + closeFile(true); + return; + } setModified(); } } @@ -1327,8 +1333,9 @@ bool MdiChild::loadFile(const QString & filename, bool resetCurrentFile) setCurrentFile(filename); } - if (!storage.isBoardCompatible(getCurrentBoard())) { - convertStorage(storage.getBoard(), getCurrentBoard()); + if (!Boards::isBoardCompatible(storage.getBoard(), getCurrentBoard())) { + if (!convertStorage(storage.getBoard(), getCurrentBoard(), true)) + return false; setModified(); } else { @@ -1362,10 +1369,8 @@ bool MdiChild::saveAs(bool isNew) if (fileName.isEmpty()) return false; g.eepromDir( QFileInfo(fileName).dir().absolutePath() ); - if (isNew) - return saveFile(fileName); - else - return saveFile(fileName, true); + + return saveFile(fileName, true); } bool MdiChild::saveFile(const QString & filename, bool setCurrent) @@ -1384,15 +1389,27 @@ bool MdiChild::saveFile(const QString & filename, bool setCurrent) return true; } +void MdiChild::closeFile(bool force) +{ + forceCloseFlag = force; + if (parentWindow) + parentWindow->close(); + else + this->close(); +} + bool MdiChild::maybeSave() { if (isWindowModified()) { 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); + (QMessageBox::Save | QMessageBox::Discard | (forceCloseFlag ? QMessageBox::NoButton : QMessageBox::Cancel)), + (forceCloseFlag ? QMessageBox::Save : QMessageBox::Cancel)); if (ret == QMessageBox::Save) return save(); - else if (ret == QMessageBox::Cancel) + else if (ret == QMessageBox::Discard) + return true; + else return false; } return true; @@ -1428,13 +1445,57 @@ void MdiChild::forceNewFilename(const QString & suffix, const QString & ext) curFile.replace(QRegExp("\\.(eepe|bin|hex|otx)$"), suffix + "." + ext); } -void MdiChild::convertStorage(Board::Type from, Board::Type to) +bool MdiChild::convertStorage(Board::Type from, Board::Type to, bool newFile) { - 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); + QMessageBox::StandardButtons btns; + QMessageBox::StandardButton dfltBtn; + QString q = tr("

Current radio type is not compatible with file %1, models and settings need to be converted.

").arg(userFriendlyCurrentFile()); + if (newFile) { + q.append(tr("Do you wish to continue with the conversion?")); + btns = (QMessageBox::Yes | QMessageBox::No); + dfltBtn = QMessageBox::Yes; + } + else{ + q.append(tr("Choose Apply to convert the file, or Close to close it without conversion.")); + btns = (QMessageBox::Apply | QMessageBox::Close); + dfltBtn = QMessageBox::Apply; + } + if (askQuestion(q, btns, dfltBtn) != dfltBtn) + return false; + + RadioDataConversionState cstate(from, to, &radioData); + if (!cstate.convert()) + return false; forceNewFilename("_converted"); initModelsList(); isUntitled = true; + + if (cstate.hasLogEntries(RadioDataConversionState::EVT_INF)) { + QDialog * msgBox = new QDialog(Q_NULLPTR, Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + + ExportableTableView * tv = new ExportableTableView(msgBox); + tv->setSortingEnabled(true); + tv->verticalHeader()->hide(); + tv->setModel(cstate.getLogModel(RadioDataConversionState::EVT_INF, tv)); + tv->resizeColumnsToContents(); + tv->resizeRowsToContents(); + + QDialogButtonBox * btnBox = new QDialogButtonBox(QDialogButtonBox::Ok, this); + + QVBoxLayout * lo = new QVBoxLayout(msgBox); + lo->addWidget(new QLabel(tr("The conversion generated some important messages, please review them below."))); + lo->addWidget(tv); + lo->addWidget(btnBox); + + connect(btnBox, &QDialogButtonBox::accepted, msgBox, &QDialog::accept); + connect(btnBox, &QDialogButtonBox::rejected, msgBox, &QDialog::reject); + + msgBox->setWindowTitle(tr("Companion :: Conversion Result for %1").arg(curFile)); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->show(); // modeless + } + + return true; } void MdiChild::showWarning(const QString & msg) @@ -1443,9 +1504,9 @@ void MdiChild::showWarning(const QString & msg) QMessageBox::warning(this, "Companion", msg); } -int MdiChild::askQuestion(const QString & msg, int button0, int button1, int button2) +int MdiChild::askQuestion(const QString & msg, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - return QMessageBox::question(this, tr("Companion"), msg, button0, button1, button2); + return QMessageBox::question(this, tr("Companion"), msg, buttons, defaultButton); } void MdiChild::writeEeprom() // write to Tx diff --git a/companion/src/mdichild.h b/companion/src/mdichild.h index 3c3cc333e..079205cc1 100644 --- a/companion/src/mdichild.h +++ b/companion/src/mdichild.h @@ -86,6 +86,7 @@ class MdiChild : public QWidget bool save(); bool saveAs(bool isNew=false); bool saveFile(const QString & fileName, bool setCurrent=true); + void closeFile(bool force = false); void writeEeprom(); void print(int model=-1, const QString & filename=""); void onFirmwareChanged(); @@ -179,9 +180,9 @@ class MdiChild : public QWidget bool maybeSave(); void setCurrentFile(const QString & fileName); void forceNewFilename(const QString & suffix = "", const QString & ext = "otx"); - void convertStorage(Board::Type from, Board::Type to); + bool convertStorage(Board::Type from, Board::Type to, bool newFile = false); void showWarning(const QString & msg); - int askQuestion(const QString & msg, int button0 = QMessageBox::Yes, int button1 = QMessageBox::No | QMessageBox::Default, int button2 = 0); + int askQuestion(const QString & msg, QMessageBox::StandardButtons buttons = (QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::No); Ui::MdiChild * ui; TreeModel * modelsListModel; @@ -200,6 +201,7 @@ class MdiChild : public QWidget int lastSelectedModel; bool isUntitled; bool showCatToolbar; + bool forceCloseFlag; const quint16 stateDataVersion; }; diff --git a/companion/src/modeledit/switchitemmodel.cpp b/companion/src/modeledit/switchitemmodel.cpp index 9d993e469..3220ea0c7 100644 --- a/companion/src/modeledit/switchitemmodel.cpp +++ b/companion/src/modeledit/switchitemmodel.cpp @@ -38,14 +38,14 @@ RawSwitchItemModel::RawSwitchItemModel(const GeneralSettings * const generalSett add(SWITCH_TYPE_VIRTUAL, -fw->getCapability(LogicalSwitches)); add(SWITCH_TYPE_ROTARY_ENCODER, -fw->getCapability(RotaryEncoders)); add(SWITCH_TYPE_TRIM, -board.getCapability(Board::NumTrimSwitches)); - add(SWITCH_TYPE_MULTIPOS_POT, -(fw->getCapability(MultiposPots) * fw->getCapability(MultiposPotsPositions))); + add(SWITCH_TYPE_MULTIPOS_POT, -(board.getCapability(Board::MultiposPots) * board.getCapability(Board::MultiposPotsPositions))); add(SWITCH_TYPE_SWITCH, -board.getCapability(Board::SwitchPositions)); // Ascending switch direction (including zero) add(SWITCH_TYPE_TIMER_MODE, 5); add(SWITCH_TYPE_NONE, 1); add(SWITCH_TYPE_SWITCH, board.getCapability(Board::SwitchPositions)); - add(SWITCH_TYPE_MULTIPOS_POT, fw->getCapability(MultiposPots) * fw->getCapability(MultiposPotsPositions)); + add(SWITCH_TYPE_MULTIPOS_POT, board.getCapability(Board::MultiposPots) * board.getCapability(Board::MultiposPotsPositions)); add(SWITCH_TYPE_TRIM, board.getCapability(Board::NumTrimSwitches)); add(SWITCH_TYPE_ROTARY_ENCODER, fw->getCapability(RotaryEncoders)); add(SWITCH_TYPE_VIRTUAL, fw->getCapability(LogicalSwitches)); @@ -73,15 +73,6 @@ void RawSwitchItemModel::add(const RawSwitchType & type, int count) rawIdxAdj = -1; for ( ; i < maxCount; ++i) { - if (generalSettings) { - if (type == SWITCH_TYPE_SWITCH && IS_HORUS_OR_TARANIS(board) && !generalSettings->switchPositionAllowedTaranis(abs(i))) - continue; - if (type == SWITCH_TYPE_MULTIPOS_POT) { - int pot = div(abs(i) - 1, getCurrentFirmware()->getCapability(MultiposPotsPositions)).quot; - if (!generalSettings->isPotAvailable(pot) || generalSettings->potConfig[pot] != Board::POT_MULTIPOS_SWITCH) - continue; - } - } RawSwitch rs(type, i + rawIdxAdj); QStandardItem * modelItem = new QStandardItem(rs.toString(board, generalSettings, modelData)); modelItem->setData(rs.toValue(), Qt::UserRole); @@ -131,7 +122,7 @@ bool RawSwitchFilterItemModel::filterAcceptsRow(int sourceRow, const QModelIndex if ((swtch.type == SWITCH_TYPE_ON || swtch.type == SWITCH_TYPE_ONE) && (context != SpecialFunctionsContext && context != GlobalFunctionsContext)) return false; - if (!modelData->isAvailable(swtch)) + if (!swtch.isAvailable(modelData, generalSettings, getCurrentBoard())) return false; return true; diff --git a/companion/src/modelprinter.cpp b/companion/src/modelprinter.cpp index 8917738a3..d72026f04 100644 --- a/companion/src/modelprinter.cpp +++ b/companion/src/modelprinter.cpp @@ -490,13 +490,7 @@ QString ModelPrinter::printFlightModeSwitch(const RawSwitch & swtch) QString ModelPrinter::printFlightModeName(int index) { - const FlightModeData & fm = model.flightModeData[index]; - if (strlen(fm.name) > 0) { - return QString("%1").arg(fm.name); - } - else { - return tr("FM%1").arg(index); - } + return model.flightModeData[index].toString(index); } QString ModelPrinter::printFlightModes(unsigned int flightModes) diff --git a/companion/src/radiodata.cpp b/companion/src/radiodata.cpp index f624b137f..94e45c3a1 100644 --- a/companion/src/radiodata.cpp +++ b/companion/src/radiodata.cpp @@ -19,43 +19,70 @@ */ #include "radiodata.h" +#include "radiodataconversionstate.h" #include "eeprominterface.h" // TODO here we will move a lot of functions from eeprominterface.cpp when no merge risk -RawSource RawSource::convert(Board::Type before, Board::Type after) +RawSource RawSource::convert(RadioDataConversionState & cstate) { - if (type == SOURCE_TYPE_STICK && index >= 4 + getBoardCapability(before, Board::Pots)) { - // 1st slider alignment - index += getBoardCapability(after, Board::Pots) - getBoardCapability(before, Board::Pots); - } + cstate.setItemType("SRC", 1); + RadioDataConversionState::EventType evt = RadioDataConversionState::EVT_NONE; + RadioDataConversionState::LogField oldData(index, toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType)); - if (IS_HORUS(after)) { - if (IS_TARANIS_X9D(before)) { - if (type == SOURCE_TYPE_STICK && index >= 7) { - // LS and RS on Horus are after sliders L1 and L2 - index += 2; + if (type == SOURCE_TYPE_STICK) { + if (cstate.toBoard.getCapability(Board::Sliders)) { + if (index >= cstate.fromBoard.getCapability(Board::Sticks) + cstate.fromBoard.getCapability(Board::Pots)) { + // 1st slider alignment + index += cstate.toBoard.getCapability(Board::Pots) - cstate.fromBoard.getCapability(Board::Pots); + } + + if (isSlider(0, cstate.fromType)) { + // LS and RS sliders are after 2 aux sliders on X12 and X9E + if ((IS_HORUS_X12S(cstate.toType) || IS_TARANIS_X9E(cstate.toType)) && !IS_HORUS_X12S(cstate.fromType) && !IS_TARANIS_X9E(cstate.fromType)) { + if (index >= 7) { + index += 2; // LS/RS to LS/RS + } + } + else if (!IS_TARANIS_X9E(cstate.toType) && !IS_HORUS_X12S(cstate.toType) && (IS_HORUS_X12S(cstate.fromType) || IS_TARANIS_X9E(cstate.fromType))) { + if (index >= 7 && index <= 8) { + index += 2; // aux sliders to spare analogs (which may not exist, this is validated later) + evt = RadioDataConversionState::EVT_CVRT; + } + else if (index >= 9 && index <= 10) { + index -= 2; // LS/RS to LS/RS + } + } } } - } - // SWI to SWR don't exist on !X9E board - if (!IS_TARANIS_X9E(after) && IS_TARANIS_X9E(before)) { - if (type == SOURCE_TYPE_SWITCH && index >= 8) { - index = index % 8; + if (IS_TARANIS(cstate.toType) && IS_HORUS(cstate.fromType)) { + if (index == 6) + index = 5; // pot S2 to S2 + else if (index == 5) + index = -1; // 6P on Horus doesn't exist on Taranis } - } - - if (IS_TARANIS_X7(after)) { - // No S3, LS and RS on X7 board - if (type == SOURCE_TYPE_STICK && index >= 6) { - index = 5; + else if (IS_HORUS(cstate.toType) && IS_TARANIS(cstate.fromType) && index == 5) + { + index = 6; // pot S2 to S2 } - // No SE and SG on X7 board - if ((IS_TARANIS_X9(before) || IS_HORUS(before)) && type == SOURCE_TYPE_SWITCH) { + } // SOURCE_TYPE_STICK + + if (type == SOURCE_TYPE_SWITCH) { + // SWI to SWR don't exist on !X9E board + if (!IS_TARANIS_X9E(cstate.toType) && IS_TARANIS_X9E(cstate.fromType)) { + if (index >= 8) { + index = index % 8; + evt = RadioDataConversionState::EVT_CVRT; + } + } + + if (IS_TARANIS_X7(cstate.toType) && (IS_TARANIS_X9(cstate.fromType) || IS_HORUS(cstate.fromType))) { + // No SE and SG on X7 board if (index == 4 || index == 6) { index = 3; // SG and SE to SD + evt = RadioDataConversionState::EVT_CVRT; } else if (index == 5) { index = 4; // SF to SF @@ -64,170 +91,269 @@ RawSource RawSource::convert(Board::Type before, Board::Type after) index = 5; // SH to SH } } - } + // Compensate for SE and SG on X9/Horus board if converting from X7 + else if ((IS_TARANIS_X9(cstate.toType) || IS_HORUS(cstate.toType)) && IS_TARANIS_X7(cstate.fromType)) { + if (index == 4) { + index = 5; // SF to SF + } + else if (index == 5) { + index = 7; // SH to SH + } + } + } // SOURCE_TYPE_SWITCH - // Compensate for SE and SG on X9/Horus board if converting from X7 - if ((IS_TARANIS_X9(after) || IS_HORUS(after)) && IS_TARANIS_X7(before) && type == SOURCE_TYPE_SWITCH) { - if (index == 4) { - index = 5; // SF to SF - } - else if (index == 5) { - index = 7; // SH to SH - } + // final validation (we do not pass model to isAvailable() because we don't know what has or hasn't been converted) + if (!isAvailable(NULL, cstate.toGS(), cstate.toType)) { + cstate.setInvalid(oldData); + index = -1; // TODO: better way to flag invalid sources? + type = MAX_SOURCE_TYPE; + } + else if (evt == RadioDataConversionState::EVT_CVRT) { + cstate.setConverted(oldData, RadioDataConversionState::LogField(index, toString(cstate.toModel(), cstate.toGS(), cstate.toType))); + } + else if (oldData.id != index) { + // provide info by default if anything changed + cstate.setMoved(oldData, RadioDataConversionState::LogField(index, toString(cstate.toModel(), cstate.toGS(), cstate.toType))); } return *this; } -RawSwitch RawSwitch::convert(Board::Type before, Board::Type after) +RawSwitch RawSwitch::convert(RadioDataConversionState & cstate) { - if (!index || type != SWITCH_TYPE_SWITCH) { - return *this; // no changes + if (!index) + return *this; + + cstate.setItemType("SW", 2); + RadioDataConversionState::EventType evt = RadioDataConversionState::EVT_NONE; + RadioDataConversionState::LogField oldData(index, toString(cstate.fromType, cstate.fromGS(), cstate.fromModel())); + + if (type == SWITCH_TYPE_SWITCH) { + int srcIdx = div(abs(index)-1, 3).quot; // raw source index + int delta = 0; + + // SWI to SWR don't exist on !X9E board + if (!IS_TARANIS_X9E(cstate.toType) && IS_TARANIS_X9E(cstate.fromType)) { + if (srcIdx > 7) { + index %= 24; + evt = RadioDataConversionState::EVT_CVRT; + } + } + + // No SE and SG on X7 board + if (IS_TARANIS_X7(cstate.toType) && (IS_TARANIS_X9(cstate.fromType) || IS_HORUS(cstate.fromType))) { + if (srcIdx == 4 || srcIdx == 5) { + delta = 3; // SE to SD & SF to SF + if (srcIdx == 4) + evt = RadioDataConversionState::EVT_CVRT; + } + else if (srcIdx == 6) { + delta = 9; // SG to SD + evt = RadioDataConversionState::EVT_CVRT; + } + else if (srcIdx == 7) { + delta = 6; // SH to SH + } + } + // Compensate for SE and SG on X9/Horus board if converting from X7 + else if ((IS_TARANIS_X9(cstate.toType) || IS_HORUS(cstate.toType)) && IS_TARANIS_X7(cstate.fromType)) { + if (srcIdx == 4) { + delta = -3; // SF to SF + } + else if (srcIdx == 5) { + delta = -6; // SH to SH + } + } + + if (index < 0) { + delta = -delta; // invert for !switch + } + + index -= delta; + + } // SWITCH_TYPE_SWITCH + + // final validation (we do not pass model to isAvailable() because we don't know what has or hasn't been converted) + if (!isAvailable(NULL, cstate.toGS(), cstate.toType)) { + cstate.setInvalid(oldData); + type = MAX_SWITCH_TYPE; // TODO: better way to flag invalid switches? } - - // SWI to SWR don't exist on !X9E board - if (!IS_TARANIS_X9E(after) && IS_TARANIS_X9E(before)) { - if (abs(index) > 24) { - index = index % 24; - } + else if (evt == RadioDataConversionState::EVT_CVRT) { + cstate.setConverted(oldData, RadioDataConversionState::LogField(index, toString(cstate.toType, cstate.toGS(), cstate.toModel()))); } - - int srcIdx = div(abs(index)-1, 3).quot; // raw source index - int delta = 0; - - // No SE and SG on X7 board - if (IS_TARANIS_X7(after) && (IS_TARANIS_X9(before) || IS_HORUS(before))) { - if (srcIdx == 4 || srcIdx == 5) { - delta = 3; // SE to SD & SF to SF - } - else if (srcIdx == 6) { - delta = 9; // SG to SD - } - else if (srcIdx == 7) { - delta = 6; // SH to SH - } + else if (oldData.id != index) { + // provide info by default if anything changed + cstate.setMoved(oldData, RadioDataConversionState::LogField(index, toString(cstate.toType, cstate.toGS(), cstate.toModel()))); } - // Compensate for SE and SG on X9/Horus board if converting from X7 - if ((IS_TARANIS_X9(after) || IS_HORUS(after)) && IS_TARANIS_X7(before)) { - if (srcIdx == 4) { - delta = -3; // SF to SF - } - else if (srcIdx == 5) { - delta = -6; // SH to SH - } - } - - if (index < 0) { - delta = -delta; // invert for !switch - } - - index -= delta; - return *this; } -void ExpoData::convert(Board::Type before, Board::Type after) +void ExpoData::convert(RadioDataConversionState & cstate) { - srcRaw.convert(before, after); - swtch.convert(before, after); + cstate.setComponent(QObject::tr("INP"), 3); + cstate.setSubComp(RawSource(SOURCE_TYPE_VIRTUAL_INPUT, chn).toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType) % QObject::tr(" (@%1)").arg(cstate.subCompIdx)); + srcRaw.convert(cstate); + swtch.convert(cstate); } -void MixData::convert(Board::Type before, Board::Type after) +void MixData::convert(RadioDataConversionState & cstate) { - srcRaw.convert(before, after); - swtch.convert(before, after); + cstate.setComponent(QObject::tr("MIX"), 4); + cstate.setSubComp(RawSource(SOURCE_TYPE_CH, destCh-1).toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType) % QObject::tr(" (@%1)").arg(cstate.subCompIdx)); + srcRaw.convert(cstate); + swtch.convert(cstate); } -void LogicalSwitchData::convert(Board::Type before, Board::Type after) +void LogicalSwitchData::convert(RadioDataConversionState & cstate) { + cstate.setComponent("LSW", 7); + cstate.setSubComp(RawSwitch(SWITCH_TYPE_VIRTUAL, cstate.subCompIdx + 1).toString(cstate.fromType, cstate.fromGS(), cstate.fromModel())); CSFunctionFamily family = getFunctionFamily(); switch(family) { case LS_FAMILY_VOFS: - val1 = RawSource(val1).convert(before, after).toValue(); + val1 = RawSource(val1).convert(cstate.withComponentField("V1")).toValue(); break; case LS_FAMILY_STICKY: case LS_FAMILY_VBOOL: - val1 = RawSwitch(val1).convert(before, after).toValue(); - val2 = RawSwitch(val2).convert(before, after).toValue(); + val1 = RawSwitch(val1).convert(cstate.withComponentField("V1")).toValue(); + val2 = RawSwitch(val2).convert(cstate.withComponentField("V2")).toValue(); break; case LS_FAMILY_EDGE: - val1 = RawSwitch(val1).convert(before, after).toValue(); + val1 = RawSwitch(val1).convert(cstate.withComponentField("V1")).toValue(); break; case LS_FAMILY_VCOMP: - val1 = RawSource(val1).convert(before, after).toValue(); - val2 = RawSource(val2).convert(before, after).toValue(); + val1 = RawSource(val1).convert(cstate.withComponentField("V1")).toValue(); + val2 = RawSource(val2).convert(cstate.withComponentField("V2")).toValue(); break; default: break; } - andsw = RawSwitch(andsw).convert(before, after).toValue(); + andsw = RawSwitch(andsw).convert(cstate.withComponentField("AND")).toValue(); } -void CustomFunctionData::convert(Board::Type before, Board::Type after) +void CustomFunctionData::convert(RadioDataConversionState & cstate) { - swtch.convert(before, after); - if (func == FuncVolume || func == FuncPlayValue || (func >= FuncAdjustGV1 && func <= FuncAdjustGVLast && adjustMode == 1)) - param = RawSource(param).convert(before, after).toValue(); + cstate.setComponent("CFN", 8); + cstate.setSubComp(toString(cstate.subCompIdx, (cstate.toModel() ? false : true))); + swtch.convert(cstate); + if (func == FuncVolume || func == FuncPlayValue || (func >= FuncAdjustGV1 && func <= FuncAdjustGVLast && adjustMode == 1)) { + param = RawSource(param).convert(cstate.withComponentField("PARAM")).toValue(); + } } -void FlightModeData::convert(Board::Type before, Board::Type after) +void FlightModeData::convert(RadioDataConversionState & cstate) { - swtch.convert(before, after); + cstate.setComponent("FMD", 2); + cstate.setSubComp(toString(cstate.subCompIdx)); + swtch.convert(cstate); } -void ModelData::convert(Board::Type before, Board::Type after) +void TimerData::convert(RadioDataConversionState & cstate) +{ + cstate.setComponent("TMR", 1); + cstate.setSubComp(QObject::tr("Timer %1").arg(cstate.subCompIdx + 1)); + mode.convert(cstate); +} + +void ModelData::convert(RadioDataConversionState & cstate) { // Here we can add explicit conversions when moving from one board to another + + QString origin = QString(name); + if (origin.isEmpty()) + origin = QString::number(cstate.modelIdx+1); + cstate.setOrigin(QObject::tr("Model: ") % origin); + + cstate.setComponent("SET", 0); + if (thrTraceSrc && (int)thrTraceSrc < cstate.fromBoard.getCapability(Board::Pots) + cstate.fromBoard.getCapability(Board::Sliders)) { + cstate.setSubComp(QObject::tr("Throttle Source")); + thrTraceSrc = RawSource(SOURCE_TYPE_STICK, (int)thrTraceSrc + 3).convert(cstate).index - 3; + } + + for (int i=0; igetCapability(MinContrast); - unsigned int max_contrast = getCurrentFirmware()->getCapability(MaxContrast); - - if (contrast < min_contrast) - contrast = min_contrast; - else if (contrast > max_contrast) - contrast = max_contrast; + // LS and RS sliders are after 2 aux sliders on X12 and X9E + if ((IS_HORUS_X12S(cstate.toType) || IS_TARANIS_X9E(cstate.toType)) && !IS_HORUS_X12S(cstate.fromType) && !IS_TARANIS_X9E(cstate.fromType)) { + strncpy(sliderName[0], sliderName[2], sizeof(sliderName[0])); + strncpy(sliderName[1], sliderName[3], sizeof(sliderName[0])); } + else if (!IS_TARANIS_X9E(cstate.toType) && !IS_HORUS_X12S(cstate.toType) && (IS_HORUS_X12S(cstate.fromType) || IS_TARANIS_X9E(cstate.fromType))) { + strncpy(sliderName[2], sliderName[0], sizeof(sliderName[0])); + strncpy(sliderName[3], sliderName[1], sizeof(sliderName[0])); + } + + if (IS_HORUS(cstate.toType)) { + // 6P switch is only on Horus (by default) + if (cstate.fromBoard.getCapability(Board::FactoryInstalledPots) == 2) { + strncpy(potName[2], potName[1], sizeof(potName[0])); + potName[1][0] = '\0'; + } + } + + if (IS_TARANIS(cstate.toType)) { + // No S3 pot on Taranis boards by default + if (cstate.fromBoard.getCapability(Board::FactoryInstalledPots) > 2) + strncpy(potName[1], potName[2], sizeof(potName[0])); + + contrast = qBound(getCurrentFirmware()->getCapability(MinContrast), contrast, getCurrentFirmware()->getCapability(MaxContrast)); + } + + // TODO: Would be nice at this point to have GUI pause and ask the user to set up any custom hardware they have on the destination radio. + + // Convert all global functions (do this after HW adjustments) + for (int i=0; i class Firmware; +class ModelData; +class GeneralSettings; +class RadioDataConversionState; enum Switches { SWITCH_NONE, @@ -80,9 +83,6 @@ enum HeliSwashTypes { HELI_SWASH_TYPE_90 }; -class ModelData; -class GeneralSettings; - enum TelemetrySource { TELEMETRY_SOURCE_TX_BATT, TELEMETRY_SOURCE_TX_TIME, @@ -211,16 +211,19 @@ class RawSource { { } - RawSource convert(Board::Type before, Board::Type after); - inline const int toValue() const { return index >= 0 ? (type * 65536 + index) : -(type * 65536 - index); } - QString toString(const ModelData * model = NULL, const GeneralSettings * const generalSettings = NULL) const; - + RawSource convert(RadioDataConversionState & cstate); + QString toString(const ModelData * model = NULL, const GeneralSettings * const generalSettings = NULL, Board::Type board = Board::BOARD_UNKNOWN) const; RawSourceRange getRange(const ModelData * model, const GeneralSettings & settings, unsigned int flags=0) const; + bool isStick(int * potsIndex = NULL, Board::Type board = Board::BOARD_UNKNOWN) const; + bool isPot(int * potsIndex = NULL, Board::Type board = Board::BOARD_UNKNOWN) const; + bool isSlider(int * sliderIndex = NULL, Board::Type board = Board::BOARD_UNKNOWN) const; + bool isTimeBased(Board::Type board = Board::BOARD_UNKNOWN) const; + bool isAvailable(const ModelData * const model = NULL, const GeneralSettings * const gs = NULL, Board::Type board = Board::BOARD_UNKNOWN); bool operator == ( const RawSource & other) { return (this->type == other.type) && (this->index == other.index); @@ -230,11 +233,6 @@ class RawSource { return (this->type != other.type) || (this->index != other.index); } - bool isTimeBased() const; - bool isStick(int * potsIndex = NULL) const; - bool isPot(int * potsIndex = NULL) const; - bool isSlider(int * sliderIndex = NULL) const; - RawSourceType type; int index; }; @@ -253,6 +251,7 @@ enum RawSwitchType { SWITCH_TYPE_TIMER_MODE, SWITCH_TYPE_TELEMETRY, SWITCH_TYPE_SENSOR, + MAX_SWITCH_TYPE }; class RawSwitch { @@ -280,7 +279,9 @@ class RawSwitch { return index >= 0 ? (type * 256 + index) : -(type * 256 - index); } + RawSwitch convert(RadioDataConversionState & cstate); QString toString(Board::Type board = Board::BOARD_UNKNOWN, const GeneralSettings * const generalSettings = NULL, const ModelData * const modelData = NULL) const; + bool isAvailable(const ModelData * const model = NULL, const GeneralSettings * const gs = NULL, Board::Type board = Board::BOARD_UNKNOWN); bool operator== ( const RawSwitch& other) { return (this->type == other.type) && (this->index == other.index); @@ -290,7 +291,6 @@ class RawSwitch { return (this->type != other.type) || (this->index != other.index); } - RawSwitch convert(Board::Type before, Board::Type after); RawSwitchType type; int index; @@ -343,7 +343,7 @@ class ExpoData { int carryTrim; char name[10+1]; void clear() { memset(this, 0, sizeof(ExpoData)); } - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); }; class CurvePoint { @@ -401,7 +401,7 @@ enum MltpxValue { class MixData { public: MixData() { clear(); } - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); unsigned int destCh; // 1..CPN_MAX_CHNOUT RawSource srcRaw; @@ -478,7 +478,7 @@ class LogicalSwitchData { // Logical Switches data CSFunctionFamily getFunctionFamily() const; unsigned int getRangeFlags() const; QString funcToString() const; - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); }; enum AssignFunc { @@ -540,6 +540,7 @@ class CustomFunctionData { // Function Switches data void clear(); bool isEmpty() const; + QString toString(int index, bool globalContext = false) const; QString funcToString(const ModelData * model = NULL) const; QString paramToString(const ModelData * model) const; QString repeatToString() const; @@ -549,7 +550,7 @@ class CustomFunctionData { // Function Switches data static void populatePlaySoundParams(QStringList & qs); static void populateHapticParams(QStringList & qs); - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); }; @@ -566,7 +567,8 @@ class FlightModeData { int rotaryEncoders[CPN_MAX_ENCODERS]; int gvars[CPN_MAX_GVARS]; void clear(const int phase); - void convert(Board::Type before, Board::Type after); + QString toString(int index) const; + void convert(RadioDataConversionState & cstate); }; class SwashRingData { // Swash Ring data @@ -754,6 +756,7 @@ class TimerData { unsigned int persistent; int pvalue; void clear() { memset(this, 0, sizeof(TimerData)); mode = RawSwitch(SWITCH_TYPE_TIMER_MODE, 0); } + void convert(RadioDataConversionState & cstate); }; enum PulsesProtocol { @@ -1071,7 +1074,7 @@ class ModelData { ModelData(const ModelData & src); ModelData & operator = (const ModelData & src); - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); ExpoData * insertInput(const int idx); void removeInput(const int idx); @@ -1197,8 +1200,9 @@ class GeneralSettings { }; GeneralSettings(); - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); + void setDefaultControlTypes(Board::Type board); int getDefaultStick(unsigned int channel) const; RawSource getDefaultSource(unsigned int channel) const; int getDefaultChannel(unsigned int stick) const; @@ -1314,7 +1318,7 @@ class RadioData { std::vector categories; std::vector models; - void convert(Board::Type before, Board::Type after); + void convert(RadioDataConversionState & cstate); void setCurrentModel(unsigned int index); void fixModelFilenames(); diff --git a/companion/src/radiodataconversionstate.cpp b/companion/src/radiodataconversionstate.cpp new file mode 100644 index 000000000..5901754d2 --- /dev/null +++ b/companion/src/radiodataconversionstate.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) OpenTX + * + * Based on code named + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "radiodataconversionstate.h" +#include "radiodata.h" + +#include + +RadioDataConversionState::RadioDataConversionState(Board::Type before, Board::Type after, RadioData * rdata) : + fromType(before), + toType(after), + fromBoard(Boards(before)), + toBoard(Boards(after)), + rd(NULL), + rdCopy(NULL), + maxEventType(EVT_NONE), + modelIdx(-1), + subCompIdx(-1) +{ + setRadioData(rdata); +} + +RadioDataConversionState::~RadioDataConversionState() +{ + if (rdCopy) + delete rdCopy; +} + +RadioDataConversionState & RadioDataConversionState::withModelIndex(int index) +{ + modelIdx = index; + subCompIdx = -1; + setLogField(FLD_ORIGIN); + return *this; +} + +RadioDataConversionState & RadioDataConversionState::withComponentIndex(int index) +{ + subCompIdx = index; + setLogField(FLD_SUB_COMP); + return *this; +} + +RadioDataConversionState &RadioDataConversionState::withComponentField(const QString & name) +{ + setLogField(FLD_COMP_FIELD, LogField(0, name)); + return *this; +} + +bool RadioDataConversionState::convert() +{ + if (rd) { + rd->convert(*this); + return true; + } + + setErr(("No radio data to convert.")); + return false; +} + +void RadioDataConversionState::setRadioData(RadioData * rdata) +{ + rd = rdata; + if (rdCopy) + delete rdCopy; + rdCopy = new RadioData(*rdata); + logCache = LogRecord(); +} + +void RadioDataConversionState::setLogField(LogFieldType type, const LogField & data, bool clearRest) +{ + if (type >= FLD_ENUM_COUNT) + return; + + logCache.fields[type].id = data.id; + logCache.fields[type].name = data.name; + if (clearRest && type + 1 < FLD_ENUM_COUNT) + setLogField(LogFieldType(type + 1)); +} + +void RadioDataConversionState::addLogEntry(EventType event, const QString & msg) +{ + if (event != EVT_NONE) { + setLogField(FLD_EVT_TYPE, LogField(event, eventTypeToString(event)), false); + setLogField(FLD_MSG, LogField(event, msg), false); + log.append(logCache); + } + maxEventType = qMax(event, maxEventType); + logCache.fields[FLD_MSG].clear(); +} + +void RadioDataConversionState::setInvalid(const LogField & item) +{ + setLogField(FLD_ITM_BEFORE, item); + addLogEntry(EVT_INV, tr("is invalid")); +} + +void RadioDataConversionState::setConverted(const LogField & from, const LogField & to) +{ + setLogField(FLD_ITM_BEFORE, from); + setLogField(FLD_ITM_AFTER, to); + addLogEntry(EVT_CVRT, tr("converted to")); +} + +void RadioDataConversionState::setMoved(const LogField & from, const LogField & to) +{ + setLogField(FLD_ITM_BEFORE, from); + setLogField(FLD_ITM_AFTER, to); + addLogEntry(EVT_INF, tr("verify is")); +} + +const ModelData * RadioDataConversionState::toModel() const +{ + if (rd && modelIdx > -1 && modelIdx < (int)rd->models.size()) + return &rd->models[modelIdx]; + return NULL; +} + +const ModelData *RadioDataConversionState::fromModel() const +{ + if (rdCopy && modelIdx > -1 && modelIdx < (int)rdCopy->models.size()) + return &rdCopy->models[modelIdx]; + return NULL; +} + +const GeneralSettings * RadioDataConversionState::toGS() const +{ + if (rd) + return &rd->generalSettings; + return NULL; +} + +const GeneralSettings *RadioDataConversionState::fromGS() const +{ + if (rdCopy) + return &rdCopy->generalSettings; + return NULL; +} + +bool RadioDataConversionState::hasLogEntries(EventType logLevel) const +{ + return (maxEventType >= logLevel); +} + +QString RadioDataConversionState::eventTypeToString(int type) const +{ + // enum EventType { EVT_NONE, EVT_DBG, EVT_INF, EVT_WRN, EVT_CVRT, EVT_ERR, EVT_INV + static const QStringList evtTypes = QStringList() << "" << tr("[DBG]") << tr("[NFO]") << tr("[WRN]") << tr("[CVT]") << tr("[ERR]") << tr("[INV]"); + return (type < evtTypes.size() ? evtTypes.at(type) : ""); +} + +QString RadioDataConversionState::eventTypeToColor(int type) const +{ + // enum EventType { EVT_NONE, EVT_DBG, EVT_INF, EVT_WRN, EVT_CVRT, EVT_ERR, EVT_INV + static const QStringList evtColors = QStringList() << "black" << "dimgrey" << "black" << "#ea7104" << "#ea7104" << "red" << "red"; + return (type < evtColors.size() ? evtColors.at(type) : "black"); +} + +QString RadioDataConversionState::getFieldName(int field) const +{ + static const QStringList dataFields = QStringList() << tr("Event") << tr("Origin") << tr("Comp") << tr("Sub-Component") << tr("Field") << tr("Type") << tr("Old") << tr("Action") << tr("New"); + return (field < dataFields.size() ? dataFields.at(field) : ""); +} + +QStandardItemModel * RadioDataConversionState::getLogModel(RadioDataConversionState::EventType logLevel, QObject * parent) const +{ + QStandardItemModel * stdmdl = new QStandardItemModel(parent); + const QString tooltip = "%1: %2 (ID: %3)"; + const int sortRole = Qt::UserRole + 1; + const int seqdigits = floorf(logf(log.size()) / logf(10.0f)) + 1; + + stdmdl->setSortRole(sortRole); + + QStringList header; + header << tr("Seq"); + for (int i=0; i < FLD_ENUM_COUNT; ++i) + header << getFieldName(i); + stdmdl->setHorizontalHeaderLabels(header); + + QFont fnt("Courier"); + fnt.setStyleHint(QFont::TypeWriter); + //fnt.setPointSize(fnt.pointSize()+1); + + int row = 0; + foreach (const LogRecord & lr, log) { + if (lr.fields.at(FLD_EVT_TYPE).id < logLevel) + continue; + QBrush fg(QColor(eventTypeToColor(lr.fields.at(FLD_EVT_TYPE).id))); + stdmdl->setItem(row, 0, new QStandardItem(QString("%1").arg(row+1, seqdigits, 10, QChar('0')))); + stdmdl->item(row, 0)->setData(row, sortRole); + stdmdl->item(row, 0)->setData(fnt, Qt::FontRole); + for (int i=0; i < FLD_ENUM_COUNT; ++i) { + QStandardItem * itm = new QStandardItem(lr.fields.at(i).name); + itm->setData(lr.fields.at(i).id, sortRole); + itm->setData(fnt, Qt::FontRole); + itm->setData(tooltip.arg(getFieldName(i)).arg(lr.fields.at(i).name).arg(lr.fields.at(i).id), Qt::ToolTipRole); + itm->setEditable(false); + if (i == FLD_EVT_TYPE || i == FLD_MSG) + itm->setForeground(fg); + stdmdl->setItem(row, i+1, itm); + } + ++row; + } + + return stdmdl; +} diff --git a/companion/src/radiodataconversionstate.h b/companion/src/radiodataconversionstate.h new file mode 100644 index 000000000..dd319d115 --- /dev/null +++ b/companion/src/radiodataconversionstate.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) OpenTX + * + * Based on code named + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef RADIODATACONVERSIONSTATE_H +#define RADIODATACONVERSIONSTATE_H + +#include "boards.h" +#include + +class ModelData; +class GeneralSettings; +class RadioData; +class QStandardItemModel; + +class RadioDataConversionState +{ + Q_DECLARE_TR_FUNCTIONS(RadioDataConversionState) + + public: + enum EventType { + EVT_NONE, // does not get logged + EVT_DBG, // debug + EVT_INF, // misc. info + EVT_WRN, // misc. warning, place warning-level events after this and before EVT_ERR + EVT_CVRT, // something was converted (A->B) + EVT_ERR, // misc. error, place error-level events after this + EVT_INV, // invalid, control/etc not available on destination radio + }; + + enum LogFieldType { + FLD_EVT_TYPE, // EventType + FLD_ORIGIN, // General Settings or Model with name + FLD_COMP, // Input/Mix/LS/CF/etc + FLD_SUB_COMP, // Mix channel/Input line/LS number/etc + FLD_COMP_FIELD, // optional item within subcomponent, eg. LS V1/V2/AND, etc + FLD_ITM_TYPE, // Source/Switch + FLD_ITM_BEFORE, // item before conversion + FLD_MSG, // conversion details + FLD_ITM_AFTER, // item after conversion + FLD_ENUM_COUNT + }; + + struct LogField { + LogField(int id = 0, const QString & name = QString()) : id(id), name(name) {} + void clear() { id = 0; name.clear(); } + int id; + QString name; + }; + + struct LogRecord + { + LogRecord() { fields.resize(FLD_ENUM_COUNT); } + QVector fields; // collected data fields indexed by LogColumn value + }; + + RadioDataConversionState(Board::Type before = Board::BOARD_UNKNOWN, Board::Type after = Board::BOARD_UNKNOWN, RadioData * rdata = NULL); + ~RadioDataConversionState(); + + RadioDataConversionState & withModelIndex(int index); + RadioDataConversionState & withComponentIndex(int index); + RadioDataConversionState & withComponentField(const QString & name); + + bool convert(); + void setRadioData(RadioData * rdata); + void setLogField(LogFieldType type, const LogField & data = LogField(), bool clearRest = true); + inline void setOrigin(const QString & name = QString()) { setLogField(FLD_ORIGIN, LogField(modelIdx, name)); } + inline void setSubComp(const QString & name = QString()) { setLogField(FLD_SUB_COMP, LogField(subCompIdx, name)); } + inline void setComponent(const QString & name = QString(), int id = 0) { setLogField(FLD_COMP, LogField(id, name)); } + inline void setItemType(const QString & name = QString(), int id = 0) { setLogField(FLD_ITM_TYPE, LogField(id, name)); } + + void addLogEntry(EventType event, const QString & msg = QString()); + void setInvalid(const LogField & item); + void setConverted(const LogField & from, const LogField & to); + void setMoved(const LogField & from, const LogField & to); + inline void setDbg(const QString & msg) { addLogEntry(EVT_DBG, msg); } + inline void setInf(const QString & msg) { addLogEntry(EVT_INF, msg); } + inline void setWrn(const QString & msg) { addLogEntry(EVT_WRN, msg); } + inline void setErr(const QString & msg) { addLogEntry(EVT_ERR, msg); } + + const ModelData * toModel() const; + const ModelData * fromModel() const; + const GeneralSettings * toGS() const; + const GeneralSettings * fromGS() const; + bool hasLogEntries(EventType logLevel = EVT_INF) const; + + QString eventTypeToString(int type) const; + QString eventTypeToColor(int type) const; + QString getFieldName(int field) const; + QStandardItemModel * getLogModel(EventType logLevel = EVT_INF, QObject *parent = Q_NULLPTR) const; + + Board::Type fromType; // board type before conversion + Board::Type toType; // board type after + Boards fromBoard; + Boards toBoard; + RadioData * rd; // pointer to data struct being converted + RadioData * rdCopy; // copy of original radio data before conversion + QVector log; // logged events + LogRecord logCache; // buffer for holding event data before logging + EventType maxEventType; // most severe event type recorded + int modelIdx; // model index in radio data array, -1 if none + int subCompIdx; // current row index within component (eg. model.mixData[componentIdx]), -1 if none +}; + +#endif // RADIODATACONVERSIONSTATE_H diff --git a/companion/src/storage/storage.h b/companion/src/storage/storage.h index 73b7d1506..8972570bd 100644 --- a/companion/src/storage/storage.h +++ b/companion/src/storage/storage.h @@ -67,42 +67,6 @@ class StorageFormat return board; } - static uint32_t getFourCC(Board::Type board) - { - switch (board) { - case Board::BOARD_X12S: - return 0x3478746F; - case Board::BOARD_X10: - return 0x3778746F; - case Board::BOARD_TARANIS_X7: - return 0x3678746F; - case Board::BOARD_TARANIS_X9E: - return 0x3578746F; - case Board::BOARD_TARANIS_X9D: - case Board::BOARD_TARANIS_X9DP: - return 0x3378746F; - case Board::BOARD_SKY9X: - case Board::BOARD_AR9X: - case Board::BOARD_9XRPRO: - return 0x3278746F; - case Board::BOARD_MEGA2560: - case Board::BOARD_GRUVIN9X: - return 0x3178746F; - default: - return 0; - } - } - - uint32_t getFourCC() - { - return getFourCC(board); - } - - virtual bool isBoardCompatible(Board::Type board) - { - return getFourCC() == getFourCC(board); - } - protected: void setError(const QString & error) { diff --git a/companion/src/thirdparty/maxlibqt/src/widgets/CMakeLists.txt b/companion/src/thirdparty/maxlibqt/src/widgets/CMakeLists.txt new file mode 100644 index 000000000..dd2ef22a4 --- /dev/null +++ b/companion/src/thirdparty/maxlibqt/src/widgets/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 2.8.12) +cmake_policy(SET CMP0020 NEW) +set(CMAKE_CXX_STANDARD 11) +if (CMAKE_VERSION VERSION_LESS "3.1" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND CMAKE_CXX_FLAGS --std=gnu++11) +endif () + +find_package(Qt5Core) +find_package(Qt5Widgets) + +project(maxLibQtWidgets) + +set(SRCS + "${CMAKE_CURRENT_LIST_DIR}/ExportableTableView.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/ScrollableMessageBox.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/TimerEdit.cpp" +# "${CMAKE_CURRENT_LIST_DIR}/TreeComboBox.cpp" +) + +set(HDRS + "${CMAKE_CURRENT_LIST_DIR}/ExportableTableView.h" +# "${CMAKE_CURRENT_LIST_DIR}/ScrollableMessageBox.h" +# "${CMAKE_CURRENT_LIST_DIR}/TimerEdit.h" +# "${CMAKE_CURRENT_LIST_DIR}/TreeComboBox.h" +) + +qt5_wrap_cpp(SRCS ${HDRS}) + +add_library(${PROJECT_NAME} ${SRCS} ${HDRS}) +target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core Qt5::Widgets) +target_compile_definitions(${PROJECT_NAME} PRIVATE QT_USE_QSTRINGBUILDER) +target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC "${CMAKE_CURRENT_LIST_DIR}") + +# need to push this upstream +set(BUILT_LIBRARIES ${BUILT_LIBRARIES} ${PROJECT_NAME} PARENT_SCOPE) diff --git a/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView new file mode 100644 index 000000000..97eac83d9 --- /dev/null +++ b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView @@ -0,0 +1 @@ +#include "ExportableTableView.h" diff --git a/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.cpp b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.cpp new file mode 100644 index 000000000..b384a44ea --- /dev/null +++ b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.cpp @@ -0,0 +1,242 @@ +/* + ExportableTableView + + COPYRIGHT: (c)2017 Maxim Paperno; All Right Reserved. + Contact: http://www.WorldDesign.com/contact + + LICENSE: + + Commercial License Usage + Licensees holding valid commercial licenses may use this file in + accordance with the terms contained in a written agreement between + you and the copyright holder. + + GNU General Public License Usage + Alternatively, this file may be used under the terms of the GNU + General Public License as published by the Free Software Foundation, + either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + A copy of the GNU General Public License is available at . +*/ + +#include "ExportableTableView.h" + +ExportableTableView::ExportableTableView(QWidget *parent) : QTableView(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + setHtmlTemplate(getDefaultHtmlTemplate()); + + QAction * selAll = new QAction(tr("Select All"), this); + selAll->setShortcut(QKeySequence::SelectAll); + addAction(selAll); + + QAction * cpyTab = new QAction(tr("Copy selection as TAB-delimited text"), this); + cpyTab->setShortcut(QKeySequence::Copy); + cpyTab->setProperty("delim", "\t"); + addAction(cpyTab); + + QAction * cpyCsv = new QAction(tr("Copy selection as cooma-delimited text (CSV)"), this); + cpyCsv->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C); + cpyCsv->setProperty("delim", ", "); + addAction(cpyCsv); + + QAction * cpyPipe = new QAction(tr("Copy selection as pipe-delimited text"), this); + cpyPipe->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_C); + cpyPipe->setProperty("delim", " | "); + addAction(cpyPipe); + + QAction * cpyHtml = new QAction(tr("Copy selection as HTML"), this); + cpyHtml->setProperty("delim", "html"); + addAction(cpyHtml); + + QAction * saveFile = new QAction(tr("Save selection to file"), this); + saveFile->setShortcut(QKeySequence::Save); + addAction(saveFile); + + connect(selAll, &QAction::triggered, this, &QTableView::selectAll); + connect(cpyTab, &QAction::triggered, this, &ExportableTableView::copy); + connect(cpyCsv, &QAction::triggered, this, &ExportableTableView::copy); + connect(cpyPipe, &QAction::triggered, this, &ExportableTableView::copy); + connect(cpyHtml, &QAction::triggered, this, &ExportableTableView::copy); + connect(saveFile, &QAction::triggered, this, &ExportableTableView::save); + connect(this, &QTableView::customContextMenuRequested, this, &ExportableTableView::onCustomContextMenuRequested); +} + +QString ExportableTableView::getDefaultHtmlTemplate() +{ + return "\n\n\n\n" \ + "\n" \ + "%1" \ + "
\n" \ + ""; +} + +void ExportableTableView::setHtmlTemplate(const QString &value) +{ + m_htmlTemplate = value; +} + +QString ExportableTableView::toPlainText(const QModelIndexList &indexList, const QString &delim) const +{ + QString ret, header; + const QChar rowDelim = '\n'; + bool firstRow = true; + for (int i = 0; i < indexList.count(); ++i) { + const QModelIndex & idx = indexList.at(i); + + if (firstRow) + header.append(model()->headerData(idx.column(), Qt::Horizontal).toString()); + + ret.append(idx.data(Qt::DisplayRole).toString()); + + if (i + 1 == indexList.count() || indexList.at(i+1).row() != idx.row()) { + ret.append(rowDelim); + if (firstRow && !header.isEmpty()) + header.append(rowDelim); + firstRow = false; + } + else { + ret.append(delim); + if (firstRow && !header.isEmpty()) + header.append(delim); + } + } + if (!header.isEmpty()) + ret.prepend(header); + + return ret; +} + +QString ExportableTableView::toHtml(const QModelIndexList &indexList) const +{ + QString ret, header, row; + bool firstRow = true; + + for (int i = 0; i < indexList.count(); ++i) { + const QModelIndex & idx = indexList.at(i); + const int algn = (idx.data(Qt::TextAlignmentRole).isValid() ? idx.data(Qt::TextAlignmentRole).toInt() : (Qt::AlignLeft | Qt::AlignVCenter)); + const QString fg = (idx.data(Qt::ForegroundRole).isValid() ? idx.data(Qt::ForegroundRole).value().color().name(QColor::HexRgb) : "initial"); + const QString bg = (idx.data(Qt::BackgroundRole).isValid() ? idx.data(Qt::BackgroundRole).value().color().name(QColor::HexRgb) : "initial"); + const QString ttl = (idx.data(Qt::ToolTipRole).isValid() ? idx.data(Qt::ToolTipRole).toString().replace("\"", "\"\"") : QString()); + + QString fnt; + if (idx.data(Qt::FontRole).isValid()) { + const QFont font = idx.data(Qt::FontRole).value(); + fnt = "font: \"" % font.family() % "\" " % QString::number(font.pointSize()) % "pt;"; + } + + if (firstRow) + header.append(QString("%1\n").arg(model()->headerData(idx.column(), Qt::Horizontal).toString())); + + row.append("%1\n"); // cell template + row = row.arg(idx.data(Qt::DisplayRole).toString()).arg(fg).arg(bg).arg(fnt); + row = row.arg((algn & Qt::AlignRight) ? "right" : (algn & Qt::AlignHCenter) ? "center" : "left"); + row = row.arg((algn & Qt::AlignTop) ? "top" : (algn & Qt::AlignBottom) ? "bottom" : "middle"); + row = row.arg(ttl.isEmpty() ? ttl : QString("title=\"%1\"").arg(ttl)); + + if (i + 1 == indexList.count() || indexList.at(i+1).row() != idx.row()) { + ret.append(QString("\n%1\n").arg(row)); + row.clear(); + firstRow = false; + } + } + + ret = QString("\n%1\n").arg(ret); + if (!header.isEmpty()) + ret.prepend(QString("\n\n%1\n\n").arg(header)); + ret = m_htmlTemplate.arg(ret); + + return ret; +} + +bool ExportableTableView::saveToFile(const QModelIndexList &indexList, const QString &fileName) +{ + static QString lastDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + + if (indexList.isEmpty()) + return false; + + QString fname = fileName; + if (fname.isEmpty()) { + QString types = tr("Tab-delimited text") % " (*.tab);;" % tr("Comma-delimited text") % " (*.csv);;" % tr("Pipe-delimited text") % " (*.txt);;" % tr("HTML") % " (*.html)"; + fname = QFileDialog::getSaveFileName(this, tr("Save to file"), lastDir, types); + } + + if (fname.isEmpty()) + return false; + + QFile file(fname); + QFileInfo fi(file); + lastDir = fi.absolutePath(); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + return false; + + if (fi.suffix() == "html") + file.write(toHtml(indexList).toUtf8()); + else + file.write(toPlainText(indexList, (fi.suffix() == "tab" ? "\t" : fi.suffix() == "csv" ? ", " : " | ")).toUtf8()); + file.close(); + + return true; +} + +QSize ExportableTableView::sizeHint() const +{ + int w = 0; + for (int i = 0; i < model()->columnCount(); ++i) + w += sizeHintForColumn(i); + return QSize(w + verticalScrollBar()->sizeHint().width() + 60, sizeHintForRow(0) * model()->rowCount()); +} + +void ExportableTableView::copyText(const QString &delim) +{ + QApplication::clipboard()->setText(toPlainText(getSelectedOrAll(), delim)); +} + +void ExportableTableView::copyHtml() +{ + QMimeData * mdata = new QMimeData; + mdata->setHtml(toHtml(getSelectedOrAll())); + QApplication::clipboard()->setMimeData(mdata); +} + +void ExportableTableView::copy() +{ + QString delim = "\t"; + if (sender() && sender()->property("delim").isValid()) + delim = sender()->property("delim").toString(); + if (delim == "html") + copyHtml(); + else + copyText(delim); +} + +void ExportableTableView::save() +{ + saveToFile(getSelectedOrAll()); +} + +QModelIndexList ExportableTableView::getSelectedOrAll() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.isEmpty()) { + selectAll(); + sel = selectionModel()->selectedIndexes(); + } + return sel; +} + +void ExportableTableView::onCustomContextMenuRequested(const QPoint & pos) +{ + QMenu menu; + menu.addActions(actions()); + menu.exec(mapToGlobal(pos)); +} diff --git a/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.h b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.h new file mode 100644 index 000000000..e6569fe30 --- /dev/null +++ b/companion/src/thirdparty/maxlibqt/src/widgets/ExportableTableView.h @@ -0,0 +1,99 @@ +/* + ExportableTableView + https://github.com/mpaperno/maxLibQt + + COPYRIGHT: (c)2017 Maxim Paperno; All Right Reserved. + Contact: http://www.WorldDesign.com/contact + + LICENSE: + + Commercial License Usage + Licensees holding valid commercial licenses may use this file in + accordance with the terms contained in a written agreement between + you and the copyright holder. + + GNU General Public License Usage + Alternatively, this file may be used under the terms of the GNU + General Public License as published by the Free Software Foundation, + either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + A copy of the GNU General Public License is available at . +*/ + +#ifndef EXPORTABLETABLEVIEW_H +#define EXPORTABLETABLEVIEW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + \class ExportableTableView + \version 1.0.0 + + \brief The ExportableTableView class provides a regular QTableView but with features to export the data + as plain text or HTML. + + Any selection of data can be exported. The horizontal headings, if any, are included in the export. + The export functions are available to the user via a custom context menu or keyboard shortcuts. + 3 field delimiter choices are available for plain-text export (tab, comma, or pipe). + Data can be saved to the clipboard or to a file. + + The export functions can also be accessed programmatically via \c toPlainText(), \c toHtml(), + and \c saveToFile(). + + The overall HTML template can be customized by setting \c setHtmlTemplate(). The data itself is always + formatted as a basic HTML table and then inserted into the template at the \c %1 placeholder. + HTML version tries to preserve many data role attributes of the model items: + + \li \c Qt::FontRole + \li \c Qt::ForegroundRole + \li \c Qt::BackgroundRole + \li \c Qt::TextAlignmentRole + \li \c Qt::ToolTipRole + + Note that \c Qt::EditRole data is not specifically preserved (unless it already matches \c Qt::DisplayRole ). + + */ +class ExportableTableView : public QTableView +{ + Q_OBJECT + + public: + ExportableTableView(QWidget *parent = Q_NULLPTR); + + static QString getDefaultHtmlTemplate(); + void setHtmlTemplate(const QString &value); + + QString toPlainText(const QModelIndexList &indexList, const QString &delim = "\t") const; + QString toHtml(const QModelIndexList &indexList) const; + bool saveToFile(const QModelIndexList &indexList, const QString &fileName = QString()); + + QSize sizeHint() const Q_DECL_OVERRIDE; + + public slots: + void copyText(const QString &delim = QString("\t")); + void copyHtml(); + void copy(); + void save(); + QModelIndexList getSelectedOrAll(); + + protected: + void onCustomContextMenuRequested(const QPoint &pos); + + QString m_htmlTemplate; +}; + +#endif // EXPORTABLETABLEVIEW_H