Files
ckb-next/src/gui/kbprofiledialog.cpp
T

537 lines
20 KiB
C++

#include <QMenu>
#include "kbprofiledialog.h"
#include "ui_kbprofiledialog.h"
#include <QFileDialog>
#include <QDebug>
#include <QMessageBox>
#include <JlCompress.h>
#include <QCryptographicHash>
#include <ckbnextconfig.h>
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<KbProfile*> 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<KbProfile*> 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<QListWidgetItem*> 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<QPair<CkbExternalSettings*, QString>>& 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;
// <Pointer>, <GUID>
QList<QPair<CkbExternalSettings*, QString>> 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<CkbExternalSettings*, QString>(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<KbProfile*> 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<QListWidgetItem*> itemlist = ui->profileList->selectedItems();
if(itemlist.empty())
return;
KbProfile* profile = device->find(itemlist.first()->data(GUID).toUuid());
if(!profile)
return;
device->setCurrentProfile(profile);
}