#include #include "kbprofiledialog.h" #include "ui_kbprofiledialog.h" #include #include #include #include #include #include KbProfileDialog::KbProfileDialog(KbWidget *parent) : QDialog(parent), ui(new Ui::KbProfileDialog), device(parent->device) { ui->setupUi(this); connect(ui->profileList, SIGNAL(orderChanged()), this, SLOT(profileList_reordered())); // Populate profile list repopulate(); //activeProfile = device->newProfile(device->currentProfile()); } KbProfileDialog::~KbProfileDialog(){ delete ui; } void KbProfileDialog::profileList_reordered(){ // Rebuild profile list from items QList newProfiles; int count = ui->profileList->count(); for(int i = 0; i < count; i++){ QListWidgetItem* item = ui->profileList->item(i); KbProfile* profile = device->find(item->data(GUID).toUuid()); if(profile && !newProfiles.contains(profile)) newProfiles.append(profile); item->setFlags(item->flags() | Qt::ItemIsEditable); } // Add any missing profiles at the end of the list foreach(KbProfile* profile, device->profiles()){ if(!newProfiles.contains(profile)) newProfiles.append(profile); } device->profiles(newProfiles); } void KbProfileDialog::repopulate(){ ui->profileList->clear(); QListWidgetItem* current = nullptr; foreach(KbProfile* profile, device->profiles()){ QListWidgetItem* item = new QListWidgetItem(QIcon((profile == device->hwProfile()) ? ":/img/icon_profile_hardware.png" : ":/img/icon_profile.png"), profile->name(), ui->profileList); item->setData(GUID, profile->id().guid); item->setFlags(item->flags() | Qt::ItemIsEditable); if(profile == device->currentProfile()){ item->setSelected(true); current = item; } ui->profileList->addItem(item); } if(current) ui->profileList->setCurrentItem(current); addNewProfileItem(); } void KbProfileDialog::addNewProfileItem(){ // Add an item for creating a new profile. Make it editable but not dragable. QListWidgetItem* item = new QListWidgetItem(tr("New profile..."), ui->profileList); item->setFlags((item->flags() | Qt::ItemIsEditable) & ~Qt::ItemIsDragEnabled & ~Qt::ItemIsDropEnabled); item->setData(NEW_FLAG, 1); QFont font = item->font(); font.setItalic(true); item->setFont(font); item->setIcon(QIcon(":/img/icon_plus.png")); ui->profileList->addItem(item); } void KbProfileDialog::on_profileList_itemClicked(QListWidgetItem *item){ QUuid guid = item->data(GUID).toUuid(); if(guid.isNull() && item->data(NEW_FLAG).toInt() == 1){ // New profile item->setText(""); ui->profileList->editItem(item); item->setFlags(item->flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled); QFont font = item->font(); font.setItalic(false); item->setFont(font); item->setIcon(QIcon(":/img/icon_profile.png")); // Add the new profile and assign it to this item KbProfile* newProfile = device->newProfileWithBlankMode(); device->appendProfile(newProfile); item->setData(GUID, newProfile->id().guid); item->setData(NEW_FLAG, 0); device->setCurrentProfile(newProfile); // Create another "new profile" item to replace this one addNewProfileItem(); } } void KbProfileDialog::on_profileList_itemChanged(QListWidgetItem *item){ KbProfile* currentProfile = device->currentProfile(); if(!item || !currentProfile || item->data(GUID).toUuid() != currentProfile->id().guid) return; currentProfile->name(item->text()); // Set the text to the actual name (trimmed, "" replaced with "Unnamed") item->setText(currentProfile->name()); } void KbProfileDialog::on_profileList_customContextMenuRequested(const QPoint &pos){ QListWidgetItem* item = ui->profileList->itemAt(pos); KbProfile* currentProfile = device->currentProfile(); if(!item || !currentProfile || item->data(GUID).toUuid() != currentProfile->id().guid) return; int index = device->indexOf(currentProfile); QList profiles = device->profiles(); QMenu menu(this); QAction* rename = new QAction(tr("Rename"), this); QAction* duplicate = new QAction(tr("Duplicate"), this); QAction* del = new QAction(tr("Delete"), this); bool canDelete = (profiles.count() > 1); if(!canDelete) // Can't delete the last profile on the device del->setEnabled(false); QAction* hwsave = new QAction(tr("Save to Hardware"), this); // Disable Save to hardware button for unsupported devices if(!device->hwload){ hwsave->setDisabled(true); hwsave->setToolTip(tr("Saving to hardware is not supported on this device.")); } QAction* moveup = new QAction(tr("Move Up"), this); if(index == 0) moveup->setEnabled(false); QAction* movedown = new QAction(tr("Move Down"), this); if(index >= profiles.count() - 1) movedown->setEnabled(false); menu.addAction(rename); menu.addAction(duplicate); menu.addAction(del); menu.addSeparator(); menu.addAction(hwsave); menu.addSeparator(); menu.addAction(moveup); menu.addAction(movedown); QAction* result = menu.exec(QCursor::pos()); if(result == rename){ ui->profileList->editItem(item); // We need to return otherwise repopulate() gets called and the item is destroyed return; } else if(result == duplicate){ KbProfile* newProfile = device->newProfile(currentProfile); newProfile->newId(); profiles.insert(index + 1, newProfile); device->profiles(profiles); device->setCurrentProfile(newProfile); } else if(result == del){ if(!canDelete) return; if(QMessageBox::question(this, tr("Delete profile"), tr("Are you sure you want to delete this profile?")) != QMessageBox::Yes) return; profiles.removeAll(currentProfile); device->profiles(profiles); currentProfile->deleteLater(); // Select next profile if(index < profiles.count()) device->setCurrentProfile(profiles.at(index)); else device->setCurrentProfile(profiles.last()); } else if(result == hwsave){ device->hwSave(); // Refresh item icons int count = ui->profileList->count(); for(int i = 0; i < count; i++){ QListWidgetItem* hwitem = ui->profileList->item(i); if(hwitem->data(NEW_FLAG).toUInt() == 1) continue; KbProfile* profile = device->find(hwitem->data(GUID).toUuid()); hwitem->setIcon(QIcon((profile == device->hwProfile()) ? ":/img/icon_profile_hardware.png" : ":/img/icon_profile.png")); } // No need to repopulate here either return; } else if(result == moveup){ profiles.removeAll(currentProfile); profiles.insert(index - 1, currentProfile); device->profiles(profiles); } else if(result == movedown){ profiles.removeAll(currentProfile); profiles.insert(index + 1, currentProfile); device->profiles(profiles); } repopulate(); } void KbProfileDialog::extractedFileCleanup(const QStringList& extracted){ for(int i = 0; i < extracted.count(); i++){ // Delete extracted files QFile fdel(extracted.at(i)); fdel.remove(); } } void KbProfileDialog::on_exportButton_clicked(){ const quint16 profileVer = CKB_NEXT_PROFILE_VER; // Selected items QList selectedItems = ui->profileList->selectedItems(); // Set up the file dialog QFileDialog dialog(this); dialog.setFileMode(QFileDialog::AnyFile); dialog.setNameFilter(tr("ckb-next profiles (*.ckb)")); dialog.setViewMode(QFileDialog::List); dialog.setAcceptMode(QFileDialog::AcceptSave); // Generate a default filename QString filename; int itemcount = selectedItems.count(); for(int i = 0; i < itemcount; i++){ QString pname(selectedItems.at(i)->text()); pname.replace(QRegularExpression("[^a-zA-Z0-9_]"), QString()); if(pname.isEmpty()) continue; filename.append(pname); if(i < itemcount - 1) filename.append("_"); } if(!filename.isEmpty()) dialog.selectFile(filename); if(!dialog.exec()) return; QStringList filenames = dialog.selectedFiles(); if(filenames.empty()) return; // Pick only the first filename filename = filenames.at(0); if(!filename.endsWith(".ckb")) filename.append(".ckb"); QStringList tmpExported; { // Generate package metadata in /tmp/ QString metadatapath = "/tmp/ckbpkg.metadata"; QSettings metadata(metadatapath, QSettings::IniFormat); metadata.beginGroup("metadata"); metadata.setValue("version", profileVer); metadata.setValue("isKeyboard", device->isKeyboard()); metadata.setValue("isMouse", device->isMouse()); metadata.setValue("isMousepad", device->isMousepad()); metadata.setValue("isHeadsetStand", device->isHeadsetStand()); metadata.setValue("model", KeyMap::getModel(device->model())); metadata.setValue("layout", device->getCurrentLayout()); metadata.setValue("monochrome", device->monochrome); metadata.endGroup(); metadata.sync(); if(metadata.status() == QSettings::NoError) tmpExported.append(metadatapath); } // Create a QSettings ini in /tmp/ for each selected profile for(int p = 0; p < selectedItems.count(); p++){ // Get the profile's pointer QListWidgetItem* item = selectedItems.at(p); KbProfile* prof = device->find(item->data(GUID).toUuid()); QString tmp("/tmp/ckbprofile"); tmp.append(QString::number(p)); tmp.append(".ini"); // Used to make sure QSettings is destroyed before trying to hash the ini { // The version passed here isn't used anywhere CkbExternalSettings exportitem(tmp, QSettings::IniFormat, CKB_NEXT_PROFILE_VER); exportitem.clear(); prof->save(exportitem); exportitem.sync(); if(exportitem.status() == QSettings::NoError) tmpExported.append(tmp); } } int fileCount = tmpExported.count(); for(int p = 0; p < fileCount; p++){ // Create a hash file for each generated file QString tmp(tmpExported.at(p)); QFile tmpHashFile(tmp); if(tmpHashFile.open(QFile::ReadOnly)){ QCryptographicHash tmpHash(QCryptographicHash::Sha256); if(tmpHash.addData(&tmpHashFile)){ tmp.append(".s256"); QFile tmpHashFileW(tmp); if(tmpHashFileW.open(QFile::WriteOnly)){ tmpExported.append(tmp); QTextStream hashStream(&tmpHashFileW); hashStream << tmpHash.result().toHex() << "\n"; } } } } bool compress = JlCompress::compressFiles(filename, tmpExported); if(!compress) QMessageBox::warning(this, tr("Error"), tr("An error occurred while exporting the selected profiles."), QMessageBox::Ok); extractedFileCleanup(tmpExported); if(compress) QMessageBox::information(this, tr("Export Successful"), tr("Selected profiles have been exported successfully."), QMessageBox::Ok); } bool KbProfileDialog::verifyHash(const QString& file){ QFile iniFile(file); QFile hashFile(file + ".s256"); if(iniFile.open(QFile::ReadOnly) && hashFile.open(QFile::ReadOnly)){ // Max sha256 string length is 64 QByteArray pkgHash = QByteArray::fromHex(hashFile.read(64).data()); QCryptographicHash iniHash(QCryptographicHash::Sha256); if(iniHash.addData(&iniFile)){ hashFile.close(); iniFile.close(); if(iniHash.result() == pkgHash) return true; } } return false; } void KbProfileDialog::importCleanup(const QStringList& extracted, const QList>& profileptrs){ // Destroy the open CkbExternalSettings objects for(int i = 0; i < profileptrs.count(); i++) delete profileptrs.at(i).first; extractedFileCleanup(extracted); repopulate(); } void KbProfileDialog::on_importButton_clicked(){ QFileDialog dialog(this); dialog.setFileMode(QFileDialog::ExistingFile); dialog.setNameFilter(tr("ckb-next profiles (*.ckb)")); if(!dialog.exec()) return; QStringList filenames = dialog.selectedFiles(); // Pick only the first filename const QString& filename = filenames.at(0); if(!filename.endsWith(".ckb")){ QMessageBox::warning(this, tr("Error"), tr("Selected file is not a valid profile."), QMessageBox::Ok); return; } // Extract the ckb package QStringList extracted = JlCompress::extractDir(filename, QString("/tmp/")); if(extracted.empty()){ QMessageBox::warning(this, tr("Error"), tr("Could not read %1.").arg(filename), QMessageBox::Ok); return; } // Check and read the metadata if(!verifyHash("/tmp/ckbpkg.metadata")){ QMessageBox::warning(this, tr("Error"), tr("Package contains invalid metadata."), QMessageBox::Ok); extractedFileCleanup(extracted); return; } quint16 currentProfileVer = 0; { QSettings metadata("/tmp/ckbpkg.metadata", QSettings::IniFormat); metadata.beginGroup("metadata"); // Pkg version check currentProfileVer = metadata.value("version").toInt(); if(CKB_NEXT_PROFILE_VER < currentProfileVer){ QMessageBox::warning(this, tr("Error"), tr("Unsupported package selected. Please update ckb-next to import it."), QMessageBox::Ok); extractedFileCleanup(extracted); return; } // Don't allow importing profiles of devices that aren't the same type if(device->isKeyboard() != metadata.value("isKeyboard").toBool() || device->isMouse() != metadata.value("isMouse").toBool() || device->isMousepad() != metadata.value("isMousepad").toBool() || device->isHeadsetStand() != metadata.value("isHeadsetStand").toBool()){ QMessageBox::warning(this, tr("Error"), tr("This profile was not created for the current device type."), QMessageBox::Ok); extractedFileCleanup(extracted); return; } QString metadataDevModel = metadata.value("model").toString(); QString currentDevModel = KeyMap::getModel(device->model()); int currentDevLayout = (int)device->getCurrentLayout(); int metadataDevLayout = metadata.value("layout").toInt(); if(currentDevModel != metadataDevModel || currentDevLayout != metadataDevLayout){ currentDevModel[0] = currentDevModel[0].toUpper(); metadataDevModel[0] = metadataDevModel[0].toUpper(); QString metadataDevLayoutStr = KeyMap::getLayout((KeyMap::Layout)metadataDevLayout).replace("_", " ").toUpper(); QString currentDevLayoutStr = KeyMap::getLayout((KeyMap::Layout)currentDevLayout).replace("_", " ").toUpper(); // Only identical devices are supported for now, but let the user override this if they want int ret = QMessageBox::question(this, tr("Profile Import"), tr("This profile was created for a %1 (%2)\nbut it is going to be imported to a %3 (%4).\n\n" "You may need to manually add some keys to the appropriate animations.\n\n" "Import Anyway?").arg(metadataDevModel, metadataDevLayoutStr, currentDevModel, currentDevLayoutStr), QMessageBox::Yes, QMessageBox::No); if(ret == QMessageBox::No){ extractedFileCleanup(extracted); return; } } metadata.endGroup(); } QStringList profilestr; // , QList> profileptrs; int pkgProfileCount = 0; int pkgVerifiedCount = 0; for(int i = 0; i < extracted.count(); i++){ const QString& tmpFile = extracted.at(i); if(tmpFile.endsWith("ini")){ pkgProfileCount++; if(verifyHash(tmpFile)){ CkbExternalSettings* sptr = new CkbExternalSettings(extracted.at(i), QSettings::IniFormat, currentProfileVer); QString guid(sptr->childGroups().first()); profileptrs.append(QPair(sptr, guid)); profilestr.append(sptr->value(guid + "/Name").toString()); pkgVerifiedCount++; } } } if(pkgVerifiedCount == 0){ QMessageBox::warning(this, tr("Error"), tr("Selected package contains no valid profiles."), QMessageBox::Ok); extractedFileCleanup(extracted); return; } // TODO use a proper widget with a profile picker // For now, import everything included if(pkgVerifiedCount != pkgProfileCount) QMessageBox::warning(this, tr("Error"), tr("Selected package contains some corrupt profiles.\n\nAn attempt will be made to import as many as possible."), QMessageBox::Ok); QString profilestrf(profilestr.join("\n• ")); int msgret = QMessageBox::information(this, tr("Profile Import"), tr("The following profiles will be imported.\n\n• ").append(profilestrf), QMessageBox::Ok, QMessageBox::Cancel); if(msgret == QMessageBox::Cancel){ importCleanup(extracted, profileptrs); return; } QList profiles = device->profiles(); for(int i = 0; i < profileptrs.count(); i++){ int ret = 0; CkbExternalSettings* sptr = profileptrs.at(i).first; //QString guid = profileptrs.at(i).second; //QUuid current = guid.trimmed(); QUuid guid = sptr->childGroups().first().trimmed(); // Messy, shhhh const QString& profname = profilestr.at(i); KbProfile* profilematch = nullptr; foreach(KbProfile* profile, device->profiles()){ if(profile->id().guid == guid){ ret = QMessageBox::question(this, tr("Profile Import"), profname + tr(" already exists. Overwrite it?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel); profilematch = profile; break; } } if(ret == QMessageBox::Cancel) continue; if(ret == QMessageBox::Yes) profiles.removeAll(profilematch); QString profileGuid = sptr->childGroups().first(); if(ret == QMessageBox::No) profileGuid = QUuid::createUuid().toString(); qDebug() << "Importing" << profname; KbProfile* newProfile = device->newProfile(sptr, profileGuid); profiles.append(newProfile); } device->profiles(profiles); QMessageBox::information(this, tr("Import Successful"), tr("Profiles have been imported successfully."), QMessageBox::Ok); // Clean up importCleanup(extracted, profileptrs); } void KbProfileDialog::on_profileList_itemSelectionChanged(){ // Only change the profile if a single item is selected if(ui->profileList->selectedItems().count() > 1) return; QList itemlist = ui->profileList->selectedItems(); if(itemlist.empty()) return; KbProfile* profile = device->find(itemlist.first()->data(GUID).toUuid()); if(!profile) return; device->setCurrentProfile(profile); }