mirror of
https://github.com/ckb-next/ckb-next.git
synced 2026-10-30 13:07:47 -04:00
537 lines
20 KiB
C++
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);
|
|
}
|