From e0b1bb215e52fcd319335a00fc5932da32d2409c Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 24 Jul 2026 01:59:55 +0300 Subject: [PATCH 01/36] update ccos-disk-utils --- README.md | 8 ++ ccos_image | 2 +- dialogs/mainwindow.cpp | 301 +++++++++++++++++++++-------------------- dialogs/mainwindow.h | 18 ++- 4 files changed, 178 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 9e5e89e..e7c422d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ git submodule init git submodule update ``` +## Building +Configure and build the project in a `build` directory with CMake: +``` +cmake -S . -B build +cmake --build build +``` +After building, the resulting `GRiDISKCOM` executable will be located in the `build` directory. + ## Special thanks to * [BOOtak](https://github.com/BOOtak) for first testing, bugfixes and [core library](https://github.com/BOOtak/CCOS-disk-utils) for this application. * [vklachkov](https://github.com/vklachkov) for the current project maintenance. diff --git a/ccos_image b/ccos_image index 9d35399..5f4b127 160000 --- a/ccos_image +++ b/ccos_image @@ -1 +1 @@ -Subproject commit 9d353997dbc0b48993a4ec3af6464a020b43fec2 +Subproject commit 5f4b127975bd343f2fd6780155a7b97db9d97a62 diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index c353c15..3c595b0 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1,5 +1,8 @@ #include "mainwindow.h" +#include +#include + // GRiD disk/firmware "magic" constants for geometry/superblock/bitmap // Extracted from bubble image. @@ -19,27 +22,26 @@ #define GRID_HDD_SUPERBLOCK_FID 0x2420 #define GRID_HDD_BITMAP_FID 0x2400 +//[Service functions] + ccos_date_t ccos_get_datetime(void) { - struct timespec tp; + timespec tp; clock_gettime(CLOCK_REALTIME, &tp); - struct tm* time_struct; - time_struct = localtime(&tp.tv_sec); + tm* time_struct = localtime(&tp.tv_sec); return (ccos_date_t){ - .year = (uint16_t)(time_struct->tm_year + 1900), - .month = (uint8_t)(time_struct->tm_mon + 1), - .day = (uint8_t)(time_struct->tm_mday), - .hour = (uint8_t)(time_struct->tm_hour), - .minute = (uint8_t)(time_struct->tm_min), - .second = (uint8_t)(time_struct->tm_sec), - .tenthOfSec = (uint8_t)(tp.tv_nsec / 100000000), - .dayOfWeek = (uint8_t)(time_struct->tm_wday + 1), - .dayOfYear = (uint16_t)(time_struct->tm_yday + 1), + .year = static_cast(time_struct->tm_year + 1900), + .month = static_cast(time_struct->tm_mon + 1), + .day = static_cast(time_struct->tm_mday), + .hour = static_cast(time_struct->tm_hour), + .minute = static_cast(time_struct->tm_min), + .second = static_cast(time_struct->tm_sec), + .tenthOfSec = static_cast(tp.tv_nsec / 100000000), + .dayOfWeek = static_cast(time_struct->tm_wday + 1), + .dayOfYear = static_cast(time_struct->tm_yday + 1), }; } -//[Service functions] - void replace_char_in_place(char* src, char from, char to) { for (int i = 0; i < strlen(src); ++i) { if (src[i] == from) { @@ -48,6 +50,13 @@ void replace_char_in_place(char* src, char from, char to) { } } +QString short_string_to_qstring(const short_string_t* short_string) { + if (short_string == nullptr) { + return {}; + } + return QString::fromLatin1(short_string->data, short_string->length); +} + //*Get file version and convert to QString ("A.B.C") QString ccosGetFileVersionQstr(ccos_inode_t* file){ ccos_version_t ver = ccos_get_file_version(file); @@ -120,13 +129,10 @@ int checkFreeSp(DiskPanel& from, DiskPanel& to, QList calledElems, size_t* needs){ //*For copy - size_t frsp; - - ccos_error_t err = ccos_calc_free_space(&to.disk, &frsp); - if (err != CCOS_OK){ + size_t frsp = 0; + if (ccos_calc_free_space(to.disk, &frsp) != CCOS_OK){ return -2; } - *needs = 0; for (int i = 0; i < calledElems.size(); i+=7){ ccos_inode_t* file = from.inodes[calledElems[i]->row()]; @@ -134,7 +140,7 @@ int checkFreeSp(DiskPanel& from, DiskPanel& to, if (ccos_is_dir(file)){ uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(&from.disk, file, &fils, &dirdata); + ccos_get_dir_contents(from.disk, file, &fils, &dirdata); for(int j = 0; j < fils; j++){ *needs += dirdata[j]->desc.file_size; @@ -153,9 +159,8 @@ int checkFreeSp(DiskPanel& from, DiskPanel& to, } int checkFreeSp(ccos_disk_t* disk, QStringList files, size_t* needs){ //*For add - size_t frsp; - ccos_error_t err = ccos_calc_free_space(disk, &frsp); - if (err != CCOS_OK){ + size_t frsp = 0; + if (ccos_calc_free_space(disk, &frsp) != CCOS_OK){ return -2; } *needs = 0; @@ -236,8 +241,8 @@ int saveFileQt(QString path, uint8_t* file_data, size_t file_size, QWidget* pare //*Dump file from image to path int dumpFileQt(ccos_disk_t* disk, ccos_inode_t* file, QString path, QWidget* parent){ - char* fnam = short_string_to_string(ccos_get_file_name(file)); - replace_char_in_place(fnam, '/', '_'); + QString fnam = short_string_to_qstring(ccos_get_file_name(file)); + fnam.replace('/', '_'); QString fpath = QDir(path).filePath(fnam); @@ -247,12 +252,9 @@ int dumpFileQt(ccos_disk_t* disk, ccos_inode_t* file, QString path, QWidget* par if (ccos_read_file(disk, file, &file_data, &file_size) != CCOS_OK){ QMessageBox::critical(parent, "Failed to read file from image", QString("Unable to read file \"%1\": Unable to get file contents!").arg(fnam)); - free(fnam); return -1; } - free(fnam); - if (saveFileQt(fpath, file_data, file_size, parent) == -1){ free(file_data); return -1; @@ -265,6 +267,7 @@ int dumpFileQt(ccos_disk_t* disk, ccos_inode_t* file, QString path, QWidget* par //*Dump dir from image to path int dumpDirQt(ccos_disk_t* disk, ccos_inode_t* dir, QString path, QWidget* parent){ char name[CCOS_MAX_FILE_NAME]; + memset(name, 0, CCOS_MAX_FILE_NAME); ccos_parse_file_name(dir, name, nullptr, nullptr, nullptr); replace_char_in_place(name, '/', '_'); @@ -300,17 +303,16 @@ int dumpImgQt(ccos_disk_t* disk, QString path, QString altname, QWidget* parent) return -1; } - char* name = short_string_to_string(ccos_get_file_name(root_dir)); + QString name = short_string_to_qstring(ccos_get_file_name(root_dir)); QString dnam; - if (strcmp(name, "") == 0){ + if (name.isEmpty()){ dnam = altname; } else{ - replace_char_in_place(name, '/', '_'); dnam = name; + dnam.replace('/', '_'); } - free(name); QString dpath = QDir(path).filePath(dnam); @@ -344,7 +346,7 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) panel.inodes.clear(); uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(&panel.disk, directory, &fils, &dirdata); + ccos_get_dir_contents(panel.disk, directory, &fils, &dirdata); if (panel_idx == 0){ tableWidget = ui->tableWidget; label = ui->label; @@ -359,9 +361,9 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) } for (int row = tableWidget->rowCount(); 0<=row; row--) tableWidget->removeRow(row); - const short_string_t* labd = ccos_get_disk_label(&panel.disk); + QString labd = short_string_to_qstring(ccos_get_disk_label(panel.disk)); msg = "Disk %1 - %2%3"; - box->setTitle(msg.arg(disk_name, labd->length > 0 ? QString::fromLatin1(labd->data, labd->length) : "No label", panel.modified ? "*" : "")); + box->setTitle(msg.arg(disk_name, !labd.isEmpty() ? labd : "No label", panel.modified ? "*" : "")); char basename[CCOS_MAX_FILE_NAME]; char type[CCOS_MAX_FILE_NAME]; if (noRoot){ @@ -384,9 +386,8 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) addFile(tableWidget, text); } - size_t free_space; - ccos_error_t err = ccos_calc_free_space(&panel.disk, &free_space); - if (err != CCOS_OK){ + size_t free_space = 0; + if (ccos_calc_free_space(panel.disk, &free_space) != CCOS_OK){ label->setText("Free space: FAILED TO CALCULATE!"); free(dirdata); return; @@ -457,9 +458,6 @@ int saveBox(QString disk, QWidget* parent){ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_unique()){ ui->setupUi(this); - // FIXME: How to enable trace in new version of ccos-disk-utils? - // trace_init(false); - QMainWindow::setWindowTitle(QString("GRiDISK Commander v")+_PVER_); for (auto* tw : {ui->tableWidget, ui->tableWidget_2}) { tw->horizontalHeader()->resizeSection(0, 155); @@ -566,11 +564,10 @@ void MainWindow::Add(){ void MainWindow::AddDirs(QStringList dirs){ auto& panel = *panels[active_panel]; - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); + ccos_inode_t* root = ccos_get_root_dir(panel.disk); for (const auto& dir : dirs) { - size_t frees; - ccos_error_t err = ccos_calc_free_space(&panel.disk, &frees); - if (err != CCOS_OK){ + size_t frees = 0; + if (ccos_calc_free_space(panel.disk, &frees) != CCOS_OK){ QMessageBox::critical(this, "Calculation error", "Program can't calculate free space in the image!"); break; @@ -581,7 +578,7 @@ void MainWindow::AddDirs(QStringList dirs){ QString("Requires %1 bytes of additional disk space to make dir!").arg(1024-frees)); break; } - ccos_inode_t* newdir = ccos_create_dir(&panel.disk, root, QFileInfo(dir).fileName().toStdString().c_str()); + ccos_inode_t* newdir = ccos_create_dir(panel.disk, root, QFileInfo(dir).fileName().toStdString().c_str()); if (newdir == nullptr){ QMessageBox::critical(this, "Failed to create folder", "Program can't create a folder in the image!"); @@ -606,17 +603,17 @@ void MainWindow::AddDirs(QStringList dirs){ int MainWindow::AddFiles(QStringList files, ccos_inode_t* copyTo){ auto& panel = *panels[active_panel]; size_t needs = 0; - int retop = checkFreeSp(&panel.disk, files, &needs); + int retop = checkFreeSp(panel.disk, files, &needs); if (retop == -2) { QMessageBox::critical(this, "Calculation error", "Program can't calculate free space in the image!"); return -1; } else if (retop == -1) { - size_t free; - ccos_calc_free_space(&panel.disk, &free); + size_t free_space = 0; + ccos_calc_free_space(panel.disk, &free_space); QMessageBox::critical(this, "Not enough space", - QString("Requires %1 bytes of additional disk space to add!").arg(needs-free)); + QString("Requires %1 bytes of additional disk space to add!").arg(needs-free_space)); return -1; } for (const auto& file : files){ @@ -649,7 +646,7 @@ int MainWindow::AddFiles(QStringList files, ccos_inode_t* copyTo){ } } if (readFileQt(file, &fdat, &fsiz, this) == 0){ - if (ccos_add_file(&panel.disk, copyTo, fdat, fsiz, fname.c_str()) == nullptr){ + if (ccos_add_file(panel.disk, copyTo, fdat, fsiz, fname.c_str()) == nullptr){ QMessageBox::critical(this, "Error", QString("Can't add \"%1\" to the image! Skipping...").arg(fname.c_str())); } @@ -707,14 +704,21 @@ void MainWindow::AnotherPart(bool fromMenu){ panels[topan].emplace(); auto& dst = *panels[topan]; - dst.disk = src.disk; - dst.disk.size = parts[selctd].size; - dst.disk.data = src.hdd_data->data() + parts[selctd].offset; + if (usedisk == topan && dst.disk != nullptr) { + free(dst.disk); + dst.disk = nullptr; + } + uint8_t* part_data = src.hdd_data->data() + parts[selctd].offset; + dst.disk = (ccos_disk_sector_size(src.disk) == GRID_BUBBLE_SECTOR_SIZE) + ? ccos_disk_new_bubble(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)) + : ccos_disk_new_extdisk(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)); - ccos_inode_t* root = ccos_get_root_dir(&dst.disk); + ccos_inode_t* root = ccos_get_root_dir(dst.disk); if (root == nullptr){ QMessageBox::critical(this, "Incorrect Image File", "Image broken or have non-GRiD format!"); + free(dst.disk); + dst.disk = nullptr; if (usedisk == topan){ src.hdd_data.reset(); src.hdd_mode = false; @@ -808,8 +812,8 @@ void MainWindow::Copy(){ return; } else if (retop == -1) { - size_t frsp; - ccos_calc_free_space(&dst.disk, &frsp); + size_t frsp = 0; + ccos_calc_free_space(dst.disk, &frsp); QMessageBox::critical(this, "Not enough space", QString("Requires %1 bytes of additional disk space to copy").arg(needs-frsp)); return; @@ -818,14 +822,14 @@ void MainWindow::Copy(){ if (src.inodes[called[t]->row()]==nullptr) continue; if (ccos_is_dir(src.inodes[called[t]->row()])) { - if (dst.current_dir->header.file_id != ccos_get_root_dir(&dst.disk)->header.file_id) { + if (dst.current_dir->header.file_id != ccos_get_root_dir(dst.disk)->header.file_id) { QMessageBox::critical(this, "Copying to non-root", "Folders can be copied only to root folder!"); return; } char newname[CCOS_MAX_FILE_NAME] = {}; ccos_parse_file_name(src.inodes[called[t]->row()], newname, nullptr, nullptr, nullptr); - ccos_inode_t* newdir = ccos_create_dir(&dst.disk, ccos_get_root_dir(&dst.disk), newname); + ccos_inode_t* newdir = ccos_create_dir(dst.disk, ccos_get_root_dir(dst.disk), newname); if (newdir == nullptr){ QMessageBox::critical(this, "Failed to create folder", "Program can't create a folder in the image!"); @@ -833,19 +837,19 @@ void MainWindow::Copy(){ } uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(&src.disk, src.inodes[called[t]->row()], &fils, &dirdata); + ccos_get_dir_contents(src.disk, src.inodes[called[t]->row()], &fils, &dirdata); for (int c = 0; c < fils; c++) { - ccos_copy_file(&src.disk, dirdata[c], &dst.disk, newdir); + ccos_copy_file(src.disk, dirdata[c], dst.disk, newdir); } } else { - if (dst.current_dir->header.file_id == ccos_get_root_dir(&dst.disk)->header.file_id) { + if (dst.current_dir->header.file_id == ccos_get_root_dir(dst.disk)->header.file_id) { QMessageBox::critical(this, "Copying to root", "Files can be copied only to non-root folder!"); return; } - ccos_copy_file(&src.disk, src.inodes[called[t]->row()], - &dst.disk, dst.current_dir); + ccos_copy_file(src.disk, src.inodes[called[t]->row()], + dst.disk, dst.current_dir); } } dst.modified = true; @@ -884,8 +888,8 @@ void MainWindow::CopyLoc() { return; } else if (retop == -1) { - size_t frsp; - ccos_calc_free_space(&panel.disk, &frsp); + size_t frsp = 0; + ccos_calc_free_space(panel.disk, &frsp); QMessageBox::critical(this, "Not enough space", QString("Requires %1 bytes of additional disk space to copy").arg(needs-frsp)); return; @@ -896,11 +900,11 @@ void MainWindow::CopyLoc() { dlg.setName("Select the directory"); dlg.setInfo("Select the directory where the file(s) will be copied:"); - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); + ccos_inode_t* root = ccos_get_root_dir(panel.disk); uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(&panel.disk, root, &fils, &dirdata); + ccos_get_dir_contents(panel.disk, root, &fils, &dirdata); char basename[CCOS_MAX_FILE_NAME]; @@ -923,8 +927,8 @@ void MainWindow::CopyLoc() { for (int t = 0; t < called.size(); t+=7){ if (panel.inodes[called[t]->row()]==nullptr) continue; - ccos_copy_file(&panel.disk, panel.inodes[called[t]->row()], - &panel.disk, dirdata[dlg.getIndex()]); + ccos_copy_file(panel.disk, panel.inodes[called[t]->row()], + panel.disk, dirdata[dlg.getIndex()]); } panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); @@ -942,9 +946,9 @@ void MainWindow::CopyLoc() { if (name == "") break; else if (validString(name, true, this) != -1){ - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); + ccos_inode_t* root = ccos_get_root_dir(panel.disk); - ccos_inode_t* newdir = ccos_create_dir(&panel.disk, root, name.toStdString().c_str()); + ccos_inode_t* newdir = ccos_create_dir(panel.disk, root, name.toStdString().c_str()); if (newdir == nullptr){ QMessageBox::critical(this, "Failed to create folder", "Program can't create a folder in the image!"); @@ -953,11 +957,11 @@ void MainWindow::CopyLoc() { uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(&panel.disk, panel.inodes[called[t]->row()], &fils, &dirdata); + ccos_get_dir_contents(panel.disk, panel.inodes[called[t]->row()], &fils, &dirdata); for(int i = 0; i < fils; i++){ - ccos_copy_file(&panel.disk, dirdata[i], - &panel.disk, newdir); + ccos_copy_file(panel.disk, dirdata[i], + panel.disk, newdir); } panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); @@ -993,25 +997,23 @@ void MainWindow::Date(){ dlg.init(file->desc.name, cre, mod, exp); if (dlg.exec()){ dlg.retDates(&cre, &mod, &exp); - ccos_set_creation_date(&panel.disk, file, cre); - ccos_set_mod_date(&panel.disk, file, mod); - ccos_set_exp_date(&panel.disk, file, exp); + ccos_set_creation_date(panel.disk, file, cre); + ccos_set_mod_date(panel.disk, file, mod); + ccos_set_exp_date(panel.disk, file, exp); panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); } } void MainWindow::DebTrace(){ - if (!ui->actionDebtrace->isChecked()){ - TRACE("ccos_image debug trace disabled"); - } - - // FIXME: How to enable trace in new version of ccos-disk-utils? - // trace_init(ui->actionDebtrace->isChecked()); - if (ui->actionDebtrace->isChecked()){ + trace = fprintf; TRACE("ccos_image debug trace enabled"); } + else{ + TRACE("ccos_image debug trace disabled"); + trace = nullptr; + } } void MainWindow::Delete(){ @@ -1038,7 +1040,7 @@ void MainWindow::Delete(){ for (int t = 0; t< called.size(); t+=7){ if (panel.inodes[called[t]->row()]==nullptr) continue; - ccos_delete_file(&panel.disk, panel.inodes[called[t]->row()]); + ccos_delete_file(panel.disk, panel.inodes[called[t]->row()]); } panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); @@ -1104,9 +1106,9 @@ void MainWindow::Extract(){ if (panel.inodes[called[t]->row()]==nullptr) continue; if (ccos_is_dir(panel.inodes[called[t]->row()])) - dumpDirQt(&panel.disk, panel.inodes[called[t]->row()], todir, this); + dumpDirQt(panel.disk, panel.inodes[called[t]->row()], todir, this); else - dumpFileQt(&panel.disk, panel.inodes[called[t]->row()], todir, this); + dumpFileQt(panel.disk, panel.inodes[called[t]->row()], todir, this); } } } @@ -1118,7 +1120,7 @@ void MainWindow::ExtractAll(){ QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (todir == "") return; - int res = dumpImgQt(&panel.disk, todir, QFileInfo(panel.path).baseName(), this); + int res = dumpImgQt(panel.disk, todir, QFileInfo(panel.path).baseName(), this); if (res == -1){ QMessageBox::critical(this, "Unable to extract image", "Unable to extract image. Please check the path."); } @@ -1157,14 +1159,14 @@ void MainWindow::Label(){ dsk= "I"; else dsk= "II"; - const short_string_t* fname = ccos_get_disk_label(&panel.disk); + QString fname = short_string_to_qstring(ccos_get_disk_label(panel.disk)); QString nameQ = QInputDialog::getText(this, tr("New label"), - QString("Set new label for the disk %1:").arg(dsk), QLineEdit::Normal, QString::fromLatin1(fname->data, fname->length)); + QString("Set new label for the disk %1:").arg(dsk), QLineEdit::Normal, fname); if (validString(nameQ, false, this) == -1) return; - ccos_set_disk_label(&panel.disk, nameQ.toStdString().c_str()); + ccos_set_disk_label(panel.disk, nameQ.toStdString().c_str()); panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); } @@ -1205,38 +1207,42 @@ bool MainWindow::suggestSelectAnotherPartition() { return msgBox.exec() == QMessageBox::Yes; } -static std::optional tryOpenAs( +static ccos_disk_t* tryOpenAs( uint8_t* data, size_t size, uint16_t sector_size, uint16_t superblock_fid, uint16_t bitmap_fid ) { - ccos_disk_t disk = { sector_size, superblock_fid, bitmap_fid, size, data }; + ccos_disk_t* disk = (sector_size == GRID_BUBBLE_SECTOR_SIZE) + ? ccos_disk_new_bubble(data, size, superblock_fid, bitmap_fid) + : ccos_disk_new_extdisk(data, size, superblock_fid, bitmap_fid); + if (disk == nullptr) { + return nullptr; + } - // Open the root directory using values from the disk structure. - ccos_inode_t* root = ccos_get_root_dir(&disk); + ccos_inode_t* root = ccos_get_root_dir(disk); if (root == nullptr) { - return std::nullopt; - } else { - return std::optional(disk); + free(disk); + return nullptr; } + + return disk; } -static std::optional tryFromBootsector(uint8_t* data, size_t size) { - if (size < 512) { - return std::nullopt; +static ccos_disk_t* tryFromBootsector(uint8_t* data, size_t size) { + if (size < sizeof(ccos_boot_sector_t)) { + return nullptr; } - const ccos_boot_sector_t* boot_sector = (const ccos_boot_sector_t*)data; - - uint16_t superblock = boot_sector->superblock_fid; - uint16_t bitmap = boot_sector->bitmap_fid; + const ccos_boot_sector_t* boot = (const ccos_boot_sector_t*)data; + uint16_t superblock = boot->superblock_fid; + uint16_t bitmap = boot->bitmap_fid; if (superblock == 0 || bitmap == 0) { - return std::nullopt; + return nullptr; } - return tryOpenAs(data, size, 512, superblock, bitmap); + return tryOpenAs(data, size, GRID_FLOPPY_SECTOR_SIZE, superblock, bitmap); } -static std::optional tryDetectBySize(uint8_t* data, size_t size) { +static ccos_disk_t* tryDetectBySize(uint8_t* data, size_t size) { if (size == 384 * 1024) { return tryOpenAs(data, size, GRID_BUBBLE_SECTOR_SIZE, GRID_BUBBLE_SUPERBLOCK_FID, GRID_BUBBLE_BITMAP_FID); @@ -1251,11 +1257,11 @@ static std::optional tryDetectBySize(uint8_t* data, size_t size) { GRID_HDD_SECTOR_SIZE, GRID_HDD_SUPERBLOCK_FID, GRID_HDD_BITMAP_FID); } - return std::nullopt; + return nullptr; } -void MainWindow::openValidNonMbrDisk(QString path, ccos_disk_t disk) { - ccos_inode_t* root = ccos_get_root_dir(&disk); +void MainWindow::openValidNonMbrDisk(QString path, ccos_disk_t* disk) { + ccos_inode_t* root = ccos_get_root_dir(disk); Q_ASSERT(root != nullptr); panels[active_panel].emplace(); @@ -1273,8 +1279,8 @@ static bool isMbrDisk(const uint8_t* data, size_t size) { return size > 0x200 && data[0x1FE] == 0x55 && data[0x1FF] == 0xAA; } -static std::optional tryOpenMbrPartition(uint8_t* data, MbrPartition& partition) { - std::optional disk = tryFromBootsector(data + partition.offset, partition.size); +static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition) { + ccos_disk_t* disk = tryFromBootsector(data + partition.offset, partition.size); if (disk) { return disk; } @@ -1323,9 +1329,9 @@ void MainWindow::tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size) int selected = dlg.getIndex(); - std::optional disk = tryOpenMbrPartition(hdd_data.data(), parts[selected]); + ccos_disk_t* disk = tryOpenMbrPartition(hdd_data.data(), parts[selected]); if (disk) { - openValidMbrPartition(path, std::move(hdd_data), selected, *disk); + openValidMbrPartition(path, std::move(hdd_data), selected, disk); break; } @@ -1334,8 +1340,8 @@ void MainWindow::tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size) } } -void MainWindow::openValidMbrPartition(QString path, std::vector hdd_data, int partition_index, ccos_disk_t disk) { - ccos_inode_t* root = ccos_get_root_dir(&disk); +void MainWindow::openValidMbrPartition(QString path, std::vector hdd_data, int partition_index, ccos_disk_t* disk) { + ccos_inode_t* root = ccos_get_root_dir(disk); Q_ASSERT(root != nullptr); panels[active_panel].emplace(); @@ -1372,13 +1378,13 @@ void MainWindow::loadCustomImg(QString path, uint8_t* data, size_t size) { break; } - uint16_t sector_size, superblock; - cdlg.GetParams(§or_size, &superblock, nullptr, nullptr); + uint16_t sector_size, superblock, bitmap; + cdlg.GetParams(§or_size, &superblock, &bitmap, nullptr, nullptr); - // TODO: Allow to select bitmask. - std::optional disk = tryOpenAs(data, size, sector_size, superblock, superblock-1); + ccos_disk_t* disk = tryOpenAs(data, size, sector_size, superblock, bitmap); if (disk) { - openValidNonMbrDisk(path, *disk); + openValidNonMbrDisk(path, disk); + return; } QMessageBox::critical(this, "Failed to open", @@ -1406,15 +1412,15 @@ void MainWindow::LoadImg(QString path) { return; } - std::optional disk = tryFromBootsector(data, size); + ccos_disk_t* disk = tryFromBootsector(data, size); if (disk) { - openValidNonMbrDisk(path, *disk); + openValidNonMbrDisk(path, disk); return; } disk = tryDetectBySize(data, size); if (disk) { - openValidNonMbrDisk(path, *disk); + openValidNonMbrDisk(path, disk); return; } @@ -1441,9 +1447,8 @@ void MainWindow::MakeDir(){ if (name == "") break; else if (validString(name, true, this) != -1){ - size_t frsp; - ccos_error_t err = ccos_calc_free_space(&panel.disk, &frsp); - if (err != CCOS_OK) { + size_t frsp = 0; + if (ccos_calc_free_space(panel.disk, &frsp) != CCOS_OK) { QMessageBox::critical(this, "Calculation error", "Program can't calculate free space in the image!"); break; @@ -1453,8 +1458,8 @@ void MainWindow::MakeDir(){ QString("Requires %1 bytes of additional disk space to make dir!").arg(1024-frsp)); break; } - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); - if (ccos_create_dir(&panel.disk, root, name.toStdString().c_str()) == nullptr){ + ccos_inode_t* root = ccos_get_root_dir(panel.disk); + if (ccos_create_dir(panel.disk, root, name.toStdString().c_str()) == nullptr){ QMessageBox::critical(this, "Failed to create folder", "Program can't create a folder in the image!"); break; @@ -1477,7 +1482,7 @@ void MainWindow::NewImage(){ if (dlg.exec() == 1) { uint16_t sect, subl, isize; QString labl; - dlg.GetParams(§, &subl, &isize, &labl); + dlg.GetParams(§, &subl, nullptr, &isize, &labl); if (labl != "" && validString(labl, false, this) == -1) { continue; @@ -1496,11 +1501,11 @@ void MainWindow::NewImage(){ } if (labl != "") { - ccos_set_disk_label(&panel.disk, labl.toStdString().c_str()); + ccos_set_disk_label(panel.disk, labl.toStdString().c_str()); } panel.modified = true; - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); + ccos_inode_t* root = ccos_get_root_dir(panel.disk); panel.current_dir = root; fillTable(active_panel, root, false); break; @@ -1524,8 +1529,8 @@ void MainWindow::OpenDir(){ if (dir == nullptr && !panel.in_subdir) return; if (called->row() == 0 && panel.in_subdir){ - ccos_inode_t* root = ccos_get_root_dir(&panel.disk); - panel.current_dir = ccos_get_parent_dir(&panel.disk, panel.current_dir); + ccos_inode_t* root = ccos_get_root_dir(panel.disk); + panel.current_dir = ccos_get_parent_dir(panel.disk, panel.current_dir); if (panel.current_dir == root) panel.in_subdir = false; fillTable(active_panel, panel.current_dir, panel.in_subdir); @@ -1587,10 +1592,10 @@ void MainWindow::Rename(){ "File name or type can't be empty!"); } else if (validString(newname, true, this) != -1 && validString(newtype, true, this) != -1){ - ccos_rename_file(&panel.disk, reninode, newname.toStdString().c_str(), + ccos_rename_file(panel.disk, reninode, newname.toStdString().c_str(), newtype.toStdString().c_str()); panel.modified = true; - fillTable(active_panel, ccos_get_parent_dir(&panel.disk, reninode), + fillTable(active_panel, ccos_get_parent_dir(panel.disk, reninode), panel.in_subdir); break; } @@ -1620,7 +1625,7 @@ void MainWindow::Save(){ res = saveFileQt(panel.path, panel.hdd_data->data(), panel.hdd_data->size(), this); } else{ - res = saveFileQt(panel.path, panel.disk.data, panel.disk.size, this); + res = saveFileQt(panel.path, ccos_disk_data(panel.disk), ccos_disk_size(panel.disk), this); } if (res == -1){ @@ -1655,7 +1660,7 @@ void MainWindow::SaveAs(){ res = saveFileQt(nameQ, panel.hdd_data->data(), panel.hdd_data->size(), this); } else{ - res = saveFileQt(nameQ, panel.disk.data, panel.disk.size, this); + res = saveFileQt(nameQ, ccos_disk_data(panel.disk), ccos_disk_size(panel.disk), this); } if (res == -1){ @@ -1687,9 +1692,17 @@ void MainWindow::SavePart(){ SaveAs(); if (!panel.modified){ - uint8_t* imdat = (uint8_t*)calloc(panel.disk.size, sizeof(uint8_t)); - memcpy(imdat, panel.disk.data, panel.disk.size); - panel.disk.data = imdat; + // Detach the partition data from the shared HDD buffer by copying it + // into a standalone buffer owned by a new disk handle. + size_t dsize = ccos_disk_size(panel.disk); + uint8_t* imdat = (uint8_t*)calloc(dsize, sizeof(uint8_t)); + memcpy(imdat, ccos_disk_data(panel.disk), dsize); + ccos_disk_t* new_disk = (ccos_disk_sector_size(panel.disk) == GRID_BUBBLE_SECTOR_SIZE) + ? ccos_disk_new_bubble(imdat, dsize, ccos_disk_superblock(panel.disk), ccos_disk_bitmap(panel.disk)) + : ccos_disk_new_extdisk(imdat, dsize, ccos_disk_superblock(panel.disk), ccos_disk_bitmap(panel.disk)); + // The old handle wraps data inside hdd_data, so release the handle only. + free(panel.disk); + panel.disk = new_disk; panel.hdd_data.reset(); } else{ @@ -1745,7 +1758,7 @@ void MainWindow::Version(){ dlg.init(file->desc.name, ver); if (dlg.exec() == 1){ ver = dlg.retVer(); - ccos_set_file_version(&panel.disk, file, ver); + ccos_set_file_version(panel.disk, file, ver); panel.modified = true; fillTable(active_panel, panel.current_dir, panel.in_subdir); } diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index 3209219..cbbe1a4 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -35,7 +34,7 @@ struct MbrPartition { struct DiskPanel { QString path; - ccos_disk_t disk = {}; + ccos_disk_t* disk = nullptr; ccos_inode_t* current_dir = nullptr; std::vector inodes; bool modified = false; @@ -46,8 +45,15 @@ struct DiskPanel { std::optional hdd_partition; ~DiskPanel() { - if (!hdd_data && disk.data != nullptr) - free(disk.data); + if (disk == nullptr) + return; + if (hdd_mode) { + // The image data is shared with hdd_data (an offset into the HDD + // buffer), so only release the opaque disk handle and keep the data. + free(disk); + } else { + ccos_disk_free(disk); + } } }; @@ -101,9 +107,9 @@ public slots: bool isFileAlreadyOpened(const QString& path); void handleAlreadyOpenedImg(QString path); bool suggestSelectAnotherPartition(); - void openValidNonMbrDisk(QString path, ccos_disk_t disk); + void openValidNonMbrDisk(QString path, ccos_disk_t* disk); void tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size); - void openValidMbrPartition(QString path, std::vector hdddata, int partition_index, ccos_disk_t disk); + void openValidMbrPartition(QString path, std::vector hdddata, int partition_index, ccos_disk_t* disk); void loadCustomImg(QString path, uint8_t* data, size_t size); void fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot); From c988d9652237b89365b44056e01df498c84dd81d Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 24 Jul 2026 02:08:10 +0300 Subject: [PATCH 02/36] add bitmap block selector to custom image dialog --- dialogs/custdlg.cpp | 27 ++++++++-- dialogs/custdlg.h | 3 +- dialogs/custdlg.ui | 119 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 6 deletions(-) diff --git a/dialogs/custdlg.cpp b/dialogs/custdlg.cpp index 5b4be1b..cb6f437 100644 --- a/dialogs/custdlg.cpp +++ b/dialogs/custdlg.cpp @@ -12,14 +12,15 @@ CustDlg::CustDlg(QWidget *parent, bool openMode) : ui->spinBox->setFont(HexFont); connect(ui->radioButton_5, SIGNAL(toggled(bool)), this, SLOT(SblFocus(bool))); + connect(ui->radioButton_12, SIGNAL(toggled(bool)), this, SLOT(BmpFocus(bool))); connect(ui->radioButton_9, SIGNAL(toggled(bool)), this, SLOT(SizFocus(bool))); if (openMode) { ui->groupBox_3->setVisible(false); ui->groupBox_4->setVisible(false); CustDlg::setWindowTitle("Custom image opening"); - CustDlg::setMinimumHeight(245); - CustDlg::setMaximumHeight(245); + CustDlg::setMinimumHeight(380); + CustDlg::setMaximumHeight(380); } } @@ -39,8 +40,16 @@ void CustDlg::SizFocus(bool activ) { } } +void CustDlg::BmpFocus(bool activ) { + ui->spinBox_3->setEnabled(activ); + if (activ) { + ui->spinBox_3->setFocus(); + ui->spinBox_3->selectAll(); + } +} -void CustDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* isize, QString* labl) { + +void CustDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* isize, QString* labl) { *sect = ui->radioButton->isChecked() ? 512 : 256; if (ui->radioButton_3->isChecked()) { @@ -53,6 +62,18 @@ void CustDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* isize, QString *subl = ui->spinBox->value(); } + if (bmp != NULL) { + if (ui->radioButton_10->isChecked()) { + *bmp = 0x120; + } + else if (ui->radioButton_11->isChecked()) { + *bmp = 0x3FD; + } + else { + *bmp = ui->spinBox_3->value(); + } + } + if (isize != NULL) { if (ui->radioButton_6->isChecked()) { *isize = 360; diff --git a/dialogs/custdlg.h b/dialogs/custdlg.h index 7aefed9..d561ca4 100644 --- a/dialogs/custdlg.h +++ b/dialogs/custdlg.h @@ -13,12 +13,13 @@ class CustDlg : public QDialog public slots: void SblFocus(bool activ); + void BmpFocus(bool activ); void SizFocus(bool activ); public: explicit CustDlg(QWidget *parent = nullptr, bool openMode = false); ~CustDlg(); - void GetParams(uint16_t* sect, uint16_t* subl, uint16_t* isize, QString* labl); + void GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* isize, QString* labl); private: Ui::CustDlg *ui; diff --git a/dialogs/custdlg.ui b/dialogs/custdlg.ui index 8734f21..723d395 100644 --- a/dialogs/custdlg.ui +++ b/dialogs/custdlg.ui @@ -5,13 +5,13 @@ 400 - 445 + 580 400 - 445 + 580 @@ -179,6 +179,121 @@ + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + 0 + 114 + + + + Bitmap: + + + + + 20 + 30 + 171 + 23 + + + + 0x120 (Floppy / HDD) + + + true + + + + + + 200 + 30 + 141 + 23 + + + + 0x3FD (Bubble) + + + + + + 120 + 60 + 41 + 23 + + + + 0x + + + + + false + + + + 160 + 58 + 61 + 26 + + + + 1 + + + 65535 + + + 1 + + + 16 + + + + + + 2 + 90 + 380 + 20 + + + + + true + false + + + + Supported bitmap values: 0x0001 - 0xFFFF + + + Qt::AlignCenter + + + + From 10237e98696214b3b0e74e93b67a5f34a2df5e19 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 24 Jul 2026 19:15:59 +0300 Subject: [PATCH 03/36] fix autodetect for partial hdd dumps --- dialogs/mainwindow.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 3c595b0..41700c4 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1243,21 +1243,20 @@ static ccos_disk_t* tryFromBootsector(uint8_t* data, size_t size) { } static ccos_disk_t* tryDetectBySize(uint8_t* data, size_t size) { - if (size == 384 * 1024) { - return tryOpenAs(data, size, - GRID_BUBBLE_SECTOR_SIZE, GRID_BUBBLE_SUPERBLOCK_FID, GRID_BUBBLE_BITMAP_FID); - } - if (size == 360 * 1024 || size == 720 * 1024) { - return tryOpenAs(data, size, - GRID_FLOPPY_SECTOR_SIZE, GRID_FLOPPY_SUPERBLOCK_FID, GRID_FLOPPY_BITMAP_FID); + ccos_disk_t* disk = tryOpenAs(data, size, + GRID_BUBBLE_SECTOR_SIZE, GRID_BUBBLE_SUPERBLOCK_FID, GRID_BUBBLE_BITMAP_FID); + if (disk) { + return disk; } - if (size == 10 * 1024 * 1024 || size == 20 * 1024 * 1024) { - return tryOpenAs(data, size, - GRID_HDD_SECTOR_SIZE, GRID_HDD_SUPERBLOCK_FID, GRID_HDD_BITMAP_FID); + disk = tryOpenAs(data, size, + GRID_FLOPPY_SECTOR_SIZE, GRID_FLOPPY_SUPERBLOCK_FID, GRID_FLOPPY_BITMAP_FID); + if (disk) { + return disk; } - return nullptr; + return tryOpenAs(data, size, + GRID_HDD_SECTOR_SIZE, GRID_HDD_SUPERBLOCK_FID, GRID_HDD_BITMAP_FID); } void MainWindow::openValidNonMbrDisk(QString path, ccos_disk_t* disk) { From e48a085c59758498254bd1f4817ab780ed1a7dfc Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 24 Jul 2026 22:43:43 +0300 Subject: [PATCH 04/36] update panels and it's titles properly --- dialogs/mainwindow.cpp | 150 ++++++++++++++++++++++------------------- dialogs/mainwindow.h | 3 + 2 files changed, 85 insertions(+), 68 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 41700c4..d920de1 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -403,6 +403,18 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) free(dirdata); } +void MainWindow::updatePanelTitle(int panel_idx) { + if (!panels[panel_idx] || panels[panel_idx]->disk == nullptr) + return; + auto& panel = *panels[panel_idx]; + QGroupBox* box = (panel_idx == 0) ? ui->groupBox : ui->groupBox_2; + QString disk_name = (panel_idx == 0) ? "I" : "II"; + QString labd = short_string_to_qstring(ccos_get_disk_label(panel.disk)); + box->setTitle(QString("Disk %1 - %2%3") + .arg(disk_name, !labd.isEmpty() ? labd : "No label", + panel.modified ? "*" : "")); +} + //*Parse Hard Disk MBR std::vector parseMbr(const uint8_t* data, size_t size) { std::vector partitions; @@ -697,35 +709,47 @@ void MainWindow::AnotherPart(bool fromMenu){ int topan = (fromMenu && dlg.isChecked()) ? !active_panel : active_panel; - if (usedisk != topan && panels[topan]) - CloseImg(); + if (usedisk != topan && panels[topan]) { + int saved_active = active_panel; + active_panel = topan; + int closed = CloseImg(); + active_panel = saved_active; + if (!closed) { + refreshActivePanelUI(); + return; + } + } - if (!panels[topan]) + bool fresh_target = !panels[topan]; + if (fresh_target) panels[topan].emplace(); auto& dst = *panels[topan]; - if (usedisk == topan && dst.disk != nullptr) { - free(dst.disk); - dst.disk = nullptr; - } + uint8_t* part_data = src.hdd_data->data() + parts[selctd].offset; - dst.disk = (ccos_disk_sector_size(src.disk) == GRID_BUBBLE_SECTOR_SIZE) + ccos_disk_t* new_disk = (ccos_disk_sector_size(src.disk) == GRID_BUBBLE_SECTOR_SIZE) ? ccos_disk_new_bubble(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)) : ccos_disk_new_extdisk(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)); - ccos_inode_t* root = ccos_get_root_dir(dst.disk); + ccos_inode_t* root = (new_disk != nullptr) ? ccos_get_root_dir(new_disk) : nullptr; if (root == nullptr){ QMessageBox::critical(this, "Incorrect Image File", "Image broken or have non-GRiD format!"); - free(dst.disk); - dst.disk = nullptr; - if (usedisk == topan){ - src.hdd_data.reset(); - src.hdd_mode = false; + if (new_disk) + free(new_disk); + if (fresh_target) + panels[topan].reset(); + if ((!panels[active_panel] || panels[active_panel]->disk == nullptr) + && panels[usedisk] && panels[usedisk]->disk != nullptr) { + active_panel = usedisk; } - panels[topan].reset(); + refreshActivePanelUI(); return; } + + if (dst.disk != nullptr) + free(dst.disk); + dst.disk = new_disk; dst.hdd_mode = true; dst.current_dir = root; if (!fromMenu || dlg.isChecked()){ @@ -734,6 +758,7 @@ void MainWindow::AnotherPart(bool fromMenu){ } fillTable(topan, root, false); + refreshActivePanelUI(); } void MainWindow::closeEvent(QCloseEvent *event){ @@ -1127,21 +1152,24 @@ void MainWindow::ExtractAll(){ } } -void MainWindow::FocusChanged(QWidget *, QWidget *now){ - if (now == ui->tableWidget || now == ui->tableWidget_2){ - active_panel = (now == ui->tableWidget_2) ? 1 : 0; +void MainWindow::refreshActivePanelUI(){ + QFont font = ui->groupBox->font(); + font.setBold(active_panel == 0); + ui->groupBox->setFont(font); + ui->tableWidget->setFont(font); - QFont font = ui->groupBox->font(); - font.setBold(active_panel == 0); - ui->groupBox->setFont(font); - ui->tableWidget->setFont(font); + font = ui->groupBox_2->font(); + font.setBold(active_panel == 1); + ui->groupBox_2->setFont(font); + ui->tableWidget_2->setFont(font); - font = ui->groupBox_2->font(); - font.setBold(active_panel == 1); - ui->groupBox_2->setFont(font); - ui->tableWidget_2->setFont(font); + HDDMenu(panels[active_panel] && panels[active_panel]->hdd_mode); +} - HDDMenu(panels[active_panel] && panels[active_panel]->hdd_mode); +void MainWindow::FocusChanged(QWidget *, QWidget *now){ + if (now == ui->tableWidget || now == ui->tableWidget_2){ + active_panel = (now == ui->tableWidget_2) ? 1 : 0; + refreshActivePanelUI(); } } @@ -1612,32 +1640,28 @@ void MainWindow::Save(){ return SaveAs(); auto& panel = *panels[active_panel]; - QGroupBox* gb; - if (panel.modified){ - if (active_panel == 0) - gb = ui->groupBox; - else - gb = ui->groupBox_2; + if (!panel.modified) + return; - int res; - if (panel.hdd_mode){ - res = saveFileQt(panel.path, panel.hdd_data->data(), panel.hdd_data->size(), this); - } - else{ - res = saveFileQt(panel.path, ccos_disk_data(panel.disk), ccos_disk_size(panel.disk), this); - } + int res; + if (panel.hdd_mode) + res = saveFileQt(panel.path, panel.hdd_data->data(), panel.hdd_data->size(), this); + else + res = saveFileQt(panel.path, ccos_disk_data(panel.disk), ccos_disk_size(panel.disk), this); - if (res == -1){ - QMessageBox::critical(this, "Unable to save file", - QString("Unable to save file \"%1\". Please check the path.").arg(panel.path)); - return; - } - panel.modified = false; - int other = !active_panel; - if (panel.hdd_data && panel.hdd_data.use_count() > 1 && panels[other]) - panels[other]->modified = false; - gb->setTitle(gb->title().left(gb->title().size()-1)); + if (res == -1){ + QMessageBox::critical(this, "Unable to save file", + QString("Unable to save file \"%1\". Please check the path.").arg(panel.path)); + return; } + panel.modified = false; + int other = !active_panel; + bool shared = panel.hdd_data && panel.hdd_data.use_count() > 1 && panels[other]; + if (shared) + panels[other]->modified = false; + updatePanelTitle(active_panel); + if (shared) + updatePanelTitle(other); } void MainWindow::SaveAs(){ @@ -1645,22 +1669,15 @@ void MainWindow::SaveAs(){ return; auto& panel = *panels[active_panel]; - QGroupBox* gb; - if (active_panel == 0) - gb = ui->groupBox; - else - gb = ui->groupBox_2; QString nameQ = QFileDialog::getSaveFileName(this, tr("Save as"), "", "GRiD Image Files (*.img)"); if (nameQ == "") return; int res; - if (panel.hdd_mode){ + if (panel.hdd_mode) res = saveFileQt(nameQ, panel.hdd_data->data(), panel.hdd_data->size(), this); - } - else{ + else res = saveFileQt(nameQ, ccos_disk_data(panel.disk), ccos_disk_size(panel.disk), this); - } if (res == -1){ QMessageBox::critical(this, "Unable to save file", @@ -1669,11 +1686,14 @@ void MainWindow::SaveAs(){ } panel.path = nameQ; if (panel.modified){ - gb->setTitle(gb->title().left(gb->title().size()-1)); panel.modified = false; int other = !active_panel; - if (panel.hdd_data && panel.hdd_data.use_count() > 1 && panels[other]) + bool shared = panel.hdd_data && panel.hdd_data.use_count() > 1 && panels[other]; + if (shared) panels[other]->modified = false; + updatePanelTitle(active_panel); + if (shared) + updatePanelTitle(other); } } @@ -1682,12 +1702,6 @@ void MainWindow::SavePart(){ panel.hdd_mode = false; bool oldch = panel.modified; panel.modified = true; - QGroupBox* gb; - if (active_panel == 0) - gb = ui->groupBox; - else - gb = ui->groupBox_2; - gb->setTitle(gb->title()+' '); SaveAs(); if (!panel.modified){ @@ -1707,8 +1721,8 @@ void MainWindow::SavePart(){ else{ panel.hdd_mode = true; panel.modified = oldch; - gb->setTitle(gb->title().left(gb->title().size()-1)); } + updatePanelTitle(active_panel); } void MainWindow::SetActivePart() { diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index cbbe1a4..79ebeca 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -113,5 +113,8 @@ public slots: void loadCustomImg(QString path, uint8_t* data, size_t size); void fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot); + + void updatePanelTitle(int panel_idx); + void refreshActivePanelUI(); }; #endif // MAINWINDOW_H From 295712b0c350be5cb7d78f7471c825c9e52cbb4b Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 24 Jul 2026 22:48:41 +0300 Subject: [PATCH 05/36] fix opening another partition --- dialogs/mainwindow.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index d920de1..54e70f1 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -24,6 +24,8 @@ //[Service functions] +static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition); + ccos_date_t ccos_get_datetime(void) { timespec tp; clock_gettime(CLOCK_REALTIME, &tp); @@ -726,11 +728,7 @@ void MainWindow::AnotherPart(bool fromMenu){ auto& dst = *panels[topan]; - uint8_t* part_data = src.hdd_data->data() + parts[selctd].offset; - ccos_disk_t* new_disk = (ccos_disk_sector_size(src.disk) == GRID_BUBBLE_SECTOR_SIZE) - ? ccos_disk_new_bubble(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)) - : ccos_disk_new_extdisk(part_data, parts[selctd].size, ccos_disk_superblock(src.disk), ccos_disk_bitmap(src.disk)); - + ccos_disk_t* new_disk = tryOpenMbrPartition(src.hdd_data->data(), parts[selctd]); ccos_inode_t* root = (new_disk != nullptr) ? ccos_get_root_dir(new_disk) : nullptr; if (root == nullptr){ QMessageBox::critical(this, "Incorrect Image File", From 6f73966e975e06a4a180782edee9887d16469f3d Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sat, 25 Jul 2026 00:01:39 +0300 Subject: [PATCH 06/36] preview files --- CMakeLists.txt | 7 ++++ dialogs/mainwindow.cpp | 43 +++++++++++++++++++++++ dialogs/mainwindow.h | 4 +++ dialogs/mainwindow.ui | 12 +++++++ dialogs/preview/canvaspreview.cpp | 49 ++++++++++++++++++++++++++ dialogs/preview/canvaspreview.h | 13 +++++++ dialogs/preview/filepreview.h | 20 +++++++++++ dialogs/preview/previewdlg.cpp | 58 +++++++++++++++++++++++++++++++ dialogs/preview/previewdlg.h | 16 +++++++++ dialogs/preview/textpreview.cpp | 56 +++++++++++++++++++++++++++++ dialogs/preview/textpreview.h | 13 +++++++ 11 files changed, 291 insertions(+) create mode 100644 dialogs/preview/canvaspreview.cpp create mode 100644 dialogs/preview/canvaspreview.h create mode 100644 dialogs/preview/filepreview.h create mode 100644 dialogs/preview/previewdlg.cpp create mode 100644 dialogs/preview/previewdlg.h create mode 100644 dialogs/preview/textpreview.cpp create mode 100644 dialogs/preview/textpreview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 30e222d..a8e0906 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,13 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/datedlg.cpp dialogs/datedlg.h dialogs/datedlg.ui + dialogs/preview/filepreview.h + dialogs/preview/textpreview.cpp + dialogs/preview/textpreview.h + dialogs/preview/canvaspreview.cpp + dialogs/preview/canvaspreview.h + dialogs/preview/previewdlg.cpp + dialogs/preview/previewdlg.h dialogs/rendlg.cpp dialogs/rendlg.h dialogs/rendlg.ui diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 54e70f1..9a98b0e 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1,4 +1,5 @@ #include "mainwindow.h" +#include "preview/previewdlg.h" #include #include @@ -483,6 +484,8 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu addFile(tw, 0); tw->verticalHeader()->hide(); tw->setSelectionBehavior(QAbstractItemView::SelectRows); + tw->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tw, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(ShowPreview(QPoint))); } QFont diskfont; diskfont.setFamily(QString::fromUtf8("Arial")); @@ -548,6 +551,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu connect(ui->actionMake_dir, SIGNAL(triggered()), this, SLOT(MakeDir())); connect(ui->actionNewImage, SIGNAL(triggered()), this, SLOT(NewImage())); connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(OpenImg())); + connect(ui->actionPreview, SIGNAL(triggered()), this, SLOT(ShowPreview())); connect(ui->actionRename, SIGNAL(triggered()), this, SLOT(Rename())); connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(Save())); connect(ui->actionSave_as, SIGNAL(triggered()), this, SLOT(SaveAs())); @@ -1177,6 +1181,45 @@ void MainWindow::HDDMenu(bool enab){ ui->actionSep_save->setEnabled(enab); } +void MainWindow::doPreview(int panel_idx, ccos_inode_t* file){ + if (!panels[panel_idx] || file == nullptr) + return; + if (ccos_is_dir(file)) + return; + PreviewDlg dlg(panels[panel_idx]->disk, file, this); + dlg.exec(); +} + +void MainWindow::ShowPreview(const QPoint& pos){ + QTableWidget* tw = qobject_cast(sender()); + if (tw == nullptr) + return; + int panel_idx = (tw == ui->tableWidget_2) ? 1 : 0; + if (!panels[panel_idx]) + return; + auto& panel = *panels[panel_idx]; + + QTableWidgetItem* it = tw->itemAt(pos); + if (it == nullptr) + return; + int row = it->row(); + if (row < 0 || row >= static_cast(panel.inodes.size())) + return; + doPreview(panel_idx, panel.inodes[row]); +} + +void MainWindow::ShowPreview(){ + int panel_idx = active_panel; + if (!panels[panel_idx]) + return; + auto& panel = *panels[panel_idx]; + QTableWidget* tw = panel_idx ? ui->tableWidget_2 : ui->tableWidget; + int row = tw->currentRow(); + if (row < 0 || row >= static_cast(panel.inodes.size())) + return; + doPreview(panel_idx, panel.inodes[row]); +} + void MainWindow::Label(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index 79ebeca..d314eb8 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -82,6 +82,8 @@ public slots: void Delete(); void OpenDir(); void FocusChanged(QWidget*, QWidget* now); + void ShowPreview(const QPoint& pos); + void ShowPreview(); void HDDMenu(bool activ); void Extract(); void ExtractAll(); @@ -116,5 +118,7 @@ public slots: void updatePanelTitle(int panel_idx); void refreshActivePanelUI(); + + void doPreview(int panel_idx, ccos_inode_t* file); }; #endif // MAINWINDOW_H diff --git a/dialogs/mainwindow.ui b/dialogs/mainwindow.ui index ac451d8..96a1d89 100644 --- a/dialogs/mainwindow.ui +++ b/dialogs/mainwindow.ui @@ -499,6 +499,7 @@ + @@ -663,6 +664,17 @@ Ctrl+Shift+X + + + Preview + + + + + + F3 + + Change disk label diff --git a/dialogs/preview/canvaspreview.cpp b/dialogs/preview/canvaspreview.cpp new file mode 100644 index 0000000..0953f85 --- /dev/null +++ b/dialogs/preview/canvaspreview.cpp @@ -0,0 +1,49 @@ +#include "canvaspreview.h" + +#include +#include +#include +#include + +static const int CANVAS_W = 320; +static const int CANVAS_H = 240; +static const int BYTES_PER_ROW = CANVAS_W / 8; +static const size_t CANVAS_SIZE = 9600; + +bool CanvasPreview::supports(const QString& fileType, size_t fileSize) const { + bool const canvas = fileType.compare("canvas", Qt::CaseInsensitive) == 0 && fileSize == CANVAS_SIZE; + bool const scrimage = fileType.compare("screenimage", Qt::CaseInsensitive) == 0; + return canvas || scrimage; +} + +QWidget* CanvasPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { + uint8_t* data = nullptr; + size_t size = 0; + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { + QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); + return nullptr; + } + + QImage img(CANVAS_W, CANVAS_H, QImage::Format_RGB32); + img.fill(qRgb(0, 0, 0)); + + int height = static_cast(size / BYTES_PER_ROW); + for (int y = 0; y < height; y++) { + const uint8_t* row = data + y * BYTES_PER_ROW; + QRgb* scan = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < CANVAS_W; x++) { + int wordIdx = x / 16; + uint16_t word = static_cast(row[wordIdx * 2] | (row[wordIdx * 2 + 1] << 8)); + int bit = 15 - (x % 16); + bool on = (word >> bit) & 1; + scan[x] = on ? qRgb(255, 255, 255) : qRgb(0, 0, 0); + } + } + free(data); + + auto* label = new QLabel(parent); + label->setPixmap(QPixmap::fromImage(img).scaled(CANVAS_W * 2, CANVAS_H * 2, + Qt::KeepAspectRatio, Qt::FastTransformation)); + label->setAlignment(Qt::AlignCenter); + return label; +} diff --git a/dialogs/preview/canvaspreview.h b/dialogs/preview/canvaspreview.h new file mode 100644 index 0000000..bece8ce --- /dev/null +++ b/dialogs/preview/canvaspreview.h @@ -0,0 +1,13 @@ +#ifndef CANVASPREVIEW_H +#define CANVASPREVIEW_H + +#include "filepreview.h" + +class CanvasPreview : public FilePreview { +public: + QString name() const override { return "Canvas"; } + bool supports(const QString& fileType, size_t fileSize) const override; + QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) override; +}; + +#endif // CANVASPREVIEW_H diff --git a/dialogs/preview/filepreview.h b/dialogs/preview/filepreview.h new file mode 100644 index 0000000..370b129 --- /dev/null +++ b/dialogs/preview/filepreview.h @@ -0,0 +1,20 @@ +#ifndef FILEPREVIEW_H +#define FILEPREVIEW_H + +#include +#include + +class QWidget; + +class FilePreview { +public: + virtual ~FilePreview() = default; + + virtual QString name() const = 0; + + virtual bool supports(const QString& fileType, size_t fileSize) const = 0; + + virtual QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) = 0; +}; + +#endif // FILEPREVIEW_H diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp new file mode 100644 index 0000000..5c488cc --- /dev/null +++ b/dialogs/preview/previewdlg.cpp @@ -0,0 +1,58 @@ +#include "previewdlg.h" + +#include "filepreview.h" +#include "textpreview.h" +#include "canvaspreview.h" + +#include +#include + +static QList previewRegistry() { + static QList registry = { + new TextPreview(), + new CanvasPreview(), + }; + return registry; +} + +PreviewDlg::PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) : QDialog(parent) { + setWindowTitle("File preview"); + setModal(true); + setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + setSizeGripEnabled(false); + + auto* layout = new QVBoxLayout(this); + + char basename[CCOS_MAX_FILE_NAME] = {0}; + char type[CCOS_MAX_FILE_NAME] = {0}; + ccos_parse_file_name(file, basename, type, nullptr, nullptr); + size_t fileSize = file->desc.file_size; + + FilePreview* matched = nullptr; + for (FilePreview* p : previewRegistry()) { + if (p->supports(QString::fromLatin1(type), fileSize)) { + matched = p; + break; + } + } + + QWidget* content = nullptr; + if (matched) { + content = matched->createWidget(disk, file, this); + } + if (content == nullptr) { + content = new QLabel(QString("No preview available for \"~%1~\".") + .arg(QString::fromLatin1(type)), this); + static_cast(content)->setAlignment(Qt::AlignCenter); + } + layout->addWidget(content, 1); + + setFixedSize(660, 540); +} + +void PreviewDlg::showEvent(QShowEvent* event) { + QDialog::showEvent(event); + if (auto* p = parentWidget()) { + move(p->geometry().center() - QPoint(width() / 2, height() / 2)); + } +} diff --git a/dialogs/preview/previewdlg.h b/dialogs/preview/previewdlg.h new file mode 100644 index 0000000..5d81922 --- /dev/null +++ b/dialogs/preview/previewdlg.h @@ -0,0 +1,16 @@ +#ifndef PREVIEWDLG_H +#define PREVIEWDLG_H + +#include +#include + +class PreviewDlg : public QDialog { + Q_OBJECT +public: + PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent = nullptr); + +protected: + void showEvent(QShowEvent* event) override; +}; + +#endif // PREVIEWDLG_H diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp new file mode 100644 index 0000000..aaa44ed --- /dev/null +++ b/dialogs/preview/textpreview.cpp @@ -0,0 +1,56 @@ +#include "textpreview.h" + +#include +#include +#include + +// Decode file bytes as Latin-1, rendering control characters as Unicode +// control-picture glyphs (NUL -> ␀, BEL -> ␇, ESC -> ␛, DEL -> ␡, etc.) so they +// are visible instead of blank. Tab and newline are kept as-is to preserve layout. +static QString decodeText(const uint8_t* data, size_t size) { + QString out; + out.reserve(static_cast(size)); + for (size_t i = 0; i < size; i++) { + unsigned char c = data[i]; + if (c == '\t' || c == '\n') { + out += QLatin1Char(static_cast(c)); + } else if (c < 0x20) { + out += QChar(static_cast(0x2400 + c)); + } else if (c == 0x7F) { + out += QChar(static_cast(0x2421)); // SYMBOL FOR DELETE + } else { + out += QLatin1Char(static_cast(c)); + } + } + return out; +} + +bool TextPreview::supports(const QString& fileType, size_t fileSize) const { + (void)fileSize; + return fileType.compare("text", Qt::CaseInsensitive) == 0; +} + +QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { + uint8_t* data = nullptr; + size_t size = 0; + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { + QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); + return nullptr; + } + + auto* edit = new QPlainTextEdit(parent); + edit->setReadOnly(true); + edit->setLineWrapMode(QPlainTextEdit::NoWrap); + QFont font("Monospace"); + font.setStyleHint(QFont::TypeWriter); + edit->setFont(font); + + // The first `prop_length` bytes of the file store properties and are not part of the text. + uint32_t prop_length = file->desc.prop_length; + size_t offset = (prop_length <= size) ? prop_length : size; + size_t text_len = size - offset; + edit->setPlainText(decodeText(data + offset, text_len)); + + free(data); + return edit; +} diff --git a/dialogs/preview/textpreview.h b/dialogs/preview/textpreview.h new file mode 100644 index 0000000..ca44823 --- /dev/null +++ b/dialogs/preview/textpreview.h @@ -0,0 +1,13 @@ +#ifndef TEXTPREVIEW_H +#define TEXTPREVIEW_H + +#include "filepreview.h" + +class TextPreview : public FilePreview { +public: + QString name() const override { return "Text"; } + bool supports(const QString& fileType, size_t fileSize) const override; + QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) override; +}; + +#endif // TEXTPREVIEW_H From e57e58ec6f058a7c7e36a6b18c97a7b689835730 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 11:52:34 +0300 Subject: [PATCH 07/36] use ugly but stable qt file dialog for extract (all) instead of native --- dialogs/mainwindow.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 9a98b0e..f5cf92a 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1126,7 +1126,7 @@ void MainWindow::Extract(){ if (called.size() == 7 && panel.inodes[called[0]->row()] == nullptr) return; QString todir = QFileDialog::getExistingDirectory(this, tr("Extract to"), "", - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks | QFileDialog::DontUseNativeDialog); if (todir == "") return; for (int t = 0; t < called.size(); t+=7){ @@ -1144,7 +1144,7 @@ void MainWindow::ExtractAll(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; QString todir = QFileDialog::getExistingDirectory(this, tr("Extract all to"), "", - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks | QFileDialog::DontUseNativeDialog); if (todir == "") return; int res = dumpImgQt(panel.disk, todir, QFileInfo(panel.path).baseName(), this); From fd381f6e1d4a33d2cce6233200649b96af8649d8 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 12:14:11 +0300 Subject: [PATCH 08/36] show LF in text preview panel --- dialogs/preview/textpreview.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp index aaa44ed..7df1704 100644 --- a/dialogs/preview/textpreview.cpp +++ b/dialogs/preview/textpreview.cpp @@ -12,8 +12,11 @@ static QString decodeText(const uint8_t* data, size_t size) { out.reserve(static_cast(size)); for (size_t i = 0; i < size; i++) { unsigned char c = data[i]; - if (c == '\t' || c == '\n') { - out += QLatin1Char(static_cast(c)); + if (c == '\t') { + out += QLatin1Char('\t'); + } else if (c == '\n') { + out += QChar(static_cast(0x2400 + c)); + out += QLatin1Char('\n'); } else if (c < 0x20) { out += QChar(static_cast(0x2400 + c)); } else if (c == 0x7F) { From b2982c559a9ebb6bd799d1af9546eb996999a8fc Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 13:54:46 +0300 Subject: [PATCH 09/36] preview all kinds of pictures from grid: raw, raw+dims, gridpaint canvas --- dialogs/preview/canvaspreview.cpp | 256 +++++++++++++++++++++++++++--- 1 file changed, 230 insertions(+), 26 deletions(-) diff --git a/dialogs/preview/canvaspreview.cpp b/dialogs/preview/canvaspreview.cpp index 0953f85..178b86f 100644 --- a/dialogs/preview/canvaspreview.cpp +++ b/dialogs/preview/canvaspreview.cpp @@ -4,16 +4,214 @@ #include #include #include +#include +#include -static const int CANVAS_W = 320; -static const int CANVAS_H = 240; -static const int BYTES_PER_ROW = CANVAS_W / 8; -static const size_t CANVAS_SIZE = 9600; +#include + +namespace { + +enum RecordType : uint8_t { + MetadataRecord = 0xFE, // metadata / dimensions + LabelRecord = 0xFD, // labelled block (owner / font / objects) +}; + +// Sanity guards to prevent garbage from being decoded. +constexpr int kMaxImageWidth = 8192; +constexpr int kMaxImageHeight = 4096; + +struct CanvasImage { + QString format; + int w = 0; + int h = 0; + const uint8_t* raster = nullptr; + size_t rasterLen = 0; +}; + +inline uint16_t u16le(const uint8_t* p) { + return uint16_t(p[0]) | (uint16_t(p[1]) << 8); +} + +inline size_t rasterBytes(int w, int h) { + return size_t(w / 8) * size_t(h); +} + +bool plausibleDims(int w, int h) { + // Width must be a whole number of 16-bit words: the pixel packing reads + // two bytes per 16 pixels, so a non-multiple-of-16 width would misalign. + return w > 0 && w <= kMaxImageWidth && h > 0 && h <= kMaxImageHeight && (w % 16) == 0; +} + +bool isGridpaintSignature(const uint8_t* data, size_t size) { + return size >= 6 && data[0] == MetadataRecord && + data[3] == 0x61 && data[4] == 0xDE && data[5] == 0x52; +} + +CanvasImage decodeGridpaint(const uint8_t* data, size_t size) { + const uint8_t* p = data; + const uint8_t* end = data + size; + + while (end - p >= 3) { + uint8_t type = *p++; + + size_t len = u16le(p); + p += 2; + + if (type != MetadataRecord && type != LabelRecord) { + break; + } + + if (p + len > end) { + break; + } + + const uint8_t* payload = p; + p += len; + + // The dimensions record is a MetadataRecord with a 4-byte payload + // (u16 width + u16 height), the raster follows it directly. + // + // The only other 4-byte MetadataRecord is the signature, whose payload fails + // plausibleDims, so the first match here is the real dims record. + if (type == MetadataRecord && len == 4) { + int w = u16le(payload); + int h = u16le(payload + 2); + size_t expect = rasterBytes(w, h); + + const uint8_t* r = p; + if (r < end && *r == 0xFF) { + r++; // skip the block marker + } + + if (plausibleDims(w, h) && size_t(end - r) >= expect) { + CanvasImage img; + img.format = QStringLiteral("gridpaint"); + img.w = w; + img.h = h; + img.raster = r; + img.rasterLen = expect; + return img; + } + } + } + + return CanvasImage{}; +} + +CanvasImage decodeRawWithHeader(const uint8_t* data, size_t size) { + const uint8_t* p = data; + const uint8_t* end = data + size; + + if (end - p < 4) { + return CanvasImage{}; + } + + int w = u16le(p); p += 2; + int h = u16le(p); p += 2; + + size_t expect = rasterBytes(w, h); + if (plausibleDims(w, h) && size_t(end - p) == expect) { + CanvasImage img; + img.format = QStringLiteral("raw"); + img.w = w; + img.h = h; + img.raster = p; + img.rasterLen = expect; + return img; + } + + return CanvasImage{}; +} + +CanvasImage decodeRawHeaderless(const uint8_t* data, size_t size) { + const size_t compassScreenWidth = 320; + + const size_t bytesPerRow = compassScreenWidth / 8; + if (size == 0 || size % bytesPerRow != 0) { + return CanvasImage{}; + } + + int h = static_cast(size / bytesPerRow); + if (h <= 0 || h > kMaxImageHeight) { + return CanvasImage{}; + } + + CanvasImage img; + img.format = QStringLiteral("raw"); + img.w = compassScreenWidth; + img.h = h; + img.raster = data; + img.rasterLen = size; + + return img; +} + +CanvasImage detectCanvas(const uint8_t* data, size_t size) { + if (isGridpaintSignature(data, size)) { + return decodeGridpaint(data, size); + } + if (CanvasImage img = decodeRawWithHeader(data, size); img.raster) { + return img; + } + return decodeRawHeaderless(data, size); +} + +QImage renderMono(const uint8_t* raster, size_t rasterLen, int w, int h) { + QImage img(w, h, QImage::Format_RGB32); + img.fill(qRgb(0, 0, 0)); + + const int bytesPerRow = w / 8; + const int rows = std::min(h, static_cast(rasterLen / bytesPerRow)); + const QRgb on = qRgb(255, 255, 255); + const QRgb off = qRgb(0, 0, 0); + + for (int y = 0; y < rows; y++) { + const uint8_t* row = raster + y * bytesPerRow; + QRgb* scan = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < w; x++) { + const int bo = (x / 16) * 2; + uint16_t word = static_cast(row[bo] | (row[bo + 1] << 8)); + scan[x] = (word >> (15 - (x % 16))) & 1 ? on : off; + } + } + return img; +} + +// QLabel that rescales its pixmap to fit on resize, keeping aspect ratio. +// FastTransformation keeps the 1-bpp pixel art crisp instead of smearing it. +class ImageLabel : public QLabel { +public: + using QLabel::QLabel; + + void setSourceImage(const QImage& image) { + source = image; + rescale(size()); + } + +protected: + void resizeEvent(QResizeEvent* event) override { + QLabel::resizeEvent(event); + rescale(event->size()); + } + +private: + void rescale(const QSize& area) { + if (source.isNull()) { + return; + } + setPixmap(QPixmap::fromImage( + source.scaled(area, Qt::KeepAspectRatio, Qt::FastTransformation))); + } + + QImage source; +}; + +} // namespace bool CanvasPreview::supports(const QString& fileType, size_t fileSize) const { - bool const canvas = fileType.compare("canvas", Qt::CaseInsensitive) == 0 && fileSize == CANVAS_SIZE; - bool const scrimage = fileType.compare("screenimage", Qt::CaseInsensitive) == 0; - return canvas || scrimage; + return fileSize >= 4 && + (fileType.compare("canvas", Qt::CaseInsensitive) == 0 || + fileType.compare("screenimage", Qt::CaseInsensitive) == 0); } QWidget* CanvasPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { @@ -24,26 +222,32 @@ QWidget* CanvasPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWid return nullptr; } - QImage img(CANVAS_W, CANVAS_H, QImage::Format_RGB32); - img.fill(qRgb(0, 0, 0)); - - int height = static_cast(size / BYTES_PER_ROW); - for (int y = 0; y < height; y++) { - const uint8_t* row = data + y * BYTES_PER_ROW; - QRgb* scan = reinterpret_cast(img.scanLine(y)); - for (int x = 0; x < CANVAS_W; x++) { - int wordIdx = x / 16; - uint16_t word = static_cast(row[wordIdx * 2] | (row[wordIdx * 2 + 1] << 8)); - int bit = 15 - (x % 16); - bool on = (word >> bit) & 1; - scan[x] = on ? qRgb(255, 255, 255) : qRgb(0, 0, 0); - } + CanvasImage img = detectCanvas(data, size); + if (img.raster == nullptr) { + free(data); + QMessageBox::warning(parent, "Preview", "Could not decode this canvas image (unknown format)."); + return nullptr; } + + QImage image = renderMono(img.raster, img.rasterLen, img.w, img.h); free(data); - auto* label = new QLabel(parent); - label->setPixmap(QPixmap::fromImage(img).scaled(CANVAS_W * 2, CANVAS_H * 2, - Qt::KeepAspectRatio, Qt::FastTransformation)); - label->setAlignment(Qt::AlignCenter); - return label; + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + auto* imageLabel = new ImageLabel(container); + imageLabel->setAlignment(Qt::AlignCenter); + imageLabel->setSourceImage(image); + layout->addWidget(imageLabel, 1); + + auto* info = new QLabel( + QStringLiteral("Format: %1 | Resolution: %2 × %3").arg(img.format).arg(img.w).arg(img.h), + container); + info->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + info->setTextInteractionFlags(Qt::TextSelectableByMouse); + layout->addWidget(info); + + return container; } From 4b3802035d3d54172d3cc3fb66fdd4a60c717540 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 16:59:59 +0300 Subject: [PATCH 10/36] add word wrap to text file preview --- dialogs/preview/textpreview.cpp | 119 ++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp index 7df1704..54b58ca 100644 --- a/dialogs/preview/textpreview.cpp +++ b/dialogs/preview/textpreview.cpp @@ -1,8 +1,16 @@ #include "textpreview.h" #include +#include +#include +#include +#include +#include #include #include +#include +#include +#include // Decode file bytes as Latin-1, rendering control characters as Unicode // control-picture glyphs (NUL -> ␀, BEL -> ␇, ESC -> ␛, DEL -> ␡, etc.) so they @@ -28,9 +36,94 @@ static QString decodeText(const uint8_t* data, size_t size) { return out; } +// QPlainTextEdit subclass that paints a soft wrap marker ("↵") at the end of +// each visual line that was wrapped by word-wrap, so wrap points are visible. +// Only applies when lineWrapMode() is WidgetWidth; NoWrap draws nothing. +class WrapPreviewEdit : public QPlainTextEdit { +public: + using QPlainTextEdit::QPlainTextEdit; + + // Toggle wrap mode while keeping the same block visible at the top, + // so flipping the checkbox doesn't snap the view back to the start. + void setLineWrapModePreservingScroll(LineWrapMode mode); + +protected: + void paintEvent(QPaintEvent* event) override; +}; + +void WrapPreviewEdit::setLineWrapModePreservingScroll(LineWrapMode mode) { + if (lineWrapMode() == mode) { + return; + } + + // Remember the first visible block and how far into it we've scrolled, + // so we can restore roughly the same view after the document reflows. + const QTextBlock first = firstVisibleBlock(); + const int oldBlockTop = first.isValid() ? static_cast(blockBoundingGeometry(first).top()) : 0; + const int offsetWithinBlock = verticalScrollBar()->value() - oldBlockTop; + + setLineWrapMode(mode); + + if (first.isValid()) { + const int newBlockTop = static_cast(blockBoundingGeometry(first).top()); + verticalScrollBar()->setValue(newBlockTop + offsetWithinBlock); + } +} + +void WrapPreviewEdit::paintEvent(QPaintEvent* event) { + QPlainTextEdit::paintEvent(event); + + // Nothing to mark when wrapping is off. + if (lineWrapMode() == QPlainTextEdit::NoWrap) { + return; + } + + QPainter p(viewport()); + p.setPen(palette().color(QPalette::Disabled, QPalette::Text)); + p.setFont(font()); + + static const QString wrapMark = QStringLiteral("\u21B5"); + + const QRectF dirty = event->rect(); + const QPointF offset = contentOffset(); + QTextBlock block = firstVisibleBlock(); + + while (block.isValid()) { + const QRectF blockRect = blockBoundingGeometry(block).translated(offset); + + if (blockRect.top() > dirty.bottom()) { + break; // Rest of the document is below the dirty region. + } + if (blockRect.bottom() < dirty.top()) { + block = block.next(); + continue; // Block is above the dirty region. + } + + // For a wrapped block, every visual line except the last continues + // onto the next line, so mark those. + QTextLayout* tl = block.layout(); + if (tl != nullptr) { + const int lineCount = tl->lineCount(); + for (int i = 0; i < lineCount - 1; i++) { + const QTextLine line = tl->lineAt(i); + if (!line.isValid()) { + continue; + } + const qreal x = blockRect.left() + line.position().x() + line.naturalTextWidth() + 2; + const qreal y = blockRect.top() + line.position().y() + line.ascent(); + p.drawText(QPointF(x, y), wrapMark); + } + } + + block = block.next(); + } +} + bool TextPreview::supports(const QString& fileType, size_t fileSize) const { (void)fileSize; - return fileType.compare("text", Qt::CaseInsensitive) == 0; + return fileType.compare("text", Qt::CaseInsensitive) == 0 || + fileType.compare("develop", Qt::CaseInsensitive) == 0 || + fileType.compare("lst", Qt::CaseInsensitive) == 0; } QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { @@ -41,9 +134,9 @@ QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidge return nullptr; } - auto* edit = new QPlainTextEdit(parent); + auto* edit = new WrapPreviewEdit(parent); edit->setReadOnly(true); - edit->setLineWrapMode(QPlainTextEdit::NoWrap); + edit->setLineWrapMode(QPlainTextEdit::WidgetWidth); // word wrap on by default QFont font("Monospace"); font.setStyleHint(QFont::TypeWriter); edit->setFont(font); @@ -53,7 +146,23 @@ QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidge size_t offset = (prop_length <= size) ? prop_length : size; size_t text_len = size - offset; edit->setPlainText(decodeText(data + offset, text_len)); - free(data); - return edit; + + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + auto* wrapCheck = new QCheckBox(QStringLiteral("Word wrap"), container); + wrapCheck->setChecked(true); + layout->addWidget(wrapCheck); + + layout->addWidget(edit, 1); + + QObject::connect(wrapCheck, &QCheckBox::toggled, edit, [edit](bool checked) { + edit->setLineWrapModePreservingScroll( + checked ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap); + }); + + return container; } From 105d9a3422087788ec5a5758b13143d6a5c37bb3 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 22:27:39 +0300 Subject: [PATCH 11/36] preview worksheet files --- CMakeLists.txt | 2 + dialogs/preview/previewdlg.cpp | 2 + dialogs/preview/worksheetpreview.cpp | 230 +++++++++++++++++++++++++++ dialogs/preview/worksheetpreview.h | 13 ++ 4 files changed, 247 insertions(+) create mode 100644 dialogs/preview/worksheetpreview.cpp create mode 100644 dialogs/preview/worksheetpreview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a8e0906..569b138 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/preview/textpreview.h dialogs/preview/canvaspreview.cpp dialogs/preview/canvaspreview.h + dialogs/preview/worksheetpreview.cpp + dialogs/preview/worksheetpreview.h dialogs/preview/previewdlg.cpp dialogs/preview/previewdlg.h dialogs/rendlg.cpp diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp index 5c488cc..b4111cd 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/dialogs/preview/previewdlg.cpp @@ -3,6 +3,7 @@ #include "filepreview.h" #include "textpreview.h" #include "canvaspreview.h" +#include "worksheetpreview.h" #include #include @@ -11,6 +12,7 @@ static QList previewRegistry() { static QList registry = { new TextPreview(), new CanvasPreview(), + new WorksheetPreview(), }; return registry; } diff --git a/dialogs/preview/worksheetpreview.cpp b/dialogs/preview/worksheetpreview.cpp new file mode 100644 index 0000000..eecfacc --- /dev/null +++ b/dialogs/preview/worksheetpreview.cpp @@ -0,0 +1,230 @@ +#include "worksheetpreview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +enum RecordType : uint8_t { + MetadataRecord = 0xFE, // metadata: title, signature + LabelRecord = 0xFD, // labelled block (column labels / formulas) +}; + +constexpr uint8_t SignatureByte = 0x61; +constexpr uint8_t TitleSubtype = 0x68; +constexpr uint8_t ColumnLabelSubtype = 0x00; + +// Sanity guards to prevent garbage from being decoded. +constexpr int kMaxRows = 50000; +constexpr int kMaxCols = 256; + +struct Worksheet { + QString title; + QMap columnLabels; // 1-based column index -> label text + QList rows; // each row is a list of cell strings + bool valid = false; // set only after the signature matches +}; + +inline uint16_t u16le(const uint8_t* p) { + return uint16_t(p[0]) | (uint16_t(p[1]) << 8); +} + +bool isWorksheetSignature(const uint8_t* data, size_t size) { + // The opening record of every GRiDPlan II worksheet is `FE 04 00 61 …` + // (a 4-byte metadata record whose payload begins with the signature byte). + return size >= 4 && data[0] == MetadataRecord && data[3] == SignatureByte; +} + +QList splitCsvRows(const QByteArray& csv) { + QString text = QString::fromLatin1(csv.constData(), csv.size()); + QStringList lines = text.split(QLatin1Char('\n')); + if (!lines.isEmpty() && lines.last().isEmpty() && csv.endsWith('\n')) { + lines.removeLast(); + } + QList rows; + rows.reserve(lines.size()); + for (const QString& line : lines) { + QString row = line; + if (row.endsWith(QLatin1Char('\r'))) { + row.chop(1); + } + rows.append(row.split(QLatin1Char('\t'), Qt::KeepEmptyParts)); + } + return rows; +} + +Worksheet parseWorksheet(const uint8_t* data, size_t size) { + Worksheet ws; + if (!isWorksheetSignature(data, size)) { + printf("Invalid worksheet\n"); + return ws; + } + + const uint8_t* p = data; + const uint8_t* end = data + size; + + // Phase 1 — header records. Stop at the first byte that is not a record + // leader; that byte begins the CSV cell-value block. + while (end - p >= 3) { + uint8_t type = *p++; + if (type != MetadataRecord && type != LabelRecord) { + break; + } + + size_t len = u16le(p); + p += 2; + + if (len < 1) { + continue; + } + + const uint8_t* payload = p; + p += len; + + if (payload + len > end) { + break; + } + + uint8_t sub = *payload++; + + if (type == MetadataRecord) { + if (sub == TitleSubtype) { + ws.title = QString::fromLatin1(reinterpret_cast(payload), int(len - 1)); + } + } else { // LabelRecord + if (sub == ColumnLabelSubtype && len >= 2) { + int col = *payload++; + ws.columnLabels[col] = QString::fromLatin1(reinterpret_cast(payload), int(len - 2)); + } + } + } + + // Phase 2 — data section: gather the raw CSV bytes, skipping over any + // interleaved FE/FD records (e.g. per-row formula byte-code, FD 0x02). + QByteArray csv; + csv.reserve(static_cast(end - p)); + while (p < end) { + uint8_t type = *p++; + if (type != MetadataRecord && type != LabelRecord) { + csv.append(static_cast(type)); + p++; + continue; + } + + if (end - p < 3) { + break; // truncated leader; leave the tail uncollected + } + + size_t len = u16le(p); + p += 2; + + const uint8_t* payload = p; + p += len; + + if (payload + len > end) { + // Truncated trailing record — treat the remainder as CSV. + csv.append(reinterpret_cast(p), static_cast(end - p)); + break; + } + } + + ws.rows = splitCsvRows(csv); + ws.valid = true; + return ws; +} + +} // namespace + +bool WorksheetPreview::supports(const QString& fileType, size_t fileSize) const { + return fileSize >= 4 && + fileType.compare("worksheet", Qt::CaseInsensitive) == 0; +} + +QWidget* WorksheetPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { + uint8_t* data = nullptr; + size_t size = 0; + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { + QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); + return nullptr; + } + + Worksheet ws = parseWorksheet(data, size); + if (!ws.valid) { + free(data); + QMessageBox::warning(parent, "Preview", "Could not decode this worksheet (unknown format)."); + return nullptr; + } + free(data); + + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + if (!ws.title.isEmpty()) { + auto* titleLabel = new QLabel(ws.title, container); + QFont titleFont = titleLabel->font(); + titleFont.setBold(true); + titleLabel->setFont(titleFont); + titleLabel->setWordWrap(true); + layout->addWidget(titleLabel); + } + + int rowCount = qMin(ws.rows.size(), kMaxRows); + int colCount = 0; + for (const QStringList& row : ws.rows) { + colCount = qMax(colCount, row.size()); + } + + for (auto it = ws.columnLabels.cbegin(); it != ws.columnLabels.cend(); ++it) { + if (it.key() <= kMaxCols) { + colCount = qMax(colCount, it.key()); + } + } + colCount = qMin(colCount, kMaxCols); + + auto* table = new QTableWidget(rowCount, colCount, container); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setSelectionBehavior(QAbstractItemView::SelectItems); + table->setSelectionMode(QAbstractItemView::ContiguousSelection); + table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + QFont font("Monospace"); + font.setStyleHint(QFont::TypeWriter); + table->setFont(font); + + if (!ws.columnLabels.isEmpty()) { + QStringList headerLabels; + headerLabels.reserve(colCount); + for (int c = 0; c < colCount; ++c) { + headerLabels << ws.columnLabels.value(c + 1); + } + table->setHorizontalHeaderLabels(headerLabels); + } else { + for (int c = 0; c < colCount; ++c) { + table->setHorizontalHeaderItem(c, new QTableWidgetItem(QString::number(c + 1))); + } + } + table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + table->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + for (int r = 0; r < rowCount; ++r) { + const QStringList& row = ws.rows[r]; + for (int c = 0; c < row.size(); ++c) { + if (!row[c].isEmpty()) { + table->setItem(r, c, new QTableWidgetItem(row[c])); + } + } + } + layout->addWidget(table, 1); + + return container; +} diff --git a/dialogs/preview/worksheetpreview.h b/dialogs/preview/worksheetpreview.h new file mode 100644 index 0000000..0ccab97 --- /dev/null +++ b/dialogs/preview/worksheetpreview.h @@ -0,0 +1,13 @@ +#ifndef WORKSHEETPREVIEW_H +#define WORKSHEETPREVIEW_H + +#include "filepreview.h" + +class WorksheetPreview : public FilePreview { +public: + QString name() const override { return "Worksheet"; } + bool supports(const QString& fileType, size_t fileSize) const override; + QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) override; +}; + +#endif // WORKSHEETPREVIEW_H From 42a42dc624e4c3147716bfcbd7672da637f593dd Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Sun, 26 Jul 2026 22:28:14 +0300 Subject: [PATCH 12/36] allow resize preview window --- dialogs/preview/previewdlg.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp index b4111cd..7f26faf 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/dialogs/preview/previewdlg.cpp @@ -49,7 +49,9 @@ PreviewDlg::PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) : } layout->addWidget(content, 1); - setFixedSize(660, 540); + resize(660, 540); + setMinimumSize(360, 280); + setSizeGripEnabled(true); } void PreviewDlg::showEvent(QShowEvent* event) { From aa017b9ea3fe82225edbe654343ed6326f67df74 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 17:25:42 +0300 Subject: [PATCH 13/36] escape to go up, enter to open preview --- dialogs/mainwindow.cpp | 50 +++++++++++++++++++++++++++++++++++++----- dialogs/mainwindow.h | 7 ++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index f5cf92a..f2b93ac 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -485,6 +485,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu tw->verticalHeader()->hide(); tw->setSelectionBehavior(QAbstractItemView::SelectRows); tw->setContextMenuPolicy(Qt::CustomContextMenu); + tw->installEventFilter(this); connect(tw, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(ShowPreview(QPoint))); } QFont diskfont; @@ -1181,6 +1182,27 @@ void MainWindow::HDDMenu(bool enab){ ui->actionSep_save->setEnabled(enab); } +bool MainWindow::eventFilter(QObject* obj, QEvent* event) { + if (event->type() != QEvent::KeyPress) { + return QMainWindow::eventFilter(obj, event); + } + + const int panel_idx = (obj == ui->tableWidget_2) ? 1 : 0; + + auto* keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: + { + if (goToParentDir(panel_idx)) { + return true; + } + break; + } + } + + return QMainWindow::eventFilter(obj, event); +} + void MainWindow::doPreview(int panel_idx, ccos_inode_t* file){ if (!panels[panel_idx] || file == nullptr) return; @@ -1597,18 +1619,34 @@ void MainWindow::OpenDir(){ if (dir == nullptr && !panel.in_subdir) return; if (called->row() == 0 && panel.in_subdir){ - ccos_inode_t* root = ccos_get_root_dir(panel.disk); - panel.current_dir = ccos_get_parent_dir(panel.disk, panel.current_dir); - if (panel.current_dir == root) - panel.in_subdir = false; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + goToParentDir(active_panel); } else if (!panel.in_subdir){ //All files in the root are directories panel.current_dir = dir; panel.in_subdir = true; fillTable(active_panel, dir, panel.in_subdir); } + else if (!ccos_is_dir(dir)) { + doPreview(active_panel, dir); + } + } +} + +bool MainWindow::goToParentDir(int panel_idx) { + if (!panels[panel_idx]) { + return false; + } + auto& panel = *panels[panel_idx]; + if (!panel.in_subdir) { + return false; // already at the disk root, nothing above it + } + ccos_inode_t* root = ccos_get_root_dir(panel.disk); + panel.current_dir = ccos_get_parent_dir(panel.disk, panel.current_dir); + if (panel.current_dir == root) { + panel.in_subdir = false; } + fillTable(panel_idx, panel.current_dir, panel.in_subdir); + return true; } void MainWindow::OpenImg(){ @@ -1618,7 +1656,7 @@ void MainWindow::OpenImg(){ LoadImg(path); } -void MainWindow::Rename(){ +void MainWindow::Rename(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; QTableWidget* tw; diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index d314eb8..5aee93a 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "ui_mainwindow.h" @@ -103,6 +104,10 @@ public slots: ~MainWindow(); std::array, 2> panels; int active_panel = 0; + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + private: std::unique_ptr ui; @@ -119,6 +124,8 @@ public slots: void updatePanelTitle(int panel_idx); void refreshActivePanelUI(); + bool goToParentDir(int panel_idx); + void doPreview(int panel_idx, ccos_inode_t* file); }; #endif // MAINWINDOW_H From dda966740af373eae03d1f7cdb790f860e11abaf Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 18:03:50 +0300 Subject: [PATCH 14/36] refactor canvas, worksheet preview plugins using new omf parser --- CMakeLists.txt | 2 + dialogs/preview/canvaspreview.cpp | 176 ++++++++++----------------- dialogs/preview/omf/omf_record.cpp | 62 ++++++++++ dialogs/preview/omf/omf_record.h | 41 +++++++ dialogs/preview/textpreview.cpp | 11 +- dialogs/preview/worksheetpreview.cpp | 173 ++++++++++---------------- 6 files changed, 243 insertions(+), 222 deletions(-) create mode 100644 dialogs/preview/omf/omf_record.cpp create mode 100644 dialogs/preview/omf/omf_record.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 569b138..18b7aad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/datedlg.h dialogs/datedlg.ui dialogs/preview/filepreview.h + dialogs/preview/omf/omf_record.cpp + dialogs/preview/omf/omf_record.h dialogs/preview/textpreview.cpp dialogs/preview/textpreview.h dialogs/preview/canvaspreview.cpp diff --git a/dialogs/preview/canvaspreview.cpp b/dialogs/preview/canvaspreview.cpp index 178b86f..348b668 100644 --- a/dialogs/preview/canvaspreview.cpp +++ b/dialogs/preview/canvaspreview.cpp @@ -1,5 +1,7 @@ #include "canvaspreview.h" +#include "omf/omf_record.h" + #include #include #include @@ -11,12 +13,6 @@ namespace { -enum RecordType : uint8_t { - MetadataRecord = 0xFE, // metadata / dimensions - LabelRecord = 0xFD, // labelled block (owner / font / objects) -}; - -// Sanity guards to prevent garbage from being decoded. constexpr int kMaxImageWidth = 8192; constexpr int kMaxImageHeight = 4096; @@ -24,114 +20,72 @@ struct CanvasImage { QString format; int w = 0; int h = 0; + int stride = 0; const uint8_t* raster = nullptr; size_t rasterLen = 0; }; -inline uint16_t u16le(const uint8_t* p) { - return uint16_t(p[0]) | (uint16_t(p[1]) << 8); +// Gridpaint packs pixels into 16-bit words (MSB first). The logical width need +// not be a multiple of 16, but each row is padded to a whole number of words: +// e.g. a 70-pixel-wide image is stored in 80-pixel (10-byte) rows. +inline int wordAlignedStride(int w) { + return ((w + 15) / 16) * 2; } inline size_t rasterBytes(int w, int h) { - return size_t(w / 8) * size_t(h); + return size_t(wordAlignedStride(w)) * size_t(h); } bool plausibleDims(int w, int h) { - // Width must be a whole number of 16-bit words: the pixel packing reads - // two bytes per 16 pixels, so a non-multiple-of-16 width would misalign. - return w > 0 && w <= kMaxImageWidth && h > 0 && h <= kMaxImageHeight && (w % 16) == 0; + return w > 0 && w <= kMaxImageWidth && h > 0 && h <= kMaxImageHeight; } -bool isGridpaintSignature(const uint8_t* data, size_t size) { - return size >= 6 && data[0] == MetadataRecord && - data[3] == 0x61 && data[4] == 0xDE && data[5] == 0x52; -} - -CanvasImage decodeGridpaint(const uint8_t* data, size_t size) { - const uint8_t* p = data; - const uint8_t* end = data + size; - - while (end - p >= 3) { - uint8_t type = *p++; - - size_t len = u16le(p); - p += 2; +// Decodes GRiDPaint Canvas files. The image size is stored as a TLV record +// terminated by 0xFF: FE 04 00 FF . +CanvasImage decodeFromMetadata(const OmfMetadata& md) { + const uint8_t* end = md.content + md.contentSize; - if (type != MetadataRecord && type != LabelRecord) { - break; - } - - if (p + len > end) { - break; - } - - const uint8_t* payload = p; - p += len; - - // The dimensions record is a MetadataRecord with a 4-byte payload - // (u16 width + u16 height), the raster follows it directly. - // - // The only other 4-byte MetadataRecord is the signature, whose payload fails - // plausibleDims, so the first match here is the real dims record. - if (type == MetadataRecord && len == 4) { - int w = u16le(payload); - int h = u16le(payload + 2); - size_t expect = rasterBytes(w, h); - - const uint8_t* r = p; - if (r < end && *r == 0xFF) { - r++; // skip the block marker - } - - if (plausibleDims(w, h) && size_t(end - r) >= expect) { - CanvasImage img; - img.format = QStringLiteral("gridpaint"); - img.w = w; - img.h = h; - img.raster = r; - img.rasterLen = expect; - return img; - } - } + const OmfRecord dims = readOmfRecord(md.content, end); + if (dims.type != kOmfMetadataRecord || dims.length != 4) { + return CanvasImage{}; } - return CanvasImage{}; -} - -CanvasImage decodeRawWithHeader(const uint8_t* data, size_t size) { - const uint8_t* p = data; - const uint8_t* end = data + size; + const int w = omfU16LE(dims.payload); + const int h = omfU16LE(dims.payload + 2); + if (!plausibleDims(w, h)) { + return CanvasImage{}; + } - if (end - p < 4) { + const uint8_t* raster = dims.payload + 4; + if (*raster++ != 0xFF) { return CanvasImage{}; } - int w = u16le(p); p += 2; - int h = u16le(p); p += 2; - - size_t expect = rasterBytes(w, h); - if (plausibleDims(w, h) && size_t(end - p) == expect) { - CanvasImage img; - img.format = QStringLiteral("raw"); - img.w = w; - img.h = h; - img.raster = p; - img.rasterLen = expect; - return img; + const size_t expect = rasterBytes(w, h); + if (end - raster < expect) { + return CanvasImage{}; } - return CanvasImage{}; + CanvasImage img; + img.format = QStringLiteral("gridpaint"); + img.w = w; + img.h = h; + img.stride = wordAlignedStride(w); + img.raster = raster; + img.rasterLen = expect; + + return img; } CanvasImage decodeRawHeaderless(const uint8_t* data, size_t size) { - const size_t compassScreenWidth = 320; + constexpr int compassScreenWidth = 320; - const size_t bytesPerRow = compassScreenWidth / 8; - if (size == 0 || size % bytesPerRow != 0) { + const int stride = wordAlignedStride(compassScreenWidth); + if (size == 0 || size % stride != 0) { return CanvasImage{}; } - int h = static_cast(size / bytesPerRow); + int h = static_cast(size / stride); if (h <= 0 || h > kMaxImageHeight) { return CanvasImage{}; } @@ -140,45 +94,44 @@ CanvasImage decodeRawHeaderless(const uint8_t* data, size_t size) { img.format = QStringLiteral("raw"); img.w = compassScreenWidth; img.h = h; + img.stride = stride; img.raster = data; img.rasterLen = size; return img; } -CanvasImage detectCanvas(const uint8_t* data, size_t size) { - if (isGridpaintSignature(data, size)) { - return decodeGridpaint(data, size); - } - if (CanvasImage img = decodeRawWithHeader(data, size); img.raster) { +CanvasImage detectCanvas(const uint8_t* data, size_t size, uint32_t propLength) { + OmfMetadata md = parseOmfMetadata(data, size, propLength); + if (CanvasImage img = decodeFromMetadata(md); img.raster) { return img; + } else { + return decodeRawHeaderless(md.content, md.contentSize); } - return decodeRawHeaderless(data, size); } -QImage renderMono(const uint8_t* raster, size_t rasterLen, int w, int h) { - QImage img(w, h, QImage::Format_RGB32); - img.fill(qRgb(0, 0, 0)); - - const int bytesPerRow = w / 8; - const int rows = std::min(h, static_cast(rasterLen / bytesPerRow)); - const QRgb on = qRgb(255, 255, 255); - const QRgb off = qRgb(0, 0, 0); +QImage renderMono(const uint8_t* data, size_t dataLen, int w, int h, int bytesPerRow) { + QImage img(w, h, QImage::Format_Mono); + img.setColor(0, qRgb(0, 0, 0)); + img.setColor(1, qRgb(255, 255, 255)); + img.fill(0); + const int wordsPerRow = bytesPerRow / 2; + const int rows = std::min(h, static_cast(dataLen / bytesPerRow)); for (int y = 0; y < rows; y++) { - const uint8_t* row = raster + y * bytesPerRow; - QRgb* scan = reinterpret_cast(img.scanLine(y)); - for (int x = 0; x < w; x++) { - const int bo = (x / 16) * 2; - uint16_t word = static_cast(row[bo] | (row[bo + 1] << 8)); - scan[x] = (word >> (15 - (x % 16))) & 1 ? on : off; + const uint8_t* src = data + y * bytesPerRow; + uint8_t* dst = img.scanLine(y); + + for (int i = 0; i < wordsPerRow; i++) { + dst[2 * i] = src[2 * i + 1]; + dst[2 * i + 1] = src[2 * i]; } } return img; } -// QLabel that rescales its pixmap to fit on resize, keeping aspect ratio. -// FastTransformation keeps the 1-bpp pixel art crisp instead of smearing it. +// QLabel that rescales its pixmap on resize, keeping aspect ratio with crisp +// nearest-neighbour scaling for pixel art. class ImageLabel : public QLabel { public: using QLabel::QLabel; @@ -217,19 +170,20 @@ bool CanvasPreview::supports(const QString& fileType, size_t fileSize) const { QWidget* CanvasPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { uint8_t* data = nullptr; size_t size = 0; + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); return nullptr; } - CanvasImage img = detectCanvas(data, size); + CanvasImage img = detectCanvas(data, size, file->desc.prop_length); if (img.raster == nullptr) { + QMessageBox::warning(parent, "Preview", "Could not decode this canvas image."); free(data); - QMessageBox::warning(parent, "Preview", "Could not decode this canvas image (unknown format)."); return nullptr; } - QImage image = renderMono(img.raster, img.rasterLen, img.w, img.h); + QImage image = renderMono(img.raster, img.rasterLen, img.w, img.h, img.stride); free(data); auto* container = new QWidget(parent); diff --git a/dialogs/preview/omf/omf_record.cpp b/dialogs/preview/omf/omf_record.cpp new file mode 100644 index 0000000..5f8b923 --- /dev/null +++ b/dialogs/preview/omf/omf_record.cpp @@ -0,0 +1,62 @@ +#include "omf_record.h" + +OmfRecord readOmfRecord(const uint8_t* p, const uint8_t* end) { + OmfRecord r; + if (end - p < 3) { + return r; // not even a header fits + } + r.type = p[0]; + r.length = omfU16LE(p + 1); + r.payload = p + 3; + if (r.payload + r.length > end) { + return OmfRecord{}; // payload doesn't fit + } + return r; +} + +OmfMetadata parseOmfMetadata(const uint8_t* data, size_t fileSize, uint32_t propLength) { + OmfMetadata md; + + const size_t metaEnd = (propLength <= fileSize) ? static_cast(propLength) : fileSize; + md.content = data + metaEnd; + md.contentSize = fileSize - metaEnd; + + const uint8_t* p = data; + const uint8_t* end = data + metaEnd; + for (OmfRecord rec = readOmfRecord(p, end); + rec.payload != nullptr; + rec = readOmfRecord(p, end)) { + md.records.push_back(rec); + p = rec.payload + rec.length; + } + + return md; +} + +const OmfRecord* OmfMetadata::find(uint8_t type, uint8_t subtype) const { + for (const OmfRecord& r : records) { + if (r.type == type && r.subtype() == subtype) { + return &r; + } + } + return nullptr; +} + +const OmfRecord* OmfMetadata::findFirst(uint8_t type) const { + for (const OmfRecord& r : records) { + if (r.type == type) { + return &r; + } + } + return nullptr; +} + +std::vector OmfMetadata::findAll(uint8_t type, uint8_t subtype) const { + std::vector out; + for (const OmfRecord& r : records) { + if (r.type == type && r.subtype() == subtype) { + out.push_back(&r); + } + } + return out; +} diff --git a/dialogs/preview/omf/omf_record.h b/dialogs/preview/omf/omf_record.h new file mode 100644 index 0000000..9b5ec4b --- /dev/null +++ b/dialogs/preview/omf/omf_record.h @@ -0,0 +1,41 @@ +#ifndef GRIDISKCOM_OMF_RECORD_H +#define GRIDISKCOM_OMF_RECORD_H + +#include +#include + +#include + +constexpr uint8_t kOmfMetadataRecord = 0xFE; +constexpr uint8_t kOmfLabelRecord = 0xFD; + +inline uint16_t omfU16LE(const uint8_t* p) { + return uint16_t(p[0]) | (uint16_t(p[1]) << 8); +} + +struct OmfRecord { + uint8_t type = 0; + const uint8_t* payload = nullptr; + uint16_t length = 0; + + uint8_t subtype() const { + return length ? payload[0] : 0; + } +}; + +struct OmfMetadata { + const uint8_t* content = nullptr; + size_t contentSize = 0; + + std::vector records; + + const OmfRecord* find(uint8_t type, uint8_t subtype) const; + const OmfRecord* findFirst(uint8_t type) const; + std::vector findAll(uint8_t type, uint8_t subtype) const; +}; + +OmfRecord readOmfRecord(const uint8_t* p, const uint8_t* end); + +OmfMetadata parseOmfMetadata(const uint8_t* data, size_t fileSize, uint32_t propLength); + +#endif // GRIDISKCOM_OMF_RECORD_H diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp index 54b58ca..00f9bc0 100644 --- a/dialogs/preview/textpreview.cpp +++ b/dialogs/preview/textpreview.cpp @@ -1,5 +1,7 @@ #include "textpreview.h" +#include "omf/omf_record.h" + #include #include #include @@ -141,11 +143,10 @@ QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidge font.setStyleHint(QFont::TypeWriter); edit->setFont(font); - // The first `prop_length` bytes of the file store properties and are not part of the text. - uint32_t prop_length = file->desc.prop_length; - size_t offset = (prop_length <= size) ? prop_length : size; - size_t text_len = size - offset; - edit->setPlainText(decodeText(data + offset, text_len)); + // The first `prop_length` bytes are the OMF metadata stream. + // The rest is the text body. + OmfMetadata md = parseOmfMetadata(data, size, file->desc.prop_length); + edit->setPlainText(decodeText(md.content, md.contentSize)); free(data); auto* container = new QWidget(parent); diff --git a/dialogs/preview/worksheetpreview.cpp b/dialogs/preview/worksheetpreview.cpp index eecfacc..197619c 100644 --- a/dialogs/preview/worksheetpreview.cpp +++ b/dialogs/preview/worksheetpreview.cpp @@ -1,5 +1,7 @@ #include "worksheetpreview.h" +#include "omf/omf_record.h" + #include #include #include @@ -13,131 +15,89 @@ namespace { -enum RecordType : uint8_t { - MetadataRecord = 0xFE, // metadata: title, signature - LabelRecord = 0xFD, // labelled block (column labels / formulas) -}; - -constexpr uint8_t SignatureByte = 0x61; -constexpr uint8_t TitleSubtype = 0x68; -constexpr uint8_t ColumnLabelSubtype = 0x00; +constexpr uint8_t kTitleSubtype = 0x68; +constexpr uint8_t kColumnLabelSubtype = 0x00; -// Sanity guards to prevent garbage from being decoded. constexpr int kMaxRows = 50000; constexpr int kMaxCols = 256; struct Worksheet { QString title; QMap columnLabels; // 1-based column index -> label text - QList rows; // each row is a list of cell strings - bool valid = false; // set only after the signature matches + QList rows; + bool valid = false; }; -inline uint16_t u16le(const uint8_t* p) { - return uint16_t(p[0]) | (uint16_t(p[1]) << 8); -} - -bool isWorksheetSignature(const uint8_t* data, size_t size) { - // The opening record of every GRiDPlan II worksheet is `FE 04 00 61 …` - // (a 4-byte metadata record whose payload begins with the signature byte). - return size >= 4 && data[0] == MetadataRecord && data[3] == SignatureByte; -} - -QList splitCsvRows(const QByteArray& csv) { - QString text = QString::fromLatin1(csv.constData(), csv.size()); - QStringList lines = text.split(QLatin1Char('\n')); - if (!lines.isEmpty() && lines.last().isEmpty() && csv.endsWith('\n')) { - lines.removeLast(); - } +// Parse worksheet content into rows of tab-separated cells in a single pass, +// skipping over interleaved FE/FD OMF records (e.g. per-row formula byte-code). +// CSV text is plain ASCII, so 0xFE/0xFD never appear inside a cell value. +QList parseCsvRows(const uint8_t* data, size_t size) { QList rows; - rows.reserve(lines.size()); - for (const QString& line : lines) { - QString row = line; - if (row.endsWith(QLatin1Char('\r'))) { - row.chop(1); - } - rows.append(row.split(QLatin1Char('\t'), Qt::KeepEmptyParts)); - } - return rows; -} - -Worksheet parseWorksheet(const uint8_t* data, size_t size) { - Worksheet ws; - if (!isWorksheetSignature(data, size)) { - printf("Invalid worksheet\n"); - return ws; - } + QStringList row; + QByteArray cell; const uint8_t* p = data; const uint8_t* end = data + size; - // Phase 1 — header records. Stop at the first byte that is not a record - // leader; that byte begins the CSV cell-value block. - while (end - p >= 3) { - uint8_t type = *p++; - if (type != MetadataRecord && type != LabelRecord) { - break; - } - - size_t len = u16le(p); - p += 2; - - if (len < 1) { - continue; + while (p < end) { + if (*p == kOmfMetadataRecord || *p == kOmfLabelRecord) { + const OmfRecord rec = readOmfRecord(p, end); + if (rec.payload != nullptr) { + p = rec.payload + rec.length; + continue; + } } - const uint8_t* payload = p; - p += len; - - if (payload + len > end) { - break; + const char c = static_cast(*p++); + if (c == '\t') { + row.append(QString::fromLatin1(cell)); + cell.clear(); + } else if (c == '\n') { + row.append(QString::fromLatin1(cell)); + rows.append(row); + row.clear(); + cell.clear(); + } else if (c != '\r') { + cell.append(c); } + } - uint8_t sub = *payload++; - - if (type == MetadataRecord) { - if (sub == TitleSubtype) { - ws.title = QString::fromLatin1(reinterpret_cast(payload), int(len - 1)); - } - } else { // LabelRecord - if (sub == ColumnLabelSubtype && len >= 2) { - int col = *payload++; - ws.columnLabels[col] = QString::fromLatin1(reinterpret_cast(payload), int(len - 2)); - } - } + if (!cell.isEmpty() || !row.isEmpty()) { + row.append(QString::fromLatin1(cell)); + rows.append(row); } - // Phase 2 — data section: gather the raw CSV bytes, skipping over any - // interleaved FE/FD records (e.g. per-row formula byte-code, FD 0x02). - QByteArray csv; - csv.reserve(static_cast(end - p)); - while (p < end) { - uint8_t type = *p++; - if (type != MetadataRecord && type != LabelRecord) { - csv.append(static_cast(type)); - p++; - continue; - } + return rows; +} - if (end - p < 3) { - break; // truncated leader; leave the tail uncollected - } +Worksheet parseWorksheet(const uint8_t* data, size_t size, uint32_t propLength) { + Worksheet ws; - size_t len = u16le(p); - p += 2; + OmfMetadata md = parseOmfMetadata(data, size, propLength); + if (md.records.empty()) { + return ws; // not a worksheet: no metadata stream + } - const uint8_t* payload = p; - p += len; + if (const OmfRecord* title = md.find(kOmfMetadataRecord, kTitleSubtype)) { + // payload[0] = subtype 'h', payload[1..] = title text. + ws.title = QString::fromLatin1( + reinterpret_cast(title->payload + 1), int(title->length - 1)); + } - if (payload + len > end) { - // Truncated trailing record — treat the remainder as CSV. - csv.append(reinterpret_cast(p), static_cast(end - p)); - break; + // payload[0] = subtype 0x00, payload[1] = 1-based column index, rest = label. + // length >= 2 is required: payload[1] must exist. (subtype 0x00 also matches + // zero-length records via subtype(), so the guard is not redundant.) + for (const OmfRecord* r : md.findAll(kOmfLabelRecord, kColumnLabelSubtype)) { + if (r->length >= 2) { + int col = r->payload[1]; + ws.columnLabels[col] = QString::fromLatin1( + reinterpret_cast(r->payload + 2), int(r->length - 2)); } } - ws.rows = splitCsvRows(csv); + ws.rows = parseCsvRows(md.content, md.contentSize); ws.valid = true; + return ws; } @@ -156,13 +116,13 @@ QWidget* WorksheetPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, Q return nullptr; } - Worksheet ws = parseWorksheet(data, size); + Worksheet ws = parseWorksheet(data, size, file->desc.prop_length); + free(data); + if (!ws.valid) { - free(data); QMessageBox::warning(parent, "Preview", "Could not decode this worksheet (unknown format)."); return nullptr; } - free(data); auto* container = new QWidget(parent); auto* layout = new QVBoxLayout(container); @@ -183,7 +143,6 @@ QWidget* WorksheetPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, Q for (const QStringList& row : ws.rows) { colCount = qMax(colCount, row.size()); } - for (auto it = ws.columnLabels.cbegin(); it != ws.columnLabels.cend(); ++it) { if (it.key() <= kMaxCols) { colCount = qMax(colCount, it.key()); @@ -201,18 +160,19 @@ QWidget* WorksheetPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, Q font.setStyleHint(QFont::TypeWriter); table->setFont(font); - if (!ws.columnLabels.isEmpty()) { + if (ws.columnLabels.isEmpty()) { + for (int c = 0; c < colCount; ++c) { + table->setHorizontalHeaderItem(c, new QTableWidgetItem(QString::number(c + 1))); + } + } else { QStringList headerLabels; headerLabels.reserve(colCount); for (int c = 0; c < colCount; ++c) { headerLabels << ws.columnLabels.value(c + 1); } table->setHorizontalHeaderLabels(headerLabels); - } else { - for (int c = 0; c < colCount; ++c) { - table->setHorizontalHeaderItem(c, new QTableWidgetItem(QString::number(c + 1))); - } } + table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); table->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); @@ -224,6 +184,7 @@ QWidget* WorksheetPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, Q } } } + layout->addWidget(table, 1); return container; From 7d5b97a5ac389595c57d7c74f86d9918cdaaaf48 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 18:15:27 +0300 Subject: [PATCH 15/36] strp pcl escape seq from worksheet titles --- dialogs/preview/worksheetpreview.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/dialogs/preview/worksheetpreview.cpp b/dialogs/preview/worksheetpreview.cpp index 197619c..965a163 100644 --- a/dialogs/preview/worksheetpreview.cpp +++ b/dialogs/preview/worksheetpreview.cpp @@ -28,6 +28,26 @@ struct Worksheet { bool valid = false; }; +// Strip PCL escape sequences from raw bytes. +QString stripPclEsc(const uint8_t* data, int size) { + QString out; + out.reserve(size); + + int i = 0; + while (i < size) { + if (data[i] == 0x1B) { + i++; + while (i < size && !(data[i] >= 'A' && data[i] <= 'Z')) i++; + if (i < size) i++; // consume the terminator + } else { + out += QLatin1Char(static_cast(data[i])); + i++; + } + } + + return out.trimmed(); +} + // Parse worksheet content into rows of tab-separated cells in a single pass, // skipping over interleaved FE/FD OMF records (e.g. per-row formula byte-code). // CSV text is plain ASCII, so 0xFE/0xFD never appear inside a cell value. @@ -80,8 +100,7 @@ Worksheet parseWorksheet(const uint8_t* data, size_t size, uint32_t propLength) if (const OmfRecord* title = md.find(kOmfMetadataRecord, kTitleSubtype)) { // payload[0] = subtype 'h', payload[1..] = title text. - ws.title = QString::fromLatin1( - reinterpret_cast(title->payload + 1), int(title->length - 1)); + ws.title = stripPclEsc(title->payload + 1, int(title->length - 1)); } // payload[0] = subtype 0x00, payload[1] = 1-based column index, rest = label. From 01e632bce44ad43ac9ec15e180308d8a98167e1c Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 19:32:05 +0300 Subject: [PATCH 16/36] use yellow instead of white in canvas preview window --- dialogs/preview/canvaspreview.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialogs/preview/canvaspreview.cpp b/dialogs/preview/canvaspreview.cpp index 348b668..2fe3a86 100644 --- a/dialogs/preview/canvaspreview.cpp +++ b/dialogs/preview/canvaspreview.cpp @@ -113,7 +113,7 @@ CanvasImage detectCanvas(const uint8_t* data, size_t size, uint32_t propLength) QImage renderMono(const uint8_t* data, size_t dataLen, int w, int h, int bytesPerRow) { QImage img(w, h, QImage::Format_Mono); img.setColor(0, qRgb(0, 0, 0)); - img.setColor(1, qRgb(255, 255, 255)); + img.setColor(1, qRgb(0xFF, 0xEB, 0x00)); img.fill(0); const int wordsPerRow = bytesPerRow / 2; From 6ce3b321673e0947438b1ed458c888044e6295cd Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 19:32:42 +0300 Subject: [PATCH 17/36] display file name in preview window title --- dialogs/preview/previewdlg.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp index 7f26faf..5b77fac 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/dialogs/preview/previewdlg.cpp @@ -18,7 +18,6 @@ static QList previewRegistry() { } PreviewDlg::PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) : QDialog(parent) { - setWindowTitle("File preview"); setModal(true); setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); setSizeGripEnabled(false); @@ -28,6 +27,8 @@ PreviewDlg::PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) : char basename[CCOS_MAX_FILE_NAME] = {0}; char type[CCOS_MAX_FILE_NAME] = {0}; ccos_parse_file_name(file, basename, type, nullptr, nullptr); + setWindowTitle(QString("Preview %1~%2~ file") + .arg(QString::fromLatin1(basename), QString::fromLatin1(type))); size_t fileSize = file->desc.file_size; FilePreview* matched = nullptr; From 08838616aa0f48185b64552e26b6de2e3a616452 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Mon, 27 Jul 2026 20:52:34 +0300 Subject: [PATCH 18/36] preview font files --- CMakeLists.txt | 2 + dialogs/preview/filepreview.h | 7 + dialogs/preview/fontpreview.cpp | 454 ++++++++++++++++++++++++++++++++ dialogs/preview/fontpreview.h | 16 ++ dialogs/preview/previewdlg.cpp | 22 +- 5 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 dialogs/preview/fontpreview.cpp create mode 100644 dialogs/preview/fontpreview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 18b7aad..c26e6b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/preview/textpreview.h dialogs/preview/canvaspreview.cpp dialogs/preview/canvaspreview.h + dialogs/preview/fontpreview.cpp + dialogs/preview/fontpreview.h dialogs/preview/worksheetpreview.cpp dialogs/preview/worksheetpreview.h dialogs/preview/previewdlg.cpp diff --git a/dialogs/preview/filepreview.h b/dialogs/preview/filepreview.h index 370b129..2bb4216 100644 --- a/dialogs/preview/filepreview.h +++ b/dialogs/preview/filepreview.h @@ -1,6 +1,7 @@ #ifndef FILEPREVIEW_H #define FILEPREVIEW_H +#include #include #include @@ -15,6 +16,12 @@ class FilePreview { virtual bool supports(const QString& fileType, size_t fileSize) const = 0; virtual QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) = 0; + + // Optional hint: a preview may request a larger initial dialog than the + // default (e.g. the font preview needs room for its glyph table). An empty + // size means "use the dialog default". The dialog still clamps to its own + // minimum/maximum and to the available screen space. + virtual QSize preferredDialogSize() const { return QSize(); } }; #endif // FILEPREVIEW_H diff --git a/dialogs/preview/fontpreview.cpp b/dialogs/preview/fontpreview.cpp new file mode 100644 index 0000000..1d32d5d --- /dev/null +++ b/dialogs/preview/fontpreview.cpp @@ -0,0 +1,454 @@ +#include "fontpreview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "omf/omf_record.h" + +namespace { + +constexpr int kHeaderSize = 8; +constexpr int kGridColumns = 16; // glyphs per row in the table + +constexpr int kFullCharCount = 256; // numChars == 0 encodes the full set +constexpr int kMaxGlyphDim = 64; +constexpr int kMaxLineHeight = 80; + +constexpr QRgb kFg = qRgb(0xFF, 0xEB, 0x00); +constexpr QRgb kBg = qRgb(0x00, 0x00, 0x00); +constexpr QRgb kBorder = qRgb(0x40, 0x40, 0x40); + +struct RawFontInfo { + int charCount = 0; + int charWidth = 0; + int charHeight = 0; + int effectiveLineHeight = 0; + int baseLine = 0; + int bytesPerRow = 0; + size_t expectedSize = 0; +}; + +struct FoundFont { + size_t offset = 0; + long long leftover = 0; + RawFontInfo info; +}; + +bool parseHeaderAt(const uint8_t* data, size_t size, size_t offset, RawFontInfo& out) { + if (offset + size_t(kHeaderSize) > size) + return false; + + const uint8_t* h = data + offset; + const int numChars = h[0]; + const int lineHeight = h[3]; + + out.charWidth = h[1]; + if (out.charWidth <= 0 || out.charWidth > kMaxGlyphDim) + return false; + + out.charHeight = h[2]; + if (out.charHeight <= 0 || out.charHeight > kMaxGlyphDim) + return false; + + out.baseLine = h[4]; + + out.charCount = numChars ? numChars : kFullCharCount; + if (out.charCount <= 0 || out.charCount > kFullCharCount) + return false; + + out.bytesPerRow = (out.charWidth + 7) / 8; + if (out.bytesPerRow <= 0) + return false; + + out.effectiveLineHeight = lineHeight ? lineHeight : (out.charHeight + 1); + out.expectedSize = size_t(kHeaderSize) + + size_t(out.bytesPerRow) * size_t(out.charHeight) * size_t(out.charCount); + + return out.effectiveLineHeight >= out.charHeight && + out.effectiveLineHeight <= kMaxLineHeight && + out.baseLine <= out.effectiveLineHeight && + out.expectedSize <= size - offset; +} + +bool findRawFont(const uint8_t* data, size_t size, FoundFont& out) { + RawFontInfo info; + if (!parseHeaderAt(data, size, 0, info)) return false; + out = { 0, (long long)size - (long long)info.expectedSize, info }; + return true; +} + +bool readOmfIndex(const uint8_t* p, const uint8_t* end, int& value, const uint8_t*& next) { + if (p >= end) return false; + const uint8_t first = *p; + if (first & 0x80) { + if (p + 1 >= end) return false; + value = (int(first & 0x7F) << 8) | p[1]; + next = p + 2; + } else { + value = first; + next = p + 1; + } + return true; +} + +void placeChunk(QByteArray& seg, int offset, const uint8_t* chunk, int len) { + const int need = offset + len; + if (seg.size() < need) { + const int oldSize = seg.size(); + seg.resize(need); + std::memset(seg.data() + oldSize, 0, size_t(need - oldSize)); + } + std::memcpy(seg.data() + offset, chunk, size_t(len)); +} + +bool tryExtractOmfFont(const uint8_t* data, size_t size, QByteArray& outPayload, RawFontInfo& outInfo) { + constexpr uint8_t kLedata = 0xA0; + + QMap segments; + int ledataCount = 0; + const uint8_t* p = data; + const uint8_t* end = data + size; + while (p < end) { + const OmfRecord rec = readOmfRecord(p, end); // shared OMF record reader + if (rec.payload == nullptr) break; + p = rec.payload + rec.length; // advance past this record + if (rec.type != kLedata || rec.length < 2) continue; + + const uint8_t* contentEnd = rec.payload + rec.length - 1; // drop the checksum byte + int segmentIndex = 0; + const uint8_t* q = rec.payload; + if (!readOmfIndex(q, contentEnd, segmentIndex, q)) continue; + if (q + 2 > contentEnd) continue; + const int dataOffset = int(q[0]) | (int(q[1]) << 8); + q += 2; + + const int chunkLen = int(contentEnd - q); + if (chunkLen <= 0) continue; + QByteArray& seg = segments[segmentIndex]; + placeChunk(seg, dataOffset, q, chunkLen); + ledataCount++; + } + + if (ledataCount == 0) return false; + + bool found = false; + long long bestLeftover = 0; + for (const QByteArray& seg : segments) { + FoundFont candidate; + if (!findRawFont(reinterpret_cast(seg.constData()), seg.size(), candidate)) { + continue; + } + if (!found || std::llabs(candidate.leftover) < std::llabs(bestLeftover)) { + bestLeftover = candidate.leftover; + outPayload = seg.mid(int(candidate.offset), int(candidate.info.expectedSize)); + outInfo = candidate.info; + found = true; + } + } + return found; +} + +bool extractFontPayload(const uint8_t* data, size_t size, + QByteArray& outPayload, RawFontInfo& outInfo) { + return tryExtractOmfFont(data, size, outPayload, outInfo); +} + +QImage buildGlyphImage(const uint8_t* payload, const RawFontInfo& info, int gi) { + QImage img(info.charWidth, info.charHeight, QImage::Format_Mono); + img.setColor(0, kBg); + img.setColor(1, kFg); + img.fill(0); + + for (int y = 0; y < info.charHeight; y++) { + const size_t rowOffset = size_t(kHeaderSize) + + size_t(y) * info.charCount * info.bytesPerRow + + size_t(gi) * info.bytesPerRow; + if (rowOffset + size_t(info.bytesPerRow) > info.expectedSize) break; + std::memcpy(img.scanLine(y), payload + rowOffset, info.bytesPerRow); + } + return img; +} + +QImage renderFontGrid(const uint8_t* payload, const RawFontInfo& info, int scale) { + const int cellSize = std::max(info.charWidth, info.effectiveLineHeight) * scale; + const int rows = (info.charCount + kGridColumns - 1) / kGridColumns; + const int border = 1; + const int imgW = kGridColumns * (cellSize + border) + border; + const int imgH = rows * (cellSize + border) + border; + + QImage img(imgW, imgH, QImage::Format_RGB32); + img.fill(kBorder); + + const int glyphW = info.charWidth * scale; + const int glyphH = info.charHeight * scale; + const int offX = (cellSize - glyphW) / 2; + const int offY = (cellSize - glyphH) / 2; + + QPainter p(&img); + const int totalCells = rows * kGridColumns; + for (int i = 0; i < totalCells; i++) { + const int col = i % kGridColumns; + const int row = i / kGridColumns; + const int cellX = border + col * (cellSize + border); + const int cellY = border + row * (cellSize + border); + + p.fillRect(cellX, cellY, cellSize, cellSize, QColor(kBg)); + if (i >= info.charCount) continue; + + const QImage glyph = buildGlyphImage(payload, info, i) + .scaled(glyphW, glyphH, Qt::IgnoreAspectRatio, Qt::FastTransformation); + p.drawImage(cellX + offX, cellY + offY, glyph); + } + return img; +} + +int pickTableScale(int charHeight) { + if (charHeight <= 0) { + return 2; + } + int scale = int(std::lround(24.0 / charHeight)); + return std::max(2, std::min(3, scale)); +} + +constexpr const char* kDefaultSampleText = "GRiDISKCOM loves GRiD-OS files"; + +QStringList wrapText(const QString& text, int wrapCols) { + QStringList lines; + QString line; + for (const QString& word : text.split(QLatin1Char(' '))) { + const int add = word.length() + (line.isEmpty() ? 0 : 1); + if (!line.isEmpty() && line.length() + add > wrapCols) { + lines << line; + line = word; + } else { + line = line.isEmpty() ? word : (line + QLatin1Char(' ') + word); + } + } + lines << line; + return lines; +} + +class FontTextPreview : public QWidget { +public: + explicit FontTextPreview(QWidget* parent = nullptr) : QWidget(parent) { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + setAutoFillBackground(true); + QPalette pal = palette(); + pal.setColor(QPalette::Window, Qt::black); + setPalette(pal); + } + + void setFontData(const QByteArray& payload, const RawFontInfo& info) { + m_payload = payload; + m_info = info; + + constexpr int kTargetGlyphH = 24; + const double s = double(kTargetGlyphH) / std::max(1, info.charHeight); + m_dispW = std::max(1, int(std::lround(info.charWidth * s))); + m_dispH = std::max(1, int(std::lround(info.charHeight * s))); + m_dispLineH = std::max(m_dispH, int(std::lround(info.effectiveLineHeight * s))); + + buildCodeMap(); + buildGlyphCache(); + rewrap(); + updateGeometry(); + update(); + } + + void setText(const QString& text) { + m_text = text; + rewrap(); + updateGeometry(); + update(); + } + + QSize sizeHint() const override { + return QSize(width() > 0 ? width() : 600, heightForLines(m_lineCount)); + } + + QSize minimumSizeHint() const override { + return QSize(0, heightForLines(1)); + } + +protected: + void resizeEvent(QResizeEvent*) override { + rewrap(); + updateGeometry(); + update(); + } + + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.fillRect(rect(), Qt::black); + + const int maxLines = height() / m_dispLineH; + + int y = 0; + for (int li = 0; li < m_lines.size() && li < maxLines; li++) { + const QString& line = m_lines[li]; + int x = 0; + for (int ci = 0; ci < line.length(); ci++) { + int code = line[ci].unicode(); + if (code > 255) code = '?'; // out of the font's byte range + const int gi = m_codeToGlyph[code]; + if (gi >= 0) { + p.drawImage(x, y, m_glyphCache[gi]); + } + x += m_dispW; + } + y += m_dispLineH; + } + } + +private: + int heightForLines(int lines) const { + const int n = std::max(2, std::min(6, lines)); + return n * m_dispLineH; + } + + void buildCodeMap() { + const int firstChar = (m_info.charCount >= 95 && m_info.charCount <= 96) ? 32 : 0; + const int qGlyph = 63 - firstChar; + const int missing = (qGlyph >= 0 && qGlyph < m_info.charCount) ? qGlyph : -1; + for (int c = 0; c < 256; c++) { + const int gi = c - firstChar; + m_codeToGlyph[c] = (gi >= 0 && gi < m_info.charCount) ? gi : missing; + } + } + + void buildGlyphCache() { + m_glyphCache.clear(); + m_glyphCache.reserve(m_info.charCount); + const uint8_t* payload = reinterpret_cast(m_payload.constData()); + for (int gi = 0; gi < m_info.charCount; gi++) { + QImage base = buildGlyphImage(payload, m_info, gi); + m_glyphCache.append(base.scaled(m_dispW, m_dispH, + Qt::IgnoreAspectRatio, Qt::FastTransformation)); + } + } + + void rewrap() { + const int w = width() > 0 ? width() : 600; + const int wrapCols = std::max(1, w / m_dispW); + m_lines = wrapText(m_text, wrapCols); + m_lineCount = std::max(1, m_lines.size()); + } + + QByteArray m_payload; + RawFontInfo m_info; + QString m_text; + QVector m_glyphCache; + std::array m_codeToGlyph{}; // byte code -> glyph index (-1 = none) + QStringList m_lines; + int m_dispW = 8; + int m_dispH = 8; + int m_dispLineH = 10; + int m_lineCount = 1; +}; + +} // namespace + +bool FontPreview::supports(const QString& fileType, size_t fileSize) const { + return fileSize >= size_t(kHeaderSize) && + fileType.compare("font", Qt::CaseInsensitive) == 0; +} + +QWidget* FontPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { + uint8_t* data = nullptr; + size_t size = 0; + + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { + QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); + return nullptr; + } + + QByteArray payload; + RawFontInfo info; + if (!extractFontPayload(data, size, payload, info)) { + QMessageBox::warning(parent, "Preview", "Could not decode this font (no GRiD font payload found)."); + free(data); + return nullptr; + } + free(data); + + QImage grid = renderFontGrid(reinterpret_cast(payload.constData()), + info, pickTableScale(info.charHeight)); + + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + // --- Typed-text preview --- + auto* sampleEdit = new QLineEdit(container); + sampleEdit->setText(QString::fromUtf8(kDefaultSampleText)); + sampleEdit->setClearButtonEnabled(true); + sampleEdit->setValidator(new QRegularExpressionValidator( + QRegularExpression(QStringLiteral("[^\n]*")), sampleEdit)); + QFont mono("Monospace"); + mono.setStyleHint(QFont::TypeWriter); + sampleEdit->setFont(mono); + layout->addWidget(sampleEdit); + + auto* textPreview = new FontTextPreview(container); + textPreview->setFontData(payload, info); + textPreview->setText(sampleEdit->text()); + layout->addWidget(textPreview); + + QObject::connect(sampleEdit, &QLineEdit::textChanged, textPreview, [textPreview](const QString& text) { + textPreview->setText(text); + }); + + // --- Full glyph table --- + auto* scroll = new QScrollArea(container); + scroll->setWidgetResizable(false); + scroll->setAlignment(Qt::AlignCenter); + scroll->setFocusPolicy(Qt::NoFocus); + // Plain black background, no native frame: KDE/Breeze otherwise paints a + // blue rounded focus/hover rectangle around the viewport on mouse-over. + scroll->setStyleSheet(QStringLiteral( + "QScrollArea { border: none; background: black; }" + "QScrollArea > QWidget > QWidget { background: black; border: none; }")); + auto* imageLabel = new QLabel(scroll); + imageLabel->setPixmap(QPixmap::fromImage(grid)); + imageLabel->setFixedSize(grid.size()); + // The label is purely decorative: ignore mouse events so the toolkit never + // enters a hover/focus state (which is what draws that blue rounded box). + imageLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); + imageLabel->setFocusPolicy(Qt::NoFocus); + scroll->setWidget(imageLabel); + layout->addWidget(scroll, 1); + + // --- Info label --- + auto* infoLabel = new QLabel( + QStringLiteral("Cell: %1 × %2 | Line height: %3 | Baseline: %4") + .arg(info.charWidth) + .arg(info.charHeight) + .arg(info.effectiveLineHeight) + .arg(info.baseLine), + container); + infoLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + infoLabel->setWordWrap(true); + infoLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + layout->addWidget(infoLabel); + + return container; +} diff --git a/dialogs/preview/fontpreview.h b/dialogs/preview/fontpreview.h new file mode 100644 index 0000000..da166e5 --- /dev/null +++ b/dialogs/preview/fontpreview.h @@ -0,0 +1,16 @@ +#ifndef FONTPREVIEW_H +#define FONTPREVIEW_H + +#include "filepreview.h" + +class FontPreview : public FilePreview { +public: + QString name() const override { return "Font"; } + bool supports(const QString& fileType, size_t fileSize) const override; + QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) override; + // The glyph table (16 columns) plus the typed-text panel need more room + // than the default 660x540 to be comfortable. + QSize preferredDialogSize() const override { return QSize(860, 640); } +}; + +#endif // FONTPREVIEW_H diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp index 5b77fac..12f338d 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/dialogs/preview/previewdlg.cpp @@ -4,15 +4,19 @@ #include "textpreview.h" #include "canvaspreview.h" #include "worksheetpreview.h" +#include "fontpreview.h" #include #include +#include +#include static QList previewRegistry() { static QList registry = { new TextPreview(), new CanvasPreview(), new WorksheetPreview(), + new FontPreview(), }; return registry; } @@ -50,7 +54,23 @@ PreviewDlg::PreviewDlg(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) : } layout->addWidget(content, 1); - resize(660, 540); + // A preview may ask for a roomier window than the default (e.g. the font + // preview's glyph table). Honour it, but never go below the default or the + // minimum, and never larger than ~85% of the screen so it stays usable. + QSize size(660, 540); + if (matched) { + const QSize want = matched->preferredDialogSize(); + if (want.isValid() && !want.isEmpty()) { + size = size.expandedTo(want); + } + } + const QSize screen = QGuiApplication::primaryScreen() + ? QGuiApplication::primaryScreen()->availableSize() + : QSize(); + if (screen.isValid()) { + size = size.boundedTo(QSize(screen.width() * 85 / 100, screen.height() * 85 / 100)); + } + resize(size); setMinimumSize(360, 280); setSizeGripEnabled(true); } From 0cc750fefd87ae9bb8a6e8ad9108d3d38a8f89a9 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Tue, 28 Jul 2026 20:32:54 +0300 Subject: [PATCH 19/36] preview database files --- CMakeLists.txt | 2 + dialogs/preview/databasepreview.cpp | 224 ++++++++++++++++++++++++++++ dialogs/preview/databasepreview.h | 13 ++ dialogs/preview/previewdlg.cpp | 2 + 4 files changed, 241 insertions(+) create mode 100644 dialogs/preview/databasepreview.cpp create mode 100644 dialogs/preview/databasepreview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c26e6b3..fadede9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/preview/fontpreview.h dialogs/preview/worksheetpreview.cpp dialogs/preview/worksheetpreview.h + dialogs/preview/databasepreview.cpp + dialogs/preview/databasepreview.h dialogs/preview/previewdlg.cpp dialogs/preview/previewdlg.h dialogs/rendlg.cpp diff --git a/dialogs/preview/databasepreview.cpp b/dialogs/preview/databasepreview.cpp new file mode 100644 index 0000000..9b24ceb --- /dev/null +++ b/dialogs/preview/databasepreview.cpp @@ -0,0 +1,224 @@ +#include "databasepreview.h" + +#include "omf/omf_record.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint8_t kTitleSubtype = 0x68; +constexpr uint8_t kColumnLabelSubtype = 0x00; +constexpr uint8_t kColumnFormulaSubtype = 0x02; + +constexpr int kMaxRows = 50000; +constexpr int kMaxCols = 256; + +struct Database { + QString title; + QMap columnLabels; // 1-based column index -> label text + QMap columnFormulas; // 1-based column index -> formula text + QList rows; + bool valid = false; +}; + +// Strip PCL escape sequences from raw bytes. +QString stripPclEsc(const uint8_t* data, int size) { + QString out; + out.reserve(size); + + int i = 0; + while (i < size) { + if (data[i] == 0x1B) { + i++; + while (i < size && !(data[i] >= 'A' && data[i] <= 'Z')) i++; + if (i < size) i++; // consume the terminator + } else { + out += QLatin1Char(static_cast(data[i])); + i++; + } + } + + return out.trimmed(); +} + +// Parse data-section content into rows of tab-separated cells in a single pass, +// skipping over interleaved FE/FD OMF records (the FD 'R' row markers and the +// optional FD 'I' defaults record). TSV text is plain ASCII, so 0xFE/0xFD never +// appear inside a cell value. +QList parseTsvRows(const uint8_t* data, size_t size) { + QList rows; + QStringList row; + QByteArray cell; + + const uint8_t* p = data; + const uint8_t* end = data + size; + + while (p < end) { + if (*p == kOmfMetadataRecord || *p == kOmfLabelRecord) { + const OmfRecord rec = readOmfRecord(p, end); + if (rec.payload != nullptr) { + p = rec.payload + rec.length; + continue; + } + } + + const char c = static_cast(*p++); + if (c == '\t') { + row.append(QString::fromLatin1(cell)); + cell.clear(); + } else if (c == '\n') { + row.append(QString::fromLatin1(cell)); + rows.append(row); + row.clear(); + cell.clear(); + } else if (c != '\r') { + cell.append(c); + } + } + + if (!cell.isEmpty() || !row.isEmpty()) { + row.append(QString::fromLatin1(cell)); + rows.append(row); + } + + return rows; +} + +Database parseDatabase(const uint8_t* data, size_t size, uint32_t propLength) { + Database db; + + OmfMetadata md = parseOmfMetadata(data, size, propLength); + if (md.records.empty()) { + return db; // not a database: no metadata stream + } + + if (const OmfRecord* title = md.find(kOmfMetadataRecord, kTitleSubtype)) { + // payload[0] = subtype 'h', payload[1..] = title text. + db.title = stripPclEsc(title->payload + 1, int(title->length - 1)); + } + + // payload[0] = subtype 0x00, payload[1] = 1-based column index, rest = label. + // length >= 2 is required: payload[1] must exist. (subtype 0x00 also matches + // zero-length records via subtype(), so the guard is not redundant.) + for (const OmfRecord* r : md.findAll(kOmfLabelRecord, kColumnLabelSubtype)) { + if (r->length >= 2) { + int col = r->payload[1]; + db.columnLabels[col] = QString::fromLatin1( + reinterpret_cast(r->payload + 2), int(r->length - 2)); + } + } + + // Column formula (text form): payload[0] = subtype 0x02, payload[1] = column, + // rest = formula text. Formulas reference columns by letter (a=col 1, ...) or + // by name; the text is shown verbatim, so no substitution is applied. + for (const OmfRecord* r : md.findAll(kOmfLabelRecord, kColumnFormulaSubtype)) { + if (r->length >= 2) { + int col = r->payload[1]; + db.columnFormulas[col] = QString::fromLatin1( + reinterpret_cast(r->payload + 2), int(r->length - 2)); + } + } + + db.rows = parseTsvRows(md.content, md.contentSize); + db.valid = true; + + return db; +} + +} // namespace + +bool DatabasePreview::supports(const QString& fileType, size_t fileSize) const { + return fileSize >= 4 && + fileType.compare("database", Qt::CaseInsensitive) == 0; +} + +QWidget* DatabasePreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { + uint8_t* data = nullptr; + size_t size = 0; + if (ccos_read_file(disk, file, &data, &size) != CCOS_OK || data == nullptr) { + QMessageBox::critical(parent, "Preview", "Failed to read file contents!"); + return nullptr; + } + + Database db = parseDatabase(data, size, file->desc.prop_length); + free(data); + + if (!db.valid) { + QMessageBox::warning(parent, "Preview", "Could not decode this database (unknown format)."); + return nullptr; + } + + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + if (!db.title.isEmpty()) { + auto* titleLabel = new QLabel(db.title, container); + QFont titleFont = titleLabel->font(); + titleFont.setBold(true); + titleLabel->setFont(titleFont); + titleLabel->setWordWrap(true); + layout->addWidget(titleLabel); + } + + int rowCount = qMin(db.rows.size(), kMaxRows); + int colCount = 0; + for (const QStringList& row : db.rows) { + colCount = qMax(colCount, row.size()); + } + for (auto it = db.columnLabels.cbegin(); it != db.columnLabels.cend(); ++it) { + if (it.key() <= kMaxCols) { + colCount = qMax(colCount, it.key()); + } + } + colCount = qMin(colCount, kMaxCols); + + auto* table = new QTableWidget(rowCount, colCount, container); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setSelectionBehavior(QAbstractItemView::SelectItems); + table->setSelectionMode(QAbstractItemView::ContiguousSelection); + table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + QFont font("Monospace"); + font.setStyleHint(QFont::TypeWriter); + table->setFont(font); + + for (int c = 0; c < colCount; ++c) { + QString label = db.columnLabels.value(c + 1); + if (label.isEmpty()) { + label = QString::number(c + 1); + } + auto* headerItem = new QTableWidgetItem(label); + // Surface computed columns: a formula tooltip on the column header. + if (db.columnFormulas.contains(c + 1)) { + headerItem->setToolTip(QString("Formula: %1").arg(db.columnFormulas.value(c + 1))); + } + table->setHorizontalHeaderItem(c, headerItem); + } + + table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + table->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + for (int r = 0; r < rowCount; ++r) { + const QStringList& row = db.rows[r]; + for (int c = 0; c < row.size(); ++c) { + if (!row[c].isEmpty()) { + table->setItem(r, c, new QTableWidgetItem(row[c])); + } + } + } + + layout->addWidget(table, 1); + + return container; +} diff --git a/dialogs/preview/databasepreview.h b/dialogs/preview/databasepreview.h new file mode 100644 index 0000000..dabc6ec --- /dev/null +++ b/dialogs/preview/databasepreview.h @@ -0,0 +1,13 @@ +#ifndef DATABASEPREVIEW_H +#define DATABASEPREVIEW_H + +#include "filepreview.h" + +class DatabasePreview : public FilePreview { +public: + QString name() const override { return "Database"; } + bool supports(const QString& fileType, size_t fileSize) const override; + QWidget* createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) override; +}; + +#endif // DATABASEPREVIEW_H diff --git a/dialogs/preview/previewdlg.cpp b/dialogs/preview/previewdlg.cpp index 12f338d..04c76ef 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/dialogs/preview/previewdlg.cpp @@ -5,6 +5,7 @@ #include "canvaspreview.h" #include "worksheetpreview.h" #include "fontpreview.h" +#include "databasepreview.h" #include #include @@ -17,6 +18,7 @@ static QList previewRegistry() { new CanvasPreview(), new WorksheetPreview(), new FontPreview(), + new DatabasePreview(), }; return registry; } From 8cce4d54326f3cd45826395fdb470d5095314874 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Tue, 28 Jul 2026 20:33:18 +0300 Subject: [PATCH 20/36] preview basic, plm, c files as text --- dialogs/preview/textpreview.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp index 00f9bc0..318e5ea 100644 --- a/dialogs/preview/textpreview.cpp +++ b/dialogs/preview/textpreview.cpp @@ -123,9 +123,12 @@ void WrapPreviewEdit::paintEvent(QPaintEvent* event) { bool TextPreview::supports(const QString& fileType, size_t fileSize) const { (void)fileSize; - return fileType.compare("text", Qt::CaseInsensitive) == 0 || - fileType.compare("develop", Qt::CaseInsensitive) == 0 || - fileType.compare("lst", Qt::CaseInsensitive) == 0; + for (const auto ext : {"text", "develop", "lst", "plm", "c", "basic"}) { + if (fileType.compare(ext, Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; } QWidget* TextPreview::createWidget(ccos_disk_t* disk, ccos_inode_t* file, QWidget* parent) { From 394a8f03a7aa20c64aaf700aaa27e58ded63cfc8 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 01:22:47 +0300 Subject: [PATCH 21/36] preview h, task, com, asm as text --- dialogs/preview/textpreview.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialogs/preview/textpreview.cpp b/dialogs/preview/textpreview.cpp index 318e5ea..002fa69 100644 --- a/dialogs/preview/textpreview.cpp +++ b/dialogs/preview/textpreview.cpp @@ -123,7 +123,7 @@ void WrapPreviewEdit::paintEvent(QPaintEvent* event) { bool TextPreview::supports(const QString& fileType, size_t fileSize) const { (void)fileSize; - for (const auto ext : {"text", "develop", "lst", "plm", "c", "basic"}) { + for (const auto ext : {"text", "develop", "lst", "plm", "c", "h", "basic", "task", "com", "asm"}) { if (fileType.compare(ext, Qt::CaseInsensitive) == 0) { return true; } From a77d87aae610be6ef228573250c5419664949a2b Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 01:54:12 +0300 Subject: [PATCH 22/36] support IMD files opening --- CMakeLists.txt | 5 +- dialogs/mainwindow.cpp | 82 ++++++++++++- dialogs/mainwindow.h | 14 +++ imd2raw/imd2raw.c | 253 +++++++++++++++++++++++++++++++++++++++++ imd2raw/imd2raw.h | 29 +++++ 5 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 imd2raw/imd2raw.c create mode 100644 imd2raw/imd2raw.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fadede9..4a1b81d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_subdirectory(ccos_image) -project(GRiDISKCOM LANGUAGES CXX) +project(GRiDISKCOM LANGUAGES CXX C) set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(ccos_image) +include_directories(imd2raw) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) @@ -25,6 +26,8 @@ endif() add_executable(GRiDISKCOM ${GUI_TYPE} main.cpp + imd2raw/imd2raw.c + imd2raw/imd2raw.h dialogs/mainwindow.cpp dialogs/mainwindow.h dialogs/mainwindow.ui diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index f2b93ac..6bff48e 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1,5 +1,9 @@ #include "mainwindow.h" #include "preview/previewdlg.h" +#include "imd2raw.h" + +#include +#include #include #include @@ -242,6 +246,34 @@ int saveFileQt(QString path, uint8_t* file_data, size_t file_size, QWidget* pare return 0; } +//*Convert an .IMD file into a temporary raw .img and return its path. +// Returns an empty string on failure (an error dialog is shown to the user). +QString convertImdToTempImg(const QString& imdPath, QWidget* parent){ + QTemporaryFile tempFile(QDir::tempPath() + "/gridiskcom_XXXXXX.img"); + tempFile.setAutoRemove(false); + if (!tempFile.open()){ + QMessageBox::critical(parent, "Unable to create temporary file", + QString("Unable to create a temporary file for IMD conversion in \"%1\".") + .arg(QDir::tempPath())); + return ""; + } + QString tempPath = tempFile.fileName(); + tempFile.close(); // release so imd2raw can open it for writing + + QByteArray inBytes = imdPath.toLocal8Bit(); + QByteArray outBytes = tempPath.toLocal8Bit(); + int res = imd2raw_convert(inBytes.constData(), outBytes.constData()); + if (res != 0){ + QFile::remove(tempPath); + QMessageBox::critical(parent, "IMD conversion failed", + QString("Failed to convert IMD file \"%1\" to a raw image (code %2).\n" + "The file may be corrupted or not a valid ImageDisk image.") + .arg(imdPath).arg(res)); + return ""; + } + return tempPath; +} + //*Dump file from image to path int dumpFileQt(ccos_disk_t* disk, ccos_inode_t* file, QString path, QWidget* parent){ QString fnam = short_string_to_qstring(ccos_get_file_name(file)); @@ -514,7 +546,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu } else if (!panels[0] || !panels[1]){ QFileInfo fil(arg); - if (fil.suffix().toLower() == "img" && fil.exists()){ + if ((fil.suffix().toLower() == "img" || fil.suffix().toLower() == "imd") && fil.exists()){ active_panel = panels[0] ? 1 : 0; LoadImg(arg); } @@ -1098,7 +1130,7 @@ void MainWindow::dropEvent(QDropEvent* event){ if (FilesList.size() == 1 && DirsList.isEmpty()) { QString ext = QFileInfo(FilesList[0]).suffix().toLower(); - if (ext == "img") { + if (ext == "img" || ext == "imd") { LoadImg(FilesList[0]); } else if (panels[active_panel] && panels[active_panel]->in_subdir) { @@ -1482,11 +1514,36 @@ void MainWindow::loadCustomImg(QString path, uint8_t* data, size_t size) { } } +// IMD-aware entry point: if the file is an .IMD image, convert it to a +// temporary raw .img first, run the standard opening pipeline, then mark +// the resulting panel so that direct Save back to IMD is blocked. void MainWindow::LoadImg(QString path) { if (path.isEmpty()) { return; } + bool fromImd = false; + if (QFileInfo(path).suffix().toLower() == "imd") { + QString tempImg = convertImdToTempImg(path, this); + if (tempImg.isEmpty()) { + return; + } + fromImd = true; + path = tempImg; + } + + loadImgStandard(path); + + if (fromImd && panels[active_panel] && panels[active_panel]->disk) { + panels[active_panel]->is_imd = true; + } +} + +void MainWindow::loadImgStandard(QString path) { + if (path.isEmpty()) { + return; + } + if (isFileAlreadyOpened(path)) { handleAlreadyOpenedImg(path); return; @@ -1651,7 +1708,7 @@ bool MainWindow::goToParentDir(int panel_idx) { void MainWindow::OpenImg(){ QString path = QFileDialog::getOpenFileName(this, "Open Image", "", - "GRiD image files (*.img);;" + "GRiD image files (*.img *.imd);;" "All files (*)"); LoadImg(path); } @@ -1718,6 +1775,14 @@ void MainWindow::Save(){ if (!panels[active_panel] || panels[active_panel]->path == "") return SaveAs(); + if (panels[active_panel]->is_imd) { + QMessageBox::warning(this, "Saving IMD is not supported", + "This image was opened from an IMD file.\n" + "Saving back to IMD format is not supported.\n\n" + "Please use \"Save as\" to export the image as a .img file."); + return; + } + auto& panel = *panels[active_panel]; if (!panel.modified) return; @@ -1763,7 +1828,16 @@ void MainWindow::SaveAs(){ QString("Unable to save file \"%1\". Please check the path.").arg(nameQ)); return; } - panel.path = nameQ; + if (panel.is_imd){ + // The old path pointed to a temporary converted image; drop it and + // turn this into a normal standalone .img from now on. + QString oldTemp = panel.path; + panel.path = nameQ; + QFile::remove(oldTemp); + panel.is_imd = false; + } else { + panel.path = nameQ; + } if (panel.modified){ panel.modified = false; int other = !active_panel; diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index 5aee93a..b2dfc32 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -41,11 +42,20 @@ struct DiskPanel { bool modified = false; bool in_subdir = false; + // True when the image was opened from an .IMD file. The actual on-disk + // file pointed to by `path` is then a temporary converted .img, which we + // own and clean up ourselves. IMD cannot be written back, so Save() is + // blocked and the user is redirected to Save As. + bool is_imd = false; + bool hdd_mode = false; std::shared_ptr> hdd_data; std::optional hdd_partition; ~DiskPanel() { + if (is_imd && !path.isEmpty()) { + QFile::remove(path); // remove the temporary converted image + } if (disk == nullptr) return; if (hdd_mode) { @@ -119,6 +129,10 @@ public slots: void openValidMbrPartition(QString path, std::vector hdddata, int partition_index, ccos_disk_t* disk); void loadCustomImg(QString path, uint8_t* data, size_t size); + // Standard image opening pipeline (bootsector/size/MBR/custom detection). + // Used by LoadImg after optional IMD->img conversion. + void loadImgStandard(QString path); + void fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot); void updatePanelTitle(int panel_idx); diff --git a/imd2raw/imd2raw.c b/imd2raw/imd2raw.c new file mode 100644 index 0000000..dd938c7 --- /dev/null +++ b/imd2raw/imd2raw.c @@ -0,0 +1,253 @@ +/* + * Fixes to sort sector list so that images with skews are correct + * Verified by actually comparing IMDU /B output byte for byte + * + * David Schmidt + * December, 2022 + * July, 2026: optional padding of missing sectors + */ + +/* originally from the bitsavers/convergent directory + * + * Used stdin/stdout that won't work in Windows. Files + * must be opened explicity in binary mode for both input and output + * + * Testing: Converts *.IMD file identical to IMDU /B as verified + * by both size and an MD5 cryptographic hash. + * + * Tom Burnett + * December, 2013 + */ +#include "imd2raw.h" + +#include +#include +#include +#include + +/* + * IMD image format + * + * IMD v.v: dd/mm/yyy hh:mm:ss (ascii header, optional) + * comment terminated with 0x1a + * for each track + * char mode (0-5) + * char cylinder + * char head (including additional optional bits, below) + * char sector count + * char sector size (0-6) + * sector numbering map + * optional cylinder map (head & 0x40) + * optional head map (head & 0x80) + * sector data records (type) (val, or data) + */ + +char *modetbl[] = { "500K FM", "300K FM", "250K FM", "500K MFM", "300K MFM", "250K MFM" }; + +FILE *fpin, *fpout; +int fill_missing = 0; +unsigned int highwater[7] = {0}; + +int comp (const void * elem1, const void * elem2) +{ + unsigned char f = *((int*)elem1); + unsigned char s = *((int*)elem2); + if (f > s) return 1; + if (f < s) return -1; + return 0; +} + +/* Integrated into GRiDISKCOM: this used to be main()/stdin/stdout that + * called exit() on error. It is now imd2raw_convert(infile, outfile) which + * returns a negative code on error, so it is safe to call from the GUI. + * The conversion algorithm below is unchanged. */ +int imd2raw_convert(const char *infile, const char *outfile) +{ + unsigned char sectormap[32]; + unsigned char sectormapsorted[32]; + unsigned char secdisp[32]; + unsigned char secdata[64][8192]; + unsigned int mode, cyl, hd, seccnt, headflags, sizecode; + unsigned int secsiz = 0; + int i, j; + unsigned char c, value, fill; + + if ((fpin = fopen(infile, "rb")) == NULL) { + return -1; + } + + if ((fpout = fopen(outfile, "wb")) == NULL) { + fclose(fpin); + return -2; + } + + /* Turns out this is a "later" construct, earlier examples may just have a 0x1a terminated comment + if( (fgetc(fpin) != 'I') || (fgetc(fpin) != 'M') || (fgetc(fpin) != 'D') ) { + fprintf(stderr,"File doesn't start with 'IMD'\n"); + leave(4); + } + */ + + // Lop off initial comments + while(1) { + c = fgetc(fpin); + if(c == 0x1a) + break; + } + + while(1) { + c = fgetc(fpin); + if(feof(fpin)) break; + mode = c; + if(mode > 6) { + fclose(fpin); + fclose(fpout); + return -4; + } + cyl = fgetc(fpin); + if(cyl > 80) { + fclose(fpin); + fclose(fpout); + return -5; + } + c = fgetc(fpin); + hd = c & 0x0f; + headflags = c & 0xf0; + if(hd > 1) { + fclose(fpin); + fclose(fpout); + return -6; + } + seccnt = fgetc(fpin); + + sizecode = fgetc(fpin); + c = sizecode; + + switch(c) { + case 0: + secsiz = 128; + break; + case 1: + secsiz = 256; + break; + case 2: + secsiz = 512; + break; + case 3: + secsiz = 1024; + break; + case 4: + secsiz = 2048; + break; + case 5: + secsiz = 4096; + break; + case 6: + secsiz = 8192; + break; + default: + break; + } + + if (seccnt > highwater[sizecode]) + highwater[sizecode] = seccnt; + + // fprintf(stderr,"Cyl:%d Hd:%d %s %d sectors size %d\n", cyl, hd, modetbl[mode], seccnt, secsiz); + + // copy sector/interleave map + for (i=0; i < seccnt; i++) { + sectormap[i] = fgetc(fpin); + sectormapsorted[i] = sectormap[i]; + } + // Sort the sectors in case of skew + // qsort(sectormapsorted, seccnt, 1, comp); + // fprintf(stderr,"Tbl "); + // for (i=0; i < seccnt; i++) fprintf(stderr,"%d ",sectormap[i]); + // fprintf(stderr, "\n"); + // fprintf(stderr,"Srt "); + // for (i=0; i < seccnt; i++) fprintf(stderr,"%d ",sectormapsorted[i]); + // fprintf(stderr, "\n"); + + if ((headflags & 64) == 64) + { + // Pull out "optional" cylinder map, discard + for (int i = 0; i < seccnt; i++) + c = fgetc(fpin); + } + if ((headflags & 128) == 128) + { + // Pull out "optional" head map, discard + for (int i = 0; i < seccnt; i++) + c = fgetc(fpin); + } + + // copy sector information indexed by the sector number + for (i=0; i < seccnt; i++) { + c = fgetc(fpin); + + switch(c) { + case 0: // Sector data unavailable - could not be read + case 5: // Deleted address marks + case 7: // Bad sector + secdisp[i] = 'X'; + fill = 0xE5; + for(j=0; j < secsiz; j++) { + if (c > 0) + fill = fgetc(fpin); // Grab whatever IMD wrote + secdata[sectormap[i]][j] = fill; + } + // fprintf(stderr,"Cyl %d Hd %d Sec %d bad, type %d\n",cyl,hd,sectormap[i],c); + break; + + case 1: // normal data 'secsiz' bytes follow + secdisp[i] = '.'; + for(j=0; j < secsiz; j++) + secdata[sectormap[i]][j] = fgetc(fpin); + break; + + case 3: // data with 'deleted data' address mark + secdisp[i] = 'd'; + for(j=0; j < secsiz; j++) + secdata[sectormap[i]][j] = fgetc(fpin); + break; + + case 2: // compressed with value in next byte + case 4: + case 6: + case 8: + secdisp[i] = 'C'; + value = fgetc(fpin); + for(j=0; j < secsiz; j++) + secdata[sectormap[i]][j] = value; + // fprintf(stderr,"Cyl %d Hd %d Sec %d all %x\n",cyl,hd,sectormap[i],value); + break; + } + } + + if (fill_missing && seccnt < highwater[sizecode]) { + unsigned char present[64] = {0}; + for (i = 0; i < seccnt; i++) + present[sectormap[i]] = 1; + for (i = 1; i <= highwater[sizecode]; i++) { + if (!present[i]) { + for (j = 0; j < secsiz; j++) + secdata[i][j] = 0x00; + sectormap[seccnt] = i; + sectormapsorted[seccnt] = i; + secdisp[seccnt] = '0'; + seccnt++; + } + } + } + qsort(sectormapsorted, seccnt, 1, comp); + + for(i=0; i raw image converter. + * + * Derived from the imd2raw utility by Tom Burnett / David Schmidt + * (originally from the bitsavers/convergent directory). See imd2raw.c + * for the original copyright/attribution notices. + * + * Exposes imd2raw_convert(): converts an ImageDisk (.IMD) file into a + * raw sector image (.img/.dsk), byte-for-byte identical to IMDU /B. + * + * Return value: + * 0 - success + * <0 - error (input/output file problems or malformed IMD stream) + */ + +#ifdef __cplusplus +extern "C" { +#endif + +int imd2raw_convert(const char* infile, const char* outfile); + +#ifdef __cplusplus +} +#endif + +#endif /* IMD2RAW_H */ From 250c45fffde3c2b82c3279f47493a31e4d6af153 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 00:51:06 +0300 Subject: [PATCH 23/36] move panel out from mainwindow to filepanel widget --- CMakeLists.txt | 2 + dialogs/filepanelwidget.cpp | 463 ++++++++++++++++++++++++++ dialogs/filepanelwidget.h | 98 ++++++ dialogs/mainwindow.cpp | 626 +++++++++++++++--------------------- dialogs/mainwindow.h | 34 +- dialogs/mainwindow.ui | 246 +------------- main.cpp | 1 - 7 files changed, 866 insertions(+), 604 deletions(-) create mode 100644 dialogs/filepanelwidget.cpp create mode 100644 dialogs/filepanelwidget.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a1b81d..38924bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/datedlg.cpp dialogs/datedlg.h dialogs/datedlg.ui + dialogs/filepanelwidget.cpp + dialogs/filepanelwidget.h dialogs/preview/filepreview.h dialogs/preview/omf/omf_record.cpp dialogs/preview/omf/omf_record.h diff --git a/dialogs/filepanelwidget.cpp b/dialogs/filepanelwidget.cpp new file mode 100644 index 0000000..a53bd0e --- /dev/null +++ b/dialogs/filepanelwidget.cpp @@ -0,0 +1,463 @@ +#include "filepanelwidget.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr int kColumnCount = 7; + +// Column geometry mirrored from the legacy setup so the layout stays identical. +struct ColumnSpec { + const char* header; + int width; +}; + +const ColumnSpec kColumns[kColumnCount] = { + {"File name", 155}, + {"File type", 80}, + {"Size", 45}, + {"Version", 80}, + {"Created", 100}, + {"Modified", 100}, + {"Expires", 100}, +}; + +QString formatDate(const QDate& date) { + // An unset date (e.g. ccos expiry) arrives as an invalid QDate -> blank cell. + if (!date.isValid()) + return {}; + return date.toString(QStringLiteral("dd.MM.yyyy")); +} + +// Builds one non-editable row from a fixed-size text array; used both for the +// ".." pseudo-entry and for real file entries so the two paths stay identical. +void appendRow(QTableWidget* table, const QString (&texts)[kColumnCount]) { + const int row = table->rowCount(); + table->insertRow(row); + for (int c = 0; c < kColumnCount; ++c) { + auto* item = new QTableWidgetItem(texts[c]); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + table->setItem(row, c, item); + } +} +} // namespace + +// Paints a soft background across an entire hovered row. QTableWidget only +// tints the single cell under the cursor; we override the background brush for +// every cell of the hovered row so the highlight is seamless. +class RowHoverDelegate : public QStyledItemDelegate { +public: + RowHoverDelegate(const QColor& hoverColor, QObject* parent) + : QStyledItemDelegate(parent), m_hoverColor(hoverColor) {} + + void setHoverRow(int row) { m_hoverRow = row; } + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override { + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + if (index.row() == m_hoverRow && !(opt.state & QStyle::State_Selected)) + opt.backgroundBrush = m_hoverColor; + const QWidget* widget = opt.widget ? opt.widget : option.widget; + QStyle* style = widget ? widget->style() : QApplication::style(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); + } + +private: + int m_hoverRow = -1; + QColor m_hoverColor; +}; + +FilePanelWidget::FilePanelWidget(QWidget* parent) : QWidget(parent) { + buildUi(); +} + +FilePanelWidget::~FilePanelWidget() = default; + +void FilePanelWidget::buildUi() { + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(4, 0, 4, 4); + layout->setSpacing(4); + + m_groupBox = new QGroupBox(this); + QFont titleFont = m_groupBox->font(); + titleFont.setPointSize(10); + titleFont.setBold(false); + m_groupBox->setFont(titleFont); + auto* boxLayout = new QVBoxLayout(m_groupBox); + + m_stack = new QStackedWidget(this); + m_stack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // --- table -------------------------------------------------------------- + m_table = new QTableWidget(this); + m_table->setColumnCount(kColumnCount); + m_table->verticalHeader()->hide(); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setContextMenuPolicy(Qt::CustomContextMenu); + m_table->setAutoFillBackground(true); + QFont tableFont = m_table->font(); + tableFont.setFamily(QStringLiteral("Arial")); + tableFont.setPointSize(9); + tableFont.setBold(false); + m_table->setFont(tableFont); + + for (int i = 0; i < kColumnCount; ++i) { + m_table->setHorizontalHeaderItem(i, new QTableWidgetItem(QString::fromLatin1(kColumns[i].header))); + if (kColumns[i].width > 0) + m_table->horizontalHeader()->resizeSection(i, kColumns[i].width); + } + QFont headerFont = m_table->horizontalHeader()->font(); + headerFont.setBold(true); + m_table->horizontalHeader()->setFont(headerFont); + + QColor hover = palette().color(QPalette::Highlight); + hover.setAlpha(60); + m_delegate = new RowHoverDelegate(hover, m_table); + m_table->setItemDelegate(m_delegate); + m_table->setMouseTracking(true); + m_table->viewport()->setMouseTracking(true); + + m_stack->addWidget(m_table); + + // --- empty placeholder -------------------------------------------------- + m_emptyLabel = new QLabel(this); + m_emptyLabel->setAlignment(Qt::AlignCenter); + m_stack->addWidget(m_emptyLabel); + boxLayout->addWidget(m_stack); + layout->addWidget(m_groupBox); + + // --- status line -------------------------------------------------------- + m_statusLabel = new QLabel(this); + QFont statusFont = m_statusLabel->font(); + statusFont.setPointSize(9); + m_statusLabel->setFont(statusFont); + m_statusLabel->setVisible(false); + layout->addWidget(m_statusLabel); + + // --- drop overlay ------------------------------------------------------ + // A layout-less child shown on top while a drag hovers the panel: an opaque + // panel with a highlight border and centered "Drop here" text. It doubles as + // both the border highlight and the drop hint, and works the same whether the + // panel is empty (covers the icon) or full (covers the table). resizeEvent + // keeps it pinned to rect(). + m_dropOverlay = new QLabel(QStringLiteral("Drop here"), this); + m_dropOverlay->setAlignment(Qt::AlignCenter); + QFont dropFont = m_dropOverlay->font(); + dropFont.setPointSize(14); + dropFont.setBold(true); + m_dropOverlay->setFont(dropFont); + m_dropOverlay->setStyleSheet(QStringLiteral( + "QLabel { background: palette(window); color: palette(text);" + " border: 3px solid palette(highlight); border-radius: 5px; }")); + m_dropOverlay->hide(); + + connect(m_table, &QTableWidget::cellActivated, this, &FilePanelWidget::onCellActivated); + connect(m_table, &QTableWidget::customContextMenuRequested, this, + &FilePanelWidget::onContextMenuRequested); + m_table->installEventFilter(this); + m_table->viewport()->installEventFilter(this); + // QLabel doesn't propagate mouse events to its parent, so filter the + // placeholder to turn a click into openRequested(). + m_emptyLabel->installEventFilter(this); + + setAcceptDrops(true); // children don't accept drops => events bubble here + + // Redirect focus to the table: panelWidget->setFocus() then lands on the + // table, whose FocusIn bolds the title and emits panelActivated(). + setFocusProxy(m_table); + + rebuildTable(); +} + +void FilePanelWidget::setFiles(const QVector& files, bool inSubdir) { + m_files = files; + m_inSubdir = inSubdir; + rebuildTable(); +} + +void FilePanelWidget::rebuildTable() { + m_table->setRowCount(0); + + if (m_files.isEmpty()) { + m_stack->setCurrentWidget(m_emptyLabel); + updateEmptyIcon(); + return; + } + m_stack->setCurrentWidget(m_table); + + if (m_inSubdir) { + const QString parentRow[kColumnCount] = { + QStringLiteral(".."), QStringLiteral("Subject"), + {}, {}, {}, {}, {} + }; + appendRow(m_table, parentRow); + } + + for (const auto& entry : m_files) { + const QString texts[kColumnCount] = { + entry.name, + entry.type, + QString::number(entry.size), + entry.version, + formatDate(entry.creationDate), + formatDate(entry.modificationDate), + formatDate(entry.expirationDate), + }; + appendRow(m_table, texts); + } +} + +void FilePanelWidget::restoreSelection(int fileIndex, int scroll) { + if (m_table->rowCount() == 0) + return; + // fileIndex < 0 (no selection / focus on the ".." row) falls back to the + // top row. In a subdir the ".." entry lives at row 0, and -1 + 1 == 0, so + // the parent-row focus is preserved naturally. + const int row = m_inSubdir ? fileIndex + 1 : fileIndex; + m_table->setCurrentCell(qMax(0, row), 0); + + // Defer the scroll restore to the next event-loop tick: the table must + // finish laying out (row heights / scrollbar range) before the value can + // stick. Doing it synchronously right after inserting rows is what made + // the scroll "snap back". The deferred call also runs after any auto-scroll + // triggered by setCurrentCell(), so it wins. + if (scroll >= 0) { + QTimer::singleShot(0, this, [this, scroll]() { + m_table->verticalScrollBar()->setValue(scroll); + }); + } +} + +int FilePanelWidget::verticalScrollValue() const { + return m_table->verticalScrollBar()->value(); +} + +void FilePanelWidget::setTitle(const QString& title) { + m_groupBox->setTitle(title); +} + +void FilePanelWidget::setStatusText(const QString& text) { + m_statusLabel->setText(text); + m_statusLabel->setVisible(!text.isEmpty()); +} + +void FilePanelWidget::setDiskPresent(bool present) { + m_diskPresent = present; + if (m_files.isEmpty()) + updateEmptyIcon(); +} + +void FilePanelWidget::updateEmptyIcon() { + // Folder icon when a disk is loaded (even if its root is empty), drive + // icon when no disk is present at all. + const QStyle::StandardPixmap sp = m_diskPresent ? QStyle::SP_DirIcon : QStyle::SP_DriveFDIcon; + const QIcon icon = QApplication::style()->standardIcon(sp); + m_emptyLabel->setPixmap(icon.pixmap(64, 64)); +} + +void FilePanelWidget::setActiveTitle(bool active) { + QFont font = m_groupBox->font(); + if (font.bold() == active) + return; + font.setBold(active); + m_groupBox->setFont(font); +} + +int FilePanelWidget::visualRowToFileIndex(int row) const { + return m_inSubdir ? row - 1 : row; +} + +int FilePanelWidget::currentFileIndex() const { + if (m_files.isEmpty()) + return -1; + + QTableWidgetItem* current = m_table->currentItem(); + if (current == nullptr) + return -1; + + const int idx = visualRowToFileIndex(current->row()); + return idx < 0 ? -1 : idx; +} + +QVector FilePanelWidget::selectedFileIndices() const { + QVector result; + if (m_files.isEmpty()) + return result; + + const auto rows = m_table->selectionModel()->selectedRows(); + result.reserve(rows.size()); + for (const auto& index : rows) { + const int idx = visualRowToFileIndex(index.row()); + if (idx >= 0 && idx < m_files.size()) + result.append(idx); + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +void FilePanelWidget::onCellActivated(int row, int /*column*/) { + const int idx = visualRowToFileIndex(row); + if (idx < 0) { + if (m_inSubdir) // ".." row -> step one directory up + emit goUpToParent(); + return; + } + emit fileDoubleClicked(idx); +} + +void FilePanelWidget::onContextMenuRequested(const QPoint& pos) { + QTableWidgetItem* item = m_table->itemAt(pos); + if (item == nullptr) + return; + const int idx = visualRowToFileIndex(item->row()); + if (idx < 0) + return; + emit fileRightClicked(idx, m_table->viewport()->mapToGlobal(pos)); +} + +bool FilePanelWidget::eventFilter(QObject* watched, QEvent* event) { + if (watched == m_table) + return handleTableEvent(event) || QWidget::eventFilter(watched, event); + if (watched == m_table->viewport()) + return handleViewportEvent(event) || QWidget::eventFilter(watched, event); + if (watched == m_emptyLabel) + return handleEmptyLabelEvent(event) || QWidget::eventFilter(watched, event); + return QWidget::eventFilter(watched, event); +} + +bool FilePanelWidget::handleTableEvent(QEvent* event) { + if (event->type() == QEvent::FocusIn) { + setActiveTitle(true); + emit panelActivated(); + } else if (event->type() == QEvent::FocusOut) { + setActiveTitle(false); + } else if (event->type() == QEvent::KeyPress) { + auto* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape && m_inSubdir) { + emit goUpToParent(); + return true; + } + } + return false; +} + +bool FilePanelWidget::handleViewportEvent(QEvent* event) { + if (event->type() == QEvent::MouseMove) { + auto* me = static_cast(event); + setHoverRow(m_table->rowAt(me->pos().y())); + } else if (event->type() == QEvent::Leave) { + setHoverRow(-1); + } + return false; +} + +bool FilePanelWidget::handleEmptyLabelEvent(QEvent* event) { + // Only react to an actual press (not a stray release after a drag) and + // only when no disk is loaded. + if (event->type() == QEvent::MouseButtonPress && !m_diskPresent) { + emit openRequested(); + return true; + } + return false; +} + +void FilePanelWidget::setHoverRow(int row) { + if (row == m_hoverRow) + return; + m_hoverRow = row; + m_delegate->setHoverRow(row); + m_table->viewport()->update(); +} + +void FilePanelWidget::setDragOver(bool on) { + if (on == m_dragOver) + return; + m_dragOver = on; + m_dropOverlay->setVisible(on); + if (on) { + m_dropOverlay->setGeometry(rect()); + m_dropOverlay->raise(); + } +} + +void FilePanelWidget::dragEnterEvent(QDragEnterEvent* event) { + if (event->mimeData()->hasUrls()) { + event->acceptProposedAction(); + setDragOver(true); + } else { + event->ignore(); + } +} + +void FilePanelWidget::dragMoveEvent(QDragMoveEvent* event) { + if (event->mimeData()->hasUrls()) + event->acceptProposedAction(); + else + event->ignore(); +} + +void FilePanelWidget::dragLeaveEvent(QDragLeaveEvent* event) { + setDragOver(false); + event->accept(); +} + +void FilePanelWidget::dropEvent(QDropEvent* event) { + setDragOver(false); + + QStringList files; + const QMimeData* mimeData = event->mimeData(); + if (mimeData && mimeData->hasUrls()) { + for (const auto& url : mimeData->urls()) { + const QString path = url.toLocalFile(); + if (path.isEmpty()) + continue; + if (QFileInfo(path).isDir()) + continue; // folders are ignored at the panel level + files.append(path); + } + } + + if (!files.isEmpty()) + emit urlsDropped(files); + event->acceptProposedAction(); +} + +void FilePanelWidget::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + // The drop overlay is a layout-less child kept on top, so it has to be + // pinned to the panel manually. This is the idiomatic Qt pattern for a + // widget that must cover its parent without influencing its geometry. + if (m_dropOverlay->isVisible()) + m_dropOverlay->setGeometry(rect()); +} diff --git a/dialogs/filepanelwidget.h b/dialogs/filepanelwidget.h new file mode 100644 index 0000000..7507c64 --- /dev/null +++ b/dialogs/filepanelwidget.h @@ -0,0 +1,98 @@ +#ifndef FILEPANELWIDGET_H +#define FILEPANELWIDGET_H + +#include +#include +#include +#include + +class QGroupBox; +class QLabel; +class QStackedWidget; +class QTableWidget; +class RowHoverDelegate; + +struct PanelFileEntry { + QString name; + QString type; + quint64 size = 0; + QString version; + QDate creationDate; + QDate modificationDate; + QDate expirationDate; +}; + +// Reusable file-list panel: a titled table (or an icon placeholder when empty) +// plus a status line. Pure UI -- the host feeds entries and drives selection. +class FilePanelWidget : public QWidget { + Q_OBJECT +public: + explicit FilePanelWidget(QWidget* parent = nullptr); + ~FilePanelWidget() override; + + void setFiles(const QVector& files, bool inSubdir); + void setTitle(const QString& title); + // Hidden when empty. A non-empty value also implies "a disk is loaded". + void setStatusText(const QString& text); + // Tells the panel whether a disk image is loaded, so the empty-state icon + // (folder vs drive) and the click-to-open behaviour are correct even for a + // disk whose root happens to have no files. + void setDiskPresent(bool present); + + // Per-directory view state, restored by the host after setFiles(). + int verticalScrollValue() const; + int currentFileIndex() const; // -1 if none / ".." + void restoreSelection(int fileIndex, int scroll); // fileIndex<0 => top row, scroll<0 => leave as-is + + QVector selectedFileIndices() const; + +signals: + void fileDoubleClicked(int index); + void fileRightClicked(int index, const QPoint& globalPos); // globalPos for QMenu::exec + void goUpToParent(); // ".." double-click or Esc + void panelActivated(); // table gained focus + void urlsDropped(const QStringList& files); // folders are ignored + void openRequested(); // click on empty placeholder while no disk is loaded + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void dragLeaveEvent(QDragLeaveEvent* event) override; + void dropEvent(QDropEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +private slots: + void onCellActivated(int row, int column); + void onContextMenuRequested(const QPoint& pos); + +private: + void buildUi(); + void rebuildTable(); + void updateEmptyIcon(); + void setDragOver(bool on); + void setHoverRow(int row); + void setActiveTitle(bool active); // toggle the title bold (focus-driven) + int visualRowToFileIndex(int row) const; // table row -> m_files index (-1 for "..") + + // eventFilter dispatchers, one per watched object. + bool handleTableEvent(QEvent* event); + bool handleViewportEvent(QEvent* event); + bool handleEmptyLabelEvent(QEvent* event); + + QGroupBox* m_groupBox = nullptr; + QStackedWidget* m_stack = nullptr; + QTableWidget* m_table = nullptr; + QLabel* m_emptyLabel = nullptr; + QLabel* m_statusLabel = nullptr; + QLabel* m_dropOverlay = nullptr; // "Drop here" + border, shown while a drag hovers + RowHoverDelegate* m_delegate = nullptr; + + QVector m_files; + bool m_inSubdir = false; + bool m_diskPresent = false; + bool m_dragOver = false; + int m_hoverRow = -1; +}; + +#endif // FILEPANELWIDGET_H diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 6bff48e..ac1040c 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -71,46 +71,29 @@ QString ccosGetFileVersionQstr(ccos_inode_t* file){ QString::number(ver.patch)); } -//*Convert date ("dd.MM.yyyy") -QString ccosDateToQstr(ccos_date_t date){ - QString day = QString::number(date.day); - QString month = QString::number(date.month); - QString year = QString::number(date.year); - while (day.size() < 2) - day = "0" + day; - while (month.size() < 2) - month = "0" + month; - while (year.size() < 4) - year = "0" + year; - return QString("%1.%2.%3").arg(day, month, year); -} - -//*Insert file row to the widget -void addFile(QTableWidget* tableWidget, int mode){ //*For empty - QTableWidgetItem* rows[7]; - tableWidget->insertRow(0); - for (int i = 0; i < 7; i++){ - rows[i] = new QTableWidgetItem(); - rows[i]->setFlags(rows[i]->flags() ^ Qt::ItemIsEditable); - tableWidget->setItem(0, i, rows[i]); - } - rows[0]->setText(mode == 1 ? "" : mode == 2 ? ".." : ""); - if (mode == 2) - rows[1]->setText(""); -} - -void addFile(QTableWidget* tableWidget, QString text[]){ //*For normal - QTableWidgetItem* rows[7]; - int row = tableWidget->rowCount(); - tableWidget->insertRow(row); - for (int i = 0; i < 7; i++){ - rows[i] = new QTableWidgetItem(); - rows[i]->setFlags(rows[i]->flags() ^ Qt::ItemIsEditable); - rows[i]->setText(text[i]); - tableWidget->setItem(row, i, rows[i]); - } +//*Convert ccos_date_t to QDate +static QDate ccosDateToQDate(ccos_date_t date){ + return QDate(date.year, date.month, date.day); +} + +//*Format a byte count as a human-readable "X MB (Y bytes)" string. +static QString formatFreeSpace(size_t bytes) { + const double KB = 1024.0; + const double MB = 1024.0 * 1024.0; + const double GB = 1024.0 * 1024.0 * 1024.0; + QString human; + if (bytes >= GB) + human = QString::number(bytes / GB, 'f', 2) + " GB"; + else if (bytes >= MB) + human = QString::number(bytes / MB, 'f', 2) + " MB"; + else if (bytes >= KB) + human = QString::number(bytes / KB, 'f', 1) + " KB"; + else + human = QString::number(bytes) + " B"; + return QString("Free space: %1 (%2 bytes)").arg(human, QString::number(bytes)); } + //*Check if real file named as ~~ int tildaCheck(std::string parse_str){ std::vector output; @@ -133,7 +116,7 @@ int tildaCheck(std::string parse_str){ //*Check if space is enough to add files int checkFreeSp(DiskPanel& from, DiskPanel& to, - QList calledElems, + const QVector& fileIndices, size_t* needs){ //*For copy size_t frsp = 0; @@ -141,8 +124,8 @@ int checkFreeSp(DiskPanel& from, DiskPanel& to, return -2; } *needs = 0; - for (int i = 0; i < calledElems.size(); i+=7){ - ccos_inode_t* file = from.inodes[calledElems[i]->row()]; + for (int idx : fileIndices){ + ccos_inode_t* file = from.inodes[idx]; if (file != nullptr){ if (ccos_is_dir(file)){ uint16_t fils = 0; @@ -371,83 +354,114 @@ int validString(QString string, bool ifpath, QWidget* parent){ return 0; } -//*Get directory listing and fill it to table -void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) { +// --- Panel helpers ---------------------------------------------------------- + +FilePanelWidget* MainWindow::panelWidget(int panel_idx) { + return (panel_idx == 0) ? ui->panel1 : ui->panel2; +} + +// Builds the UI-facing entry list for a directory and keeps panel.inodes in +// sync (real files only, no nullptr sentinels). +QVector MainWindow::buildFileEntries(int panel_idx, ccos_inode_t* directory) { auto& panel = *panels[panel_idx]; - QTableWidget* tableWidget; - QLabel* label; - QGroupBox* box; - QString disk_name, msg; panel.inodes.clear(); + uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; ccos_get_dir_contents(panel.disk, directory, &fils, &dirdata); - if (panel_idx == 0){ - tableWidget = ui->tableWidget; - label = ui->label; - box = ui->groupBox; - disk_name = "I"; - } - else{ - tableWidget = ui->tableWidget_2; - label = ui->label_2; - box = ui->groupBox_2; - disk_name = "II"; - } - for (int row = tableWidget->rowCount(); 0<=row; row--) - tableWidget->removeRow(row); - QString labd = short_string_to_qstring(ccos_get_disk_label(panel.disk)); - msg = "Disk %1 - %2%3"; - box->setTitle(msg.arg(disk_name, !labd.isEmpty() ? labd : "No label", panel.modified ? "*" : "")); + + QVector entries; + entries.reserve(fils); + char basename[CCOS_MAX_FILE_NAME]; char type[CCOS_MAX_FILE_NAME]; - if (noRoot){ - addFile(tableWidget, 2); - panel.inodes.insert(panel.inodes.begin(), nullptr); - } - for(int c = 0; c < fils; c++){ + for (int c = 0; c < fils; c++) { memset(basename, 0, CCOS_MAX_FILE_NAME); memset(type, 0, CCOS_MAX_FILE_NAME); ccos_parse_file_name(dirdata[c], basename, type, nullptr, nullptr); - QString qtype = type; - if (!noRoot) //All files in the root are directories - qtype = qtype + " "; - QString text[] = {basename, qtype, QString::number(dirdata[c]->desc.file_size), - ccosGetFileVersionQstr(dirdata[c]), - ccosDateToQstr(dirdata[c]->desc.creation_date), - ccosDateToQstr(dirdata[c]->desc.mod_date), - ccosDateToQstr(dirdata[c]->desc.expiration_date)}; + + PanelFileEntry entry; + entry.name = basename; + entry.type = type; + entry.size = dirdata[c]->desc.file_size; + entry.version = ccosGetFileVersionQstr(dirdata[c]); + entry.creationDate = ccosDateToQDate(dirdata[c]->desc.creation_date); + entry.modificationDate = ccosDateToQDate(dirdata[c]->desc.mod_date); + entry.expirationDate = ccosDateToQDate(dirdata[c]->desc.expiration_date); + panel.inodes.push_back(dirdata[c]); - addFile(tableWidget, text); + entries.append(std::move(entry)); } + free(dirdata); + return entries; +} + +//*Get directory listing and push it to the panel widget +void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) { + auto& panel = *panels[panel_idx]; + FilePanelWidget* pw = panelWidget(panel_idx); + + pw->setDiskPresent(true); + auto entries = buildFileEntries(panel_idx, directory); + pw->setFiles(entries, panel.in_subdir); + + updatePanelTitle(panel_idx); + size_t free_space = 0; - if (ccos_calc_free_space(panel.disk, &free_space) != CCOS_OK){ - label->setText("Free space: FAILED TO CALCULATE!"); - free(dirdata); - return; + if (ccos_calc_free_space(panel.disk, &free_space) != CCOS_OK) { + pw->setStatusText("Free space: FAILED TO CALCULATE!"); + } else { + pw->setStatusText(formatFreeSpace(free_space)); } - msg = "Free space: %1 bytes."; - label->setText(msg.arg(free_space)); - if (panel.inodes.empty()) { - addFile(tableWidget, 1); - panel.inodes.insert(panel.inodes.begin(), nullptr); - } + // Re-establish whatever view state we remembered for this directory + // (selection + scroll). For a brand-new directory there is none, and the + // widget falls back to the top row. + applyViewState(panel_idx, directory); +} - free(dirdata); +void MainWindow::saveCurrentViewState(int panel_idx) { + if (!panels[panel_idx] || panels[panel_idx]->current_dir == nullptr) + return; + auto& panel = *panels[panel_idx]; + FilePanelWidget* pw = panelWidget(panel_idx); + DirViewState st; + st.scroll = pw->verticalScrollValue(); + st.selectIndex = pw->currentFileIndex(); + panel.view_state.insert(reinterpret_cast(panel.current_dir), st); +} + +void MainWindow::applyViewState(int panel_idx, ccos_inode_t* directory) { + if (!panels[panel_idx] || directory == nullptr) + return; + auto& panel = *panels[panel_idx]; + const quintptr key = reinterpret_cast(directory); + auto it = panel.view_state.constFind(key); + if (it != panel.view_state.constEnd()) + panelWidget(panel_idx)->restoreSelection(it->selectIndex, it->scroll); + else + panelWidget(panel_idx)->restoreSelection(-1, -1); +} + +void MainWindow::refreshPanel(int panel_idx) { + if (!panels[panel_idx]) + return; + saveCurrentViewState(panel_idx); + auto& panel = *panels[panel_idx]; + fillTable(panel_idx, panel.current_dir, panel.in_subdir); } void MainWindow::updatePanelTitle(int panel_idx) { if (!panels[panel_idx] || panels[panel_idx]->disk == nullptr) return; auto& panel = *panels[panel_idx]; - QGroupBox* box = (panel_idx == 0) ? ui->groupBox : ui->groupBox_2; QString disk_name = (panel_idx == 0) ? "I" : "II"; QString labd = short_string_to_qstring(ccos_get_disk_label(panel.disk)); - box->setTitle(QString("Disk %1 - %2%3") - .arg(disk_name, !labd.isEmpty() ? labd : "No label", - panel.modified ? "*" : "")); + panelWidget(panel_idx)->setTitle( + QString("Disk %1 - %2%3").arg(disk_name, + !labd.isEmpty() ? labd : "No label", + panel.modified ? "*" : "")); } //*Parse Hard Disk MBR @@ -506,31 +520,24 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu ui->setupUi(this); QMainWindow::setWindowTitle(QString("GRiDISK Commander v")+_PVER_); - for (auto* tw : {ui->tableWidget, ui->tableWidget_2}) { - tw->horizontalHeader()->resizeSection(0, 155); - tw->horizontalHeader()->resizeSection(2, 45); - tw->horizontalHeader()->resizeSection(3, 80); - tw->horizontalHeader()->resizeSection(4, 80); - tw->horizontalHeader()->resizeSection(5, 80); - tw->horizontalHeader()->resizeSection(6, 80); - addFile(tw, 0); - tw->verticalHeader()->hide(); - tw->setSelectionBehavior(QAbstractItemView::SelectRows); - tw->setContextMenuPolicy(Qt::CustomContextMenu); - tw->installEventFilter(this); - connect(tw, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(ShowPreview(QPoint))); - } - QFont diskfont; - diskfont.setFamily(QString::fromUtf8("Arial")); - diskfont.setPointSize(9); - diskfont.setUnderline(false); - diskfont.setWeight(50); - diskfont.setBold(true); - ui->groupBox->setFont(diskfont); - ui->tableWidget->setFont(diskfont); - diskfont.setBold(false); - ui->groupBox_2->setFont(diskfont); - ui->tableWidget_2->setFont(diskfont); + + // Each panel widget manages its own table, empty placeholder, context + // menu and keyboard handling; we just wire up the callbacks here. + FilePanelWidget* pws[2] = {ui->panel1, ui->panel2}; + for (int i = 0; i < 2; ++i) { + FilePanelWidget* pw = pws[i]; + pw->setFiles({}, false); + connect(pw, &FilePanelWidget::panelActivated, this, [this, i]() { onPanelActivated(i); }); + connect(pw, &FilePanelWidget::fileDoubleClicked, this, + [this, i](int idx) { onFileDoubleClicked(i, idx); }); + connect(pw, &FilePanelWidget::fileRightClicked, this, + [this, i](int idx, const QPoint& pos) { onFileRightClicked(i, idx, pos); }); + connect(pw, &FilePanelWidget::goUpToParent, this, [this, i]() { onGoUpToParent(i); }); + connect(pw, &FilePanelWidget::urlsDropped, this, + [this, i](const QStringList& files) { onUrlsDropped(i, files, {}); }); + connect(pw, &FilePanelWidget::openRequested, this, [this, i]() { onOpenRequested(i); }); + } + refreshActivePanelUI(); QStringList argv = QCoreApplication::arguments(); int argc = argv.size(); @@ -589,10 +596,6 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(Save())); connect(ui->actionSave_as, SIGNAL(triggered()), this, SLOT(SaveAs())); connect(ui->actionSep_save, SIGNAL(triggered()), this, SLOT(SavePart())); - // Cell activating connect - connect(ui->tableWidget, SIGNAL(cellActivated(int,int)), this, SLOT(OpenDir())); - connect(ui->tableWidget_2, SIGNAL(cellActivated(int,int)), this, SLOT(OpenDir())); - } void MainWindow::AboutShow(){ @@ -647,7 +650,7 @@ void MainWindow::AddDirs(QStringList dirs){ } if (!dirs.empty()){ panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } } @@ -705,7 +708,7 @@ int MainWindow::AddFiles(QStringList files, ccos_inode_t* copyTo){ } if (files.size() != 0){ panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } return 0; } @@ -787,6 +790,7 @@ void MainWindow::AnotherPart(bool fromMenu){ dst.disk = new_disk; dst.hdd_mode = true; dst.current_dir = root; + dst.view_state.clear(); // stale keys belong to the previous disk if (!fromMenu || dlg.isChecked()){ dst.path = src.path; dst.hdd_data = src.hdd_data; @@ -824,20 +828,11 @@ int MainWindow::CloseImg(){ panels[active_panel].reset(); HDDMenu(false); - QTableWidget* tableWidget; - if (active_panel == 0){ - tableWidget = ui->tableWidget; - ui->label->setText("Free space:"); - ui->groupBox->setTitle("Disk I - No disk"); - } - else{ - tableWidget = ui->tableWidget_2; - ui->label_2->setText("Free space:"); - ui->groupBox_2->setTitle("Disk II - No disk"); - } - for(int row = tableWidget->rowCount(); 0<=row; row--) - tableWidget-> removeRow(row); - addFile(tableWidget, 0); + FilePanelWidget* pw = panelWidget(active_panel); + pw->setDiskPresent(false); + pw->setFiles({}, false); + pw->setTitle({}); + pw->setStatusText({}); return 1; } @@ -846,26 +841,18 @@ void MainWindow::Copy(){ if (panels[active_panel] && panels[other]){ auto& src = *panels[active_panel]; auto& dst = *panels[other]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - QList called = tw->selectedItems(); - if (called.isEmpty()) + QVector selected = panelWidget(active_panel)->selectedFileIndices(); + if (selected.isEmpty()) return; - if (called.size() == 7 && src.inodes[called[0]->row()] == nullptr) - return; - bool selpar = (src.inodes[called[0]->row()] == nullptr) ? true : false; QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Question); - msgBox.setText(QString("Do you want to copy %1 file(s)?").arg((called.size()/7)-selpar)); + msgBox.setText(QString("Do you want to copy %1 file(s)?").arg(selected.size())); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); if (msgBox.exec() != QMessageBox::Yes) return; size_t needs = 0; - int retop = checkFreeSp(src, dst, called, &needs); + int retop = checkFreeSp(src, dst, selected, &needs); if (retop == -2) { QMessageBox::critical(this, "Calculation error", "Program can't calculate free space in the image!"); @@ -878,17 +865,16 @@ void MainWindow::Copy(){ QString("Requires %1 bytes of additional disk space to copy").arg(needs-frsp)); return; } - for (int t = 0; t < called.size(); t+=7){ - if (src.inodes[called[t]->row()]==nullptr) - continue; - if (ccos_is_dir(src.inodes[called[t]->row()])) { + for (int idx : selected){ + ccos_inode_t* file = src.inodes[idx]; + if (ccos_is_dir(file)) { if (dst.current_dir->header.file_id != ccos_get_root_dir(dst.disk)->header.file_id) { QMessageBox::critical(this, "Copying to non-root", "Folders can be copied only to root folder!"); return; } char newname[CCOS_MAX_FILE_NAME] = {}; - ccos_parse_file_name(src.inodes[called[t]->row()], newname, nullptr, nullptr, nullptr); + ccos_parse_file_name(file, newname, nullptr, nullptr, nullptr); ccos_inode_t* newdir = ccos_create_dir(dst.disk, ccos_get_root_dir(dst.disk), newname); if (newdir == nullptr){ QMessageBox::critical(this, "Failed to create folder", @@ -897,7 +883,7 @@ void MainWindow::Copy(){ } uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(src.disk, src.inodes[called[t]->row()], &fils, &dirdata); + ccos_get_dir_contents(src.disk, file, &fils, &dirdata); for (int c = 0; c < fils; c++) { ccos_copy_file(src.disk, dirdata[c], dst.disk, newdir); } @@ -908,13 +894,12 @@ void MainWindow::Copy(){ "Files can be copied only to non-root folder!"); return; } - ccos_copy_file(src.disk, src.inodes[called[t]->row()], - dst.disk, dst.current_dir); + ccos_copy_file(src.disk, file, dst.disk, dst.current_dir); } } dst.modified = true; - fillTable(other, dst.current_dir, dst.in_subdir); - fillTable(active_panel, src.current_dir, src.in_subdir); + refreshPanel(other); + refreshPanel(active_panel); } } @@ -925,23 +910,13 @@ void MainWindow::CopyLoc() { auto& panel = *panels[active_panel]; - QTableWidget const* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - - QList called = tw->selectedItems(); - if (called.isEmpty()) { - return; - } - - if (called.size() == 7 && panel.inodes[called[0]->row()] == nullptr) { + QVector selected = panelWidget(active_panel)->selectedFileIndices(); + if (selected.isEmpty()) { return; } size_t needs = 0; - int retop = checkFreeSp(panel, panel, called, &needs); + int retop = checkFreeSp(panel, panel, selected, &needs); if (retop == -2) { QMessageBox::critical(this, "Calculation error", "Program can't calculate free space in the image!"); @@ -975,8 +950,7 @@ void MainWindow::CopyLoc() { } dlg.exec(); - ccos_inode_t* firfil = panel.inodes[called[0]->row()] == nullptr ? - panel.inodes[called[6]->row()] : panel.inodes[called[0]->row()]; + ccos_inode_t* firfil = panel.inodes[selected[0]]; if (dirdata[dlg.getIndex()]->header.file_id == firfil->desc.dir_file_id){ QMessageBox::critical(this, "Copy to parent dir", @@ -984,20 +958,18 @@ void MainWindow::CopyLoc() { return; } - for (int t = 0; t < called.size(); t+=7){ - if (panel.inodes[called[t]->row()]==nullptr) - continue; - ccos_copy_file(panel.disk, panel.inodes[called[t]->row()], + for (int idx : selected){ + ccos_copy_file(panel.disk, panel.inodes[idx], panel.disk, dirdata[dlg.getIndex()]); } panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } else{ - for (int t = 0; t < called.size(); t+=7){ + for (int idx : selected){ char basename[CCOS_MAX_FILE_NAME]; memset(basename, 0, CCOS_MAX_FILE_NAME); - ccos_parse_file_name(panel.inodes[called[t]->row()], basename, nullptr, nullptr, nullptr); + ccos_parse_file_name(panel.inodes[idx], basename, nullptr, nullptr, nullptr); QString name; while (true){ @@ -1017,14 +989,14 @@ void MainWindow::CopyLoc() { uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; - ccos_get_dir_contents(panel.disk, panel.inodes[called[t]->row()], &fils, &dirdata); + ccos_get_dir_contents(panel.disk, panel.inodes[idx], &fils, &dirdata); for(int i = 0; i < fils; i++){ ccos_copy_file(panel.disk, dirdata[i], panel.disk, newdir); } panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); break; } } @@ -1039,16 +1011,10 @@ void MainWindow::Date(){ auto& panel = *panels[active_panel]; - QTableWidget const* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - - ccos_inode_t* file = panel.inodes[tw->currentItem()->row()]; - if (file == nullptr) { + int idx = panelWidget(active_panel)->currentFileIndex(); + if (idx < 0) return; - } + ccos_inode_t* file = panel.inodes[idx]; ccos_date_t cre = file->desc.creation_date; ccos_date_t mod = file->desc.mod_date; @@ -1061,7 +1027,7 @@ void MainWindow::Date(){ ccos_set_mod_date(panel.disk, file, mod); ccos_set_exp_date(panel.disk, file, exp); panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } } @@ -1079,31 +1045,21 @@ void MainWindow::DebTrace(){ void MainWindow::Delete(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - QList called = tw->selectedItems(); - if (called.size() == 0) - return; - if (called.size() == 7 && panel.inodes[called[0]->row()] == nullptr) + QVector selected = panelWidget(active_panel)->selectedFileIndices(); + if (selected.isEmpty()) return; - bool selpar = (panel.inodes[called[0]->row()] == nullptr) ? true : false; QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Question); - msgBox.setText(QString("Do you want to delete %1 file(s)?").arg((called.size()/7)-selpar)); + msgBox.setText(QString("Do you want to delete %1 file(s)?").arg(selected.size())); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); if (msgBox.exec() != QMessageBox::Yes) return; - for (int t = 0; t< called.size(); t+=7){ - if (panel.inodes[called[t]->row()]==nullptr) - continue; - ccos_delete_file(panel.disk, panel.inodes[called[t]->row()]); + for (int idx : selected){ + ccos_delete_file(panel.disk, panel.inodes[idx]); } panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } } @@ -1113,62 +1069,75 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event){ } void MainWindow::dropEvent(QDropEvent* event){ - QStringList FilesList; - QStringList DirsList; - + // Fallback for drops that land on the window background rather than on + // a panel: route them to the active panel. + QStringList files, dirs; const QMimeData* mimeData = event->mimeData(); - if (mimeData->hasUrls()){ - QList urlList = mimeData->urls(); - for (const auto& url : urlList) { - QString file = url.toLocalFile(); - if (QFileInfo(file).isDir()) - DirsList.append(file); + if (mimeData && mimeData->hasUrls()) { + for (const auto& url : mimeData->urls()) { + const QString path = url.toLocalFile(); + if (path.isEmpty()) + continue; + if (QFileInfo(path).isDir()) + dirs.append(path); else - FilesList.append(file); + files.append(path); } } + if (!files.isEmpty() || !dirs.isEmpty()) + onUrlsDropped(active_panel, files, dirs); + event->acceptProposedAction(); +} + +void MainWindow::onUrlsDropped(int panel_idx, const QStringList& FilesList, const QStringList& DirsList) { + // LoadImg / Add* / CloseImg all operate on the active panel, so redirect + // there before dispatching. This is always safe and makes sure an .img + // dropped onto an empty panel opens in that exact panel. + active_panel = panel_idx; if (FilesList.size() == 1 && DirsList.isEmpty()) { QString ext = QFileInfo(FilesList[0]).suffix().toLower(); if (ext == "img" || ext == "imd") { LoadImg(FilesList[0]); } - else if (panels[active_panel] && panels[active_panel]->in_subdir) { - AddFiles(FilesList, panels[active_panel]->current_dir); + else if (panels[panel_idx] && panels[panel_idx]->in_subdir) { + AddFiles(FilesList, panels[panel_idx]->current_dir); } } - else if (panels[active_panel] && !panels[active_panel]->in_subdir) { + else if (panels[panel_idx] && !panels[panel_idx]->in_subdir) { AddDirs(DirsList); } - else if (panels[active_panel] && panels[active_panel]->in_subdir) { - AddFiles(FilesList, panels[active_panel]->current_dir); + else if (panels[panel_idx] && panels[panel_idx]->in_subdir) { + AddFiles(FilesList, panels[panel_idx]->current_dir); } + + refreshActivePanelUI(); +} + +void MainWindow::onOpenRequested(int panel_idx) { + // The empty placeholder was clicked while no disk is loaded -- behave as + // if the user had pushed the "open" button for this specific panel. + active_panel = panel_idx; + refreshActivePanelUI(); + OpenImg(); } void MainWindow::Extract(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - QList called = tw->selectedItems(); - if (called.size() == 0) - return; - if (called.size() == 7 && panel.inodes[called[0]->row()] == nullptr) + QVector selected = panelWidget(active_panel)->selectedFileIndices(); + if (selected.isEmpty()) return; QString todir = QFileDialog::getExistingDirectory(this, tr("Extract to"), "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks | QFileDialog::DontUseNativeDialog); if (todir == "") return; - for (int t = 0; t < called.size(); t+=7){ - if (panel.inodes[called[t]->row()]==nullptr) - continue; - if (ccos_is_dir(panel.inodes[called[t]->row()])) - dumpDirQt(panel.disk, panel.inodes[called[t]->row()], todir, this); + for (int idx : selected){ + ccos_inode_t* file = panel.inodes[idx]; + if (ccos_is_dir(file)) + dumpDirQt(panel.disk, file, todir, this); else - dumpFileQt(panel.disk, panel.inodes[called[t]->row()], todir, this); + dumpFileQt(panel.disk, file, todir, this); } } } @@ -1188,53 +1157,45 @@ void MainWindow::ExtractAll(){ } void MainWindow::refreshActivePanelUI(){ - QFont font = ui->groupBox->font(); - font.setBold(active_panel == 0); - ui->groupBox->setFont(font); - ui->tableWidget->setFont(font); + // Focus drives the active panel: setFocus() forwards to the table, whose + // FocusIn bolds the title and emits panelActivated (which in turn sets + // active_panel). HDDMenu still needs the logical active_panel here. + panelWidget(active_panel)->setFocus(); + HDDMenu(panels[active_panel] && panels[active_panel]->hdd_mode); +} - font = ui->groupBox_2->font(); - font.setBold(active_panel == 1); - ui->groupBox_2->setFont(font); - ui->tableWidget_2->setFont(font); +void MainWindow::onPanelActivated(int panel_idx) { + active_panel = panel_idx; + refreshActivePanelUI(); +} - HDDMenu(panels[active_panel] && panels[active_panel]->hdd_mode); +void MainWindow::onGoUpToParent(int panel_idx) { + goToParentDir(panel_idx); } -void MainWindow::FocusChanged(QWidget *, QWidget *now){ - if (now == ui->tableWidget || now == ui->tableWidget_2){ - active_panel = (now == ui->tableWidget_2) ? 1 : 0; - refreshActivePanelUI(); +void MainWindow::onFileDoubleClicked(int panel_idx, int fileIndex) { + auto& panel = *panels[panel_idx]; + ccos_inode_t* entry = panel.inodes[fileIndex]; + if (ccos_is_dir(entry)) { + saveCurrentViewState(panel_idx); // remember the root view for when we come back + panel.current_dir = entry; + panel.in_subdir = true; + fillTable(panel_idx, entry, panel.in_subdir); + } else { + doPreview(panel_idx, entry); } } +void MainWindow::onFileRightClicked(int panel_idx, int fileIndex, const QPoint& /*globalPos*/) { + doPreview(panel_idx, panels[panel_idx]->inodes[fileIndex]); +} + void MainWindow::HDDMenu(bool enab){ ui->actionAct_part->setEnabled(enab); ui->actionAno_part->setEnabled(enab); ui->actionSep_save->setEnabled(enab); } -bool MainWindow::eventFilter(QObject* obj, QEvent* event) { - if (event->type() != QEvent::KeyPress) { - return QMainWindow::eventFilter(obj, event); - } - - const int panel_idx = (obj == ui->tableWidget_2) ? 1 : 0; - - auto* keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: - { - if (goToParentDir(panel_idx)) { - return true; - } - break; - } - } - - return QMainWindow::eventFilter(obj, event); -} - void MainWindow::doPreview(int panel_idx, ccos_inode_t* file){ if (!panels[panel_idx] || file == nullptr) return; @@ -1244,34 +1205,14 @@ void MainWindow::doPreview(int panel_idx, ccos_inode_t* file){ dlg.exec(); } -void MainWindow::ShowPreview(const QPoint& pos){ - QTableWidget* tw = qobject_cast(sender()); - if (tw == nullptr) - return; - int panel_idx = (tw == ui->tableWidget_2) ? 1 : 0; - if (!panels[panel_idx]) - return; - auto& panel = *panels[panel_idx]; - - QTableWidgetItem* it = tw->itemAt(pos); - if (it == nullptr) - return; - int row = it->row(); - if (row < 0 || row >= static_cast(panel.inodes.size())) - return; - doPreview(panel_idx, panel.inodes[row]); -} - void MainWindow::ShowPreview(){ int panel_idx = active_panel; if (!panels[panel_idx]) return; - auto& panel = *panels[panel_idx]; - QTableWidget* tw = panel_idx ? ui->tableWidget_2 : ui->tableWidget; - int row = tw->currentRow(); - if (row < 0 || row >= static_cast(panel.inodes.size())) + int idx = panelWidget(panel_idx)->currentFileIndex(); + if (idx < 0) return; - doPreview(panel_idx, panel.inodes[row]); + doPreview(panel_idx, panels[panel_idx]->inodes[idx]); } void MainWindow::Label(){ @@ -1291,7 +1232,7 @@ void MainWindow::Label(){ ccos_set_disk_label(panel.disk, nameQ.toStdString().c_str()); panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } } @@ -1612,7 +1553,7 @@ void MainWindow::MakeDir(){ break; } panel.modified = true; - fillTable(active_panel, root, panel.in_subdir); + refreshPanel(active_panel); break; } } @@ -1663,31 +1604,7 @@ void MainWindow::NewImage(){ } } -void MainWindow::OpenDir(){ - if (panels[active_panel]){ - auto& panel = *panels[active_panel]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - QTableWidgetItem* called = tw->currentItem(); - ccos_inode_t *dir = panel.inodes[called->row()]; - if (dir == nullptr && !panel.in_subdir) - return; - if (called->row() == 0 && panel.in_subdir){ - goToParentDir(active_panel); - } - else if (!panel.in_subdir){ //All files in the root are directories - panel.current_dir = dir; - panel.in_subdir = true; - fillTable(active_panel, dir, panel.in_subdir); - } - else if (!ccos_is_dir(dir)) { - doPreview(active_panel, dir); - } - } -} + bool MainWindow::goToParentDir(int panel_idx) { if (!panels[panel_idx]) { @@ -1698,6 +1615,7 @@ bool MainWindow::goToParentDir(int panel_idx) { return false; // already at the disk root, nothing above it } ccos_inode_t* root = ccos_get_root_dir(panel.disk); + saveCurrentViewState(panel_idx); // remember the subfolder view before leaving panel.current_dir = ccos_get_parent_dir(panel.disk, panel.current_dir); if (panel.current_dir == root) { panel.in_subdir = false; @@ -1716,22 +1634,12 @@ void MainWindow::OpenImg(){ void MainWindow::Rename(){ if (panels[active_panel]){ auto& panel = *panels[active_panel]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - QList called = tw->selectedItems(); - if (called.empty()) - return; - if (called.size() == 7 && panel.inodes[called[0]->row()] == nullptr) + QVector selected = panelWidget(active_panel)->selectedFileIndices(); + if (selected.isEmpty()) return; - for (int i = 0; i < called.size(); i+=7){ - ccos_inode_t* reninode = panel.inodes[called[i]->row()]; - if (reninode == nullptr){ - continue; - } + for (int idx : selected){ + ccos_inode_t* reninode = panel.inodes[idx]; char basename[CCOS_MAX_FILE_NAME]; char type[CCOS_MAX_FILE_NAME]; @@ -1758,8 +1666,7 @@ void MainWindow::Rename(){ ccos_rename_file(panel.disk, reninode, newname.toStdString().c_str(), newtype.toStdString().c_str()); panel.modified = true; - fillTable(active_panel, ccos_get_parent_dir(panel.disk, reninode), - panel.in_subdir); + refreshPanel(active_panel); break; } } @@ -1905,30 +1812,29 @@ void MainWindow::SetActivePart() { mbrtab[(selctd-1)*16] = 0x80; panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); + refreshPanel(active_panel); } } void MainWindow::Version(){ - if (panels[active_panel]){ - auto& panel = *panels[active_panel]; - QTableWidget* tw; - if (active_panel == 0) - tw = ui->tableWidget; - else - tw = ui->tableWidget_2; - ccos_inode_t* file = panel.inodes[tw->currentItem()->row()]; - if (file != nullptr){ - ccos_version_t ver = ccos_get_file_version(file); - VerDlg dlg(this); - dlg.init(file->desc.name, ver); - if (dlg.exec() == 1){ - ver = dlg.retVer(); - ccos_set_file_version(panel.disk, file, ver); - panel.modified = true; - fillTable(active_panel, panel.current_dir, panel.in_subdir); - } - } + if (!panels[active_panel]) { + return; + } + + auto& panel = *panels[active_panel]; + + int idx = panelWidget(active_panel)->currentFileIndex(); + if (idx < 0) return; + + ccos_inode_t* file = panel.inodes[idx]; + ccos_version_t ver = ccos_get_file_version(file); + VerDlg dlg(this); + dlg.init(file->desc.name, ver); + if (dlg.exec() == 1){ + ver = dlg.retVer(); + ccos_set_file_version(panel.disk, file, ver); + panel.modified = true; + refreshPanel(active_panel); } } diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index b2dfc32..b9eed40 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -9,6 +9,7 @@ #include #include #include "ui_mainwindow.h" +#include "filepanelwidget.h" #include "abdlg.h" #include "custdlg.h" #include "chsedlg.h" @@ -25,6 +26,12 @@ #include #include #include +#include + +struct DirViewState { + int scroll = 0; + int selectIndex = -1; +}; struct MbrPartition { size_t index; @@ -52,6 +59,10 @@ struct DiskPanel { std::shared_ptr> hdd_data; std::optional hdd_partition; + // Keyed by the directory inode pointer, so going back into a previously + // visited directory (e.g. leaving a subfolder) restores selection + scroll. + QHash view_state; + ~DiskPanel() { if (is_imd && !path.isEmpty()) { QFile::remove(path); // remove the temporary converted image @@ -91,9 +102,6 @@ public slots: void dropEvent(QDropEvent* event); void DebTrace(); void Delete(); - void OpenDir(); - void FocusChanged(QWidget*, QWidget* now); - void ShowPreview(const QPoint& pos); void ShowPreview(); void HDDMenu(bool activ); void Extract(); @@ -115,12 +123,20 @@ public slots: std::array, 2> panels; int active_panel = 0; -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - private: std::unique_ptr ui; + // --- Panel helpers ----------------------------------------------------- + FilePanelWidget* panelWidget(int panel_idx); + void onPanelActivated(int panel_idx); + + // Signal handlers wired to both FilePanelWidgets. + void onFileDoubleClicked(int panel_idx, int fileIndex); + void onFileRightClicked(int panel_idx, int fileIndex, const QPoint& globalPos); + void onGoUpToParent(int panel_idx); + void onUrlsDropped(int panel_idx, const QStringList& files, const QStringList& dirs); + void onOpenRequested(int panel_idx); + bool isFileAlreadyOpened(const QString& path); void handleAlreadyOpenedImg(QString path); bool suggestSelectAnotherPartition(); @@ -135,11 +151,17 @@ public slots: void fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot); + void saveCurrentViewState(int panel_idx); + void applyViewState(int panel_idx, ccos_inode_t* directory); + void refreshPanel(int panel_idx); + void updatePanelTitle(int panel_idx); void refreshActivePanelUI(); bool goToParentDir(int panel_idx); void doPreview(int panel_idx, ccos_inode_t* file); + + QVector buildFileEntries(int panel_idx, ccos_inode_t* directory); }; #endif // MAINWINDOW_H diff --git a/dialogs/mainwindow.ui b/dialogs/mainwindow.ui index 96a1d89..aa600e6 100644 --- a/dialogs/mainwindow.ui +++ b/dialogs/mainwindow.ui @@ -222,250 +222,15 @@ 0 - - - - - - 10 - true - false - - - - Disk I - No disk - - - - - - - 9 - false - false - true - - - - true - - - true - - - - File name - - - - true - - - - - - File type - - - - true - - - - - - Size - - - - true - - - - - - Version - - - - true - - - - - - Created - - - - true - - - - - - Modificated - - - - true - - - - - - Expires - - - - true - - - - - - - - - + - - - - - - 10 - false - - - - Disk II - No disk - - - - - - - 9 - false - - - - true - - - true - - - - File name - - - - true - - - - - - File type - - - - true - - - - - - Size - - - - true - - - - - - Version - - - - true - - - - - - Created - - - - true - - - - - - Modificated - - - - true - - - - - - Expires - - - - true - - - - - - - - - + - - - - - - - 9 - - - - Free space: - - - - - - - - 9 - - - - Free space: - - - - - @@ -770,6 +535,13 @@ + + + FilePanelWidget + QWidget +
dialogs/filepanelwidget.h
+
+
diff --git a/main.cpp b/main.cpp index 8811cbb..1daa078 100644 --- a/main.cpp +++ b/main.cpp @@ -7,7 +7,6 @@ int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; - QObject::connect(&a, SIGNAL(focusChanged(QWidget*,QWidget*)), &w, SLOT(FocusChanged(QWidget*,QWidget*))); w.show(); return a.exec(); } From 78cadbad35d5bd325235582705b97493b4a8fb05 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 02:35:49 +0300 Subject: [PATCH 24/36] rebuild menu --- dialogs/mainwindow.cpp | 22 ++++++- dialogs/mainwindow.h | 1 + dialogs/mainwindow.ui | 135 ++++++++++++++--------------------------- 3 files changed, 66 insertions(+), 92 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index ac1040c..65b0ebf 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -419,6 +419,9 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) // (selection + scroll). For a brand-new directory there is none, and the // widget falls back to the top row. applyViewState(panel_idx, directory); + + if (panel_idx == active_panel) + updateActionStates(); } void MainWindow::saveCurrentViewState(int panel_idx) { @@ -545,7 +548,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu if (argc > 1){ for (int i = 1; i < argc; i++){ QString arg = argv[i]; - if (arg == "--trace") { + if (arg == "--debug") { if (!ui->actionDebtrace->isChecked()){ ui->actionDebtrace->setChecked(true); DebTrace(); @@ -1162,6 +1165,23 @@ void MainWindow::refreshActivePanelUI(){ // active_panel). HDDMenu still needs the logical active_panel here. panelWidget(active_panel)->setFocus(); HDDMenu(panels[active_panel] && panels[active_panel]->hdd_mode); + updateActionStates(); +} + +void MainWindow::updateActionStates() { + const bool has_panel = panels[active_panel].has_value(); + const bool in_subdir = has_panel && panels[active_panel]->in_subdir; + + // "Add Files" is only meaningful inside a directory; "Create Directory" + // only in the root. Both share the same shortcut and slot position, so + // toggling visibility makes the menu show exactly one of them. + ui->actionAdd->setVisible(has_panel && in_subdir); + ui->actionMake_dir->setVisible(has_panel && !in_subdir); + + // Disk vs HDD-partition wording for the label action. + const bool hdd = has_panel && panels[active_panel]->hdd_mode; + ui->actionChange_label->setText(hdd ? tr("Change Partition Name...") + : tr("Change Disk Label...")); } void MainWindow::onPanelActivated(int panel_idx) { diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index b9eed40..ae1c5cb 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -157,6 +157,7 @@ public slots: void updatePanelTitle(int panel_idx); void refreshActivePanelUI(); + void updateActionStates(); // context-sensitive menu items (Add/MakeDir, label) bool goToParentDir(int panel_idx); diff --git a/dialogs/mainwindow.ui b/dialogs/mainwindow.ui index aa600e6..8ccdf8b 100644 --- a/dialogs/mainwindow.ui +++ b/dialogs/mainwindow.ui @@ -251,64 +251,50 @@ - - - + - Actions + Edit - - + - - + + - + - + - About - - - - - - false - - - Debug - - - - - - Hard Disk + Disk + + + + + Help + + + - - - + + - Open - - - + Open... Ctrl+O @@ -318,19 +304,13 @@ Save - - - Ctrl+S - Save as... - - - + Save As... Ctrl+Shift+S @@ -338,54 +318,39 @@ - Add - - - + Add Files... - Ctrl+A + Ctrl+D Delete - - - Del - Extract - - - + Extract... - Ctrl+X + Ctrl+E Exit - - - - Ctrl+E + Ctrl+Q - Copy to other disk - - - + Copy to Other Panel Ctrl+C @@ -393,83 +358,71 @@ - Rename - - - + Rename... - Ctrl+R + F2 Close - - - - Ctrl+L + Ctrl+W About GRiDISK Commander - - F1 - - Extract all + Extract All... - Ctrl+Shift+X + Ctrl+Shift+E Preview - - - F3 - Change disk label + Change Disk Label - Ctrl+H + Ctrl+L - Make dir + Create Directory... - Ctrl+M + Ctrl+D - Change file date + Change Dates... - Ctrl+D + Ctrl+Shift+D - Change file version + Change Version... - Ctrl+V + Ctrl+Shift+V @@ -496,7 +449,7 @@ - Copy locally + Copy Locally... Ctrl+Shift+C @@ -507,7 +460,7 @@ false - Open another partition + Open Another Partition... @@ -515,7 +468,7 @@ false - Save partition to a separate file + Save Partition to a Separate File... @@ -523,12 +476,12 @@ false - Change active partition + Set Active Partition... - New image + New Image Ctrl+N From c7bf496558c92c11fe37a193dc03c62e69f64c03 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 02:44:05 +0300 Subject: [PATCH 25/36] do not overwrite disk label on cancelled rename --- dialogs/mainwindow.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index 65b0ebf..c8c3201 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -1244,10 +1244,12 @@ void MainWindow::Label(){ else dsk= "II"; QString fname = short_string_to_qstring(ccos_get_disk_label(panel.disk)); + bool ok = false; QString nameQ = QInputDialog::getText(this, tr("New label"), - QString("Set new label for the disk %1:").arg(dsk), QLineEdit::Normal, fname); + QString("Set new label for the disk %1:").arg(dsk), + QLineEdit::Normal, fname, &ok); - if (validString(nameQ, false, this) == -1) + if (!ok || validString(nameQ, false, this) == -1) return; ccos_set_disk_label(panel.disk, nameQ.toStdString().c_str()); From 5fccd584c0985e6678edd340e26db04397887033 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 03:10:36 +0300 Subject: [PATCH 26/36] fix hdd image closing prematurely on drag-and-drop --- dialogs/mainwindow.cpp | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index c8c3201..a619093 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -721,11 +721,8 @@ void MainWindow::AnPartMenu(){ } void MainWindow::AnotherPart(bool fromMenu){ - int usedisk; - if (fromMenu) - usedisk = active_panel; - else - usedisk = !active_panel; + const int panel_at_entry = active_panel; + const int usedisk = fromMenu ? panel_at_entry : !panel_at_entry; auto& src = *panels[usedisk]; @@ -752,7 +749,7 @@ void MainWindow::AnotherPart(bool fromMenu){ int selctd = dlg.getIndex(); - int topan = (fromMenu && dlg.isChecked()) ? !active_panel : active_panel; + const int topan = (fromMenu && dlg.isChecked()) ? !panel_at_entry : panel_at_entry; if (usedisk != topan && panels[topan]) { int saved_active = active_panel; @@ -780,7 +777,7 @@ void MainWindow::AnotherPart(bool fromMenu){ free(new_disk); if (fresh_target) panels[topan].reset(); - if ((!panels[active_panel] || panels[active_panel]->disk == nullptr) + if ((!panels[panel_at_entry] || panels[panel_at_entry]->disk == nullptr) && panels[usedisk] && panels[usedisk]->disk != nullptr) { active_panel = usedisk; } @@ -800,6 +797,7 @@ void MainWindow::AnotherPart(bool fromMenu){ } fillTable(topan, root, false); + active_panel = topan; refreshActivePanelUI(); } @@ -1276,10 +1274,9 @@ void MainWindow::handleAlreadyOpenedImg(QString path) { return; } - if (!CloseImg()) { - return; - } - + // AnotherPart(false) opens its own partition-selection dialog and closes + // the target panel only after the user confirms -- so cancelling the + // partition picker leaves everything untouched. AnotherPart(false); } @@ -1416,6 +1413,10 @@ void MainWindow::tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size) ccos_disk_t* disk = tryOpenMbrPartition(hdd_data.data(), parts[selected]); if (disk) { + if (!CloseImg()) { + free(disk); + break; + } openValidMbrPartition(path, std::move(hdd_data), selected, disk); break; } @@ -1468,6 +1469,11 @@ void MainWindow::loadCustomImg(QString path, uint8_t* data, size_t size) { ccos_disk_t* disk = tryOpenAs(data, size, sector_size, superblock, bitmap); if (disk) { + if (!CloseImg()) { + free(data); + free(disk); + return; + } openValidNonMbrDisk(path, disk); return; } @@ -1507,15 +1513,18 @@ void MainWindow::loadImgStandard(QString path) { return; } - if (isFileAlreadyOpened(path)) { - handleAlreadyOpenedImg(path); + // Same image already open on this panel -- nothing to do. + if (panels[active_panel] && panels[active_panel]->path == path) return; - } - if (!CloseImg()) { + // Same image open on the other panel. + if (isFileAlreadyOpened(path)) { + handleAlreadyOpenedImg(path); return; } + // Read the file BEFORE closing the current image, so that a read error or + // a user-cancelled dialog leaves the existing disk intact. uint8_t* data; size_t size; if (readFileQt(path, &data, &size, this)) { @@ -1523,22 +1532,27 @@ void MainWindow::loadImgStandard(QString path) { } ccos_disk_t* disk = tryFromBootsector(data, size); + if (!disk) + disk = tryDetectBySize(data, size); if (disk) { - openValidNonMbrDisk(path, disk); - return; - } - - disk = tryDetectBySize(data, size); - if (disk) { + if (!CloseImg()) { + free(data); + free(disk); + return; + } openValidNonMbrDisk(path, disk); return; } if (isMbrDisk(data, size)) { + // tryToOpenValidMbrDisk takes ownership of data and calls CloseImg() + // itself, only after the user confirms a partition. tryToOpenValidMbrDisk(path, data, size); return; } + // loadCustomImg takes ownership of data and calls CloseImg() itself, only + // after the user successfully picks parameters. loadCustomImg(path, data, size); } From d159d143170e513796a128b531b82823b1510948 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 04:29:44 +0300 Subject: [PATCH 27/36] rewrite partitions dialog --- CMakeLists.txt | 6 +- dialogs/chsedlg.cpp | 38 ----------- dialogs/chsedlg.h | 28 -------- dialogs/chsedlg.ui | 142 --------------------------------------- dialogs/mainwindow.cpp | 126 ++++++++++++++++++---------------- dialogs/mainwindow.h | 10 +-- dialogs/partitiondlg.cpp | 73 ++++++++++++++++++++ dialogs/partitiondlg.h | 53 +++++++++++++++ dialogs/partitiondlg.ui | 116 ++++++++++++++++++++++++++++++++ 9 files changed, 315 insertions(+), 277 deletions(-) delete mode 100644 dialogs/chsedlg.cpp delete mode 100644 dialogs/chsedlg.h delete mode 100644 dialogs/chsedlg.ui create mode 100644 dialogs/partitiondlg.cpp create mode 100644 dialogs/partitiondlg.h create mode 100644 dialogs/partitiondlg.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 38924bf..f741763 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,9 +34,9 @@ add_executable(GRiDISKCOM ${GUI_TYPE} dialogs/abdlg.cpp dialogs/abdlg.h dialogs/abdlg.ui - dialogs/chsedlg.cpp - dialogs/chsedlg.h - dialogs/chsedlg.ui + dialogs/partitiondlg.cpp + dialogs/partitiondlg.h + dialogs/partitiondlg.ui dialogs/custdlg.cpp dialogs/custdlg.h dialogs/custdlg.ui diff --git a/dialogs/chsedlg.cpp b/dialogs/chsedlg.cpp deleted file mode 100644 index dc1f1a6..0000000 --- a/dialogs/chsedlg.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "chsedlg.h" - -ChsDlg::ChsDlg(QWidget *parent) : - QDialog(parent), - ui(new Ui::ChsDlg) -{ - ui->setupUi(this); - ui->checkBox->hide(); -} - -void ChsDlg::setInfo(QString text){ - ui->label->setText(text); -} - -void ChsDlg::setName(QString text){ - ChsDlg::setWindowTitle(text); -} - -void ChsDlg::addItem(QString name){ - ui->comboBox->addItem(name); -} - -int ChsDlg::getIndex(){ - return ui->comboBox->currentIndex(); -} - -void ChsDlg::enCheckBox(){ - ui->checkBox->show(); -} - -bool ChsDlg::isChecked(){ - return ui->checkBox->isChecked(); -} - -ChsDlg::~ChsDlg() -{ - delete ui; -} diff --git a/dialogs/chsedlg.h b/dialogs/chsedlg.h deleted file mode 100644 index 2aff561..0000000 --- a/dialogs/chsedlg.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef CHSDLG_H -#define CHSDLG_H - -#include "ui_chsedlg.h" - -namespace Ui { -class ChsDlg; -} - -class ChsDlg : public QDialog -{ - Q_OBJECT - -public: - explicit ChsDlg(QWidget *parent = nullptr); - void setInfo(QString text); - void setName(QString text); - void addItem(QString name); - int getIndex(); - void enCheckBox(); - bool isChecked(); - ~ChsDlg(); - -private: - Ui::ChsDlg *ui; -}; - -#endif // CHSDLG_H diff --git a/dialogs/chsedlg.ui b/dialogs/chsedlg.ui deleted file mode 100644 index e2484b0..0000000 --- a/dialogs/chsedlg.ui +++ /dev/null @@ -1,142 +0,0 @@ - - - ChsDlg - - - - 0 - 0 - 383 - 166 - - - - - 0 - 0 - - - - - 383 - 166 - - - - - 383 - 166 - - - - - Sans Serif - - - - Universal combobox dialog - - - - :/icon/gridsmall.png:/icon/gridsmall.png - - - - - 30 - 120 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 20 - 10 - 351 - 41 - - - - - Sans Serif - 9 - - - - %no_text#$contact_aperture_science@@*% - - - true - - - - - - 20 - 70 - 341 - 25 - - - - - - - 20 - 100 - 351 - 23 - - - - Open in another panel - - - - - - - - - buttonBox - accepted() - ChsDlg - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - ChsDlg - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/dialogs/mainwindow.cpp b/dialogs/mainwindow.cpp index a619093..df1d482 100644 --- a/dialogs/mainwindow.cpp +++ b/dialogs/mainwindow.cpp @@ -30,6 +30,7 @@ //[Service functions] static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition); +static QString mbrPartitionLabel(uint8_t* hdd_data, const MbrPartition& part); ccos_date_t ccos_get_datetime(void) { timespec tp; @@ -467,7 +468,6 @@ void MainWindow::updatePanelTitle(int panel_idx) { panel.modified ? "*" : "")); } -//*Parse Hard Disk MBR std::vector parseMbr(const uint8_t* data, size_t size) { std::vector partitions; partitions.reserve(4); @@ -483,14 +483,11 @@ std::vector parseMbr(const uint8_t* data, size_t size) { bool isGRiD = entry[4] == 0x47; bool isActive = (entry[0] & 0x80) != 0; - if (!isGRiD) { - continue; - } - uint64_t part_offset = (entry[8] | entry[9] << 8 | entry[10] << 16 | entry[11] << 24) * 512; uint64_t part_size = (entry[12] | entry[13] << 8 | entry[14] << 16 | entry[15] << 24) * 512; - if (part_offset == 0 && part_size == 0) { + // A zeroed entry marks the end of the used partition table. + if (part_offset == 0 && part_size == 0 && !isGRiD && !isActive) { break; } @@ -728,28 +725,29 @@ void MainWindow::AnotherPart(bool fromMenu){ std::vector parts = parseMbr(src.hdd_data->data(), src.hdd_data->size()); - ChsDlg dlg(this); - dlg.setName("Select disk partition"); - dlg.setInfo("Select the GRiD disk partition you want to work with:"); - - if (fromMenu) - dlg.enCheckBox(); + QStringList labels; + for (const auto& p : parts) + labels << mbrPartitionLabel(src.hdd_data->data(), p); - for (const auto& part : parts) { - if (part.isActive){ - dlg.addItem(QString("Partition %1, active").arg(part.index + 1)); - } else { - dlg.addItem(QString("Partition %1").arg(part.index + 1)); - } + QSet alreadyOpen; + for (int i = 0; i < 2; ++i) { + if (panels[i] && panels[i]->hdd_mode && panels[i]->path == src.path && + panels[i]->hdd_partition.has_value()) + alreadyOpen.insert(panels[i]->hdd_partition.value()); } - if (dlg.exec() != 1) { + PartitionDlg dlg(this); + dlg.setTitle("Select disk partition"); + dlg.setInfo("Select the GRiD disk partition you want to work with:"); + dlg.setPartitions(parts, labels, alreadyOpen); + + if (dlg.exec() != QDialog::Accepted) { return; } - int selctd = dlg.getIndex(); + int selctd = dlg.selectedIndex(); - const int topan = (fromMenu && dlg.isChecked()) ? !panel_at_entry : panel_at_entry; + const int topan = panel_at_entry; if (usedisk != topan && panels[topan]) { int saved_active = active_panel; @@ -791,7 +789,7 @@ void MainWindow::AnotherPart(bool fromMenu){ dst.hdd_mode = true; dst.current_dir = root; dst.view_state.clear(); // stale keys belong to the previous disk - if (!fromMenu || dlg.isChecked()){ + if (!fromMenu){ dst.path = src.path; dst.hdd_data = src.hdd_data; } @@ -932,28 +930,31 @@ void MainWindow::CopyLoc() { } if (panel.in_subdir){ - ChsDlg dlg(this); - dlg.setName("Select the directory"); - dlg.setInfo("Select the directory where the file(s) will be copied:"); - ccos_inode_t* root = ccos_get_root_dir(panel.disk); uint16_t fils = 0; ccos_inode_t** dirdata = nullptr; ccos_get_dir_contents(panel.disk, root, &fils, &dirdata); - char basename[CCOS_MAX_FILE_NAME]; - - for(int i = 0; i < fils; i++){ + QStringList items; + for (int i = 0; i < fils; i++){ + char basename[CCOS_MAX_FILE_NAME]; memset(basename, 0, CCOS_MAX_FILE_NAME); ccos_parse_file_name(dirdata[i], basename, nullptr, nullptr, nullptr); - dlg.addItem(basename); + items << QString::fromLatin1(basename); } - dlg.exec(); + bool ok = false; + QString chosen = QInputDialog::getItem(this, tr("Select the directory"), + tr("Select the directory where the file(s) will be copied:"), + items, 0, false, &ok); + if (!ok || chosen.isEmpty()) + return; + + int chosenIdx = items.indexOf(chosen); ccos_inode_t* firfil = panel.inodes[selected[0]]; - if (dirdata[dlg.getIndex()]->header.file_id == firfil->desc.dir_file_id){ + if (dirdata[chosenIdx]->header.file_id == firfil->desc.dir_file_id){ QMessageBox::critical(this, "Copy to parent dir", "Can't copy files to it's parent dir!"); return; @@ -961,7 +962,7 @@ void MainWindow::CopyLoc() { for (int idx : selected){ ccos_copy_file(panel.disk, panel.inodes[idx], - panel.disk, dirdata[dlg.getIndex()]); + panel.disk, dirdata[chosenIdx]); } panel.modified = true; refreshPanel(active_panel); @@ -1371,6 +1372,20 @@ static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition) GRID_HDD_SECTOR_SIZE, GRID_HDD_SUPERBLOCK_FID, GRID_HDD_BITMAP_FID); } +static QString mbrPartitionLabel(uint8_t* hdd_data, const MbrPartition& part) { + if (!part.isGRiD) { + return {}; + } + MbrPartition mut = part; + ccos_disk_t* disk = tryOpenMbrPartition(hdd_data, mut); + if (!disk) { + return {}; + } + QString label = short_string_to_qstring(ccos_get_disk_label(disk)); + free(disk); + return label; +} + void MainWindow::tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size) { Q_ASSERT(isMbrDisk(data, size)); @@ -1392,24 +1407,21 @@ void MainWindow::tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size) return; } - ChsDlg dlg(this); - dlg.setName("MBR: Select disk partition"); - dlg.setInfo("Hard disk with MBR detected.\nSelect the GRiD disk partition you want to work with:"); + QStringList labels; + for (const auto& p : parts) + labels << mbrPartitionLabel(hdd_data.data(), p); - for (const auto& part : parts) { - if (part.isActive){ - dlg.addItem(QString("Partition %1, active").arg(part.index + 1)); - } else { - dlg.addItem(QString("Partition %1").arg(part.index + 1)); - } - } + PartitionDlg dlg(this); + dlg.setTitle("MBR: Select disk partition"); + dlg.setInfo("Hard disk with MBR detected.\nSelect the GRiD disk partition you want to work with:"); + dlg.setPartitions(parts, labels); while (true) { - if (dlg.exec() != 1) { + if (dlg.exec() != QDialog::Accepted) { break; } - int selected = dlg.getIndex(); + int selected = dlg.selectedIndex(); ccos_disk_t* disk = tryOpenMbrPartition(hdd_data.data(), parts[selected]); if (disk) { @@ -1827,25 +1839,25 @@ void MainWindow::SetActivePart() { std::vector parts = parseMbr(panel.hdd_data->data(), panel.hdd_data->size()); - ChsDlg dlg(this); - dlg.setName("Select disk partition"); - dlg.setInfo("Select the GRiD disk partition to make it active:"); - - dlg.addItem("No active partition"); + QStringList labels; + for (const auto& p : parts) + labels << mbrPartitionLabel(panel.hdd_data->data(), p); - for (const auto& part : parts) { - dlg.addItem(QString("Partition %1").arg(part.index + 1)); - } + PartitionDlg dlg(this); + dlg.setTitle("Select disk partition"); + dlg.setInfo("Select the GRiD disk partition to make it active:"); + dlg.allowNone("No active partition"); + dlg.setPartitions(parts, labels); - if (dlg.exec() == 1){ - int selctd = dlg.getIndex(); + if (dlg.exec() == QDialog::Accepted){ + int idx = dlg.selectedIndex(); mbrtab[0] = 0x0; mbrtab[16] = 0x0; mbrtab[32] = 0x0; mbrtab[48] = 0x0; - if (selctd > 0) - mbrtab[(selctd-1)*16] = 0x80; + if (idx >= 0) + mbrtab[parts[idx].index * 16] = 0x80; panel.modified = true; refreshPanel(active_panel); diff --git a/dialogs/mainwindow.h b/dialogs/mainwindow.h index ae1c5cb..c882e42 100644 --- a/dialogs/mainwindow.h +++ b/dialogs/mainwindow.h @@ -12,7 +12,7 @@ #include "filepanelwidget.h" #include "abdlg.h" #include "custdlg.h" -#include "chsedlg.h" +#include "partitiondlg.h" #include "datedlg.h" #include "rendlg.h" #include "verdlg.h" @@ -33,14 +33,6 @@ struct DirViewState { int selectIndex = -1; }; -struct MbrPartition { - size_t index; - bool isGRiD; - bool isActive; - uint64_t offset; - uint64_t size; -}; - struct DiskPanel { QString path; ccos_disk_t* disk = nullptr; diff --git a/dialogs/partitiondlg.cpp b/dialogs/partitiondlg.cpp new file mode 100644 index 0000000..1eb9561 --- /dev/null +++ b/dialogs/partitiondlg.cpp @@ -0,0 +1,73 @@ +#include "partitiondlg.h" +#include "ui_partitiondlg.h" + +#include + +PartitionDlg::PartitionDlg(QWidget *parent) : + QDialog(parent), + ui(new Ui::PartitionDlg) +{ + ui->setupUi(this); + connect(ui->listWidget, &QListWidget::itemDoubleClicked, this, &QDialog::accept); +} + +PartitionDlg::~PartitionDlg() +{ + delete ui; +} + +void PartitionDlg::setTitle(const QString &text) { + setWindowTitle(text); +} + +void PartitionDlg::setInfo(const QString &text) { + ui->label->setText(text); +} + +void PartitionDlg::allowNone(const QString &noneText) { + m_hasNone = true; + m_noneText = noneText.isEmpty() ? QStringLiteral("None") : noneText; +} + +void PartitionDlg::setPartitions(const std::vector &parts, + const QStringList &labels, + const QSet &disabledSlots) { + ui->listWidget->clear(); + + if (m_hasNone) + addRow(m_noneText, true); + + for (size_t i = 0; i < parts.size(); ++i) { + const MbrPartition &p = parts[i]; + QString text = QString("Partition %1%2").arg(p.index + 1).arg(p.isActive ? "*" : ""); + + bool enabled = true; + if (!p.isGRiD) { + text += " (not a GRiD partition)"; + enabled = false; + } else { + if ((int)i < labels.size() && !labels[i].isEmpty()) + text += QString(" - %1").arg(labels[i]); + if (disabledSlots.contains((int)p.index)) { + text += " (already open)"; + enabled = false; + } + } + addRow(text, enabled); + } +} + +int PartitionDlg::selectedIndex() const { + int row = ui->listWidget->currentRow(); + if (row < 0) + return -1; + return m_hasNone ? row - 1 : row; +} + +void PartitionDlg::addRow(const QString &text, bool enabled) { + auto *item = new QListWidgetItem(text, ui->listWidget); + if (!enabled) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled & ~Qt::ItemIsSelectable); + else if (ui->listWidget->currentRow() < 0) + ui->listWidget->setCurrentItem(item); +} diff --git a/dialogs/partitiondlg.h b/dialogs/partitiondlg.h new file mode 100644 index 0000000..9be37a7 --- /dev/null +++ b/dialogs/partitiondlg.h @@ -0,0 +1,53 @@ +#ifndef PARTITIONDLG_H +#define PARTITIONDLG_H + +#include +#include +#include +#include +#include +#include + +namespace Ui { +class PartitionDlg; +} + +struct MbrPartition { + size_t index; + bool isGRiD; + bool isActive; + uint64_t offset; + uint64_t size; +}; + +class PartitionDlg : public QDialog +{ + Q_OBJECT + +public: + explicit PartitionDlg(QWidget *parent = nullptr); + ~PartitionDlg(); + + void setTitle(const QString &text); + void setInfo(const QString &text); + + // `labels` is parallel to `parts`; empty strings are allowed. + void setPartitions(const std::vector &parts, + const QStringList &labels, + const QSet &disabledSlots = {}); + + // Prepends a selectable entry before the partitions; selectedIndex() + // returns -1 when it is chosen. + void allowNone(const QString &noneText = {}); + + int selectedIndex() const; + +private: + void addRow(const QString &text, bool enabled); + + Ui::PartitionDlg *ui; + bool m_hasNone = false; + QString m_noneText; +}; + +#endif // PARTITIONDLG_H diff --git a/dialogs/partitiondlg.ui b/dialogs/partitiondlg.ui new file mode 100644 index 0000000..d2e9ab6 --- /dev/null +++ b/dialogs/partitiondlg.ui @@ -0,0 +1,116 @@ + + + PartitionDlg + + + + 0 + 0 + 400 + 260 + + + + + 400 + 260 + + + + + Sans Serif + + + + Select disk partition + + + + :/icon/gridsmall.png:/icon/gridsmall.png + + + + + + + Sans Serif + 9 + + + + %no_text#$contact_aperture_science@@*% + + + true + + + + + + + + 360 + 120 + + + + true + + + QAbstractItemView::SingleSelection + + + Qt::ElideNone + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + PartitionDlg + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PartitionDlg + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 8375be10bf14de023bfdb8d5b8252b55ac71e40b Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Wed, 29 Jul 2026 22:27:29 +0300 Subject: [PATCH 28/36] rebuild source code structure --- .gitignore | 3 + CMakeLists.txt | 96 ++++++++++-------- {imd2raw => common}/imd2raw.c | 0 {imd2raw => common}/imd2raw.h | 0 common/mbr.cpp | 40 ++++++++ common/mbr.h | 18 ++++ .../preview/omf => common}/omf_record.cpp | 0 {dialogs/preview/omf => common}/omf_record.h | 0 dialogs/abdlg.h | 29 ------ dialogs/rendlg.cpp | 34 ------- dialogs/rendlg.h | 27 ----- dialogs/verdlg.h | 26 ----- execico.rc | 2 +- main.cpp | 2 +- dialogs/abdlg.cpp => ui/dialogs/about.cpp | 12 +-- ui/dialogs/about.h | 29 ++++++ dialogs/abdlg.ui => ui/dialogs/about.ui | 6 +- .../custdlg.cpp => ui/dialogs/customdisk.cpp | 24 ++--- dialogs/custdlg.h => ui/dialogs/customdisk.h | 16 +-- .../custdlg.ui => ui/dialogs/customdisk.ui | 8 +- dialogs/datedlg.cpp => ui/dialogs/date.cpp | 2 +- dialogs/datedlg.h => ui/dialogs/date.h | 2 +- dialogs/datedlg.ui => ui/dialogs/date.ui | 0 .../dialogs/partition.cpp | 4 +- .../partitiondlg.h => ui/dialogs/partition.h | 11 +- .../dialogs/partition.ui | 0 ui/dialogs/rename.cpp | 34 +++++++ ui/dialogs/rename.h | 27 +++++ dialogs/rendlg.ui => ui/dialogs/rename.ui | 8 +- dialogs/verdlg.cpp => ui/dialogs/version.cpp | 12 +-- ui/dialogs/version.h | 26 +++++ dialogs/verdlg.ui => ui/dialogs/version.ui | 8 +- {dialogs => ui}/filepanelwidget.cpp | 0 {dialogs => ui}/filepanelwidget.h | 0 {icon => ui/icon}/gridisk.png | Bin {icon => ui/icon}/gridsmall.ico | Bin {icon => ui/icon}/gridsmall.png | Bin {icon => ui/icon}/icon.qrc | 0 {dialogs => ui}/mainwindow.cpp | 54 ++-------- {dialogs => ui}/mainwindow.h | 12 +-- {dialogs => ui}/mainwindow.ui | 6 +- .../preview/canvas.cpp | 4 +- .../canvaspreview.h => ui/preview/canvas.h | 2 +- .../preview/database.cpp | 4 +- .../preview/database.h | 2 +- .../previewdlg.cpp => ui/preview/dialog.cpp | 14 +-- .../previewdlg.h => ui/preview/dialog.h | 0 .../filepreview.h => ui/preview/file.h | 0 .../fontpreview.cpp => ui/preview/font.cpp | 4 +- .../fontpreview.h => ui/preview/font.h | 2 +- .../textpreview.cpp => ui/preview/text.cpp | 4 +- .../textpreview.h => ui/preview/text.h | 2 +- .../preview/worksheet.cpp | 4 +- .../preview/worksheet.h | 2 +- 54 files changed, 322 insertions(+), 300 deletions(-) rename {imd2raw => common}/imd2raw.c (100%) rename {imd2raw => common}/imd2raw.h (100%) create mode 100644 common/mbr.cpp create mode 100644 common/mbr.h rename {dialogs/preview/omf => common}/omf_record.cpp (100%) rename {dialogs/preview/omf => common}/omf_record.h (100%) delete mode 100644 dialogs/abdlg.h delete mode 100644 dialogs/rendlg.cpp delete mode 100644 dialogs/rendlg.h delete mode 100644 dialogs/verdlg.h rename dialogs/abdlg.cpp => ui/dialogs/about.cpp (77%) create mode 100644 ui/dialogs/about.h rename dialogs/abdlg.ui => ui/dialogs/about.ui (99%) rename dialogs/custdlg.cpp => ui/dialogs/customdisk.cpp (77%) rename dialogs/custdlg.h => ui/dialogs/customdisk.h (53%) rename dialogs/custdlg.ui => ui/dialogs/customdisk.ui (98%) rename dialogs/datedlg.cpp => ui/dialogs/date.cpp (98%) rename dialogs/datedlg.h => ui/dialogs/date.h (94%) rename dialogs/datedlg.ui => ui/dialogs/date.ui (100%) rename dialogs/partitiondlg.cpp => ui/dialogs/partition.cpp (97%) rename dialogs/partitiondlg.h => ui/dialogs/partition.h (87%) rename dialogs/partitiondlg.ui => ui/dialogs/partition.ui (100%) create mode 100644 ui/dialogs/rename.cpp create mode 100644 ui/dialogs/rename.h rename dialogs/rendlg.ui => ui/dialogs/rename.ui (96%) rename dialogs/verdlg.cpp => ui/dialogs/version.cpp (66%) create mode 100644 ui/dialogs/version.h rename dialogs/verdlg.ui => ui/dialogs/version.ui (97%) rename {dialogs => ui}/filepanelwidget.cpp (100%) rename {dialogs => ui}/filepanelwidget.h (100%) rename {icon => ui/icon}/gridisk.png (100%) rename {icon => ui/icon}/gridsmall.ico (100%) rename {icon => ui/icon}/gridsmall.png (100%) rename {icon => ui/icon}/icon.qrc (100%) rename {dialogs => ui}/mainwindow.cpp (97%) rename {dialogs => ui}/mainwindow.h (96%) rename {dialogs => ui}/mainwindow.ui (99%) rename dialogs/preview/canvaspreview.cpp => ui/preview/canvas.cpp (99%) rename dialogs/preview/canvaspreview.h => ui/preview/canvas.h (93%) rename dialogs/preview/databasepreview.cpp => ui/preview/database.cpp (99%) rename dialogs/preview/databasepreview.h => ui/preview/database.h (93%) rename dialogs/preview/previewdlg.cpp => ui/preview/dialog.cpp (93%) rename dialogs/preview/previewdlg.h => ui/preview/dialog.h (100%) rename dialogs/preview/filepreview.h => ui/preview/file.h (100%) rename dialogs/preview/fontpreview.cpp => ui/preview/font.cpp (99%) rename dialogs/preview/fontpreview.h => ui/preview/font.h (95%) rename dialogs/preview/textpreview.cpp => ui/preview/text.cpp (99%) rename dialogs/preview/textpreview.h => ui/preview/text.h (93%) rename dialogs/preview/worksheetpreview.cpp => ui/preview/worksheet.cpp (99%) rename dialogs/preview/worksheetpreview.h => ui/preview/worksheet.h (93%) diff --git a/.gitignore b/.gitignore index 8c4b740..97a63cd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ build/ #CMake stuff CMakeCache.txt CMakeFiles + +research/ +test data/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f741763..4f194d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,10 +6,6 @@ add_subdirectory(ccos_image) project(GRiDISKCOM LANGUAGES CXX C) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -include_directories(ccos_image) -include_directories(imd2raw) - set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) @@ -24,49 +20,59 @@ if(WIN32) set(GUI_TYPE WIN32) endif() +include_directories(${CMAKE_SOURCE_DIR}) +include_directories(ccos_image) +include_directories(ui) +include_directories(ui/dialogs) +include_directories(ui/preview) +include_directories(common) + + add_executable(GRiDISKCOM ${GUI_TYPE} main.cpp - imd2raw/imd2raw.c - imd2raw/imd2raw.h - dialogs/mainwindow.cpp - dialogs/mainwindow.h - dialogs/mainwindow.ui - dialogs/abdlg.cpp - dialogs/abdlg.h - dialogs/abdlg.ui - dialogs/partitiondlg.cpp - dialogs/partitiondlg.h - dialogs/partitiondlg.ui - dialogs/custdlg.cpp - dialogs/custdlg.h - dialogs/custdlg.ui - dialogs/datedlg.cpp - dialogs/datedlg.h - dialogs/datedlg.ui - dialogs/filepanelwidget.cpp - dialogs/filepanelwidget.h - dialogs/preview/filepreview.h - dialogs/preview/omf/omf_record.cpp - dialogs/preview/omf/omf_record.h - dialogs/preview/textpreview.cpp - dialogs/preview/textpreview.h - dialogs/preview/canvaspreview.cpp - dialogs/preview/canvaspreview.h - dialogs/preview/fontpreview.cpp - dialogs/preview/fontpreview.h - dialogs/preview/worksheetpreview.cpp - dialogs/preview/worksheetpreview.h - dialogs/preview/databasepreview.cpp - dialogs/preview/databasepreview.h - dialogs/preview/previewdlg.cpp - dialogs/preview/previewdlg.h - dialogs/rendlg.cpp - dialogs/rendlg.h - dialogs/rendlg.ui - dialogs/verdlg.cpp - dialogs/verdlg.h - dialogs/verdlg.ui - icon/icon.qrc + common/imd2raw.c + common/imd2raw.h + common/mbr.cpp + common/mbr.h + common/omf_record.cpp + common/omf_record.h + ui/mainwindow.cpp + ui/mainwindow.h + ui/mainwindow.ui + ui/filepanelwidget.cpp + ui/filepanelwidget.h + ui/dialogs/about.cpp + ui/dialogs/about.h + ui/dialogs/about.ui + ui/dialogs/customdisk.cpp + ui/dialogs/customdisk.h + ui/dialogs/customdisk.ui + ui/dialogs/date.cpp + ui/dialogs/date.h + ui/dialogs/date.ui + ui/dialogs/partition.cpp + ui/dialogs/partition.h + ui/dialogs/partition.ui + ui/dialogs/rename.cpp + ui/dialogs/rename.h + ui/dialogs/rename.ui + ui/dialogs/version.cpp + ui/dialogs/version.h + ui/dialogs/version.ui + ui/preview/file.h + ui/preview/dialog.cpp + ui/preview/dialog.h + ui/preview/text.cpp + ui/preview/text.h + ui/preview/canvas.cpp + ui/preview/canvas.h + ui/preview/font.cpp + ui/preview/font.h + ui/preview/worksheet.cpp + ui/preview/worksheet.h + ui/preview/database.cpp + ui/preview/database.h + ui/icon/icon.qrc execico.rc ) diff --git a/imd2raw/imd2raw.c b/common/imd2raw.c similarity index 100% rename from imd2raw/imd2raw.c rename to common/imd2raw.c diff --git a/imd2raw/imd2raw.h b/common/imd2raw.h similarity index 100% rename from imd2raw/imd2raw.h rename to common/imd2raw.h diff --git a/common/mbr.cpp b/common/mbr.cpp new file mode 100644 index 0000000..2de33dc --- /dev/null +++ b/common/mbr.cpp @@ -0,0 +1,40 @@ +#include "mbr.h" + +bool isMbrDisk(const uint8_t* data, size_t size) { + return size > 0x200 && data[0x1FE] == 0x55 && data[0x1FF] == 0xAA; +} + +std::vector parseMbr(const uint8_t* data, size_t size) { + std::vector partitions; + partitions.reserve(4); + + if (size < 512) { + return partitions; + } + + const uint8_t* table = data + 0x1BE; + for (size_t i = 0; i < 4; i++) { + const uint8_t* entry = table + i * 16; + + bool isGRiD = entry[4] == 0x47; + bool isActive = (entry[0] & 0x80) != 0; + + uint64_t part_offset = (entry[8] | entry[9] << 8 | entry[10] << 16 | entry[11] << 24) * 512; + uint64_t part_size = (entry[12] | entry[13] << 8 | entry[14] << 16 | entry[15] << 24) * 512; + + // A zeroed entry marks the end of the used partition table. + if (part_offset == 0 && part_size == 0 && !isGRiD && !isActive) { + break; + } + + partitions.push_back(MbrPartition { + i, + isGRiD, + isActive, + part_offset, + part_size + }); + } + + return partitions; +} diff --git a/common/mbr.h b/common/mbr.h new file mode 100644 index 0000000..e6f4ae2 --- /dev/null +++ b/common/mbr.h @@ -0,0 +1,18 @@ +#ifndef MBR_H +#define MBR_H + +#include +#include + +struct MbrPartition { + size_t index; + bool isGRiD; + bool isActive; + uint64_t offset; + uint64_t size; +}; + +bool isMbrDisk(const uint8_t* data, size_t size); +std::vector parseMbr(const uint8_t* data, size_t size); + +#endif // MBR_H diff --git a/dialogs/preview/omf/omf_record.cpp b/common/omf_record.cpp similarity index 100% rename from dialogs/preview/omf/omf_record.cpp rename to common/omf_record.cpp diff --git a/dialogs/preview/omf/omf_record.h b/common/omf_record.h similarity index 100% rename from dialogs/preview/omf/omf_record.h rename to common/omf_record.h diff --git a/dialogs/abdlg.h b/dialogs/abdlg.h deleted file mode 100644 index f281a7d..0000000 --- a/dialogs/abdlg.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef ABDLG_H -#define ABDLG_H - -#include -#include -#include "ui_abdlg.h" - -#define _PVER_ "0.30-beta" - -namespace Ui { -class AbDlg; -} - -class AbDlg : public QDialog -{ - Q_OBJECT - -public slots: - void openLink(QString link); - void openRepo(); -public: - explicit AbDlg(QWidget *parent = nullptr); - ~AbDlg(); - -private: - Ui::AbDlg *ui; -}; - -#endif // ABDLG_H diff --git a/dialogs/rendlg.cpp b/dialogs/rendlg.cpp deleted file mode 100644 index fbf22a7..0000000 --- a/dialogs/rendlg.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "rendlg.h" - -RenDlg::RenDlg(QWidget *parent, bool lockedType) : - QDialog(parent), - ui(new Ui::RenDlg) -{ - ui->setupUi(this); - ui->lineEdit_2->setDisabled(lockedType); -} - -QString RenDlg::getName(){ - return ui->lineEdit->text(); -} - -QString RenDlg::getType(){ - return ui->lineEdit_2->text(); -} - -void RenDlg::setInfo(QString text){ - ui->label->setText(text); -} - -void RenDlg::setName(QString text){ - ui->lineEdit->setText(text); -} - -void RenDlg::setType(QString text){ - ui->lineEdit_2->setText(text); -} - -RenDlg::~RenDlg() -{ - delete ui; -} diff --git a/dialogs/rendlg.h b/dialogs/rendlg.h deleted file mode 100644 index 5251164..0000000 --- a/dialogs/rendlg.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef RENDLG_H -#define RENDLG_H - -#include "ui_rendlg.h" - -namespace Ui { -class RenDlg; -} - -class RenDlg : public QDialog -{ - Q_OBJECT - -public: - explicit RenDlg(QWidget *parent = nullptr, bool lockedType = false); - QString getName(); - QString getType(); - void setInfo(QString text); - void setName(QString text); - void setType(QString text); - ~RenDlg(); - -private: - Ui::RenDlg *ui; -}; - -#endif // RENDLG_H diff --git a/dialogs/verdlg.h b/dialogs/verdlg.h deleted file mode 100644 index 33a9a5e..0000000 --- a/dialogs/verdlg.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef VERDLG_H -#define VERDLG_H - -#include "ui_verdlg.h" - -#include - -namespace Ui { -class VerDlg; -} - -class VerDlg : public QDialog -{ - Q_OBJECT - -public: - explicit VerDlg(QWidget *parent = nullptr); - void init(QString fname, ccos_version_t version); - ccos_version_t retVer(); - ~VerDlg(); - -private: - Ui::VerDlg *ui; -}; - -#endif // VERDLG_H diff --git a/execico.rc b/execico.rc index 54b7cf4..9e4bdaa 100644 --- a/execico.rc +++ b/execico.rc @@ -1 +1 @@ -IDI_ICON1 ICON DISCARDABLE "icon/gridsmall.ico" +IDI_ICON1 ICON DISCARDABLE "ui/icon/gridsmall.ico" diff --git a/main.cpp b/main.cpp index 1daa078..d55a4ef 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,5 @@ #include -#include "dialogs/mainwindow.h" +#include "ui/mainwindow.h" using namespace std; diff --git a/dialogs/abdlg.cpp b/ui/dialogs/about.cpp similarity index 77% rename from dialogs/abdlg.cpp rename to ui/dialogs/about.cpp index 8bb37af..b37d67a 100644 --- a/dialogs/abdlg.cpp +++ b/ui/dialogs/about.cpp @@ -1,8 +1,8 @@ -#include "abdlg.h" +#include "about.h" -AbDlg::AbDlg(QWidget *parent) : +AboutDlg::AboutDlg(QWidget *parent) : QDialog(parent), - ui(new Ui::AbDlg) + ui(new Ui::AboutDlg) { ui->setupUi(this); ui->label_5->setText(_PVER_); @@ -12,17 +12,17 @@ AbDlg::AbDlg(QWidget *parent) : connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(openRepo())); } -void AbDlg::openLink(QString link) +void AboutDlg::openLink(QString link) { QDesktopServices::openUrl(link); } -void AbDlg::openRepo() +void AboutDlg::openRepo() { QDesktopServices::openUrl(QUrl("https://github.com/Bs0Dd/GRiDISKCOM")); } -AbDlg::~AbDlg() +AboutDlg::~AboutDlg() { delete ui; } diff --git a/ui/dialogs/about.h b/ui/dialogs/about.h new file mode 100644 index 0000000..8408625 --- /dev/null +++ b/ui/dialogs/about.h @@ -0,0 +1,29 @@ +#ifndef ABOUTDLG_H +#define ABOUTDLG_H + +#include +#include +#include "ui_about.h" + +#define _PVER_ "0.30-beta" + +namespace Ui { +class AboutDlg; +} + +class AboutDlg : public QDialog +{ + Q_OBJECT + +public slots: + void openLink(QString link); + void openRepo(); +public: + explicit AboutDlg(QWidget *parent = nullptr); + ~AboutDlg(); + +private: + Ui::AboutDlg *ui; +}; + +#endif // ABOUTDLG_H diff --git a/dialogs/abdlg.ui b/ui/dialogs/about.ui similarity index 99% rename from dialogs/abdlg.ui rename to ui/dialogs/about.ui index 9c1d8a7..5aa8039 100644 --- a/dialogs/abdlg.ui +++ b/ui/dialogs/about.ui @@ -1,7 +1,7 @@ - AbDlg - + AboutDlg + 0 @@ -328,7 +328,7 @@ pushButton_2 clicked(bool) - AbDlg + AboutDlg close() diff --git a/dialogs/custdlg.cpp b/ui/dialogs/customdisk.cpp similarity index 77% rename from dialogs/custdlg.cpp rename to ui/dialogs/customdisk.cpp index cb6f437..ceaa2cf 100644 --- a/dialogs/custdlg.cpp +++ b/ui/dialogs/customdisk.cpp @@ -1,9 +1,9 @@ -#include "custdlg.h" -#include "ui_custdlg.h" +#include "customdisk.h" +#include "ui_customdisk.h" -CustDlg::CustDlg(QWidget *parent, bool openMode) : +CustomDiskDlg::CustomDiskDlg(QWidget *parent, bool openMode) : QDialog(parent), - ui(new Ui::CustDlg) + ui(new Ui::CustomDiskDlg) { ui->setupUi(this); @@ -18,13 +18,13 @@ CustDlg::CustDlg(QWidget *parent, bool openMode) : if (openMode) { ui->groupBox_3->setVisible(false); ui->groupBox_4->setVisible(false); - CustDlg::setWindowTitle("Custom image opening"); - CustDlg::setMinimumHeight(380); - CustDlg::setMaximumHeight(380); + CustomDiskDlg::setWindowTitle("Custom image opening"); + CustomDiskDlg::setMinimumHeight(380); + CustomDiskDlg::setMaximumHeight(380); } } -void CustDlg::SblFocus(bool activ) { +void CustomDiskDlg::SblFocus(bool activ) { ui->spinBox->setEnabled(activ); if (activ) { ui->spinBox->setFocus(); @@ -32,7 +32,7 @@ void CustDlg::SblFocus(bool activ) { } } -void CustDlg::SizFocus(bool activ) { +void CustomDiskDlg::SizFocus(bool activ) { ui->spinBox_2->setEnabled(activ); if (activ) { ui->spinBox_2->setFocus(); @@ -40,7 +40,7 @@ void CustDlg::SizFocus(bool activ) { } } -void CustDlg::BmpFocus(bool activ) { +void CustomDiskDlg::BmpFocus(bool activ) { ui->spinBox_3->setEnabled(activ); if (activ) { ui->spinBox_3->setFocus(); @@ -49,7 +49,7 @@ void CustDlg::BmpFocus(bool activ) { } -void CustDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* isize, QString* labl) { +void CustomDiskDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* isize, QString* labl) { *sect = ui->radioButton->isChecked() ? 512 : 256; if (ui->radioButton_3->isChecked()) { @@ -95,7 +95,7 @@ void CustDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* } -CustDlg::~CustDlg() +CustomDiskDlg::~CustomDiskDlg() { delete ui; } diff --git a/dialogs/custdlg.h b/ui/dialogs/customdisk.h similarity index 53% rename from dialogs/custdlg.h rename to ui/dialogs/customdisk.h index d561ca4..20430f7 100644 --- a/dialogs/custdlg.h +++ b/ui/dialogs/customdisk.h @@ -1,13 +1,13 @@ -#ifndef CUSTDLG_H -#define CUSTDLG_H +#ifndef CUSTOMDISKDLG_H +#define CUSTOMDISKDLG_H #include namespace Ui { -class CustDlg; +class CustomDiskDlg; } -class CustDlg : public QDialog +class CustomDiskDlg : public QDialog { Q_OBJECT @@ -17,13 +17,13 @@ public slots: void SizFocus(bool activ); public: - explicit CustDlg(QWidget *parent = nullptr, bool openMode = false); - ~CustDlg(); + explicit CustomDiskDlg(QWidget *parent = nullptr, bool openMode = false); + ~CustomDiskDlg(); void GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uint16_t* isize, QString* labl); private: - Ui::CustDlg *ui; + Ui::CustomDiskDlg *ui; QFont HexFont; }; -#endif // CUSTDLG_H +#endif // CUSTOMDISKDLG_H diff --git a/dialogs/custdlg.ui b/ui/dialogs/customdisk.ui similarity index 98% rename from dialogs/custdlg.ui rename to ui/dialogs/customdisk.ui index 723d395..f56e36a 100644 --- a/dialogs/custdlg.ui +++ b/ui/dialogs/customdisk.ui @@ -1,7 +1,7 @@ - CustDlg - + CustomDiskDlg + 400 @@ -486,7 +486,7 @@ buttonBox accepted() - CustDlg + CustomDiskDlg accept() @@ -502,7 +502,7 @@ buttonBox rejected() - CustDlg + CustomDiskDlg reject() diff --git a/dialogs/datedlg.cpp b/ui/dialogs/date.cpp similarity index 98% rename from dialogs/datedlg.cpp rename to ui/dialogs/date.cpp index a5f5402..0e833d8 100644 --- a/dialogs/datedlg.cpp +++ b/ui/dialogs/date.cpp @@ -1,4 +1,4 @@ -#include "datedlg.h" +#include "date.h" DateDlg::DateDlg(QWidget *parent) : QDialog(parent), diff --git a/dialogs/datedlg.h b/ui/dialogs/date.h similarity index 94% rename from dialogs/datedlg.h rename to ui/dialogs/date.h index 61609d5..a355d30 100644 --- a/dialogs/datedlg.h +++ b/ui/dialogs/date.h @@ -1,7 +1,7 @@ #ifndef DATEDLG_H #define DATEDLG_H -#include "ui_datedlg.h" +#include "ui_date.h" #include diff --git a/dialogs/datedlg.ui b/ui/dialogs/date.ui similarity index 100% rename from dialogs/datedlg.ui rename to ui/dialogs/date.ui diff --git a/dialogs/partitiondlg.cpp b/ui/dialogs/partition.cpp similarity index 97% rename from dialogs/partitiondlg.cpp rename to ui/dialogs/partition.cpp index 1eb9561..968362a 100644 --- a/dialogs/partitiondlg.cpp +++ b/ui/dialogs/partition.cpp @@ -1,5 +1,5 @@ -#include "partitiondlg.h" -#include "ui_partitiondlg.h" +#include "partition.h" +#include "ui_partition.h" #include diff --git a/dialogs/partitiondlg.h b/ui/dialogs/partition.h similarity index 87% rename from dialogs/partitiondlg.h rename to ui/dialogs/partition.h index 9be37a7..b68d642 100644 --- a/dialogs/partitiondlg.h +++ b/ui/dialogs/partition.h @@ -5,21 +5,14 @@ #include #include #include -#include #include +#include "mbr.h" + namespace Ui { class PartitionDlg; } -struct MbrPartition { - size_t index; - bool isGRiD; - bool isActive; - uint64_t offset; - uint64_t size; -}; - class PartitionDlg : public QDialog { Q_OBJECT diff --git a/dialogs/partitiondlg.ui b/ui/dialogs/partition.ui similarity index 100% rename from dialogs/partitiondlg.ui rename to ui/dialogs/partition.ui diff --git a/ui/dialogs/rename.cpp b/ui/dialogs/rename.cpp new file mode 100644 index 0000000..a9edc8a --- /dev/null +++ b/ui/dialogs/rename.cpp @@ -0,0 +1,34 @@ +#include "rename.h" + +RenameDlg::RenameDlg(QWidget *parent, bool lockedType) : + QDialog(parent), + ui(new Ui::RenameDlg) +{ + ui->setupUi(this); + ui->lineEdit_2->setDisabled(lockedType); +} + +QString RenameDlg::getName(){ + return ui->lineEdit->text(); +} + +QString RenameDlg::getType(){ + return ui->lineEdit_2->text(); +} + +void RenameDlg::setInfo(QString text){ + ui->label->setText(text); +} + +void RenameDlg::setName(QString text){ + ui->lineEdit->setText(text); +} + +void RenameDlg::setType(QString text){ + ui->lineEdit_2->setText(text); +} + +RenameDlg::~RenameDlg() +{ + delete ui; +} diff --git a/ui/dialogs/rename.h b/ui/dialogs/rename.h new file mode 100644 index 0000000..1a05342 --- /dev/null +++ b/ui/dialogs/rename.h @@ -0,0 +1,27 @@ +#ifndef RENAMEDLG_H +#define RENAMEDLG_H + +#include "ui_rename.h" + +namespace Ui { +class RenameDlg; +} + +class RenameDlg : public QDialog +{ + Q_OBJECT + +public: + explicit RenameDlg(QWidget *parent = nullptr, bool lockedType = false); + QString getName(); + QString getType(); + void setInfo(QString text); + void setName(QString text); + void setType(QString text); + ~RenameDlg(); + +private: + Ui::RenameDlg *ui; +}; + +#endif // RENAMEDLG_H diff --git a/dialogs/rendlg.ui b/ui/dialogs/rename.ui similarity index 96% rename from dialogs/rendlg.ui rename to ui/dialogs/rename.ui index 7d40482..032ed76 100644 --- a/dialogs/rendlg.ui +++ b/ui/dialogs/rename.ui @@ -1,7 +1,7 @@ - RenDlg - + RenameDlg + 0 @@ -159,7 +159,7 @@ buttonBox accepted() - RenDlg + RenameDlg accept() @@ -175,7 +175,7 @@ buttonBox rejected() - RenDlg + RenameDlg reject() diff --git a/dialogs/verdlg.cpp b/ui/dialogs/version.cpp similarity index 66% rename from dialogs/verdlg.cpp rename to ui/dialogs/version.cpp index 314cda7..de8c428 100644 --- a/dialogs/verdlg.cpp +++ b/ui/dialogs/version.cpp @@ -1,25 +1,25 @@ -#include "verdlg.h" +#include "version.h" -VerDlg::VerDlg(QWidget *parent) : +VersionDlg::VersionDlg(QWidget *parent) : QDialog(parent), - ui(new Ui::VerDlg) + ui(new Ui::VersionDlg) { ui->setupUi(this); } -void VerDlg::init(QString fname, ccos_version_t version){ +void VersionDlg::init(QString fname, ccos_version_t version){ ui->label->setText(ui->label->text().arg(fname)); ui->major->setValue(version.major); ui->minor->setValue(version.minor); ui->patch->setValue(version.patch); } -ccos_version_t VerDlg::retVer(){ +ccos_version_t VersionDlg::retVer(){ return ccos_version_t{static_cast(ui->major->value()), static_cast(ui->minor->value()), static_cast(ui->patch->value())}; } -VerDlg::~VerDlg() +VersionDlg::~VersionDlg() { delete ui; } diff --git a/ui/dialogs/version.h b/ui/dialogs/version.h new file mode 100644 index 0000000..07d1e57 --- /dev/null +++ b/ui/dialogs/version.h @@ -0,0 +1,26 @@ +#ifndef VERSIONDLG_H +#define VERSIONDLG_H + +#include "ui_version.h" + +#include + +namespace Ui { +class VersionDlg; +} + +class VersionDlg : public QDialog +{ + Q_OBJECT + +public: + explicit VersionDlg(QWidget *parent = nullptr); + void init(QString fname, ccos_version_t version); + ccos_version_t retVer(); + ~VersionDlg(); + +private: + Ui::VersionDlg *ui; +}; + +#endif // VERSIONDLG_H diff --git a/dialogs/verdlg.ui b/ui/dialogs/version.ui similarity index 97% rename from dialogs/verdlg.ui rename to ui/dialogs/version.ui index 8a82713..0725a9d 100644 --- a/dialogs/verdlg.ui +++ b/ui/dialogs/version.ui @@ -1,7 +1,7 @@ - VerDlg - + VersionDlg + 0 @@ -240,7 +240,7 @@ buttonBox accepted() - VerDlg + VersionDlg accept() @@ -256,7 +256,7 @@ buttonBox rejected() - VerDlg + VersionDlg reject() diff --git a/dialogs/filepanelwidget.cpp b/ui/filepanelwidget.cpp similarity index 100% rename from dialogs/filepanelwidget.cpp rename to ui/filepanelwidget.cpp diff --git a/dialogs/filepanelwidget.h b/ui/filepanelwidget.h similarity index 100% rename from dialogs/filepanelwidget.h rename to ui/filepanelwidget.h diff --git a/icon/gridisk.png b/ui/icon/gridisk.png similarity index 100% rename from icon/gridisk.png rename to ui/icon/gridisk.png diff --git a/icon/gridsmall.ico b/ui/icon/gridsmall.ico similarity index 100% rename from icon/gridsmall.ico rename to ui/icon/gridsmall.ico diff --git a/icon/gridsmall.png b/ui/icon/gridsmall.png similarity index 100% rename from icon/gridsmall.png rename to ui/icon/gridsmall.png diff --git a/icon/icon.qrc b/ui/icon/icon.qrc similarity index 100% rename from icon/icon.qrc rename to ui/icon/icon.qrc diff --git a/dialogs/mainwindow.cpp b/ui/mainwindow.cpp similarity index 97% rename from dialogs/mainwindow.cpp rename to ui/mainwindow.cpp index df1d482..3ecd114 100644 --- a/dialogs/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" -#include "preview/previewdlg.h" +#include "preview/dialog.h" +#include "mbr.h" #include "imd2raw.h" #include @@ -468,41 +469,6 @@ void MainWindow::updatePanelTitle(int panel_idx) { panel.modified ? "*" : "")); } -std::vector parseMbr(const uint8_t* data, size_t size) { - std::vector partitions; - partitions.reserve(4); - - if (size < 512) { - return partitions; - } - - const uint8_t* table = data + 0x1BE; - for (size_t i = 0; i < 4; i++) { - const uint8_t* entry = table + i * 16; - - bool isGRiD = entry[4] == 0x47; - bool isActive = (entry[0] & 0x80) != 0; - - uint64_t part_offset = (entry[8] | entry[9] << 8 | entry[10] << 16 | entry[11] << 24) * 512; - uint64_t part_size = (entry[12] | entry[13] << 8 | entry[14] << 16 | entry[15] << 24) * 512; - - // A zeroed entry marks the end of the used partition table. - if (part_offset == 0 && part_size == 0 && !isGRiD && !isActive) { - break; - } - - partitions.push_back(MbrPartition { - i, - isGRiD, - isActive, - part_offset, - part_size - }); - } - - return partitions; -} - //*Ask if user wants to save a file int saveBox(QString disk, QWidget* parent){ QMessageBox msgBox(parent); @@ -599,7 +565,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu } void MainWindow::AboutShow(){ - AbDlg dlg(this); + AboutDlg dlg(this); dlg.exec(); } @@ -675,7 +641,7 @@ int MainWindow::AddFiles(QStringList files, ccos_inode_t* copyTo){ size_t fsiz = 0; std::string fname = QFileInfo(file).fileName().toStdString(); if (tildaCheck(fname) == -1){ - RenDlg dlg(this); + RenameDlg dlg(this); dlg.setInfo(QString("Set correct name and type for %1:").arg(fname.c_str())); while (true){ if (dlg.exec() == 1){ @@ -1358,10 +1324,6 @@ void MainWindow::openValidNonMbrDisk(QString path, ccos_disk_t* disk) { HDDMenu(false); } -static bool isMbrDisk(const uint8_t* data, size_t size) { - return size > 0x200 && data[0x1FE] == 0x55 && data[0x1FF] == 0xAA; -} - static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition) { ccos_disk_t* disk = tryFromBootsector(data + partition.offset, partition.size); if (disk) { @@ -1469,7 +1431,7 @@ void MainWindow::loadCustomImg(QString path, uint8_t* data, size_t size) { return; } - CustDlg cdlg(this, true); + CustomDiskDlg cdlg(this, true); while (true) { if (cdlg.exec() != 1) { free(data); @@ -1612,7 +1574,7 @@ void MainWindow::NewImage(){ if (panels[active_panel]) if (!CloseImg()) return; - CustDlg dlg(this); + CustomDiskDlg dlg(this); while (true){ if (dlg.exec() == 1) { @@ -1694,7 +1656,7 @@ void MainWindow::Rename(){ memset(basename, 0, CCOS_MAX_FILE_NAME); memset(type, 0, CCOS_MAX_FILE_NAME); ccos_parse_file_name(reninode, basename, type, nullptr, nullptr); - RenDlg dlg(this, !panel.in_subdir); //All files in the root are directories + RenameDlg dlg(this, !panel.in_subdir); //All files in the root are directories dlg.setName(basename); dlg.setType(type); dlg.setInfo((QString("Set new name and type for %1:").arg(reninode->desc.name))); @@ -1876,7 +1838,7 @@ void MainWindow::Version(){ ccos_inode_t* file = panel.inodes[idx]; ccos_version_t ver = ccos_get_file_version(file); - VerDlg dlg(this); + VersionDlg dlg(this); dlg.init(file->desc.name, ver); if (dlg.exec() == 1){ ver = dlg.retVer(); diff --git a/dialogs/mainwindow.h b/ui/mainwindow.h similarity index 96% rename from dialogs/mainwindow.h rename to ui/mainwindow.h index c882e42..c9c3824 100644 --- a/dialogs/mainwindow.h +++ b/ui/mainwindow.h @@ -10,12 +10,12 @@ #include #include "ui_mainwindow.h" #include "filepanelwidget.h" -#include "abdlg.h" -#include "custdlg.h" -#include "partitiondlg.h" -#include "datedlg.h" -#include "rendlg.h" -#include "verdlg.h" +#include "dialogs/about.h" +#include "dialogs/customdisk.h" +#include "dialogs/partition.h" +#include "dialogs/date.h" +#include "dialogs/rename.h" +#include "dialogs/version.h" #include #include diff --git a/dialogs/mainwindow.ui b/ui/mainwindow.ui similarity index 99% rename from dialogs/mainwindow.ui rename to ui/mainwindow.ui index 8ccdf8b..df317e0 100644 --- a/dialogs/mainwindow.ui +++ b/ui/mainwindow.ui @@ -27,7 +27,7 @@ GRiDISK Commander - + :/icon/gridsmall.png:/icon/gridsmall.png @@ -492,11 +492,11 @@ FilePanelWidget QWidget -
dialogs/filepanelwidget.h
+
filepanelwidget.h
- +
diff --git a/dialogs/preview/canvaspreview.cpp b/ui/preview/canvas.cpp similarity index 99% rename from dialogs/preview/canvaspreview.cpp rename to ui/preview/canvas.cpp index 2fe3a86..8b6a768 100644 --- a/dialogs/preview/canvaspreview.cpp +++ b/ui/preview/canvas.cpp @@ -1,6 +1,6 @@ -#include "canvaspreview.h" +#include "canvas.h" -#include "omf/omf_record.h" +#include "omf_record.h" #include #include diff --git a/dialogs/preview/canvaspreview.h b/ui/preview/canvas.h similarity index 93% rename from dialogs/preview/canvaspreview.h rename to ui/preview/canvas.h index bece8ce..c10b55f 100644 --- a/dialogs/preview/canvaspreview.h +++ b/ui/preview/canvas.h @@ -1,7 +1,7 @@ #ifndef CANVASPREVIEW_H #define CANVASPREVIEW_H -#include "filepreview.h" +#include "file.h" class CanvasPreview : public FilePreview { public: diff --git a/dialogs/preview/databasepreview.cpp b/ui/preview/database.cpp similarity index 99% rename from dialogs/preview/databasepreview.cpp rename to ui/preview/database.cpp index 9b24ceb..4b5bd4e 100644 --- a/dialogs/preview/databasepreview.cpp +++ b/ui/preview/database.cpp @@ -1,6 +1,6 @@ -#include "databasepreview.h" +#include "database.h" -#include "omf/omf_record.h" +#include "omf_record.h" #include #include diff --git a/dialogs/preview/databasepreview.h b/ui/preview/database.h similarity index 93% rename from dialogs/preview/databasepreview.h rename to ui/preview/database.h index dabc6ec..e2c3680 100644 --- a/dialogs/preview/databasepreview.h +++ b/ui/preview/database.h @@ -1,7 +1,7 @@ #ifndef DATABASEPREVIEW_H #define DATABASEPREVIEW_H -#include "filepreview.h" +#include "file.h" class DatabasePreview : public FilePreview { public: diff --git a/dialogs/preview/previewdlg.cpp b/ui/preview/dialog.cpp similarity index 93% rename from dialogs/preview/previewdlg.cpp rename to ui/preview/dialog.cpp index 04c76ef..c3a1e51 100644 --- a/dialogs/preview/previewdlg.cpp +++ b/ui/preview/dialog.cpp @@ -1,11 +1,11 @@ -#include "previewdlg.h" +#include "dialog.h" -#include "filepreview.h" -#include "textpreview.h" -#include "canvaspreview.h" -#include "worksheetpreview.h" -#include "fontpreview.h" -#include "databasepreview.h" +#include "file.h" +#include "text.h" +#include "canvas.h" +#include "worksheet.h" +#include "font.h" +#include "database.h" #include #include diff --git a/dialogs/preview/previewdlg.h b/ui/preview/dialog.h similarity index 100% rename from dialogs/preview/previewdlg.h rename to ui/preview/dialog.h diff --git a/dialogs/preview/filepreview.h b/ui/preview/file.h similarity index 100% rename from dialogs/preview/filepreview.h rename to ui/preview/file.h diff --git a/dialogs/preview/fontpreview.cpp b/ui/preview/font.cpp similarity index 99% rename from dialogs/preview/fontpreview.cpp rename to ui/preview/font.cpp index 1d32d5d..5bd9d50 100644 --- a/dialogs/preview/fontpreview.cpp +++ b/ui/preview/font.cpp @@ -1,4 +1,4 @@ -#include "fontpreview.h" +#include "font.h" #include #include @@ -21,7 +21,7 @@ #include #include -#include "omf/omf_record.h" +#include "omf_record.h" namespace { diff --git a/dialogs/preview/fontpreview.h b/ui/preview/font.h similarity index 95% rename from dialogs/preview/fontpreview.h rename to ui/preview/font.h index da166e5..19a2fb0 100644 --- a/dialogs/preview/fontpreview.h +++ b/ui/preview/font.h @@ -1,7 +1,7 @@ #ifndef FONTPREVIEW_H #define FONTPREVIEW_H -#include "filepreview.h" +#include "file.h" class FontPreview : public FilePreview { public: diff --git a/dialogs/preview/textpreview.cpp b/ui/preview/text.cpp similarity index 99% rename from dialogs/preview/textpreview.cpp rename to ui/preview/text.cpp index 002fa69..63a8779 100644 --- a/dialogs/preview/textpreview.cpp +++ b/ui/preview/text.cpp @@ -1,6 +1,6 @@ -#include "textpreview.h" +#include "text.h" -#include "omf/omf_record.h" +#include "omf_record.h" #include #include diff --git a/dialogs/preview/textpreview.h b/ui/preview/text.h similarity index 93% rename from dialogs/preview/textpreview.h rename to ui/preview/text.h index ca44823..c8a5016 100644 --- a/dialogs/preview/textpreview.h +++ b/ui/preview/text.h @@ -1,7 +1,7 @@ #ifndef TEXTPREVIEW_H #define TEXTPREVIEW_H -#include "filepreview.h" +#include "file.h" class TextPreview : public FilePreview { public: diff --git a/dialogs/preview/worksheetpreview.cpp b/ui/preview/worksheet.cpp similarity index 99% rename from dialogs/preview/worksheetpreview.cpp rename to ui/preview/worksheet.cpp index 965a163..ff8e45b 100644 --- a/dialogs/preview/worksheetpreview.cpp +++ b/ui/preview/worksheet.cpp @@ -1,6 +1,6 @@ -#include "worksheetpreview.h" +#include "worksheet.h" -#include "omf/omf_record.h" +#include "omf_record.h" #include #include diff --git a/dialogs/preview/worksheetpreview.h b/ui/preview/worksheet.h similarity index 93% rename from dialogs/preview/worksheetpreview.h rename to ui/preview/worksheet.h index 0ccab97..78b92f4 100644 --- a/dialogs/preview/worksheetpreview.h +++ b/ui/preview/worksheet.h @@ -1,7 +1,7 @@ #ifndef WORKSHEETPREVIEW_H #define WORKSHEETPREVIEW_H -#include "filepreview.h" +#include "file.h" class WorksheetPreview : public FilePreview { public: From d44eb7e411506f3e41c4bb39649234c11c92b4b4 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Thu, 30 Jul 2026 01:24:05 +0300 Subject: [PATCH 29/36] new image new dialog ui --- CMakeLists.txt | 2 + common/mbr.cpp | 47 ++++++ common/mbr.h | 9 + ui/dialogs/imagewizard.cpp | 330 +++++++++++++++++++++++++++++++++++++ ui/dialogs/imagewizard.h | 46 ++++++ ui/filepanelwidget.cpp | 7 +- ui/filepanelwidget.h | 10 +- ui/mainwindow.cpp | 108 ++++++++---- ui/mainwindow.h | 3 + 9 files changed, 520 insertions(+), 42 deletions(-) create mode 100644 ui/dialogs/imagewizard.cpp create mode 100644 ui/dialogs/imagewizard.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f194d1..9e9a252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,8 @@ add_executable(GRiDISKCOM ${GUI_TYPE} ui/dialogs/customdisk.cpp ui/dialogs/customdisk.h ui/dialogs/customdisk.ui + ui/dialogs/imagewizard.cpp + ui/dialogs/imagewizard.h ui/dialogs/date.cpp ui/dialogs/date.h ui/dialogs/date.ui diff --git a/common/mbr.cpp b/common/mbr.cpp index 2de33dc..a3fc26c 100644 --- a/common/mbr.cpp +++ b/common/mbr.cpp @@ -1,5 +1,52 @@ #include "mbr.h" +#include + +namespace { + +constexpr size_t kMbrSectorSize = 512; +constexpr size_t kMbrPartitionTableOffset = 0x1BE; +constexpr uint8_t kGridPartitionType = 0x47; + +void writeLittleEndian32(uint8_t* destination, uint32_t value) { + for (int byte = 0; byte < 4; ++byte) { + destination[byte] = uint8_t(value >> (byte * 8)); + } +} + +} // namespace + +std::optional buildMbrImage(const std::vector>& partitions) { + if (partitions.empty() || partitions.size() > 4) return std::nullopt; + + size_t totalPartitionBytes = 0; + for (const auto& partition : partitions) { + if (partition.empty() || partition.size() % kMbrSectorSize != 0) return std::nullopt; + totalPartitionBytes += partition.size(); + } + + MbrImage image; + image.data.resize(kMbrSectorSize + totalPartitionBytes, 0); + size_t offset = kMbrSectorSize; + for (size_t index = 0; index < partitions.size(); ++index) { + const auto& partition = partitions[index]; + std::copy(partition.begin(), partition.end(), image.data.begin() + offset); + + uint8_t* entry = image.data.data() + kMbrPartitionTableOffset + index * 16; + entry[0] = index == 0 ? 0x80 : 0x00; + entry[4] = kGridPartitionType; + writeLittleEndian32(entry + 8, uint32_t(offset / kMbrSectorSize)); + writeLittleEndian32(entry + 12, uint32_t(partition.size() / kMbrSectorSize)); + if (index == 0) { + image.firstPartition = {0, true, true, offset, partition.size()}; + } + offset += partition.size(); + } + image.data[0x1FE] = 0x55; + image.data[0x1FF] = 0xAA; + return image; +} + bool isMbrDisk(const uint8_t* data, size_t size) { return size > 0x200 && data[0x1FE] == 0x55 && data[0x1FF] == 0xAA; } diff --git a/common/mbr.h b/common/mbr.h index e6f4ae2..43ac63a 100644 --- a/common/mbr.h +++ b/common/mbr.h @@ -2,6 +2,7 @@ #define MBR_H #include +#include #include struct MbrPartition { @@ -12,6 +13,14 @@ struct MbrPartition { uint64_t size; }; +struct MbrImage { + std::vector data; + MbrPartition firstPartition; +}; + +// Creates a GRiD-OS MBR image from up to four sector-aligned partition images. +std::optional buildMbrImage(const std::vector>& partitions); + bool isMbrDisk(const uint8_t* data, size_t size); std::vector parseMbr(const uint8_t* data, size_t size); diff --git a/ui/dialogs/imagewizard.cpp b/ui/dialogs/imagewizard.cpp new file mode 100644 index 0000000..e5877fd --- /dev/null +++ b/ui/dialogs/imagewizard.cpp @@ -0,0 +1,330 @@ +#include "imagewizard.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int kMaxHardDiskMiB = 30; +constexpr int kMaxPartitions = 4; + +void configureWizardPage(QWizardPage* page) { + page->setContentsMargins(0, 0, 0, 0); +} + +QIcon plusIcon(const QPalette& palette) { + QPixmap pixmap(16, 16); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + QPen pen(palette.color(QPalette::ButtonText)); + pen.setWidth(2); + pen.setCapStyle(Qt::RoundCap); + painter.setPen(pen); + painter.drawLine(8, 3, 8, 13); + painter.drawLine(3, 8, 13, 8); + return QIcon(pixmap); +} + +void addPageHeader(QVBoxLayout* layout, const QString& title, const QString& description) { + auto* titleLabel = new QLabel(title); + QFont titleFont = titleLabel->font(); + titleFont.setPointSize(12); + titleFont.setBold(true); + titleLabel->setFont(titleFont); + + auto* descriptionLabel = new QLabel(description); + descriptionLabel->setWordWrap(true); + + auto* separator = new QFrame; + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + + layout->addWidget(titleLabel); + layout->addWidget(descriptionLabel); + layout->addWidget(separator); + layout->addSpacing(6); +} + +class MediaPage : public QWizardPage { +public: + explicit MediaPage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "Image type", "Choose the storage medium for the new GRiD-OS image."); + auto* bubble = new QRadioButton("Bubble memory", this); + auto* floppy = new QRadioButton("Floppy disk", this); + auto* hardDisk = new QRadioButton("Hard disk", this); + bubble->setChecked(true); + layout->addWidget(bubble); + layout->addWidget(floppy); + layout->addWidget(hardDisk); + layout->addStretch(); + + m_buttons.addButton(bubble, int(ImageCreationWizard::ImageKind::Bubble)); + m_buttons.addButton(floppy, int(ImageCreationWizard::ImageKind::Floppy)); + m_buttons.addButton(hardDisk, int(ImageCreationWizard::ImageKind::HardDisk)); + } + + int nextId() const override { + switch (ImageCreationWizard::ImageKind(m_buttons.checkedId())) { + case ImageCreationWizard::ImageKind::Bubble: return ImageCreationWizard::LabelPage; + case ImageCreationWizard::ImageKind::Floppy: return ImageCreationWizard::FloppyPage; + case ImageCreationWizard::ImageKind::HardDisk: return ImageCreationWizard::HardDiskLayoutPage; + } + return -1; + } + + ImageCreationWizard::ImageKind imageKind() const { + return ImageCreationWizard::ImageKind(m_buttons.checkedId()); + } + +private: + QButtonGroup m_buttons; +}; + +class FloppyPage : public QWizardPage { +public: + explicit FloppyPage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "Floppy type", "Choose the target floppy format."); + auto* compass = new QRadioButton("Compass floppy - 360 KB", this); + auto* gridCase = new QRadioButton("GRiDCASE floppy - 720 KB", this); + compass->setChecked(true); + layout->addWidget(compass); + layout->addWidget(gridCase); + layout->addStretch(); + + m_buttons.addButton(compass, int(ImageCreationWizard::FloppyKind::Compass360K)); + m_buttons.addButton(gridCase, int(ImageCreationWizard::FloppyKind::GridCase720K)); + } + + int nextId() const override { return ImageCreationWizard::LabelPage; } + + ImageCreationWizard::FloppyKind floppyKind() const { + return ImageCreationWizard::FloppyKind(m_buttons.checkedId()); + } + +private: + QButtonGroup m_buttons; +}; + +class HardDiskLayoutPage : public QWizardPage { +public: + explicit HardDiskLayoutPage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "Hard disk layout", + "Choose a single GRiD-OS disk or an MBR disk with up to four GRiD partitions."); + auto* single = new QRadioButton("Single GRiD-OS disk", this); + auto* mbr = new QRadioButton("MBR disk with GRiD partitions", this); + single->setChecked(true); + layout->addWidget(single); + layout->addWidget(mbr); + layout->addStretch(); + + m_buttons.addButton(single, 0); + m_buttons.addButton(mbr, 1); + } + + int nextId() const override { + return m_buttons.checkedId() == 1 ? ImageCreationWizard::PartitionPage + : ImageCreationWizard::HardDiskSizePage; + } + + bool useMbr() const { return m_buttons.checkedId() == 1; } + +private: + QButtonGroup m_buttons; +}; + +class HardDiskSizePage : public QWizardPage { +public: + explicit HardDiskSizePage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "Hard disk size", + "Choose the size of the single GRiD-OS disk. The maximum is 30 MiB."); + auto* form = new QFormLayout; + form->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_size.setRange(1, kMaxHardDiskMiB); + m_size.setValue(10); + m_size.setSuffix(" MiB"); + form->addRow("Disk size:", &m_size); + layout->addLayout(form); + layout->addStretch(); + } + + int nextId() const override { return ImageCreationWizard::LabelPage; } + int sizeMiB() const { return m_size.value(); } + +private: + QSpinBox m_size; +}; + +class PartitionPage : public QWizardPage { +public: + explicit PartitionPage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "MBR partitions", + "Set the label and size of each GRiD partition. At least one partition is required. Each can be at most 30 MiB."); + m_table.setColumnCount(3); + m_table.setRowCount(0); + m_table.setHorizontalHeaderLabels({"#", "Label", "Size (MiB)"}); + m_table.verticalHeader()->setVisible(false); + m_table.horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_table.horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table.horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_table.setSelectionMode(QAbstractItemView::NoSelection); + m_table.setEditTriggers(QAbstractItemView::NoEditTriggers); + + addPartitionRow(); + layout->addWidget(&m_table); + + auto* actions = new QHBoxLayout; + actions->addStretch(); + m_addButton.setIcon(plusIcon(m_addButton.palette())); + m_addButton.setText("Add partition"); + m_addButton.setEnabled(m_table.rowCount() < kMaxPartitions); + actions->addWidget(&m_addButton); + layout->addLayout(actions); + connect(&m_addButton, &QPushButton::clicked, this, [this] { addPartitionRow(); }); + } + + int nextId() const override { return -1; } + + QVector partitionSizesMiB() const { + QVector sizes; + sizes.reserve(m_table.rowCount()); + for (int row = 0; row < m_table.rowCount(); ++row) { + sizes.append(sizeAt(row)); + } + return sizes; + } + + QVector partitionLabels() const { + QVector labels; + labels.reserve(m_table.rowCount()); + for (int row = 0; row < m_table.rowCount(); ++row) { + labels.append(static_cast(m_table.cellWidget(row, 1))->text()); + } + return labels; + } + +private: + void addPartitionRow() { + const int row = m_table.rowCount(); + if (row >= kMaxPartitions) return; + + m_table.insertRow(row); + auto* number = new QTableWidgetItem(QString::number(row + 1)); + number->setTextAlignment(Qt::AlignCenter); + number->setFlags(number->flags() & ~Qt::ItemIsEditable); + m_table.setItem(row, 0, number); + + auto* label = new QLineEdit(&m_table); + label->setPlaceholderText("No label"); + label->setMaxLength(40); + label->setValidator(new QRegularExpressionValidator( + QRegularExpression(QStringLiteral("[\x00-\xFF]*")), label)); + m_table.setCellWidget(row, 1, label); + + auto* size = new QSpinBox(&m_table); + size->setRange(1, kMaxHardDiskMiB); + size->setValue(10); + m_table.setCellWidget(row, 2, size); + m_addButton.setEnabled(m_table.rowCount() < kMaxPartitions); + } + + int sizeAt(int row) const { + return static_cast(m_table.cellWidget(row, 2))->value(); + } + + QTableWidget m_table; + QPushButton m_addButton; +}; + + + +class LabelPage : public QWizardPage { +public: + explicit LabelPage(QWidget* parent = nullptr) : QWizardPage(parent) { + configureWizardPage(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + addPageHeader(layout, "Disk label", "Optionally enter a label for the image."); + layout->setSpacing(6); + + auto* label = new QLabel("Label:", this); + m_label.setMaxLength(40); + m_label.setValidator(new QRegularExpressionValidator( + QRegularExpression(QStringLiteral("[\x00-\xFF]*")), &m_label)); + layout->addWidget(label); + layout->addWidget(&m_label); + layout->addStretch(); + } + + QString label() const { return m_label.text(); } + +private: + QLineEdit m_label; +}; + +} // namespace + +ImageCreationWizard::ImageCreationWizard(QWidget* parent) : QWizard(parent) { + setWindowTitle("Create GRiD-OS image"); + setOption(QWizard::NoBackButtonOnStartPage, true); + setOption(QWizard::IgnoreSubTitles, true); + setPage(MediaPage, new ::MediaPage(this)); + setPage(FloppyPage, new ::FloppyPage(this)); + setPage(HardDiskLayoutPage, new ::HardDiskLayoutPage(this)); + setPage(HardDiskSizePage, new ::HardDiskSizePage(this)); + setPage(PartitionPage, new ::PartitionPage(this)); + setPage(LabelPage, new ::LabelPage(this)); + setStartId(MediaPage); +} + +ImageCreationWizard::Result ImageCreationWizard::result() const { + Result value; + value.kind = static_cast(page(MediaPage))->imageKind(); + value.floppyKind = static_cast(page(FloppyPage))->floppyKind(); + value.useMbr = static_cast(page(HardDiskLayoutPage))->useMbr(); + value.sizeMiB = static_cast(page(HardDiskSizePage))->sizeMiB(); + value.partitionSizesMiB = static_cast(page(PartitionPage))->partitionSizesMiB(); + if (value.kind == ImageKind::HardDisk && value.useMbr) { + value.partitionLabels = static_cast(page(PartitionPage))->partitionLabels(); + } else { + value.label = static_cast(page(LabelPage))->label(); + } + return value; +} diff --git a/ui/dialogs/imagewizard.h b/ui/dialogs/imagewizard.h new file mode 100644 index 0000000..363ad4b --- /dev/null +++ b/ui/dialogs/imagewizard.h @@ -0,0 +1,46 @@ +#ifndef IMAGECREATIONWIZARD_H +#define IMAGECREATIONWIZARD_H + +#include +#include + +class ImageCreationWizard : public QWizard { + Q_OBJECT + +public: + enum class ImageKind { + Bubble, + Floppy, + HardDisk, + }; + + enum class FloppyKind { + Compass360K, + GridCase720K, + }; + + struct Result { + ImageKind kind = ImageKind::Bubble; + FloppyKind floppyKind = FloppyKind::Compass360K; + bool useMbr = false; + int sizeMiB = 10; + QVector partitionSizesMiB; + QVector partitionLabels; + QString label; + }; + + enum PageId { + MediaPage, + FloppyPage, + HardDiskLayoutPage, + HardDiskSizePage, + PartitionPage, + LabelPage, + }; + + explicit ImageCreationWizard(QWidget* parent = nullptr); + + Result result() const; +}; + +#endif // IMAGECREATIONWIZARD_H diff --git a/ui/filepanelwidget.cpp b/ui/filepanelwidget.cpp index a53bd0e..e098d97 100644 --- a/ui/filepanelwidget.cpp +++ b/ui/filepanelwidget.cpp @@ -208,7 +208,7 @@ void FilePanelWidget::setFiles(const QVector& files, bool inSubd void FilePanelWidget::rebuildTable() { m_table->setRowCount(0); - if (m_files.isEmpty()) { + if (m_files.isEmpty() && !m_diskPresent) { m_stack->setCurrentWidget(m_emptyLabel); updateEmptyIcon(); return; @@ -278,10 +278,7 @@ void FilePanelWidget::setDiskPresent(bool present) { } void FilePanelWidget::updateEmptyIcon() { - // Folder icon when a disk is loaded (even if its root is empty), drive - // icon when no disk is present at all. - const QStyle::StandardPixmap sp = m_diskPresent ? QStyle::SP_DirIcon : QStyle::SP_DriveFDIcon; - const QIcon icon = QApplication::style()->standardIcon(sp); + const QIcon icon = QApplication::style()->standardIcon(QStyle::SP_DriveFDIcon); m_emptyLabel->setPixmap(icon.pixmap(64, 64)); } diff --git a/ui/filepanelwidget.h b/ui/filepanelwidget.h index 7507c64..c148723 100644 --- a/ui/filepanelwidget.h +++ b/ui/filepanelwidget.h @@ -22,8 +22,9 @@ struct PanelFileEntry { QDate expirationDate; }; -// Reusable file-list panel: a titled table (or an icon placeholder when empty) -// plus a status line. Pure UI -- the host feeds entries and drives selection. +// Reusable file-list panel: a titled table for loaded disks and a drive-icon +// placeholder before an image is opened. Pure UI -- the host feeds entries and +// drives selection. class FilePanelWidget : public QWidget { Q_OBJECT public: @@ -34,9 +35,8 @@ class FilePanelWidget : public QWidget { void setTitle(const QString& title); // Hidden when empty. A non-empty value also implies "a disk is loaded". void setStatusText(const QString& text); - // Tells the panel whether a disk image is loaded, so the empty-state icon - // (folder vs drive) and the click-to-open behaviour are correct even for a - // disk whose root happens to have no files. + // Tells the panel whether a disk image is loaded, so an empty loaded disk + // still shows the file table while the unopened panel shows a drive icon. void setDiskPresent(bool present); // Per-directory view state, restored by the host after setFiles(). diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 3ecd114..2cf7450 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -1571,47 +1571,91 @@ void MainWindow::MakeDir(){ } void MainWindow::NewImage(){ - if (panels[active_panel]) - if (!CloseImg()) return; + ImageCreationWizard wizard(this); + if (wizard.exec() != QDialog::Accepted) return; - CustomDiskDlg dlg(this); + const ImageCreationWizard::Result settings = wizard.result(); + if (panels[active_panel] && !CloseImg()) return; - while (true){ - if (dlg.exec() == 1) { - uint16_t sect, subl, isize; - QString labl; - dlg.GetParams(§, &subl, nullptr, &isize, &labl); + if (settings.kind == ImageCreationWizard::ImageKind::HardDisk && settings.useMbr) { + createMbrImage(settings); + } else { + createMonolithicImage(settings); + } +} - if (labl != "" && validString(labl, false, this) == -1) { - continue; - } +void MainWindow::createMbrImage(const ImageCreationWizard::Result& settings) { + std::vector> partitions; + partitions.reserve(settings.partitionSizesMiB.size()); + for (int index = 0; index < settings.partitionSizesMiB.size(); ++index) { + const size_t partitionBytes = size_t(settings.partitionSizesMiB[index]) * 1024 * 1024; + ccos_disk_t* partition = nullptr; + if (ccos_new_disk_image(CCOS_DISK_FORMAT_COMPASS, partitionBytes, &partition) != 0 || partition == nullptr) { + QMessageBox::critical(this, "Creation error", "Program can't create a new MBR partition!"); + return; + } + if (!settings.partitionLabels.value(index).isEmpty()) { + ccos_set_disk_label(partition, settings.partitionLabels[index].toStdString().c_str()); + } + partitions.emplace_back(ccos_disk_data(partition), ccos_disk_data(partition) + partitionBytes); + ccos_disk_free(partition); + } - panels[active_panel].emplace(); - auto& panel = *panels[active_panel]; + std::optional image = buildMbrImage(partitions); + if (!image.has_value()) { + QMessageBox::critical(this, "Creation error", "Program can't create a new MBR image!"); + return; + } - disk_format_t format = sect == 256 ? CCOS_DISK_FORMAT_BUBMEM : CCOS_DISK_FORMAT_COMPASS; + auto hddData = std::make_shared>(std::move(image->data)); + ccos_disk_t* disk = tryOpenMbrPartition(hddData->data(), image->firstPartition); + if (disk == nullptr) { + QMessageBox::critical(this, "Creation error", "Program can't open the newly created MBR partition!"); + return; + } - if (ccos_new_disk_image(format, isize * 1024, &panel.disk) != 0) { - QMessageBox::critical(this, "Creation error", - "Program can't create new image!"); - panels[active_panel].reset(); - return; - } + panels[active_panel].emplace(); + auto& panel = *panels[active_panel]; + panel.disk = disk; + panel.hdd_mode = true; + panel.hdd_data = std::move(hddData); + panel.hdd_partition = 0; + panel.modified = true; + panel.current_dir = ccos_get_root_dir(disk); + fillTable(active_panel, panel.current_dir, false); + HDDMenu(true); +} - if (labl != "") { - ccos_set_disk_label(panel.disk, labl.toStdString().c_str()); - } +void MainWindow::createMonolithicImage(const ImageCreationWizard::Result& settings) { + disk_format_t format = CCOS_DISK_FORMAT_COMPASS; + size_t imageSize = 0; + if (settings.kind == ImageCreationWizard::ImageKind::Bubble) { + format = CCOS_DISK_FORMAT_BUBMEM; + imageSize = 384 * 1024; + } else if (settings.kind == ImageCreationWizard::ImageKind::Floppy) { + format = settings.floppyKind == ImageCreationWizard::FloppyKind::GridCase720K + ? CCOS_DISK_FORMAT_GRIDCASE : CCOS_DISK_FORMAT_COMPASS; + imageSize = settings.floppyKind == ImageCreationWizard::FloppyKind::GridCase720K + ? 720 * 1024 : 360 * 1024; + } else { + imageSize = size_t(settings.sizeMiB) * 1024 * 1024; + } - panel.modified = true; - ccos_inode_t* root = ccos_get_root_dir(panel.disk); - panel.current_dir = root; - fillTable(active_panel, root, false); - break; - } - else{ - break; - } + ccos_disk_t* disk = nullptr; + if (ccos_new_disk_image(format, imageSize, &disk) != 0 || disk == nullptr) { + QMessageBox::critical(this, "Creation error", "Program can't create new image!"); + return; } + if (!settings.label.isEmpty()) { + ccos_set_disk_label(disk, settings.label.toStdString().c_str()); + } + + panels[active_panel].emplace(); + auto& panel = *panels[active_panel]; + panel.disk = disk; + panel.modified = true; + panel.current_dir = ccos_get_root_dir(disk); + fillTable(active_panel, panel.current_dir, false); } diff --git a/ui/mainwindow.h b/ui/mainwindow.h index c9c3824..e960af2 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -12,6 +12,7 @@ #include "filepanelwidget.h" #include "dialogs/about.h" #include "dialogs/customdisk.h" +#include "dialogs/imagewizard.h" #include "dialogs/partition.h" #include "dialogs/date.h" #include "dialogs/rename.h" @@ -140,6 +141,8 @@ public slots: // Standard image opening pipeline (bootsector/size/MBR/custom detection). // Used by LoadImg after optional IMD->img conversion. void loadImgStandard(QString path); + void createMonolithicImage(const ImageCreationWizard::Result& settings); + void createMbrImage(const ImageCreationWizard::Result& settings); void fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot); From bcca060a9c47c66d00e30c243c83ec1e32b63d0a Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Thu, 30 Jul 2026 02:03:18 +0300 Subject: [PATCH 30/36] cleanup about dialog --- ui/dialogs/about.ui | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/dialogs/about.ui b/ui/dialogs/about.ui index 5aa8039..55a93dd 100644 --- a/ui/dialogs/about.ui +++ b/ui/dialogs/about.ui @@ -76,7 +76,7 @@
- <html><head/><body><p align="center"><span style=" font-size:20pt; color:#000000;">GRiDISK<br/>Commander</span></p></body></html> + <html><head/><body><p align="center"><span style=" font-size:20pt;">GRiDISK<br/>Commander</span></p></body></html>
@@ -186,7 +186,7 @@ - <html><head/><body><p><span style=" font-weight:600;">GRiDISK Commander</span> - Simple double-panel file manager for GRiD disks, which is written on C++ and Qt. Based on BOOtak's &quot;<a href="https://github.com/BOOtak/CCOS-disk-utils"><span style=" text-decoration: underline; color:#0000ff;">ccos_image</span></a>&quot; C library.</p><p>It can interact with content in disks, remove files and directories, adds new from other image or PC filesystem and dump files.</p><p>We know, that this program needs only for small number of users, but it will seriously simplify data exchange for GRiD Compass laptop and modern IBM-PC device.</p></body></html> + <html><head/><body><p>GRiDISK Commander is a dual-pane file manager for GRiD-OS disk images.</p></body></html> true @@ -234,7 +234,7 @@ - <html><head/><body><p><a href="http://bs0dd.net"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">Bs(/)Dd</span></a> - Commander coding, interface design, beginner in C++ and Qt</p></body></html> + <html><head/><body><p><a href="http://bs0dd.net"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">Bs0dd</span></a> — project founder</p></body></html> @@ -247,7 +247,7 @@ - <html><head/><body><p><a href="https://www.youtube.com/@kogdazjasdohnu"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">BOOtak</span></a> - ccos_image coding, first testing, owner of real GRiD Compass</p></body></html> + <html><head/><body><p><a href="https://www.youtube.com/@kogdazjasdohnu"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">Bootak</span></a> — author of the first GRiD-OS disk library</p></body></html> @@ -260,7 +260,7 @@ - <html><head/><body><p><a href="https://github.com/vklachkov"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">Vklachkov</span></a> - ccos_image improvement, bubble memory, another GRiD owner</p></body></html> + <html><head/><body><p><a href="https://github.com/vklachkov"><span style=" font-weight:700; text-decoration: underline; color:#2980b9;">vklachkov</span></a> — current project maintainer</p></body></html> From c98cb78a677cd145179618d006cc4b3dcadba1e9 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 19:31:59 +0300 Subject: [PATCH 31/36] redesign custom disk dialog, add hdd superblock/bitmap values --- CMakeLists.txt | 1 + common/diskconstants.h | 18 +++++ ui/dialogs/customdisk.cpp | 22 +++--- ui/dialogs/customdisk.ui | 138 +++++++++----------------------------- ui/mainwindow.cpp | 19 +----- 5 files changed, 64 insertions(+), 134 deletions(-) create mode 100644 common/diskconstants.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e9a252..a4b1191 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(GRiDISKCOM ${GUI_TYPE} main.cpp common/imd2raw.c common/imd2raw.h + common/diskconstants.h common/mbr.cpp common/mbr.h common/omf_record.cpp diff --git a/common/diskconstants.h b/common/diskconstants.h new file mode 100644 index 0000000..8023940 --- /dev/null +++ b/common/diskconstants.h @@ -0,0 +1,18 @@ +#ifndef DISKCONSTANTS_H +#define DISKCONSTANTS_H + +#include + +constexpr uint16_t GRID_BUBBLE_SECTOR_SIZE = 256; +constexpr uint16_t GRID_BUBBLE_SUPERBLOCK_FID = 0x3FE; +constexpr uint16_t GRID_BUBBLE_BITMAP_FID = 0x3FD; + +constexpr uint16_t GRID_FLOPPY_SECTOR_SIZE = 512; +constexpr uint16_t GRID_FLOPPY_SUPERBLOCK_FID = 0x121; +constexpr uint16_t GRID_FLOPPY_BITMAP_FID = 0x120; + +constexpr uint16_t GRID_HDD_SECTOR_SIZE = 512; +constexpr uint16_t GRID_HDD_SUPERBLOCK_FID = 0x2420; +constexpr uint16_t GRID_HDD_BITMAP_FID = 0x2400; + +#endif // DISKCONSTANTS_H diff --git a/ui/dialogs/customdisk.cpp b/ui/dialogs/customdisk.cpp index ceaa2cf..343662e 100644 --- a/ui/dialogs/customdisk.cpp +++ b/ui/dialogs/customdisk.cpp @@ -1,4 +1,5 @@ #include "customdisk.h" +#include "diskconstants.h" #include "ui_customdisk.h" CustomDiskDlg::CustomDiskDlg(QWidget *parent, bool openMode) : @@ -18,9 +19,8 @@ CustomDiskDlg::CustomDiskDlg(QWidget *parent, bool openMode) : if (openMode) { ui->groupBox_3->setVisible(false); ui->groupBox_4->setVisible(false); - CustomDiskDlg::setWindowTitle("Custom image opening"); - CustomDiskDlg::setMinimumHeight(380); - CustomDiskDlg::setMaximumHeight(380); + setWindowTitle("Open Custom Disk Image"); + setFixedHeight(sizeHint().height()); } } @@ -53,10 +53,13 @@ void CustomDiskDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uin *sect = ui->radioButton->isChecked() ? 512 : 256; if (ui->radioButton_3->isChecked()) { - *subl = 0x121; + *subl = GRID_FLOPPY_SUPERBLOCK_FID; } else if (ui->radioButton_4->isChecked()) { - *subl = 0x3FE; + *subl = GRID_BUBBLE_SUPERBLOCK_FID; + } + else if (ui->radioButton_13->isChecked()) { + *subl = GRID_HDD_SUPERBLOCK_FID; } else { *subl = ui->spinBox->value(); @@ -64,10 +67,13 @@ void CustomDiskDlg::GetParams(uint16_t* sect, uint16_t* subl, uint16_t* bmp, uin if (bmp != NULL) { if (ui->radioButton_10->isChecked()) { - *bmp = 0x120; + *bmp = GRID_FLOPPY_BITMAP_FID; } else if (ui->radioButton_11->isChecked()) { - *bmp = 0x3FD; + *bmp = GRID_BUBBLE_BITMAP_FID; + } + else if (ui->radioButton_14->isChecked()) { + *bmp = GRID_HDD_BITMAP_FID; } else { *bmp = ui->spinBox_3->value(); @@ -99,5 +105,3 @@ CustomDiskDlg::~CustomDiskDlg() { delete ui; } - - diff --git a/ui/dialogs/customdisk.ui b/ui/dialogs/customdisk.ui index f56e36a..3475ca3 100644 --- a/ui/dialogs/customdisk.ui +++ b/ui/dialogs/customdisk.ui @@ -64,25 +64,13 @@ - - - - Qt::Vertical - - - - 20 - 20 - - - - + 0 - 114 + 94 @@ -91,7 +79,7 @@ - 200 + 20 30 141 23 @@ -104,7 +92,7 @@ - 120 + 200 60 41 23 @@ -117,48 +105,40 @@ - 20 + 200 30 151 23 - 0x121 (Floppy / HDD) + 0x121 (Floppy) true - + - 2 - 90 - 380 - 20 + 20 + 60 + 151 + 23 - - - true - false - - - Supported superblock values: 0x0001 - 0xFFFF - - - Qt::AlignCenter + 0x2420 (HDD) + false - 160 + 240 58 61 26 @@ -179,25 +159,13 @@ - - - - Qt::Vertical - - - - 20 - 20 - - - - + 0 - 114 + 94 @@ -206,14 +174,14 @@ - 20 + 200 30 171 23 - 0x120 (Floppy / HDD) + 0x120 (Floppy) true @@ -222,7 +190,7 @@ - 200 + 20 30 141 23 @@ -235,7 +203,7 @@ - 120 + 200 60 41 23 @@ -251,7 +219,7 @@ - 160 + 240 58 61 26 @@ -270,43 +238,23 @@ 16 - + - 2 - 90 - 380 - 20 + 20 + 60 + 151 + 23 - - - true - false - - - Supported bitmap values: 0x0001 - 0xFFFF - - - Qt::AlignCenter + 0x2400 (HDD) + - - - - Qt::Vertical - - - - 20 - 20 - - - - + @@ -418,19 +366,7 @@ - - - - Qt::Vertical - - - - 20 - 20 - - - - + @@ -454,19 +390,7 @@ - - - - Qt::Vertical - - - - 20 - 20 - - - - + diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 2cf7450..eecb8be 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "preview/dialog.h" +#include "diskconstants.h" #include "mbr.h" #include "imd2raw.h" @@ -9,24 +10,6 @@ #include #include -// GRiD disk/firmware "magic" constants for geometry/superblock/bitmap - -// Extracted from bubble image. -#define GRID_BUBBLE_SECTOR_SIZE 256 -#define GRID_BUBBLE_SUPERBLOCK_FID 0x3FE -#define GRID_BUBBLE_ALT_SUPERBLOCK_FID 0x121 -#define GRID_BUBBLE_BITMAP_FID 0x3FD -#define GRID_BUBBLE_ALT_BITMAP_FID 0x120 - -// Extracted from 2102 firmware. -#define GRID_FLOPPY_SECTOR_SIZE 512 -#define GRID_FLOPPY_SUPERBLOCK_FID 0x121 -#define GRID_FLOPPY_BITMAP_FID 0x120 - -// Extracted from 2101 firmware. -#define GRID_HDD_SECTOR_SIZE 512 -#define GRID_HDD_SUPERBLOCK_FID 0x2420 -#define GRID_HDD_BITMAP_FID 0x2400 //[Service functions] From ecf5ee132c32b969c8b19df6074ee921b6a5f10e Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 20:07:20 +0300 Subject: [PATCH 32/36] rename ui/icon to ui/resources, store icons as svg --- CMakeLists.txt | 3 ++- ui/dialogs/about.ui | 8 ++++---- ui/dialogs/customdisk.ui | 6 +++--- ui/dialogs/date.ui | 6 +++--- ui/dialogs/imagewizard.cpp | 20 +++----------------- ui/dialogs/partition.ui | 6 +++--- ui/dialogs/rename.ui | 6 +++--- ui/dialogs/version.ui | 6 +++--- ui/icon/icon.qrc | 6 ------ ui/mainwindow.ui | 6 +++--- ui/resources/add.svg | 3 +++ ui/{icon => resources}/gridisk.png | Bin ui/{icon => resources}/gridsmall.ico | Bin ui/{icon => resources}/gridsmall.png | Bin ui/resources/icon.qrc | 9 +++++++++ ui/themedicon.h | 20 ++++++++++++++++++++ 16 files changed, 59 insertions(+), 46 deletions(-) delete mode 100644 ui/icon/icon.qrc create mode 100644 ui/resources/add.svg rename ui/{icon => resources}/gridisk.png (100%) rename ui/{icon => resources}/gridsmall.ico (100%) rename ui/{icon => resources}/gridsmall.png (100%) create mode 100644 ui/resources/icon.qrc create mode 100644 ui/themedicon.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a4b1191..dd1f43c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(GRiDISKCOM ${GUI_TYPE} ui/mainwindow.ui ui/filepanelwidget.cpp ui/filepanelwidget.h + ui/themedicon.h ui/dialogs/about.cpp ui/dialogs/about.h ui/dialogs/about.ui @@ -75,7 +76,7 @@ add_executable(GRiDISKCOM ${GUI_TYPE} ui/preview/worksheet.h ui/preview/database.cpp ui/preview/database.h - ui/icon/icon.qrc + ui/resources/icon.qrc execico.rc ) diff --git a/ui/dialogs/about.ui b/ui/dialogs/about.ui index 55a93dd..03f456a 100644 --- a/ui/dialogs/about.ui +++ b/ui/dialogs/about.ui @@ -37,8 +37,8 @@ About GRiDISK Commander - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -62,7 +62,7 @@ true - <html><head/><body><p><img src=":/icon/gridisk.png"/></p></body></html> + <html><head/><body><p><img src=":/resources/gridisk.png"/></p></body></html> @@ -322,7 +322,7 @@ - + diff --git a/ui/dialogs/customdisk.ui b/ui/dialogs/customdisk.ui index 3475ca3..9343c09 100644 --- a/ui/dialogs/customdisk.ui +++ b/ui/dialogs/customdisk.ui @@ -18,8 +18,8 @@ New image - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -404,7 +404,7 @@ - + diff --git a/ui/dialogs/date.ui b/ui/dialogs/date.ui index c6e4c96..7a56b8a 100644 --- a/ui/dialogs/date.ui +++ b/ui/dialogs/date.ui @@ -37,8 +37,8 @@ Set date - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -242,7 +242,7 @@ - + diff --git a/ui/dialogs/imagewizard.cpp b/ui/dialogs/imagewizard.cpp index e5877fd..73b7ae2 100644 --- a/ui/dialogs/imagewizard.cpp +++ b/ui/dialogs/imagewizard.cpp @@ -1,11 +1,10 @@ #include "imagewizard.h" +#include "themedicon.h" #include #include #include #include -#include -#include #include #include #include @@ -27,20 +26,7 @@ void configureWizardPage(QWizardPage* page) { page->setContentsMargins(0, 0, 0, 0); } -QIcon plusIcon(const QPalette& palette) { - QPixmap pixmap(16, 16); - pixmap.fill(Qt::transparent); - - QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing); - QPen pen(palette.color(QPalette::ButtonText)); - pen.setWidth(2); - pen.setCapStyle(Qt::RoundCap); - painter.setPen(pen); - painter.drawLine(8, 3, 8, 13); - painter.drawLine(3, 8, 13, 8); - return QIcon(pixmap); -} + void addPageHeader(QVBoxLayout* layout, const QString& title, const QString& description) { auto* titleLabel = new QLabel(title); @@ -211,7 +197,7 @@ class PartitionPage : public QWizardPage { auto* actions = new QHBoxLayout; actions->addStretch(); - m_addButton.setIcon(plusIcon(m_addButton.palette())); + m_addButton.setIcon(themedSvgIcon(":/resources/add.svg", m_addButton.palette())); m_addButton.setText("Add partition"); m_addButton.setEnabled(m_table.rowCount() < kMaxPartitions); actions->addWidget(&m_addButton); diff --git a/ui/dialogs/partition.ui b/ui/dialogs/partition.ui index d2e9ab6..c91c50a 100644 --- a/ui/dialogs/partition.ui +++ b/ui/dialogs/partition.ui @@ -25,8 +25,8 @@ Select disk partition - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -77,7 +77,7 @@ - + diff --git a/ui/dialogs/rename.ui b/ui/dialogs/rename.ui index 032ed76..d7dd97d 100644 --- a/ui/dialogs/rename.ui +++ b/ui/dialogs/rename.ui @@ -37,8 +37,8 @@ Set name - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -153,7 +153,7 @@ - + diff --git a/ui/dialogs/version.ui b/ui/dialogs/version.ui index 0725a9d..be9c216 100644 --- a/ui/dialogs/version.ui +++ b/ui/dialogs/version.ui @@ -31,8 +31,8 @@ Set version - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -234,7 +234,7 @@ - + diff --git a/ui/icon/icon.qrc b/ui/icon/icon.qrc deleted file mode 100644 index a5d1831..0000000 --- a/ui/icon/icon.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - gridisk.png - gridsmall.png - - diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index df317e0..3417eff 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -27,8 +27,8 @@ GRiDISK Commander - - :/icon/gridsmall.png:/icon/gridsmall.png + + :/resources/gridsmall.png:/resources/gridsmall.png @@ -496,7 +496,7 @@ - + diff --git a/ui/resources/add.svg b/ui/resources/add.svg new file mode 100644 index 0000000..4d40eea --- /dev/null +++ b/ui/resources/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/icon/gridisk.png b/ui/resources/gridisk.png similarity index 100% rename from ui/icon/gridisk.png rename to ui/resources/gridisk.png diff --git a/ui/icon/gridsmall.ico b/ui/resources/gridsmall.ico similarity index 100% rename from ui/icon/gridsmall.ico rename to ui/resources/gridsmall.ico diff --git a/ui/icon/gridsmall.png b/ui/resources/gridsmall.png similarity index 100% rename from ui/icon/gridsmall.png rename to ui/resources/gridsmall.png diff --git a/ui/resources/icon.qrc b/ui/resources/icon.qrc new file mode 100644 index 0000000..7bca1c4 --- /dev/null +++ b/ui/resources/icon.qrc @@ -0,0 +1,9 @@ + + + gridisk.png + gridsmall.png + add.svg + search.svg + switch-partition.svg + + diff --git a/ui/themedicon.h b/ui/themedicon.h new file mode 100644 index 0000000..cd29209 --- /dev/null +++ b/ui/themedicon.h @@ -0,0 +1,20 @@ +#ifndef THEMEDICON_H +#define THEMEDICON_H + +#include +#include +#include +#include +#include + +inline QIcon themedSvgIcon(const QString& path, const QPalette& palette) { + QPixmap pixmap(path); + if (pixmap.isNull()) return {}; + + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), palette.color(QPalette::ButtonText)); + return QIcon(pixmap); +} + +#endif // THEMEDICON_H From ec0b24993665f5e4b9dd7a22c1bb55e730b4d358 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 20:25:54 +0300 Subject: [PATCH 33/36] add search ui --- ui/filepanelwidget.cpp | 117 +++++++++++++++++++++++++++--- ui/filepanelwidget.h | 20 ++++- ui/mainwindow.cpp | 17 +++++ ui/mainwindow.h | 2 + ui/mainwindow.ui | 9 +++ ui/resources/help.svg | 5 ++ ui/resources/icon.qrc | 1 + ui/resources/search.svg | 4 + ui/resources/switch-partition.svg | 3 + 9 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 ui/resources/help.svg create mode 100644 ui/resources/search.svg create mode 100644 ui/resources/switch-partition.svg diff --git a/ui/filepanelwidget.cpp b/ui/filepanelwidget.cpp index e098d97..1bede3e 100644 --- a/ui/filepanelwidget.cpp +++ b/ui/filepanelwidget.cpp @@ -1,7 +1,9 @@ #include "filepanelwidget.h" +#include "themedicon.h" #include +#include #include #include #include @@ -10,24 +12,30 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -51,6 +59,8 @@ const ColumnSpec kColumns[kColumnCount] = { {"Expires", 100}, }; + + QString formatDate(const QDate& date) { // An unset date (e.g. ccos expiry) arrives as an invalid QDate -> blank cell. if (!date.isValid()) @@ -113,7 +123,48 @@ void FilePanelWidget::buildUi() { titleFont.setPointSize(10); titleFont.setBold(false); m_groupBox->setFont(titleFont); + m_groupBox->setTitle({}); auto* boxLayout = new QVBoxLayout(m_groupBox); + auto* titleLayout = new QHBoxLayout; + titleLayout->setContentsMargins(0, 0, 0, 0); + titleLayout->setSpacing(2); + + m_titleLabel = new QLabel(m_groupBox); + m_titleLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + titleLayout->addWidget(m_titleLabel); + + m_partitionButton = new QToolButton(m_groupBox); + m_partitionButton->setIcon(themedSvgIcon(":/resources/switch-partition.svg", m_partitionButton->palette())); + m_partitionButton->setToolTip("Switch disk partition"); + m_partitionButton->setAccessibleName("Switch disk partition"); + m_partitionButton->setVisible(false); + titleLayout->addWidget(m_partitionButton); + + m_searchButton = new QToolButton(m_groupBox); + m_searchButton->setCheckable(true); + m_searchButton->setIcon(themedSvgIcon(":/resources/search.svg", m_searchButton->palette())); + m_searchButton->setStyleSheet( + "QToolButton:checked { border: 1px solid palette(highlight); border-radius: 3px; }"); + m_searchButton->setToolTip("Search"); + m_searchButton->setAccessibleName("Search"); + m_searchButton->setVisible(false); + titleLayout->addWidget(m_searchButton); + boxLayout->addLayout(titleLayout); + + m_searchField = new QLineEdit(m_groupBox); + m_searchField->setPlaceholderText("Search files"); + m_searchField->setValidator(new QRegularExpressionValidator( + QRegularExpression(QStringLiteral("[\\x00-\\xFF]*")), m_searchField)); + m_searchHelpAction = m_searchField->addAction( + themedSvgIcon(":/resources/help.svg", m_searchField->palette()), QLineEdit::TrailingPosition); + m_searchHelpAction->setToolTip("This is help text"); + m_searchHelpAction->setVisible(false); + m_searchField->hide(); + boxLayout->addWidget(m_searchField); + + m_searchTimer = new QTimer(this); + m_searchTimer->setSingleShot(true); + m_searchTimer->setInterval(200); m_stack = new QStackedWidget(this); m_stack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -182,6 +233,10 @@ void FilePanelWidget::buildUi() { m_dropOverlay->hide(); connect(m_table, &QTableWidget::cellActivated, this, &FilePanelWidget::onCellActivated); + connect(m_partitionButton, &QToolButton::clicked, this, &FilePanelWidget::partitionSwitchRequested); + connect(m_searchButton, &QToolButton::toggled, this, &FilePanelWidget::onSearchToggled); + connect(m_searchField, &QLineEdit::textChanged, this, &FilePanelWidget::onSearchTextChanged); + connect(m_searchTimer, &QTimer::timeout, this, [this] { emit searchRequested(m_searchField->text()); }); connect(m_table, &QTableWidget::customContextMenuRequested, this, &FilePanelWidget::onContextMenuRequested); m_table->installEventFilter(this); @@ -263,7 +318,29 @@ int FilePanelWidget::verticalScrollValue() const { } void FilePanelWidget::setTitle(const QString& title) { - m_groupBox->setTitle(title); + m_titleText = title; + updateTitle(); +} + +void FilePanelWidget::setHddMode(bool enabled) { + m_partitionButton->setVisible(enabled); + updateTitle(); +} + +void FilePanelWidget::toggleSearch() { + if (m_diskPresent) + m_searchButton->toggle(); +} + +void FilePanelWidget::updateTitle() { + const QFontMetrics metrics(m_titleLabel->font()); + int buttonsWidth = 0; + if (m_partitionButton->isVisible()) + buttonsWidth += m_partitionButton->sizeHint().width(); + if (m_searchButton->isVisible()) + buttonsWidth += m_searchButton->sizeHint().width(); + const int availableWidth = qMax(0, m_groupBox->contentsRect().width() - buttonsWidth - 16); + m_titleLabel->setText(metrics.elidedText(m_titleText, Qt::ElideRight, availableWidth)); } void FilePanelWidget::setStatusText(const QString& text) { @@ -273,6 +350,16 @@ void FilePanelWidget::setStatusText(const QString& text) { void FilePanelWidget::setDiskPresent(bool present) { m_diskPresent = present; + m_searchButton->setVisible(present); + if (!present) { + m_searchButton->setChecked(false); + const QSignalBlocker blocker(m_searchField); + m_searchField->clear(); + m_searchTimer->stop(); + m_searchField->hide(); + m_searchHelpAction->setVisible(false); + } + updateTitle(); if (m_files.isEmpty()) updateEmptyIcon(); } @@ -282,13 +369,7 @@ void FilePanelWidget::updateEmptyIcon() { m_emptyLabel->setPixmap(icon.pixmap(64, 64)); } -void FilePanelWidget::setActiveTitle(bool active) { - QFont font = m_groupBox->font(); - if (font.bold() == active) - return; - font.setBold(active); - m_groupBox->setFont(font); -} + int FilePanelWidget::visualRowToFileIndex(int row) const { return m_inSubdir ? row - 1 : row; @@ -333,6 +414,22 @@ void FilePanelWidget::onCellActivated(int row, int /*column*/) { emit fileDoubleClicked(idx); } +void FilePanelWidget::onSearchTextChanged(const QString& /*text*/) { + m_searchTimer->start(); +} + +void FilePanelWidget::onSearchToggled(bool visible) { + m_searchField->setVisible(visible); + m_searchHelpAction->setVisible(visible); + if (visible) { + m_searchField->setFocus(); + emit searchRequested(m_searchField->text()); + } else { + m_searchTimer->stop(); + emit searchRequested({}); // Empty query tells the host to clear the filter. + } +} + void FilePanelWidget::onContextMenuRequested(const QPoint& pos) { QTableWidgetItem* item = m_table->itemAt(pos); if (item == nullptr) @@ -355,10 +452,7 @@ bool FilePanelWidget::eventFilter(QObject* watched, QEvent* event) { bool FilePanelWidget::handleTableEvent(QEvent* event) { if (event->type() == QEvent::FocusIn) { - setActiveTitle(true); emit panelActivated(); - } else if (event->type() == QEvent::FocusOut) { - setActiveTitle(false); } else if (event->type() == QEvent::KeyPress) { auto* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape && m_inSubdir) { @@ -452,6 +546,7 @@ void FilePanelWidget::dropEvent(QDropEvent* event) { void FilePanelWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); + updateTitle(); // The drop overlay is a layout-less child kept on top, so it has to be // pinned to the panel manually. This is the idiomatic Qt pattern for a // widget that must cover its parent without influencing its geometry. diff --git a/ui/filepanelwidget.h b/ui/filepanelwidget.h index c148723..561a199 100644 --- a/ui/filepanelwidget.h +++ b/ui/filepanelwidget.h @@ -8,7 +8,11 @@ class QGroupBox; class QLabel; +class QLineEdit; +class QAction; class QStackedWidget; +class QTimer; +class QToolButton; class QTableWidget; class RowHoverDelegate; @@ -38,6 +42,8 @@ class FilePanelWidget : public QWidget { // Tells the panel whether a disk image is loaded, so an empty loaded disk // still shows the file table while the unopened panel shows a drive icon. void setDiskPresent(bool present); + void setHddMode(bool enabled); + void toggleSearch(); // Per-directory view state, restored by the host after setFiles(). int verticalScrollValue() const; @@ -53,6 +59,8 @@ class FilePanelWidget : public QWidget { void panelActivated(); // table gained focus void urlsDropped(const QStringList& files); // folders are ignored void openRequested(); // click on empty placeholder while no disk is loaded + void partitionSwitchRequested(); + void searchRequested(const QString& query); protected: bool eventFilter(QObject* watched, QEvent* event) override; @@ -65,14 +73,17 @@ class FilePanelWidget : public QWidget { private slots: void onCellActivated(int row, int column); void onContextMenuRequested(const QPoint& pos); + void onSearchTextChanged(const QString& text); + void onSearchToggled(bool visible); private: void buildUi(); void rebuildTable(); void updateEmptyIcon(); + void updateTitle(); void setDragOver(bool on); void setHoverRow(int row); - void setActiveTitle(bool active); // toggle the title bold (focus-driven) + int visualRowToFileIndex(int row) const; // table row -> m_files index (-1 for "..") // eventFilter dispatchers, one per watched object. @@ -81,6 +92,12 @@ private slots: bool handleEmptyLabelEvent(QEvent* event); QGroupBox* m_groupBox = nullptr; + QLabel* m_titleLabel = nullptr; + QToolButton* m_partitionButton = nullptr; + QToolButton* m_searchButton = nullptr; + QLineEdit* m_searchField = nullptr; + QAction* m_searchHelpAction = nullptr; + QTimer* m_searchTimer = nullptr; QStackedWidget* m_stack = nullptr; QTableWidget* m_table = nullptr; QLabel* m_emptyLabel = nullptr; @@ -93,6 +110,7 @@ private slots: bool m_diskPresent = false; bool m_dragOver = false; int m_hoverRow = -1; + QString m_titleText; }; #endif // FILEPANELWIDGET_H diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index eecb8be..0825387 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -388,6 +388,7 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) FilePanelWidget* pw = panelWidget(panel_idx); pw->setDiskPresent(true); + pw->setHddMode(panel.hdd_mode); auto entries = buildFileEntries(panel_idx, directory); pw->setFiles(entries, panel.in_subdir); @@ -485,6 +486,9 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu connect(pw, &FilePanelWidget::urlsDropped, this, [this, i](const QStringList& files) { onUrlsDropped(i, files, {}); }); connect(pw, &FilePanelWidget::openRequested, this, [this, i]() { onOpenRequested(i); }); + connect(pw, &FilePanelWidget::partitionSwitchRequested, this, &MainWindow::AnPartMenu); + connect(pw, &FilePanelWidget::searchRequested, this, + [this, i](const QString& query) { onSearchRequested(i, query); }); } refreshActivePanelUI(); @@ -544,6 +548,7 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(std::make_uniqu connect(ui->actionRename, SIGNAL(triggered()), this, SLOT(Rename())); connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(Save())); connect(ui->actionSave_as, SIGNAL(triggered()), this, SLOT(SaveAs())); + connect(ui->actionSearch, SIGNAL(triggered()), this, SLOT(Search())); connect(ui->actionSep_save, SIGNAL(triggered()), this, SLOT(SavePart())); } @@ -736,6 +741,7 @@ void MainWindow::AnotherPart(bool fromMenu){ free(dst.disk); dst.disk = new_disk; dst.hdd_mode = true; + dst.hdd_partition = int(parts[selctd].index); dst.current_dir = root; dst.view_state.clear(); // stale keys belong to the previous disk if (!fromMenu){ @@ -778,6 +784,7 @@ int MainWindow::CloseImg(){ FilePanelWidget* pw = panelWidget(active_panel); pw->setDiskPresent(false); + pw->setHddMode(false); pw->setFiles({}, false); pw->setTitle({}); pw->setStatusText({}); @@ -1132,6 +1139,16 @@ void MainWindow::updateActionStates() { : tr("Change Disk Label...")); } +void MainWindow::Search() { + panelWidget(active_panel)->toggleSearch(); +} + +void MainWindow::onSearchRequested(int panel_idx, const QString& query) { + fprintf(stderr, "Search query for panel %d: %s\n", + panel_idx, query.toLocal8Bit().constData()); + fflush(stderr); +} + void MainWindow::onPanelActivated(int panel_idx) { active_panel = panel_idx; refreshActivePanelUI(); diff --git a/ui/mainwindow.h b/ui/mainwindow.h index e960af2..abe20ba 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -108,6 +108,7 @@ public slots: void Save(); void SaveAs(); void SavePart(); + void Search(); void SetActivePart(); void Version(); public: @@ -129,6 +130,7 @@ public slots: void onGoUpToParent(int panel_idx); void onUrlsDropped(int panel_idx, const QStringList& files, const QStringList& dirs); void onOpenRequested(int panel_idx); + void onSearchRequested(int panel_idx, const QString& query); bool isFileAlreadyOpened(const QString& path); void handleAlreadyOpenedImg(QString path); diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 3417eff..ccf400a 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -260,6 +260,7 @@ + @@ -479,6 +480,14 @@ Set Active Partition... + + + Search + + + Ctrl+F + + New Image diff --git a/ui/resources/help.svg b/ui/resources/help.svg new file mode 100644 index 0000000..61b1715 --- /dev/null +++ b/ui/resources/help.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/resources/icon.qrc b/ui/resources/icon.qrc index 7bca1c4..7c03ba0 100644 --- a/ui/resources/icon.qrc +++ b/ui/resources/icon.qrc @@ -4,6 +4,7 @@ gridsmall.png add.svg search.svg + help.svg switch-partition.svg diff --git a/ui/resources/search.svg b/ui/resources/search.svg new file mode 100644 index 0000000..e8f3b3c --- /dev/null +++ b/ui/resources/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/resources/switch-partition.svg b/ui/resources/switch-partition.svg new file mode 100644 index 0000000..bcb15b3 --- /dev/null +++ b/ui/resources/switch-partition.svg @@ -0,0 +1,3 @@ + + + From cc2aed8b80369a7415ae2108b57cd6fd7e5bded3 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 20:44:50 +0300 Subject: [PATCH 34/36] fix warning when extracting partitions with label --- ui/mainwindow.cpp | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 0825387..523d1c5 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -269,13 +269,17 @@ int dumpFileQt(ccos_disk_t* disk, ccos_inode_t* file, QString path, QWidget* par //*Dump dir from image to path int dumpDirQt(ccos_disk_t* disk, ccos_inode_t* dir, QString path, QWidget* parent){ - char name[CCOS_MAX_FILE_NAME]; - memset(name, 0, CCOS_MAX_FILE_NAME); - ccos_parse_file_name(dir, name, nullptr, nullptr, nullptr); + char name[CCOS_MAX_FILE_NAME] = {}; + if (dir->header.file_id == dir->desc.dir_file_id) { + // A root directory is named by the disk label, without a file type. + const short_string_t* label = ccos_get_file_name(dir); + memcpy(name, label->data, label->length); + } else { + ccos_parse_file_name(dir, name, nullptr, nullptr, nullptr); + } replace_char_in_place(name, '/', '_'); QString dpath = QDir(path).filePath(name); - if (!QDir(dpath).exists() && !QDir().mkdir(dpath)){ QMessageBox::critical(parent, "Failed to create directory", QString("Failed to create directory \"%1\"!").arg(name)); @@ -306,26 +310,12 @@ int dumpImgQt(ccos_disk_t* disk, QString path, QString altname, QWidget* parent) return -1; } - QString name = short_string_to_qstring(ccos_get_file_name(root_dir)); - QString dnam; - - if (name.isEmpty()){ - dnam = altname; - } - else{ - dnam = name; - dnam.replace('/', '_'); - } - - QString dpath = QDir(path).filePath(dnam); - - if (!QDir(dpath).exists() && !QDir().mkdir(dpath)){ - QMessageBox::critical(parent, "Failed to create directory", - QString("Failed to create directory \"%1\"!").arg(dnam)); - return -1; + if (ccos_get_file_name(root_dir)->length == 0) { + altname.replace('/', '_'); + return dumpDirQt(disk, root_dir, QDir(path).filePath(altname), parent); } - return dumpDirQt(disk, root_dir, dpath, parent); + return dumpDirQt(disk, root_dir, path, parent); } //*Check if string is valid for CCOS (does not contain unicode and reserved characters) From 8d29094948033d4ffdfcf1a678ba7c5c7e9c827f Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 20:59:58 +0300 Subject: [PATCH 35/36] fix segfault when opening the same mbr disk --- ui/mainwindow.cpp | 35 ++++++++++++++++++++++++----------- ui/mainwindow.h | 3 ++- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 523d1c5..83b4d59 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -664,7 +664,17 @@ void MainWindow::AnPartMenu(){ void MainWindow::AnotherPart(bool fromMenu){ const int panel_at_entry = active_panel; const int usedisk = fromMenu ? panel_at_entry : !panel_at_entry; + openAnotherPartition(panel_at_entry, usedisk); +} +void MainWindow::openAnotherPartition(int targetPanel, int sourcePanel) { + if (!panels[sourcePanel] || !panels[sourcePanel]->hdd_mode || + !panels[sourcePanel]->hdd_data) { + return; + } + + const int panel_at_entry = targetPanel; + const int usedisk = sourcePanel; auto& src = *panels[usedisk]; std::vector parts = parseMbr(src.hdd_data->data(), src.hdd_data->size()); @@ -734,7 +744,7 @@ void MainWindow::AnotherPart(bool fromMenu){ dst.hdd_partition = int(parts[selctd].index); dst.current_dir = root; dst.view_state.clear(); // stale keys belong to the previous disk - if (!fromMenu){ + if (usedisk != topan) { dst.path = src.path; dst.hdd_data = src.hdd_data; } @@ -1218,10 +1228,11 @@ bool MainWindow::isFileAlreadyOpened(const QString& path) { return panels[other_panel] && panels[other_panel]->path == path; } -void MainWindow::handleAlreadyOpenedImg(QString path) { - Q_ASSERT(isFileAlreadyOpened(path)); +void MainWindow::handleAlreadyOpenedImg(const QString& path, int targetPanel) { + const int sourcePanel = !targetPanel; + Q_ASSERT(panels[sourcePanel] && panels[sourcePanel]->path == path); - if (!panels[!active_panel]->hdd_mode) { + if (!panels[sourcePanel]->hdd_mode) { QMessageBox::critical(this, "Image already open", "This image is already open in the other panel!"); return; @@ -1231,10 +1242,9 @@ void MainWindow::handleAlreadyOpenedImg(QString path) { return; } - // AnotherPart(false) opens its own partition-selection dialog and closes - // the target panel only after the user confirms -- so cancelling the - // partition picker leaves everything untouched. - AnotherPart(false); + // Modal dialogs can change focus, and therefore active_panel. Preserve the + // source and target selected before showing them. + openAnotherPartition(targetPanel, sourcePanel); } bool MainWindow::suggestSelectAnotherPartition() { @@ -1477,13 +1487,16 @@ void MainWindow::loadImgStandard(QString path) { return; } + const int targetPanel = active_panel; + // Same image already open on this panel -- nothing to do. - if (panels[active_panel] && panels[active_panel]->path == path) + if (panels[targetPanel] && panels[targetPanel]->path == path) return; // Same image open on the other panel. - if (isFileAlreadyOpened(path)) { - handleAlreadyOpenedImg(path); + const int otherPanel = !targetPanel; + if (panels[otherPanel] && panels[otherPanel]->path == path) { + handleAlreadyOpenedImg(path, targetPanel); return; } diff --git a/ui/mainwindow.h b/ui/mainwindow.h index abe20ba..dd9ef0d 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -133,7 +133,8 @@ public slots: void onSearchRequested(int panel_idx, const QString& query); bool isFileAlreadyOpened(const QString& path); - void handleAlreadyOpenedImg(QString path); + void handleAlreadyOpenedImg(const QString& path, int targetPanel); + void openAnotherPartition(int targetPanel, int sourcePanel); bool suggestSelectAnotherPartition(); void openValidNonMbrDisk(QString path, ccos_disk_t* disk); void tryToOpenValidMbrDisk(QString path, uint8_t* data, size_t size); From f082956864095fee7bfec75ff27f1f48a19dc070 Mon Sep 17 00:00:00 2001 From: Valerii Klachkov Date: Fri, 31 Jul 2026 23:27:41 +0300 Subject: [PATCH 36/36] search --- ui/filepanelwidget.cpp | 8 +- ui/mainwindow.cpp | 191 ++++++++++++++++++++++++++++++++++++++--- ui/mainwindow.h | 3 + 3 files changed, 189 insertions(+), 13 deletions(-) diff --git a/ui/filepanelwidget.cpp b/ui/filepanelwidget.cpp index 1bede3e..121d237 100644 --- a/ui/filepanelwidget.cpp +++ b/ui/filepanelwidget.cpp @@ -157,7 +157,13 @@ void FilePanelWidget::buildUi() { QRegularExpression(QStringLiteral("[\\x00-\\xFF]*")), m_searchField)); m_searchHelpAction = m_searchField->addAction( themedSvgIcon(":/resources/help.svg", m_searchField->palette()), QLineEdit::TrailingPosition); - m_searchHelpAction->setToolTip("This is help text"); + m_searchHelpAction->setToolTip( + "Search syntax:\n" + "Use ` to separate path components.\n" + "Use * as a wildcard; matching is case-insensitive.\n" + "Without *, text matches any part of a name or type.\n" + "Example: *~Font~ searches the current folder.\n" + "Example: `*`*~Font~ searches all folders from root."); m_searchHelpAction->setVisible(false); m_searchField->hide(); boxLayout->addWidget(m_searchField); diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 83b4d59..44ba8f5 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -5,6 +5,7 @@ #include "imd2raw.h" #include +#include #include #include @@ -16,6 +17,50 @@ static ccos_disk_t* tryOpenMbrPartition(uint8_t* data, MbrPartition& partition); static QString mbrPartitionLabel(uint8_t* hdd_data, const MbrPartition& part); +namespace { +struct SearchQuery { + QStringList directoryPatterns; + QString filePattern; +}; + +SearchQuery parseSearchQuery(const QString& query) { + const QStringList parts = query.split('`', Qt::SkipEmptyParts); + if (parts.isEmpty()) + return {}; + + SearchQuery result; + result.filePattern = parts.constLast(); + result.directoryPatterns = parts.mid(0, parts.size() - 1); + return result; +} + +bool wildcardMatches(const QString& value, const QString& pattern) { + if (!pattern.contains('*')) + return value.contains(pattern, Qt::CaseInsensitive); + + QString expression = "^"; + for (const QChar character : pattern) { + expression += character == '*' ? ".*" : QRegularExpression::escape(QString(character)); + } + expression += '$'; + return QRegularExpression(expression, QRegularExpression::CaseInsensitiveOption).match(value).hasMatch(); +} + +QString inodeBaseName(const ccos_inode_t* inode) { + char name[CCOS_MAX_FILE_NAME] = {}; + if (ccos_parse_file_name(inode, name, nullptr, nullptr, nullptr) != CCOS_OK) + return {}; + return QString::fromLatin1(name); +} + +QString inodeType(const ccos_inode_t* inode) { + char type[CCOS_MAX_FILE_NAME] = {}; + if (ccos_parse_file_name(inode, nullptr, type, nullptr, nullptr) != CCOS_OK) + return {}; + return QString::fromLatin1(type); +} +} // namespace + ccos_date_t ccos_get_datetime(void) { timespec tp; clock_gettime(CLOCK_REALTIME, &tp); @@ -348,15 +393,12 @@ QVector MainWindow::buildFileEntries(int panel_idx, ccos_inode_t QVector entries; entries.reserve(fils); - char basename[CCOS_MAX_FILE_NAME]; - char type[CCOS_MAX_FILE_NAME]; for (int c = 0; c < fils; c++) { - memset(basename, 0, CCOS_MAX_FILE_NAME); - memset(type, 0, CCOS_MAX_FILE_NAME); - ccos_parse_file_name(dirdata[c], basename, type, nullptr, nullptr); + const QString name = inodeBaseName(dirdata[c]); + const QString type = inodeType(dirdata[c]); PanelFileEntry entry; - entry.name = basename; + entry.name = name; entry.type = type; entry.size = dirdata[c]->desc.file_size; entry.version = ccosGetFileVersionQstr(dirdata[c]); @@ -372,6 +414,117 @@ QVector MainWindow::buildFileEntries(int panel_idx, ccos_inode_t return entries; } +QVector MainWindow::buildSearchResults(int panel_idx, const QString& query) { + auto& panel = *panels[panel_idx]; + panel.inodes.clear(); + + const SearchQuery search = parseSearchQuery(query); + if (search.filePattern.isEmpty()) + return {}; + + struct SearchDirectory { + ccos_inode_t* inode; + QString path; + }; + + ccos_inode_t* root = ccos_get_root_dir(panel.disk); + if (root == nullptr) + return {}; + + QStringList currentPathParts; + ccos_inode_t* current = panel.current_dir; + while (current != nullptr && current->header.file_id != current->desc.dir_file_id) { + currentPathParts.prepend(inodeBaseName(current)); + current = ccos_get_parent_dir(panel.disk, current); + } + const QString currentPath = currentPathParts.join('`'); + const bool startsFromRoot = !search.directoryPatterns.isEmpty() && + search.directoryPatterns.constFirst() == "*"; + QVector directories {{startsFromRoot ? root : panel.current_dir, + startsFromRoot ? QString() : currentPath}}; + auto appendSubdirectories = [&panel](const SearchDirectory& directory, + QVector* children) { + uint16_t count = 0; + ccos_inode_t** entries = nullptr; + if (ccos_get_dir_contents(panel.disk, directory.inode, &count, &entries) != CCOS_OK) + return; + + for (int index = 0; index < count; ++index) { + ccos_inode_t* entry = entries[index]; + if (!ccos_is_dir(entry)) + continue; + const QString name = inodeBaseName(entry); + if (!name.isEmpty()) { + children->append({entry, directory.path.isEmpty() + ? name : directory.path + '`' + name}); + } + } + free(entries); + }; + + for (const QString& pattern : search.directoryPatterns) { + QVector next; + if (pattern == "*") { + // A wildcard path component searches every descendant directory, + // with the root included so root-level files are considered too. + next = directories; + for (int index = 0; index < next.size(); ++index) { + const SearchDirectory directory = next[index]; + appendSubdirectories(directory, &next); + } + } else { + for (const SearchDirectory& directory : directories) { + QVector children; + appendSubdirectories(directory, &children); + for (const SearchDirectory& child : children) { + const QString name = child.path.section('`', -1); + if (wildcardMatches(name, pattern)) + next.append(child); + } + } + } + directories = std::move(next); + if (directories.isEmpty()) + return {}; + } + + QVector results; + for (const SearchDirectory& directory : directories) { + uint16_t count = 0; + ccos_inode_t** entries = nullptr; + if (ccos_get_dir_contents(panel.disk, directory.inode, &count, &entries) != CCOS_OK) + continue; + + for (int index = 0; index < count; ++index) { + ccos_inode_t* entry = entries[index]; + const QString name = inodeBaseName(entry); + const QString type = inodeType(entry); + const QString fullName = type.isEmpty() ? name : name + '~' + type + '~'; + if (!wildcardMatches(fullName, search.filePattern)) + continue; + + PanelFileEntry result; + result.name = directory.path.isEmpty() ? name : directory.path + '`' + name; + result.type = type; + result.size = entry->desc.file_size; + result.version = ccosGetFileVersionQstr(entry); + result.creationDate = ccosDateToQDate(entry->desc.creation_date); + result.modificationDate = ccosDateToQDate(entry->desc.mod_date); + result.expirationDate = ccosDateToQDate(entry->desc.expiration_date); + panel.inodes.push_back(entry); + results.append(std::move(result)); + } + free(entries); + } + + return results; +} + +void MainWindow::showSearchResults(int panel_idx) { + auto& panel = *panels[panel_idx]; + panelWidget(panel_idx)->setFiles(buildSearchResults(panel_idx, panel.search_query), false); +} + //*Get directory listing and push it to the panel widget void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) { auto& panel = *panels[panel_idx]; @@ -379,8 +532,12 @@ void MainWindow::fillTable(int panel_idx, ccos_inode_t* directory, bool noRoot) pw->setDiskPresent(true); pw->setHddMode(panel.hdd_mode); - auto entries = buildFileEntries(panel_idx, directory); - pw->setFiles(entries, panel.in_subdir); + if (panel.search_query.isEmpty()) { + auto entries = buildFileEntries(panel_idx, directory); + pw->setFiles(entries, panel.in_subdir); + } else { + showSearchResults(panel_idx); + } updatePanelTitle(panel_idx); @@ -428,7 +585,10 @@ void MainWindow::refreshPanel(int panel_idx) { return; saveCurrentViewState(panel_idx); auto& panel = *panels[panel_idx]; - fillTable(panel_idx, panel.current_dir, panel.in_subdir); + if (panel.search_query.isEmpty()) + fillTable(panel_idx, panel.current_dir, panel.in_subdir); + else + showSearchResults(panel_idx); } void MainWindow::updatePanelTitle(int panel_idx) { @@ -1144,9 +1304,16 @@ void MainWindow::Search() { } void MainWindow::onSearchRequested(int panel_idx, const QString& query) { - fprintf(stderr, "Search query for panel %d: %s\n", - panel_idx, query.toLocal8Bit().constData()); - fflush(stderr); + if (!panels[panel_idx]) + return; + + auto& panel = *panels[panel_idx]; + panel.search_query = query; + if (query.isEmpty()) { + fillTable(panel_idx, panel.current_dir, panel.in_subdir); + } else { + showSearchResults(panel_idx); + } } void MainWindow::onPanelActivated(int panel_idx) { diff --git a/ui/mainwindow.h b/ui/mainwindow.h index dd9ef0d..47964e6 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -50,6 +50,7 @@ struct DiskPanel { bool hdd_mode = false; std::shared_ptr> hdd_data; + QString search_query; std::optional hdd_partition; // Keyed by the directory inode pointer, so going back into a previously @@ -162,5 +163,7 @@ public slots: void doPreview(int panel_idx, ccos_inode_t* file); QVector buildFileEntries(int panel_idx, ccos_inode_t* directory); + QVector buildSearchResults(int panel_idx, const QString& query); + void showSearchResults(int panel_idx); }; #endif // MAINWINDOW_H