diff --git a/krusader/Archive/krarchandler.cpp b/krusader/Archive/krarchandler.cpp index 8d862950..015c442e 100644 --- a/krusader/Archive/krarchandler.cpp +++ b/krusader/Archive/krarchandler.cpp @@ -1,744 +1,746 @@ /***************************************************************************** * Copyright (C) 2001 Shie Erlich * * Copyright (C) 2001 Rafi Yanai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "krarchandler.h" // QtCore #include #include #include // QtWidgets #include #include #include #include #include #include #include #include #include #include #include "kr7zencryptionchecker.h" #include "../krglobal.h" #include "../defaults.h" #include "../krservices.h" #include "../Dialogs/krpleasewait.h" #include "../../krArc/krlinecountingprocess.h" #if 0 class DefaultKrArcObserver : public KrArcObserver { public: DefaultKrArcObserver() {} virtual ~DefaultKrArcObserver() {} virtual void processEvents() override { usleep(1000); qApp->processEvents(); } virtual void subJobStarted(const QString & jobTitle, int count) override { krApp->startWaiting(jobTitle, count, true); } virtual void subJobStopped() override { krApp->stopWait(); } virtual bool wasCancelled() override { return krApp->wasWaitingCancelled(); } virtual void error(const QString & error) override { KMessageBox::error(krApp, error, i18n("Error")); } virtual void detailedError(const QString & error, const QString & details) override { KMessageBox::detailedError(krApp, error, details, i18n("Error")); } virtual void incrementProgress(int c) override { krApp->plzWait->incProgress(c); } }; #endif static QStringList arcProtocols = QString("tar;bzip;bzip2;lzma;xz;gzip;krarc;zip").split(';'); QMap* KrArcHandler::slaveMap = nullptr; KWallet::Wallet * KrArcHandler::wallet = nullptr; KrArcHandler::KrArcHandler(QObject *parent) : QObject(parent) { // Reminder: If a mime type is added/removed/modified in that // member function, it's important to research if the type has to // be added/removed/modified in the `krarc.protocol` file, or // in `KrArcBaseManager::getShortTypeFromMime(const QString &mime)` // Hard-code these proven mimetypes openable by krarc protocol. // They cannot be listed in krarc.protocol itself // because it would baffle other file managers (like Dolphin). krarcArchiveMimetypes = { QStringLiteral("application/x-deb"), QStringLiteral("application/x-debian-package"), QStringLiteral("application/vnd.debian.binary-package"), QStringLiteral("application/x-java-archive"), QStringLiteral("application/x-rpm"), QStringLiteral("application/x-source-rpm"), QStringLiteral("application/vnd.oasis.opendocument.chart"), QStringLiteral("application/vnd.oasis.opendocument.database"), QStringLiteral("application/vnd.oasis.opendocument.formula"), QStringLiteral("application/vnd.oasis.opendocument.graphics"), QStringLiteral("application/vnd.oasis.opendocument.presentation"), QStringLiteral("application/vnd.oasis.opendocument.spreadsheet"), QStringLiteral("application/vnd.oasis.opendocument.text"), QStringLiteral("application/vnd.openxmlformats-officedocument.presentationml.presentation"), QStringLiteral("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), QStringLiteral("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), QStringLiteral("application/x-cbz"), QStringLiteral("application/vnd.comicbook+zip"), QStringLiteral("application/x-cbr"), QStringLiteral("application/vnd.comicbook-rar"), QStringLiteral("application/epub+zip"), QStringLiteral("application/x-webarchive"), QStringLiteral("application/x-plasma"), QStringLiteral("application/vnd.rar") }; #ifdef KRARC_QUERY_ENABLED - krarcArchiveMimetypes += QSet::fromList(KProtocolInfo::archiveMimetypes("krarc")); + auto mimetypes = KProtocolInfo::archiveMimetypes("krarc"); + for (const auto &mimetype : mimetypes) + krarcArchiveMimetypes.insert(mimetype); #endif } QStringList KrArcHandler::supportedPackers() { QStringList packers; // we will simply try to find the packers here.. if (KrServices::cmdExist("tar")) packers.append("tar"); if (KrServices::cmdExist("gzip")) packers.append("gzip"); if (KrServices::cmdExist("bzip2")) packers.append("bzip2"); if (KrServices::cmdExist("lzma")) packers.append("lzma"); if (KrServices::cmdExist("xz")) packers.append("xz"); if (KrServices::cmdExist("unzip")) packers.append("unzip"); if (KrServices::cmdExist("zip")) packers.append("zip"); if (KrServices::cmdExist("zip")) packers.append("cbz"); if (KrServices::cmdExist("lha")) packers.append("lha"); if (KrServices::cmdExist("cpio")) packers.append("cpio"); if (KrServices::cmdExist("unrar")) packers.append("unrar"); if (KrServices::cmdExist("rar")) packers.append("rar"); if (KrServices::cmdExist("rar")) packers.append("cbr"); if (KrServices::cmdExist("arj")) packers.append("arj"); if (KrServices::cmdExist("unarj")) packers.append("unarj"); if (KrServices::cmdExist("unace")) packers.append("unace"); if (KrServices::cmdExist("dpkg")) packers.append("dpkg"); if (KrServices::cmdExist("7z") || KrServices::cmdExist("7za")) packers.append("7z"); if (KrServices::cmdExist("rpm") && KrServices::cmdExist("rpm2cpio")) packers.append("rpm"); // qDebug() << "Supported Packers:"; //QStringList::Iterator it; //for( it = packers.begin(); it != packers.end(); ++it ) // qDebug() << *it; return packers; } bool KrArcHandler::arcSupported(QString type) { // lst will contain the supported unpacker list... const KConfigGroup group(krConfig, "Archives"); const QStringList lst = group.readEntry("Supported Packers", QStringList()); // Let's notice that in some cases the QString `type` that arrives here // represents a mimetype, and in some other cases it represents // a short identifier. // If `type` is not a short identifier then it's supposed that `type` is a mime type if (type.length() > maxLenType) { type = getShortTypeFromMime(type); } return (type == "zip" && lst.contains("unzip")) || (type == "tar" && lst.contains("tar")) || (type == "tbz" && lst.contains("tar")) || (type == "tgz" && lst.contains("tar")) || (type == "tlz" && lst.contains("tar")) || (type == "txz" && lst.contains("tar")) || (type == "tarz" && lst.contains("tar")) || (type == "gzip" && lst.contains("gzip")) || (type == "bzip2" && lst.contains("bzip2")) || (type == "lzma" && lst.contains("lzma")) || (type == "xz" && lst.contains("xz")) || (type == "lha" && lst.contains("lha")) || (type == "ace" && lst.contains("unace")) || (type == "rpm" && lst.contains("cpio")) || (type == "cpio" && lst.contains("cpio")) || (type == "rar" && (lst.contains("unrar") || lst.contains("rar"))) || (type == "arj" && (lst.contains("unarj") || lst.contains("arj"))) || (type == "deb" && (lst.contains("dpkg") && lst.contains("tar"))) || (type == "7z" && lst.contains("7z")); } long KrArcHandler::arcFileCount(const QString& archive, const QString& type, const QString& password, KrArcObserver *observer) { int divideWith = 1; // first check if supported if (!arcSupported(type)) return 0; // bzip2, gzip, etc. archives contain only one file if (type == "bzip2" || type == "gzip" || type == "lzma" || type == "xz") return 1L; // set the right lister to do the job QStringList lister; if (type == "zip") lister << KrServices::fullPathName("unzip") << "-ZTs"; else if (type == "tar") lister << KrServices::fullPathName("tar") << "-tvf"; else if (type == "tgz") lister << KrServices::fullPathName("tar") << "-tvzf"; else if (type == "tarz") lister << KrServices::fullPathName("tar") << "-tvzf"; else if (type == "tbz") lister << KrServices::fullPathName("tar") << "-tjvf"; else if (type == "tlz") lister << KrServices::fullPathName("tar") << "--lzma" << "-tvf"; else if (type == "txz") lister << KrServices::fullPathName("tar") << "--xz" << "-tvf"; else if (type == "lha") lister << KrServices::fullPathName("lha") << "l"; else if (type == "rar") lister << KrServices::fullPathName(KrServices::cmdExist("rar") ? "rar" : "unrar") << "l" << "-v"; else if (type == "ace") lister << KrServices::fullPathName("unace") << "l"; else if (type == "arj") { if (KrServices::cmdExist("arj")) lister << KrServices::fullPathName("arj") << "v" << "-y" << "-v", divideWith = 4; else lister << KrServices::fullPathName("unarj") << "l"; } else if (type == "rpm") lister << KrServices::fullPathName("rpm") << "--dump" << "-lpq"; else if (type == "deb") lister << KrServices::fullPathName("dpkg") << "-c"; else if (type == "7z") lister << find7zExecutable() << "-y" << "l"; else return 0L; if (!password.isNull()) { if (type == "arj") lister << QString("-g%1").arg(password); if (type == "ace" || type == "rar" || type == "7z") lister << QString("-p%1").arg(password); } // tell the user to wait observer->subJobStarted(i18n("Counting files in archive"), 0); // count the number of files in the archive long count = 1; KProcess list; list << lister << archive; if (type == "ace" && QFile("/dev/ptmx").exists()) // Don't remove, unace crashes if missing!!! list.setStandardInputFile("/dev/ptmx"); list.setOutputChannelMode(KProcess::SeparateChannels); // without this output redirection has no effect list.start(); // TODO make use of asynchronous process starting. waitForStarted(int msec = 30000) is blocking // it would be better to connect to started(), error() and finished() if (list.waitForStarted()) while (list.state() == QProcess::Running) { observer->processEvents(); if (observer->wasCancelled()) list.kill(); } ; // busy wait - need to find something better... observer->subJobStopped(); if (list.exitStatus() != QProcess::NormalExit || !checkStatus(type, list.exitCode())) { observer->detailedError(i18n("Failed to list the content of the archive (%1).", archive), QString::fromLocal8Bit(list.readAllStandardError())); return 0; } count = list.readAllStandardOutput().count('\n'); //make sure you call stopWait after this function return... // observer->subJobStopped(); return count / divideWith; } bool KrArcHandler::unpack(QString archive, const QString& type, const QString& password, const QString& dest, KrArcObserver *observer) { KConfigGroup group(krConfig, "Archives"); if (group.readEntry("Test Before Unpack", _TestBeforeUnpack)) { // test first - or be sorry later... if (type != "rpm" && type != "deb" && !test(archive, type, password, observer, 0)) { observer->error(i18n("Failed to unpack %1.", archive)); return false; } } // count the files in the archive long count = arcFileCount(archive, type, password, observer); if (count == 0) return false; // not supported if (count == 1) count = 0; // choose the right packer for the job QString cpioName; QStringList packer; // set the right packer to do the job if (type == "zip") packer << KrServices::fullPathName("unzip") << "-o"; else if (type == "tar") packer << KrServices::fullPathName("tar") << "-xvf"; else if (type == "tgz") packer << KrServices::fullPathName("tar") << "-xvzf"; else if (type == "tarz") packer << KrServices::fullPathName("tar") << "-xvzf"; else if (type == "tbz") packer << KrServices::fullPathName("tar") << "-xjvf"; else if (type == "tlz") packer << KrServices::fullPathName("tar") << "--lzma" << "-xvf"; else if (type == "txz") packer << KrServices::fullPathName("tar") << "--xz" << "-xvf"; else if (type == "gzip") packer << KrServices::fullPathName("gzip") << "-cd"; else if (type == "bzip2") packer << KrServices::fullPathName("bzip2") << "-cdk"; else if (type == "lzma") packer << KrServices::fullPathName("lzma") << "-cdk"; else if (type == "xz") packer << KrServices::fullPathName("xz") << "-cdk"; else if (type == "lha") packer << KrServices::fullPathName("lha") << "xf"; else if (type == "rar") packer << KrServices::fullPathName(KrServices::cmdExist("rar") ? "rar" : "unrar") << "-y" << "x"; else if (type == "ace") packer << KrServices::fullPathName("unace") << "x"; else if (type == "arj") { if (KrServices::cmdExist("arj")) packer << KrServices::fullPathName("arj") << "-y" << "-v" << "x"; else packer << KrServices::fullPathName("unarj") << "x"; } else if (type == "7z") packer << find7zExecutable() << "-y" << "x"; else if (type == "rpm") { // TODO use QTemporaryFile (setAutoRemove(false) when asynchrone) cpioName = QDir::tempPath() + QStringLiteral("/contents.cpio"); KrLinecountingProcess cpio; cpio << KrServices::fullPathName("rpm2cpio") << archive; cpio.setStandardOutputFile(cpioName); // TODO maybe no tmpfile but a pipe (setStandardOutputProcess(packer)) cpio.start(); if (!cpio.waitForFinished() || cpio.exitStatus() != QProcess::NormalExit || !checkStatus("cpio", cpio.exitCode())) { observer->detailedError(i18n("Failed to convert rpm (%1) to cpio.", archive), cpio.getErrorMsg()); return 0; } archive = cpioName; packer << KrServices::fullPathName("cpio") << "--force-local" << "--no-absolute-filenames" << "-iuvdF"; } else if (type == "deb") { // TODO use QTemporaryFile (setAutoRemove(false) when asynchrone) cpioName = QDir::tempPath() + QStringLiteral("/contents.tar"); KrLinecountingProcess dpkg; dpkg << KrServices::fullPathName("dpkg") << "--fsys-tarfile" << archive; dpkg.setStandardOutputFile(cpioName); // TODO maybe no tmpfile but a pipe (setStandardOutputProcess(packer)) dpkg.start(); if (!dpkg.waitForFinished() || dpkg.exitStatus() != QProcess::NormalExit || !checkStatus("deb", dpkg.exitCode())) { observer->detailedError(i18n("Failed to convert deb (%1) to tar.", archive), dpkg.getErrorMsg()); return 0; } archive = cpioName; packer << KrServices::fullPathName("tar") << "xvf"; } else return false; if (!password.isNull()) { if (type == "zip") packer << "-P" << password; if (type == "arj") packer << QString("-g%1").arg(password); if (type == "ace" || type == "rar" || type == "7z") packer << QString("-p%1").arg(password); } // unpack the files KrLinecountingProcess proc; proc << packer << archive; if (type == "bzip2" || type == "gzip" || type == "lzma" || type == "xz") { QString arcname = archive.mid(archive.lastIndexOf("/") + 1); if (arcname.contains(".")) arcname = arcname.left(arcname.lastIndexOf(".")); proc.setStandardOutputFile(dest + '/' + arcname); } if (type == "ace" && QFile("/dev/ptmx").exists()) // Don't remove, unace crashes if missing!!! proc.setStandardInputFile("/dev/ptmx"); proc.setWorkingDirectory(dest); // tell the user to wait observer->subJobStarted(i18n("Unpacking File(s)"), count); if (count != 0) { connect(&proc, &KrLinecountingProcess::newOutputLines, observer, &KrArcObserver::incrementProgress); if (type == "rpm") connect(&proc, &KrLinecountingProcess::newErrorLines, observer, &KrArcObserver::incrementProgress); } // start the unpacking process proc.start(); // TODO make use of asynchronous process starting. waitForStarted(int msec = 30000) is blocking // it would be better to connect to started(), error() and finished() if (proc.waitForStarted()) while (proc.state() == QProcess::Running) { observer->processEvents(); if (observer->wasCancelled()) proc.kill(); } ; // busy wait - need to find something better... observer->subJobStopped(); if (!cpioName.isEmpty()) QFile(cpioName).remove(); /* remove the cpio file */ // check the return value if (proc.exitStatus() != QProcess::NormalExit || !checkStatus(type, proc.exitCode())) { observer->detailedError(i18n("Failed to unpack %1.", archive), observer->wasCancelled() ? i18n("User cancelled.") : proc.getErrorMsg()); return false; } return true; // SUCCESS } bool KrArcHandler::test(const QString& archive, const QString& type, const QString& password, KrArcObserver *observer, long count) { // choose the right packer for the job QStringList packer; // set the right packer to do the job if (type == "zip") packer << KrServices::fullPathName("unzip") << "-t"; else if (type == "tar") packer << KrServices::fullPathName("tar") << "-tvf"; else if (type == "tgz") packer << KrServices::fullPathName("tar") << "-tvzf"; else if (type == "tarz") packer << KrServices::fullPathName("tar") << "-tvzf"; else if (type == "tbz") packer << KrServices::fullPathName("tar") << "-tjvf"; else if (type == "tlz") packer << KrServices::fullPathName("tar") << "--lzma" << "-tvf"; else if (type == "txz") packer << KrServices::fullPathName("tar") << "--xz" << "-tvf"; else if (type == "gzip") packer << KrServices::fullPathName("gzip") << "-tv"; else if (type == "bzip2") packer << KrServices::fullPathName("bzip2") << "-tv"; else if (type == "lzma") packer << KrServices::fullPathName("lzma") << "-tv"; else if (type == "xz") packer << KrServices::fullPathName("xz") << "-tv"; else if (type == "rar") packer << KrServices::fullPathName(KrServices::cmdExist("rar") ? "rar" : "unrar") << "t"; else if (type == "ace") packer << KrServices::fullPathName("unace") << "t"; else if (type == "lha") packer << KrServices::fullPathName("lha") << "t"; else if (type == "arj") packer << KrServices::fullPathName(KrServices::cmdExist("arj") ? "arj" : "unarj") << "t"; else if (type == "cpio") packer << KrServices::fullPathName("cpio") << "--only-verify-crc" << "-tvF"; else if (type == "7z") packer << find7zExecutable() << "-y" << "t"; else return false; if (!password.isNull()) { if (type == "zip") packer << "-P" << password; if (type == "arj") packer << QString("-g%1").arg(password); if (type == "ace" || type == "rar" || type == "7z") packer << QString("-p%1").arg(password); } // unpack the files KrLinecountingProcess proc; proc << packer << archive; if (type == "ace" && QFile("/dev/ptmx").exists()) // Don't remove, unace crashes if missing!!! proc.setStandardInputFile("/dev/ptmx"); // tell the user to wait observer->subJobStarted(i18n("Testing Archive"), count); if (count != 0) connect(&proc, &KrLinecountingProcess::newOutputLines, observer, &KrArcObserver::incrementProgress); // start the unpacking process proc.start(); // TODO make use of asynchronous process starting. waitForStarted(int msec = 30000) is blocking // it would be better to connect to started(), error() and finished() if (proc.waitForStarted()) while (proc.state() == QProcess::Running) { observer->processEvents(); if (observer->wasCancelled()) proc.kill(); } ; // busy wait - need to find something better... observer->subJobStopped(); // check the return value if (proc.exitStatus() != QProcess::NormalExit || !checkStatus(type, proc.exitCode())) return false; return true; // SUCCESS } bool KrArcHandler::pack(QStringList fileNames, QString type, const QString& dest, long count, QMap extraProps, KrArcObserver *observer) { // set the right packer to do the job QStringList packer; if (type == "zip") { packer << KrServices::fullPathName("zip") << "-ry"; } else if (type == "cbz") { packer << KrServices::fullPathName("zip") << "-ry"; type = "zip"; } else if (type == "tar") { packer << KrServices::fullPathName("tar") << "-cvf"; } else if (type == "tar.gz") { packer << KrServices::fullPathName("tar") << "-cvzf"; type = "tgz"; } else if (type == "tar.bz2") { packer << KrServices::fullPathName("tar") << "-cvjf"; type = "tbz"; } else if (type == "tar.lzma") { packer << KrServices::fullPathName("tar") << "--lzma" << "-cvf"; type = "tlz"; } else if (type == "tar.xz") { packer << KrServices::fullPathName("tar") << "--xz" << "-cvf"; type = "txz"; } else if (type == "rar") { packer << KrServices::fullPathName("rar") << "-r" << "a"; } else if (type == "cbr") { packer << KrServices::fullPathName("rar") << "-r" << "a"; type = "rar"; } else if (type == "lha") { packer << KrServices::fullPathName("lha") << "a"; } else if (type == "arj") { packer << KrServices::fullPathName("arj") << "-r" << "-y" << "a"; } else if (type == "7z") { packer << find7zExecutable() << "-y" << "a"; } else return false; QString password; if (extraProps.count("Password") > 0) { password = extraProps[ "Password" ]; if (!password.isNull()) { if (type == "zip") packer << "-P" << password; else if (type == "arj") packer << QString("-g%1").arg(password); else if (type == "ace" || type == "7z") packer << QString("-p%1").arg(password); else if (type == "rar") { if (extraProps.count("EncryptHeaders") > 0) packer << QString("-hp%1").arg(password); else packer << QString("-p%1").arg(password); } else password.clear(); } } if (extraProps.count("VolumeSize") > 0) { QString sizeStr = extraProps[ "VolumeSize" ]; KIO::filesize_t size = sizeStr.toLongLong(); if (size >= 10000) { if (type == "arj" || type == "rar") packer << QString("-v%1b").arg(sizeStr); } } if (extraProps.count("CompressionLevel") > 0) { int level = extraProps[ "CompressionLevel" ].toInt() - 1; if (level < 0) level = 0; if (level > 8) level = 8; if (type == "rar") { static const int rarLevels[] = { 0, 1, 2, 2, 3, 3, 4, 4, 5 }; packer << QString("-m%1").arg(rarLevels[ level ]); } else if (type == "arj") { static const int arjLevels[] = { 0, 4, 4, 3, 3, 2, 2, 1, 1 }; packer << QString("-m%1").arg(arjLevels[ level ]); } else if (type == "zip") { static const int zipLevels[] = { 0, 1, 2, 4, 5, 6, 7, 8, 9 }; packer << QString("-%1").arg(zipLevels[ level ]); } else if (type == "7z") { static const int sevenZipLevels[] = { 0, 1, 2, 4, 5, 6, 7, 8, 9 }; packer << QString("-mx%1").arg(sevenZipLevels[ level ]); } } if (extraProps.count("CommandLineSwitches") > 0) packer << QString("%1").arg(extraProps[ "CommandLineSwitches" ]); // prepare to pack KrLinecountingProcess proc; proc << packer << dest; for (auto & fileName : fileNames) { proc << fileName; } // tell the user to wait observer->subJobStarted(i18n("Packing File(s)"), count); if (count != 0) connect(&proc, &KrLinecountingProcess::newOutputLines, observer, &KrArcObserver::incrementProgress); // start the packing process proc.start(); // TODO make use of asynchronous process starting. waitForStarted(int msec = 30000) is blocking // it would be better to connect to started(), error() and finished() if (proc.waitForStarted()) while (proc.state() == QProcess::Running) { observer->processEvents(); if (observer->wasCancelled()) proc.kill(); } ; // busy wait - need to find something better... observer->subJobStopped(); // check the return value if (proc.exitStatus() != QProcess::NormalExit || !checkStatus(type, proc.exitCode())) { observer->detailedError(i18n("Failed to pack %1.", dest), observer->wasCancelled() ? i18n("User cancelled.") : proc.getErrorMsg()); return false; } KConfigGroup group(krConfig, "Archives"); if (group.readEntry("Test Archives", _TestArchives) && !test(dest, type, password, observer, count)) { observer->error(i18n("Failed to pack %1.", dest)); return false; } return true; // SUCCESS } bool KrArcHandler::openWallet() { if (!wallet) { // find a suitable parent window QWidget *actWindow = QApplication::activeWindow(); if (!actWindow) actWindow = (QWidget*) QApplication::desktop(); wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), actWindow->effectiveWinId()); } return (wallet != nullptr); } QString KrArcHandler::getPassword(const QString& path) { QString password; QString key = "krarc-" + path; if (!KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), key)) { if (!KWallet::Wallet::isOpen(KWallet::Wallet::NetworkWallet()) && wallet != nullptr) { delete wallet; wallet = nullptr; } if (openWallet() && wallet->hasFolder(KWallet::Wallet::PasswordFolder())) { wallet->setFolder(KWallet::Wallet::PasswordFolder()); QMap map; if (wallet->readMap(key, map) == 0) { QMap::const_iterator it = map.constFind("password"); if (it != map.constEnd()) password = it.value(); } } } bool keep = true; QString user = "archive"; QPointer passDlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowKeepPassword); passDlg->setPrompt(i18n("This archive is encrypted, please supply the password:") ), passDlg->setUsername(user); passDlg->setPassword(password); if (passDlg->exec() == KPasswordDialog::Accepted) { password = passDlg->password(); if (keep) { if (!KWallet::Wallet::isOpen(KWallet::Wallet::NetworkWallet()) && wallet != nullptr) { delete wallet; wallet = nullptr; } if (openWallet()) { bool ok = true; if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) ok = wallet->createFolder(KWallet::Wallet::PasswordFolder()); if (ok) { wallet->setFolder(KWallet::Wallet::PasswordFolder()); QMap map; map.insert("login", "archive"); map.insert("password", password); wallet->writeMap(key, map); } } } delete passDlg; return password; } delete passDlg; return ""; } bool KrArcHandler::isArchive(const QUrl &url) { QString protocol = url.scheme(); if (arcProtocols.indexOf(protocol) != -1) return true; else return false; } QString KrArcHandler::getType(bool &encrypted, QString fileName, const QString& mime, bool checkEncrypted, bool fast) { QString result = detectArchive(encrypted, std::move(fileName), checkEncrypted, fast); if (result.isNull()) { // Then the type is based on the mime type return getShortTypeFromMime(mime); } return result; } bool KrArcHandler::checkStatus(const QString& type, int exitCode) { return KrArcBaseManager::checkStatus(type, exitCode); } void KrArcHandler::checkIf7zIsEncrypted(bool &encrypted, QString fileName) { // Reminder: If that function is modified, it's important to research if the // changes must also be applied to `kio_krarcProtocol::checkIf7zIsEncrypted()` Kr7zEncryptionChecker proc; // TODO incorporate all this in Kr7zEncryptionChecker proc << find7zExecutable() << "-y" << "t"; proc << fileName; proc.start(); proc.waitForFinished(); encrypted = proc.isEncrypted(); } QString KrArcHandler::registeredProtocol(const QString& mimetype) { if (slaveMap == nullptr) { slaveMap = new QMap(); KConfigGroup group(krConfig, "Protocols"); QStringList protList = group.readEntry("Handled Protocols", QStringList()); for (auto & it : protList) { QStringList mimes = group.readEntry(QString("Mimes For %1").arg(it), QStringList()); for (auto & mime : mimes) (*slaveMap)[mime] = it; } } QString protocol = (*slaveMap)[mimetype]; if (protocol.isEmpty()) { if (krarcArchiveMimetypes.contains(mimetype)) { return QStringLiteral("krarc"); } protocol = KProtocolManager::protocolForArchiveMimetype(mimetype); } return protocol; } void KrArcHandler::clearProtocolCache() { if (slaveMap) delete slaveMap; slaveMap = nullptr; } diff --git a/krusader/FileSystem/krquery.h b/krusader/FileSystem/krquery.h index df8106fa..8e3cfb73 100644 --- a/krusader/FileSystem/krquery.h +++ b/krusader/FileSystem/krquery.h @@ -1,228 +1,229 @@ /***************************************************************************** * Copyright (C) 2001 Shie Erlich * * Copyright (C) 2001 Rafi Yanai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #ifndef KRQUERY_H #define KRQUERY_H // QtCore #include #include #include +#include #include #include class QTextCodec; class FileItem; /** * @brief A search query for files * * Can be used for finding or selecting files and folders by multiple limiting search criteria */ class KrQuery : public QObject { Q_OBJECT public: // null query KrQuery(); // query only with name filter explicit KrQuery(const QString &name, bool matchCase = true); // copy constructor KrQuery(const KrQuery &); // let operator KrQuery &operator=(const KrQuery &); // destructor ~KrQuery() override; // load parameters from config void load(const KConfigGroup& cfg); // save parameters to config void save(KConfigGroup cfg); // matching a file with the query bool match(FileItem *file) const; // checks if the given fileItem object matches the conditions // matching a name with the query bool match(const QString &name) const; // matching the filename only // matching the name of the directory bool matchDirName(const QString &name) const; // sets the text for name filtering void setNameFilter(const QString &text, bool cs = true); // returns the current filter mask const QString &nameFilter() const { return origFilter; } // returns whether the filter is case sensitive bool isCaseSensitive() { return matchesCaseSensitive; } // returns if the filter is null (was cancelled) bool isNull() { return bNull; } // sets the content part of the query void setContent(const QString &content, bool cs = true, bool wholeWord = false, const QString& encoding = QString(), bool regExp = false); const QString content() { return contain; } // sets the minimum file size limit void setMinimumFileSize(KIO::filesize_t); // sets the maximum file size limit void setMaximumFileSize(KIO::filesize_t); // sets the time the file newer than void setNewerThan(time_t time); // sets the time the file older than void setOlderThan(time_t time); // sets the owner void setOwner(const QString &ownerIn); // sets the group void setGroup(const QString &groupIn); // sets the permissions void setPermissions(const QString &permIn); // sets the mimetype for the query // type, must be one of the following: // 1. a valid mime type name // 2. one of: i18n("Archives"), i18n("Folders"), i18n("Image Files") // i18n("Text Files"), i18n("Video Files"), i18n("Audio Files") // 3. i18n("Custom") in which case you must supply a list of valid mime-types // in the member QStringList customType void setMimeType(const QString &typeIn, QStringList customList = QStringList()); // true if setMimeType was called bool hasMimeType() { return type.isEmpty(); } // sets the search in archive flag void setSearchInArchives(bool flag) { inArchive = flag; } // gets the search in archive flag bool searchInArchives() { return inArchive; } // sets the recursive flag void setRecursive(bool flag) { recurse = flag; } // gets the recursive flag bool isRecursive() { return recurse; } // sets whether to follow symbolic links void setFollowLinks(bool flag) { followLinksP = flag; } // gets whether to follow symbolic links bool followLinks() { return followLinksP; } // sets the folder names which the searcher will exclude from traversing void setExcludeFolderNames(const QStringList &urls); // gets the folder names which the searcher excludes const QStringList excludeFolderNames() { return excludedFolderNames; } // sets the folders where the searcher will search void setSearchInDirs(const QList &urls); // gets the folders where the searcher searches const QList &searchInDirs() { return whereToSearch; } // sets the folders where search is not permitted void setDontSearchInDirs(const QList &urls); // gets the folders where search is not permitted const QList &dontSearchInDirs() { return whereNotToSearch; } // checks if a URL is excluded bool isExcluded(const QUrl &url); // gives whether we search for content bool isContentSearched() const { return !contain.isEmpty(); } bool checkLine(const QString &line, bool backwards = false) const; const QString &foundText() const { return lastSuccessfulGrep; } int matchIndex() const { return lastSuccessfulGrepMatchIndex; } int matchLength() const { return lastSuccessfulGrepMatchLength; } protected: // important to know whether the event processor is connected void connectNotify(const QMetaMethod &signal) override; // important to know whether the event processor is connected void disconnectNotify(const QMetaMethod &signal) override; protected: QStringList matches; // what to search QStringList excludes; // what to exclude QStringList includedDirs; // what dirs to include QStringList excludedDirs; // what dirs to exclude bool matchesCaseSensitive; bool bNull; // flag if the query is null QString contain; // file must contain this string bool containCaseSensetive; bool containWholeWord; bool containRegExp; KIO::filesize_t minSize; KIO::filesize_t maxSize; time_t newerThen; time_t olderThen; QString owner; QString group; QString perm; QString type; QStringList customType; bool inArchive; // if true- search in archive. bool recurse; // if true recurse ob sub-dirs... bool followLinksP; QStringList excludedFolderNames; // substrings of paths where not to search QList whereToSearch; // directories to search QList whereNotToSearch; // directories NOT to search signals: void status(const QString &name); void processEvents(bool &stopped); private: bool matchCommon(const QString &, const QStringList &, const QStringList &) const; bool checkPerm(QString perm) const; bool checkType(const QString& mime) const; bool containsContent(const QString& file) const; bool containsContent(const QUrl& url) const; bool checkBuffer(const char *data, int len) const; bool checkTimer() const; QStringList split(QString); private slots: void containsContentData(KIO::Job *, const QByteArray &); void containsContentFinished(KJob *); private: QString origFilter; mutable bool busy; mutable bool containsContentResult; mutable char *receivedBuffer; mutable int receivedBufferLen; mutable QString lastSuccessfulGrep; mutable int lastSuccessfulGrepMatchIndex; mutable int lastSuccessfulGrepMatchLength; mutable QString fileName; mutable KIO::filesize_t receivedBytes; mutable KIO::filesize_t totalBytes; mutable int processEventsConnected; - mutable QTime timer; + mutable QElapsedTimer timer; QTextCodec *codec; const char *encodedEnter; int encodedEnterLen; QByteArray encodedEnterArray; }; #endif diff --git a/krusader/Filter/filtersettings.cpp b/krusader/Filter/filtersettings.cpp index dc16ed74..f3df4d6f 100644 --- a/krusader/Filter/filtersettings.cpp +++ b/krusader/Filter/filtersettings.cpp @@ -1,321 +1,294 @@ /***************************************************************************** * Copyright (C) 2003 Shie Erlich * * Copyright (C) 2003 Rafi Yanai * * Copyright (C) 2003 Csaba Karai * * Copyright (C) 2011 by Jan Lepper * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "filtersettings.h" #include "../krservices.h" #include #include + +FilterSettings::FileSize::FileSize(const FileSize& other) += default; + FilterSettings::FileSize& FilterSettings::FileSize::operator=(const FileSize &other) = default; KIO::filesize_t FilterSettings::FileSize::size() const { switch (unit) { case Byte: return amount; case KiloByte: return amount * 1024; case MegaByte: return amount * 1024 * 1024; case GigaByte: return amount * 1024 * 1024 * 1024; default: qWarning() << "invalid size unit: " << unit; return amount; } } +FilterSettings::TimeSpan::TimeSpan(const TimeSpan& other) += default; + FilterSettings::TimeSpan& FilterSettings::TimeSpan::operator=(const TimeSpan &other) = default; int FilterSettings::TimeSpan::days() const { switch (unit) { case Day: return amount; case Week: return amount * 7; case Month: return amount * 30; case Year: return amount * 365; default: qWarning() << "invalid time unit: " << unit; return amount; } } FilterSettings::FilterSettings() : valid(false), searchFor("*"), searchForCase(false), searchInArchives(false), recursive(false), followLinks(false), containsTextCase(false), containsWholeWord(false), containsRegExp(false), minSizeEnabled(false), maxSizeEnabled(false), modifiedBetweenEnabled(false), notModifiedAfterEnabled(false), modifiedInTheLastEnabled(false), ownerEnabled(false), groupEnabled(false), permissionsEnabled(false) { } +FilterSettings::FilterSettings(const FilterSettings& other) += default; + FilterSettings& FilterSettings::operator=(const FilterSettings& other) -{ -#define COPY(var) { var = other.var; } - COPY(valid); - COPY(searchFor); - COPY(searchForCase); - COPY(mimeType); - COPY(searchInArchives); - COPY(recursive); - COPY(followLinks); - COPY(searchIn); - COPY(dontSearchIn); - COPY(excludeFolderNames); - COPY(contentEncoding); - COPY(containsText); - COPY(containsTextCase); - COPY(containsWholeWord); - COPY(containsRegExp); - COPY(minSizeEnabled); - COPY(minSize); - COPY(maxSizeEnabled); - COPY(maxSize); - COPY(modifiedBetweenEnabled); - COPY(modifiedBetween1); - COPY(modifiedBetween2); - COPY(notModifiedAfterEnabled); - COPY(notModifiedAfter); - COPY(modifiedInTheLastEnabled); - COPY(modifiedInTheLast); - COPY(notModifiedInTheLast); - COPY(ownerEnabled); - COPY(owner); - COPY(groupEnabled); - COPY(group); - COPY(permissionsEnabled); - COPY(permissions); -#undef COPY - return *this; -} += default; void FilterSettings::load(const KConfigGroup& cfg) { *this = FilterSettings(); #define LOAD(key, var) { var = cfg.readEntry(key, var); } LOAD("IsValid", valid); if(!isValid()) return; LOAD("SearchFor", searchFor); LOAD("MimeType", mimeType); LOAD("SearchInArchives", searchInArchives); LOAD("Recursive", recursive); LOAD("FollowLinks", followLinks); searchIn = KrServices::toUrlList(cfg.readEntry("SearchIn", QStringList())); dontSearchIn = KrServices::toUrlList(cfg.readEntry("DontSearchIn", QStringList())); excludeFolderNames = QStringList(); LOAD("ContentEncoding", contentEncoding); LOAD("ContainsText", containsText); LOAD("ContainsTextCase", containsTextCase); LOAD("ContainsWholeWord", containsWholeWord); LOAD("ContainsRegExp", containsRegExp); LOAD("MinSizeEnabled", minSizeEnabled); LOAD("MinSizeAmount", minSize.amount); minSize.unit = static_cast(cfg.readEntry("MinSizeUnit", 0)); LOAD("MaxSizeEnabled", maxSizeEnabled); LOAD("MaxSizeAmount", maxSize.amount); maxSize.unit = static_cast(cfg.readEntry("MaxSizeUnit", 0)); LOAD("ModifiedBetweenEnabled", modifiedBetweenEnabled); LOAD("ModifiedBetween1", modifiedBetween1); LOAD("ModifiedBetween2", modifiedBetween2); LOAD("NotModifiedAfterEnabled", notModifiedAfterEnabled); LOAD("NotModifiedAfter", notModifiedAfter); LOAD("ModifiedInTheLastEnabled", modifiedInTheLastEnabled); LOAD("ModifiedInTheLastAmount", modifiedInTheLast.amount); modifiedInTheLast.unit = static_cast(cfg.readEntry("ModifiedInTheLastUnit", 0)); LOAD("NotModifiedInTheLastAmount", notModifiedInTheLast.amount); notModifiedInTheLast.unit = static_cast(cfg.readEntry("NotModifiedInTheLastUnit", 0)); LOAD("OwnerEnabled", ownerEnabled); LOAD("Owner", owner); LOAD("GroupEnabled", groupEnabled); LOAD("Group", group); LOAD("PermissionsEnabled", permissionsEnabled); LOAD("Permissions", permissions); #undef LOAD } void FilterSettings::saveDate(const QString& key, const QDate &date, KConfigGroup &cfg) { if(date.isValid()) cfg.writeEntry(key, date); else cfg.deleteEntry(key); } void FilterSettings::save(KConfigGroup cfg) const { cfg.writeEntry("IsValid", valid); if(!isValid()) return; cfg.writeEntry("SearchFor", searchFor); cfg.writeEntry("MimeType", mimeType); cfg.writeEntry("SearchInArchives", searchInArchives); cfg.writeEntry("Recursive", recursive); cfg.writeEntry("FollowLinks", followLinks); cfg.writeEntry("SearchIn", KrServices::toStringList(searchIn)); cfg.writeEntry("DontSearchIn", KrServices::toStringList(dontSearchIn)); cfg.writeEntry("ContentEncoding", contentEncoding); cfg.writeEntry("ContainsText", containsText); cfg.writeEntry("ContainsTextCase", containsTextCase); cfg.writeEntry("ContainsWholeWord", containsWholeWord); cfg.writeEntry("ContainsRegExp", containsRegExp); cfg.writeEntry("MinSizeEnabled", minSizeEnabled); cfg.writeEntry("MinSizeAmount", minSize.amount); cfg.writeEntry("MinSizeUnit", static_cast(minSize.unit)); cfg.writeEntry("MaxSizeEnabled", maxSizeEnabled); cfg.writeEntry("MaxSizeAmount", maxSize.amount); cfg.writeEntry("MaxSizeUnit", static_cast(maxSize.unit)); cfg.writeEntry("ModifiedBetweenEnabled", modifiedBetweenEnabled); saveDate("ModifiedBetween1", modifiedBetween1, cfg); saveDate("ModifiedBetween2", modifiedBetween2, cfg); cfg.writeEntry("NotModifiedAfterEnabled", notModifiedAfterEnabled); saveDate("NotModifiedAfter", notModifiedAfter, cfg); cfg.writeEntry("ModifiedInTheLastEnabled", modifiedInTheLastEnabled); cfg.writeEntry("ModifiedInTheLastAmount", modifiedInTheLast.amount); cfg.writeEntry("ModifiedInTheLastUnit", static_cast(modifiedInTheLast.unit)); cfg.writeEntry("NotModifiedInTheLastAmount", notModifiedInTheLast.amount); cfg.writeEntry("NotModifiedInTheLastUnit", static_cast(notModifiedInTheLast.unit)); cfg.writeEntry("OwnerEnabled", ownerEnabled); cfg.writeEntry("Owner", owner); cfg.writeEntry("GroupEnabled", groupEnabled); cfg.writeEntry("Group", group); cfg.writeEntry("PermissionsEnabled", permissionsEnabled); cfg.writeEntry("Permissions", permissions); } // bool start: set it to true if this date is the beginning of the search, // if it's the end, set it to false time_t FilterSettings::qdate2time_t (QDate d, bool start) { struct tm t; t.tm_sec = (start ? 0 : 59); t.tm_min = (start ? 0 : 59); t.tm_hour = (start ? 0 : 23); t.tm_mday = d.day(); t.tm_mon = d.month() - 1; t.tm_year = d.year() - 1900; t.tm_wday = d.dayOfWeek() - 1; // actually ignored by mktime t.tm_yday = d.dayOfYear() - 1; // actually ignored by mktime t.tm_isdst = -1; // daylight saving time information isn't available return mktime(&t); } KrQuery FilterSettings::toQuery() const { if(!isValid()) return KrQuery(); KrQuery query; ////////////// General Options ////////////// query.setNameFilter(searchFor, searchForCase); query.setMimeType(mimeType); QString charset; if (!contentEncoding.isEmpty()) charset = KCharsets::charsets()->encodingForName(contentEncoding); if (!containsText.isEmpty()) { query.setContent(containsText, containsTextCase, containsWholeWord, charset, containsRegExp); } query.setRecursive(recursive); query.setSearchInArchives(searchInArchives); query.setFollowLinks(followLinks); if (!searchIn.isEmpty()) query.setSearchInDirs(searchIn); if (!dontSearchIn.isEmpty()) query.setDontSearchInDirs(dontSearchIn); query.setExcludeFolderNames(excludeFolderNames); ////////////// Advanced Options ////////////// if (minSizeEnabled) query.setMinimumFileSize(minSize.size()); if (maxSizeEnabled) query.setMaximumFileSize(maxSize.size()); if (modifiedBetweenEnabled) { if(modifiedBetween1.isValid()) query.setNewerThan(qdate2time_t(modifiedBetween1, true)); if(modifiedBetween2.isValid()) query.setOlderThan(qdate2time_t(modifiedBetween2, false)); } else if (notModifiedAfterEnabled && notModifiedAfter.isValid()) { query.setOlderThan(qdate2time_t(notModifiedAfter, false)); } else if (modifiedInTheLastEnabled) { if (modifiedInTheLast.amount) { QDate d = QDate::currentDate().addDays((-1) * modifiedInTheLast.days()); query.setNewerThan(qdate2time_t(d, true)); } if (notModifiedInTheLast.amount) { QDate d = QDate::currentDate().addDays((-1) * notModifiedInTheLast.days()); query.setOlderThan(qdate2time_t(d, true)); } } if (ownerEnabled && !owner.isEmpty()) query.setOwner(owner); if (groupEnabled && !group.isEmpty()) query.setGroup(group); if (permissionsEnabled && !permissions.isEmpty()) query.setPermissions(permissions); return query; } diff --git a/krusader/Filter/filtersettings.h b/krusader/Filter/filtersettings.h index efb674a5..badaea0a 100644 --- a/krusader/Filter/filtersettings.h +++ b/krusader/Filter/filtersettings.h @@ -1,117 +1,120 @@ /***************************************************************************** * Copyright (C) 2011 Jan Lepper * * Copyright (C) 2011-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #ifndef FILTERSETTINGS_H #define FILTERSETTINGS_H #include "../FileSystem/krquery.h" #include class FilterSettings { friend class FilterTabs; friend class GeneralFilter; friend class AdvancedFilter; public: FilterSettings(); + FilterSettings(const FilterSettings& other); FilterSettings& operator=(const FilterSettings& other); bool isValid() const { return valid; } void load(const KConfigGroup& cfg); void save(KConfigGroup cfg) const; KrQuery toQuery() const; private: enum SizeUnit { Byte = 0, KiloByte, MegaByte, GigaByte }; enum TimeUnit { Day = 0, Week, Month, Year }; class FileSize { public: FileSize() : amount(0), unit(Byte) {} + FileSize(const FileSize &other); FileSize& operator=(const FileSize &other); KIO::filesize_t size() const; KIO::filesize_t amount; SizeUnit unit; }; class TimeSpan { public: TimeSpan() : amount(0), unit(Day) {} + TimeSpan(const TimeSpan &other); TimeSpan& operator=(const TimeSpan &other); int days() const; int amount; TimeUnit unit; }; static void saveDate(const QString& key, const QDate &date, KConfigGroup &cfg); static time_t qdate2time_t(QDate d, bool start); bool valid; QString searchFor; bool searchForCase; QString mimeType; bool searchInArchives; bool recursive; bool followLinks; QList searchIn; QList dontSearchIn; QStringList excludeFolderNames; QString contentEncoding; QString containsText; bool containsTextCase; bool containsWholeWord; bool containsRegExp; bool minSizeEnabled; FileSize minSize; bool maxSizeEnabled; FileSize maxSize; bool modifiedBetweenEnabled; QDate modifiedBetween1; QDate modifiedBetween2; bool notModifiedAfterEnabled; QDate notModifiedAfter; bool modifiedInTheLastEnabled; TimeSpan modifiedInTheLast; TimeSpan notModifiedInTheLast; bool ownerEnabled; QString owner; bool groupEnabled; QString group; bool permissionsEnabled; QString permissions; }; #endif //FILTERSETTINGS_H diff --git a/krusader/KViewer/lister.cpp b/krusader/KViewer/lister.cpp index 2e94aca3..28e38905 100644 --- a/krusader/KViewer/lister.cpp +++ b/krusader/KViewer/lister.cpp @@ -1,2277 +1,2277 @@ /***************************************************************************** * Copyright (C) 2009 Csaba Karai * * Copyright (C) 2009-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "lister.h" // QtCore #include #include #include #include #include // QtGui #include #include #include #include #include // QtWidgets #include #include #include #include #include #include #include #include #include #include #include #include // QtPrintSupport #include #include #include #include #include #include #include #include #include #include #include #include #include "../krglobal.h" #include "../icon.h" #include "../kractions.h" #include "../GUI/krremoteencodingmenu.h" #include "../compat.h" #define SEARCH_CACHE_CHARS 100000 #define SEARCH_MAX_ROW_LEN 4000 #define CONTROL_CHAR 752 #define CACHE_SIZE 1048576 // cache size set to 1MiB ListerTextArea::ListerTextArea(Lister *lister, QWidget *parent) : KTextEdit(parent), _lister(lister) { connect(this, &QTextEdit::cursorPositionChanged, this, &ListerTextArea::slotCursorPositionChanged); _tabWidth = 4; setWordWrapMode(QTextOption::NoWrap); setLineWrapMode(QTextEdit::NoWrap); // zoom shortcuts connect(new QShortcut(QKeySequence("Ctrl++"), this), &QShortcut::activated, this, [=]() { zoomIn(); }); connect(new QShortcut(QKeySequence("Ctrl+-"), this), &QShortcut::activated, this, [=]() { zoomOut(); }); // start cursor blinking connect(&_blinkTimer, &QTimer::timeout, this, [=] { if (!_cursorBlinkMutex.tryLock()) { return; } setCursorWidth(cursorWidth() == 0 ? 2 : 0); _cursorBlinkMutex.unlock(); }); _blinkTimer.start(500); } void ListerTextArea::reset() { _screenStartPos = 0; _cursorPos = 0; _cursorAnchorPos = -1; _cursorAtFirstColumn = true; calculateText(); } void ListerTextArea::sizeChanged() { if (_cursorAnchorPos > _lister->fileSize()) _cursorAnchorPos = -1; if (_cursorPos > _lister->fileSize()) _cursorPos = _lister->fileSize(); redrawTextArea(true); } void ListerTextArea::resizeEvent(QResizeEvent * event) { KTextEdit::resizeEvent(event); redrawTextArea(); } void ListerTextArea::calculateText(const bool forcedUpdate) { const QRect contentRect = viewport()->contentsRect(); const QFontMetrics fm(font()); const int fontHeight = std::max(fm.height(), 1); // This is quite accurate (although not perfect) way of getting // a single character width along with its surrounding space. const float fontWidth = (fm.QFONTMETRICS_WIDTH("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW") - fm.QFONTMETRICS_WIDTH("W")) / 99.0; const int sizeY = contentRect.height() / fontHeight; _pageSize = sizeY; const int textViewportWidth = std::max(contentRect.width() - (int) fontWidth, 0); - setTabStopWidth(fontWidth * _tabWidth); + SET_TAB_STOP_DISTANCE(fontWidth * _tabWidth); const int sizeX = textViewportWidth / fontWidth; _sizeChanged = (_sizeY != sizeY) || (_sizeX != sizeX) || forcedUpdate; _sizeY = sizeY; _sizeX = sizeX; QList rowStarts; QStringList list = readLines(_screenStartPos, _screenEndPos, _sizeY, &rowStarts); if (_sizeChanged) { _averagePageSize = _screenEndPos - _screenStartPos; setUpScrollBar(); } const QStringList listRemn = readLines(_screenEndPos, _screenEndPos, 1); list << listRemn; if (list != _rowContent) { _cursorBlinkMutex.lock(); _blinkTimer.stop(); setCursorWidth(0); setPlainText(list.join("\n")); if (_cursorAnchorPos == -1 || _cursorAnchorPos == _cursorPos) { clearSelection(); _blinkTimer.start(500); } _cursorBlinkMutex.unlock(); _rowContent = list; _rowStarts = rowStarts; if (_rowStarts.size() < _sizeY) { _rowStarts << _screenEndPos; } } } qint64 ListerTextArea::textToFilePositionOnScreen(const int x, const int y, bool &isfirst) { isfirst = (x == 0); if (y >= _rowStarts.count()) { return 0; } const qint64 rowStart = _rowStarts[ y ]; if (x == 0) { return rowStart; } if (_hexMode) { const qint64 pos = rowStart + _lister->hexPositionToIndex(_sizeX, x); if (pos > _lister->fileSize()) { return _lister->fileSize(); } return pos; } // we can't use fromUnicode because of the invalid encoded chars const int maxBytes = 2 * _sizeX * MAX_CHAR_LENGTH; QByteArray chunk = _lister->cacheChunk(rowStart, maxBytes); QTextStream stream(&chunk); stream.setCodec(_lister->codec()); stream.read(x); return rowStart + stream.pos(); } void ListerTextArea::fileToTextPositionOnScreen(const qint64 p, const bool isfirst, int &x, int &y) { // check if cursor is outside of visible area if (p < _screenStartPos || p > _screenEndPos || _rowStarts.count() < 1) { x = -1; y = (p > _screenEndPos) ? -2 : -1; return; } // find row y = 0; while (y < _rowStarts.count() && _rowStarts[ y ] <= p) { y++; } y--; if (y < 0) { x = y = -1; return; } const qint64 rowStart = _rowStarts[ y ]; if (_hexMode) { x = _lister->hexIndexToPosition(_sizeX, (int)(p - rowStart)); return; } // find column const int maxBytes = 2 * _sizeX * MAX_CHAR_LENGTH; x = 0; if (rowStart >= p) { if ((rowStart == p) && !isfirst && y > 0) { const qint64 previousRow = _rowStarts[ y - 1 ]; const QByteArray chunk = _lister->cacheChunk(previousRow, maxBytes); QByteArray cachedBuffer = chunk.left(p - previousRow); QTextStream stream(&cachedBuffer); stream.setCodec(_lister->codec()); stream.read(_rowContent[ y - 1].length()); if (previousRow + stream.pos() == p) { y--; x = _rowContent[ y ].length(); } } return; } const QByteArray chunk = _lister->cacheChunk(rowStart, maxBytes); const QByteArray cachedBuffer = chunk.left(p - rowStart); x = _lister->codec()->toUnicode(cachedBuffer).length(); } void ListerTextArea::getCursorPosition(int &x, int &y) { getScreenPosition(textCursor().position(), x, y); } void ListerTextArea::getScreenPosition(const int position, int &x, int &y) { x = position; y = 0; foreach (const QString &row, _rowContent) { const int rowLen = row.length() + 1; if (x < rowLen) { return; } x -= rowLen; y++; } } void ListerTextArea::setCursorPositionOnScreen(const int x, const int y, const int anchorX, const int anchorY) { setCursorWidth(0); int finalX = x; int finalY = y; if (finalX == -1 || finalY < 0) { if (anchorY == -1) { return; } if (finalY == -2) { finalY = _sizeY; finalX = (_rowContent.count() > _sizeY) ? _rowContent[ _sizeY ].length() : 0; } else finalX = finalY = 0; } const int realSizeY = std::min(_sizeY + 1, _rowContent.count()); const auto setUpCursor = [&] (const int cursorX, const int cursorY, const QTextCursor::MoveMode mode) -> bool { if (cursorY > realSizeY) { return false; } _skipCursorChangedListener = true; moveCursor(QTextCursor::Start, mode); for (int i = 0; i < cursorY; i++) { moveCursor(QTextCursor::Down, mode); } int finalCursorX = cursorX; if (_rowContent.count() > cursorY && finalCursorX > _rowContent[ cursorY ].length()) { finalCursorX = _rowContent[ cursorY ].length(); } for (int i = 0; i < finalCursorX; i++) { moveCursor(QTextCursor::Right, mode); } _skipCursorChangedListener = false; return true; }; QTextCursor::MoveMode mode = QTextCursor::MoveAnchor; // set cursor anchor if (anchorX != -1 && anchorY != -1) { const bool canContinue = setUpCursor(anchorX, anchorY, mode); if (!canContinue) { return; } mode = QTextCursor::KeepAnchor; } // set cursor position setUpCursor(finalX, finalY, mode); } qint64 ListerTextArea::getCursorPosition(bool &isfirst) { if (cursorWidth() == 0) { isfirst = _cursorAtFirstColumn; return _cursorPos; } int x, y; getCursorPosition(x, y); return textToFilePositionOnScreen(x, y, isfirst); } void ListerTextArea::setCursorPositionInDocument(const qint64 p, const bool isfirst) { _cursorPos = p; int x, y; fileToTextPositionOnScreen(p, isfirst, x, y); bool startBlinkTimer = _screenStartPos <= _cursorPos && _cursorPos <= _screenEndPos; int anchorX = -1, anchorY = -1; if (_cursorAnchorPos != -1 && _cursorAnchorPos != p) { int anchPos = _cursorAnchorPos; bool anchorBelow = false, anchorAbove = false; if (anchPos < _screenStartPos) { anchPos = _screenStartPos; anchorY = -2; anchorAbove = true; } if (anchPos > _screenEndPos) { anchPos = _screenEndPos; anchorY = -3; anchorBelow = true; } fileToTextPositionOnScreen(anchPos, isfirst, anchorX, anchorY); if (_hexMode) { if (anchorAbove) { anchorX = 0; } if (anchorBelow && _rowContent.count() > 0) { anchorX = _rowContent[ 0 ].length(); } } startBlinkTimer = startBlinkTimer && !anchorAbove && !anchorBelow; } if (startBlinkTimer) { _blinkTimer.start(500); } setCursorPositionOnScreen(x, y, anchorX, anchorY); _lister->slotUpdate(); } void ListerTextArea::slotCursorPositionChanged() { if (_skipCursorChangedListener) { return; } int cursorX, cursorY; getCursorPosition(cursorX, cursorY); _cursorAtFirstColumn = (cursorX == 0); _cursorPos = textToFilePositionOnScreen(cursorX, cursorY, _cursorAtFirstColumn); _lister->slotUpdate(); } QString ListerTextArea::readSection(const qint64 p1, const qint64 p2) { if (p1 == p2) return QString(); qint64 sel1 = p1; qint64 sel2 = p2; if (sel1 > sel2) { std::swap(sel1, sel2); } QString section; if (_hexMode) { while (sel1 != sel2) { const QStringList list = _lister->readHexLines(sel1, sel2, _sizeX, 1); if (list.isEmpty()) { break; } if (!section.isEmpty()) { section += QChar('\n'); } section += list.at(0); } return section; } qint64 pos = sel1; QScopedPointer decoder(_lister->codec()->makeDecoder()); do { const int maxBytes = std::min(_sizeX * _sizeY * MAX_CHAR_LENGTH, (int) (sel2 - pos)); const QByteArray chunk = _lister->cacheChunk(pos, maxBytes); if (chunk.isEmpty()) break; section += decoder->toUnicode(chunk); pos += chunk.size(); } while (pos < sel2); return section; } QStringList ListerTextArea::readLines(qint64 filePos, qint64 &endPos, const int lines, QList * locs) { QStringList list; if (_hexMode) { endPos = _lister->fileSize(); if (filePos >= endPos) { return list; } const int bytes = _lister->hexBytesPerLine(_sizeX); qint64 startPos = (filePos / bytes) * bytes; qint64 shiftPos = startPos; list = _lister->readHexLines(shiftPos, endPos, _sizeX, lines); endPos = shiftPos; if (locs) { for (int i = 0; i < list.count(); i++) { (*locs) << startPos; startPos += bytes; } } return list; } endPos = filePos; const int maxBytes = _sizeX * _sizeY * MAX_CHAR_LENGTH; const QByteArray chunk = _lister->cacheChunk(filePos, maxBytes); if (chunk.isEmpty()) return list; int byteCounter = 0; QString row = ""; int effLength = 0; if (locs) (*locs) << filePos; bool skipImmediateNewline = false; const auto performNewline = [&] (qint64 nextRowStartOffset) { list << row; effLength = 0; row = ""; if (locs) { (*locs) << (filePos + nextRowStartOffset); } }; QScopedPointer decoder(_lister->codec()->makeDecoder()); while (byteCounter < chunk.size() && list.size() < lines) { const int lastCnt = byteCounter; QString chr = decoder->toUnicode(chunk.mid(byteCounter++, 1)); if (chr.isEmpty()) { continue; } if ((chr[ 0 ] < 32) && (chr[ 0 ] != '\n') && (chr[ 0 ] != '\t')) { chr = QChar(CONTROL_CHAR); } if (chr == "\n") { if (!skipImmediateNewline) { performNewline(byteCounter); } skipImmediateNewline = false; continue; } skipImmediateNewline = false; if (chr == "\t") { effLength += _tabWidth - (effLength % _tabWidth) - 1; if (effLength > _sizeX) { performNewline(lastCnt); } } row += chr; effLength++; if (effLength >= _sizeX) { performNewline(byteCounter); skipImmediateNewline = true; } } if (list.size() < lines) list << row; endPos = filePos + byteCounter; return list; } void ListerTextArea::setUpScrollBar() { if (_averagePageSize == _lister->fileSize()) { _lister->scrollBar()->setPageStep(0); _lister->scrollBar()->setMaximum(0); _lister->scrollBar()->hide(); _lastPageStartPos = 0; } else { const int maxPage = MAX_CHAR_LENGTH * _sizeX * _sizeY; qint64 pageStartPos = _lister->fileSize() - maxPage; qint64 endPos; if (pageStartPos < 0) pageStartPos = 0; QStringList list = readLines(pageStartPos, endPos, maxPage); if (list.count() <= _sizeY) { _lastPageStartPos = 0; } else { readLines(pageStartPos, _lastPageStartPos, list.count() - _sizeY); } const int maximum = (_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX : _lastPageStartPos; int pageSize = (_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX * _averagePageSize / _lastPageStartPos : _averagePageSize; if (pageSize == 0) pageSize++; _lister->scrollBar()->setPageStep(pageSize); _lister->scrollBar()->setMaximum(maximum); _lister->scrollBar()->show(); } } void ListerTextArea::keyPressEvent(QKeyEvent * ke) { if (KrGlobal::copyShortcut == QKeySequence(ke->key() | ke->modifiers())) { copySelectedToClipboard(); ke->accept(); return; } if (ke->modifiers() == Qt::NoModifier || ke->modifiers() & Qt::ShiftModifier) { qint64 newAnchor = -1; if (ke->modifiers() & Qt::ShiftModifier) { newAnchor = _cursorAnchorPos; if (_cursorAnchorPos == -1) newAnchor = _cursorPos; } switch (ke->key()) { case Qt::Key_F3: ke->accept(); if (ke->modifiers() == Qt::ShiftModifier) _lister->searchPrev(); else _lister->searchNext(); return; case Qt::Key_Home: case Qt::Key_End: _cursorAnchorPos = newAnchor; break; case Qt::Key_Left: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); int x, y; getCursorPosition(x, y); if (y == 0 && x == 0) slotActionTriggered(QAbstractSlider::SliderSingleStepSub); } break; case Qt::Key_Right: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); if (textCursor().position() == toPlainText().length()) slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); } break; case Qt::Key_Up: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); int x, y; getCursorPosition(x, y); if (y == 0) slotActionTriggered(QAbstractSlider::SliderSingleStepSub); } break; case Qt::Key_Down: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); int x, y; getCursorPosition(x, y); if (y >= _sizeY-1) slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); } break; case Qt::Key_PageDown: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); ke->accept(); int x, y; getCursorPosition(x, y); slotActionTriggered(QAbstractSlider::SliderPageStepAdd); y += _sizeY - _skippedLines; if (y > _rowContent.count()) { y = _rowContent.count() - 1; if (y > 0) x = _rowContent[ y - 1 ].length(); else x = 0; } _cursorPos = textToFilePositionOnScreen(x, y, _cursorAtFirstColumn); setCursorPositionInDocument(_cursorPos, _cursorAtFirstColumn); } return; case Qt::Key_PageUp: { _cursorAnchorPos = newAnchor; ensureVisibleCursor(); ke->accept(); int x, y; getCursorPosition(x, y); slotActionTriggered(QAbstractSlider::SliderPageStepSub); y -= _sizeY - _skippedLines; if (y < 0) { y = 0; x = 0; } _cursorPos = textToFilePositionOnScreen(x, y, _cursorAtFirstColumn); setCursorPositionInDocument(_cursorPos, _cursorAtFirstColumn); } return; } } if (ke->modifiers() == Qt::ControlModifier) { switch (ke->key()) { case Qt::Key_G: ke->accept(); _lister->jumpToPosition(); return; case Qt::Key_F: ke->accept(); _lister->enableSearch(true); return; case Qt::Key_Home: _cursorAnchorPos = -1; ke->accept(); slotActionTriggered(QAbstractSlider::SliderToMinimum); setCursorPositionInDocument((qint64)0, true); return; case Qt::Key_A: case Qt::Key_End: { _cursorAnchorPos = (ke->key() == Qt::Key_A) ? 0 : -1; ke->accept(); slotActionTriggered(QAbstractSlider::SliderToMaximum); const qint64 endPos = _lister->fileSize(); setCursorPositionInDocument(endPos, false); return; } case Qt::Key_Down: ke->accept(); slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); return; case Qt::Key_Up: ke->accept(); slotActionTriggered(QAbstractSlider::SliderSingleStepSub); return; case Qt::Key_PageDown: ke->accept(); slotActionTriggered(QAbstractSlider::SliderPageStepAdd); return; case Qt::Key_PageUp: ke->accept(); slotActionTriggered(QAbstractSlider::SliderPageStepSub); return; } } const int oldAnchor = textCursor().anchor(); KTextEdit::keyPressEvent(ke); handleAnchorChange(oldAnchor); } void ListerTextArea::mousePressEvent(QMouseEvent * e) { KTextEdit::mousePressEvent(e); // do change anchor only when shift is not pressed if (!(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) { performAnchorChange(textCursor().anchor()); } } void ListerTextArea::mouseDoubleClickEvent(QMouseEvent * e) { _cursorAnchorPos = -1; const int oldAnchor = textCursor().anchor(); KTextEdit::mouseDoubleClickEvent(e); handleAnchorChange(oldAnchor); } void ListerTextArea::mouseMoveEvent(QMouseEvent * e) { if (e->pos().y() < 0) { slotActionTriggered(QAbstractSlider::SliderSingleStepSub); } else if (e->pos().y() > height()) { slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); } KTextEdit::mouseMoveEvent(e); } void ListerTextArea::wheelEvent(QWheelEvent * e) { int delta = e->delta(); if (delta) { // zooming if (e->modifiers() & Qt::ControlModifier) { e->accept(); if (delta > 0) { zoomIn(); } else { zoomOut(); } return; } if (delta > 0) { e->accept(); while (delta > 0) { slotActionTriggered(QAbstractSlider::SliderSingleStepSub); slotActionTriggered(QAbstractSlider::SliderSingleStepSub); slotActionTriggered(QAbstractSlider::SliderSingleStepSub); delta -= 120; } } else { e->accept(); while (delta < 0) { slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); slotActionTriggered(QAbstractSlider::SliderSingleStepAdd); delta += 120; } } setCursorPositionInDocument(_cursorPos, false); } } void ListerTextArea::slotActionTriggered(int action) { switch (action) { case QAbstractSlider::SliderSingleStepAdd: { qint64 endPos; readLines(_screenStartPos, endPos, 1); if (endPos <= _lastPageStartPos) { _screenStartPos = endPos; } } break; case QAbstractSlider::SliderSingleStepSub: { if (_screenStartPos == 0) { break; } if (_hexMode) { int bytesPerRow = _lister->hexBytesPerLine(_sizeX); _screenStartPos = (_screenStartPos / bytesPerRow) * bytesPerRow; _screenStartPos -= bytesPerRow; if (_screenStartPos < 0) { _screenStartPos = 0; } break; } int maxSize = _sizeX * _sizeY * MAX_CHAR_LENGTH; const QByteArray encodedEnter = _lister->codec()->fromUnicode(QString("\n")); qint64 readPos = _screenStartPos - maxSize; if (readPos < 0) { readPos = 0; } maxSize = _screenStartPos - readPos; const QByteArray chunk = _lister->cacheChunk(readPos, maxSize); int from = chunk.size(); while (from > 0) { from--; from = chunk.lastIndexOf(encodedEnter, from); if (from == -1) { from = 0; break; } const int backRef = std::max(from - 20, 0); const int size = from - backRef + encodedEnter.size(); const QString decoded = _lister->codec()->toUnicode(chunk.mid(backRef, size)); if (decoded.endsWith(QLatin1String("\n"))) { if (from < (chunk.size() - encodedEnter.size())) { from += encodedEnter.size(); break; } } } readPos += from; qint64 previousPos = readPos; while (readPos < _screenStartPos) { previousPos = readPos; readLines(readPos, readPos, 1); } _screenStartPos = previousPos; } break; case QAbstractSlider::SliderPageStepAdd: { _skippedLines = 0; qint64 endPos; for (int i = 0; i < _sizeY; i++) { readLines(_screenStartPos, endPos, 1); if (endPos <= _lastPageStartPos) { _screenStartPos = endPos; _skippedLines++; } else { break; } } } break; case QAbstractSlider::SliderPageStepSub: { _skippedLines = 0; if (_screenStartPos == 0) { break; } if (_hexMode) { const int bytesPerRow = _lister->hexBytesPerLine(_sizeX); _screenStartPos = (_screenStartPos / bytesPerRow) * bytesPerRow; _screenStartPos -= _sizeY * bytesPerRow; if (_screenStartPos < 0) { _screenStartPos = 0; } break; } // text lister mode int maxSize = 2 * _sizeX * _sizeY * MAX_CHAR_LENGTH; const QByteArray encodedEnter = _lister->codec()->fromUnicode(QString("\n")); qint64 readPos = _screenStartPos - maxSize; if (readPos < 0) readPos = 0; maxSize = _screenStartPos - readPos; const QByteArray chunk = _lister->cacheChunk(readPos, maxSize); maxSize = chunk.size(); int sizeY = _sizeY + 1; int origSizeY = sizeY; int from = maxSize; int lastEnter = maxSize; bool readNext = true; while (readNext) { readNext = false; while (from > 0) { from--; from = chunk.lastIndexOf(encodedEnter, from); if (from == -1) { from = 0; break; } const int backRef = std::max(from - 20, 0); const int size = from - backRef + encodedEnter.size(); QString decoded = _lister->codec()->toUnicode(chunk.mid(backRef, size)); if (decoded.endsWith(QLatin1String("\n"))) { if (from < (maxSize - encodedEnter.size())) { int arrayStart = from + encodedEnter.size(); decoded = _lister->codec()->toUnicode(chunk.mid(arrayStart, lastEnter - arrayStart)); sizeY -= ((decoded.length() / (_sizeX + 1)) + 1); if (sizeY < 0) { from = arrayStart; break; } } lastEnter = from; } } qint64 searchPos = readPos + from; QList locs; while (searchPos < _screenStartPos) { locs << searchPos; readLines(searchPos, searchPos, 1); } if (locs.count() >= _sizeY) { _screenStartPos = locs[ locs.count() - _sizeY ]; } else if (from != 0) { origSizeY += locs.count() + 1; sizeY = origSizeY; readNext = true; } else if (readPos == 0) { _screenStartPos = 0; } } } break; case QAbstractSlider::SliderToMinimum: _screenStartPos = 0; break; case QAbstractSlider::SliderToMaximum: _screenStartPos = _lastPageStartPos; break; case QAbstractSlider::SliderMove: { if (_inSliderOp) // self created call? return; qint64 pos = _lister->scrollBar()->sliderPosition(); if (pos == SLIDER_MAX) { _screenStartPos = _lastPageStartPos; break; } else if (pos == 0) { _screenStartPos = 0; break; } if (_lastPageStartPos > SLIDER_MAX) pos = _lastPageStartPos * pos / SLIDER_MAX; if (pos != 0) { if (_hexMode) { const int bytesPerRow = _lister->hexBytesPerLine(_sizeX); pos = (pos / bytesPerRow) * bytesPerRow; } else { const int maxSize = _sizeX * _sizeY * MAX_CHAR_LENGTH; qint64 readPos = pos - maxSize; if (readPos < 0) readPos = 0; qint64 previousPos = readPos; while (readPos <= pos) { previousPos = readPos; readLines(readPos, readPos, 1); } pos = previousPos; } } _screenStartPos = pos; } break; case QAbstractSlider::SliderNoAction: break; }; _inSliderOp = true; const int value = (_lastPageStartPos > SLIDER_MAX) ? SLIDER_MAX * _screenStartPos / _lastPageStartPos : _screenStartPos; _lister->scrollBar()->setSliderPosition(value); _inSliderOp = false; redrawTextArea(); } void ListerTextArea::redrawTextArea(bool forcedUpdate) { if (_redrawing) { return; } _redrawing = true; bool isfirst; const qint64 pos = getCursorPosition(isfirst); calculateText(forcedUpdate); setCursorPositionInDocument(pos, isfirst); _redrawing = false; } void ListerTextArea::ensureVisibleCursor() { if (_screenStartPos <= _cursorPos && _cursorPos <= _screenEndPos) { return; } int delta = _sizeY / 2; if (delta == 0) delta++; qint64 newScreenStart = _cursorPos; while (delta) { const int maxSize = _sizeX * MAX_CHAR_LENGTH; qint64 readPos = newScreenStart - maxSize; if (readPos < 0) readPos = 0; qint64 previousPos = readPos; while (readPos < newScreenStart) { previousPos = readPos; readLines(readPos, readPos, 1); if (readPos == previousPos) break; } newScreenStart = previousPos; delta--; } if (newScreenStart > _lastPageStartPos) { newScreenStart = _lastPageStartPos; } _screenStartPos = newScreenStart; slotActionTriggered(QAbstractSlider::SliderNoAction); } void ListerTextArea::setAnchorAndCursor(qint64 anchor, qint64 cursor) { _cursorPos = cursor; _cursorAnchorPos = anchor; ensureVisibleCursor(); setCursorPositionInDocument(cursor, false); } QString ListerTextArea::getSelectedText() { if (_cursorAnchorPos != -1 && _cursorAnchorPos != _cursorPos) { return readSection(_cursorAnchorPos, _cursorPos); } return QString(); } void ListerTextArea::copySelectedToClipboard() { const QString selection = getSelectedText(); if (!selection.isEmpty()) { QApplication::clipboard()->setText(selection); } } void ListerTextArea::clearSelection() { QTextCursor cursor = textCursor(); cursor.clearSelection(); setTextCursor(cursor); _cursorAnchorPos = -1; } void ListerTextArea::performAnchorChange(int anchor) { int x, y; bool isfirst; getScreenPosition(anchor, x, y); _cursorAnchorPos = textToFilePositionOnScreen(x, y, isfirst); } void ListerTextArea::handleAnchorChange(int oldAnchor) { const int anchor = textCursor().anchor(); if (oldAnchor != anchor) { performAnchorChange(anchor); } } void ListerTextArea::setHexMode(bool hexMode) { bool isfirst; const qint64 pos = getCursorPosition(isfirst); _hexMode = hexMode; _screenStartPos = 0; calculateText(true); setCursorPositionInDocument(pos, isfirst); ensureVisibleCursor(); } void ListerTextArea::zoomIn(int range) { KTextEdit::zoomIn(range); redrawTextArea(); } void ListerTextArea::zoomOut(int range) { KTextEdit::zoomOut(range); redrawTextArea(); } ListerPane::ListerPane(Lister *lister, QWidget *parent) : QWidget(parent), _lister(lister) { } bool ListerPane::event(QEvent *e) { const bool handled = ListerPane::handleCloseEvent(e); if (!handled) { return QWidget::event(e); } return true; } bool ListerPane::handleCloseEvent(QEvent *e) { if (e->type() == QEvent::ShortcutOverride) { auto *ke = dynamic_cast(e); if (ke->key() == Qt::Key_Escape) { if (_lister->isSearchEnabled()) { _lister->searchDelete(); _lister->enableSearch(false); ke->accept(); return true; } if (!_lister->textArea()->getSelectedText().isEmpty()) { _lister->textArea()->clearSelection(); ke->accept(); return true; } } } return false; } ListerBrowserExtension::ListerBrowserExtension(Lister * lister) : KParts::BrowserExtension(lister) { _lister = lister; emit enableAction("copy", true); emit enableAction("print", true); } void ListerBrowserExtension::copy() { _lister->textArea()->copySelectedToClipboard(); } void ListerBrowserExtension::print() { _lister->print(); } class ListerEncodingMenu : public KrRemoteEncodingMenu { public: ListerEncodingMenu(Lister *lister, const QString &text, const QString &icon, KActionCollection *parent) : KrRemoteEncodingMenu(text, icon, parent), _lister(lister) { } protected: QString currentCharacterSet() override { return _lister->characterSet(); } void chooseDefault() override { _lister->setCharacterSet(QString()); } void chooseEncoding(QString encodingName) override { QString charset = KCharsets::charsets()->encodingForName(encodingName); _lister->setCharacterSet(charset); } Lister * _lister; }; Lister::Lister(QWidget *parent) : KParts::ReadOnlyPart(parent) { setXMLFile("krusaderlisterui.rc"); _actionSaveSelected = new QAction(Icon("document-save"), i18n("Save selection..."), this); connect(_actionSaveSelected, &QAction::triggered, this, &Lister::saveSelected); actionCollection()->addAction("save_selected", _actionSaveSelected); _actionSaveAs = new QAction(Icon("document-save-as"), i18n("Save as..."), this); connect(_actionSaveAs, &QAction::triggered, this, &Lister::saveAs); actionCollection()->addAction("save_as", _actionSaveAs); _actionPrint = new QAction(Icon("document-print"), i18n("Print..."), this); connect(_actionPrint, &QAction::triggered, this, &Lister::print); actionCollection()->addAction("print", _actionPrint); actionCollection()->setDefaultShortcut(_actionPrint, Qt::CTRL + Qt::Key_P); _actionSearch = new QAction(Icon("system-search"), i18n("Search"), this); connect(_actionSearch, &QAction::triggered, this, &Lister::searchAction); actionCollection()->addAction("search", _actionSearch); actionCollection()->setDefaultShortcut(_actionSearch, Qt::CTRL + Qt::Key_F); _actionSearchNext = new QAction(Icon("go-down"), i18n("Search next"), this); connect(_actionSearchNext, &QAction::triggered, this, &Lister::searchNext); actionCollection()->addAction("search_next", _actionSearchNext); actionCollection()->setDefaultShortcut(_actionSearchNext, Qt::Key_F3); _actionSearchPrev = new QAction(Icon("go-up"), i18n("Search previous"), this); connect(_actionSearchPrev, &QAction::triggered, this, &Lister::searchPrev); actionCollection()->addAction("search_prev", _actionSearchPrev); actionCollection()->setDefaultShortcut(_actionSearchPrev, Qt::SHIFT + Qt::Key_F3); _actionJumpToPosition = new QAction(Icon("go-jump"), i18n("Jump to position"), this); connect(_actionJumpToPosition, &QAction::triggered, this, &Lister::jumpToPosition); actionCollection()->addAction("jump_to_position", _actionJumpToPosition); actionCollection()->setDefaultShortcut(_actionJumpToPosition, Qt::CTRL + Qt::Key_G); _actionHexMode = new QAction(Icon("document-preview"), i18n("Hex mode"), this); connect(_actionHexMode, &QAction::triggered, this, &Lister::toggleHexMode); actionCollection()->addAction("hex_mode", _actionHexMode); actionCollection()->setDefaultShortcut(_actionHexMode, Qt::CTRL + Qt::Key_H); new ListerEncodingMenu(this, i18n("Select charset"), "character-set", actionCollection()); QWidget * widget = new ListerPane(this, parent); widget->setFocusPolicy(Qt::StrongFocus); auto *grid = new QGridLayout(widget); _textArea = new ListerTextArea(this, widget); _textArea->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); _textArea->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); _textArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _textArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); widget->setFocusProxy(_textArea); grid->addWidget(_textArea, 0, 0); _scrollBar = new QScrollBar(Qt::Vertical, widget); grid->addWidget(_scrollBar, 0, 1); _scrollBar->hide(); QWidget * statusWidget = new QWidget(widget); auto *hbox = new QHBoxLayout(statusWidget); _listerLabel = new QLabel(i18n("Lister:"), statusWidget); hbox->addWidget(_listerLabel); _searchProgressBar = new QProgressBar(statusWidget); _searchProgressBar->setMinimum(0); _searchProgressBar->setMaximum(1000); _searchProgressBar->setValue(0); _searchProgressBar->hide(); hbox->addWidget(_searchProgressBar); _searchStopButton = new QToolButton(statusWidget); _searchStopButton->setIcon(Icon("process-stop")); _searchStopButton->setToolTip(i18n("Stop search")); _searchStopButton->hide(); connect(_searchStopButton, &QToolButton::clicked, this, &Lister::searchDelete); hbox->addWidget(_searchStopButton); _searchLineEdit = new KLineEdit(statusWidget); _searchLineEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); _originalBackground = _searchLineEdit->palette().color(QPalette::Base); _originalForeground = _searchLineEdit->palette().color(QPalette::Text); connect(_searchLineEdit, &KLineEdit::returnPressed, this, &Lister::searchNext); connect(_searchLineEdit, &KLineEdit::textChanged, this, &Lister::searchTextChanged); hbox->addWidget(_searchLineEdit); _searchNextButton = new QPushButton(Icon("go-down"), i18n("Next"), statusWidget); _searchNextButton->setToolTip(i18n("Jump to next match")); connect(_searchNextButton, &QPushButton::clicked, this, &Lister::searchNext); hbox->addWidget(_searchNextButton); _searchPrevButton = new QPushButton(Icon("go-up"), i18n("Previous"), statusWidget); _searchPrevButton->setToolTip(i18n("Jump to previous match")); connect(_searchPrevButton, &QPushButton::clicked, this, &Lister::searchPrev); hbox->addWidget(_searchPrevButton); _searchOptions = new QPushButton(i18n("Options"), statusWidget); _searchOptions->setToolTip(i18n("Modify search behavior")); auto * menu = new QMenu(); _fromCursorAction = menu->addAction(i18n("From cursor")); _fromCursorAction->setCheckable(true); _fromCursorAction->setChecked(true); _caseSensitiveAction = menu->addAction(i18n("Case sensitive")); _caseSensitiveAction->setCheckable(true); _matchWholeWordsOnlyAction = menu->addAction(i18n("Match whole words only")); _matchWholeWordsOnlyAction->setCheckable(true); _regExpAction = menu->addAction(i18n("RegExp")); _regExpAction->setCheckable(true); _hexAction = menu->addAction(i18n("Hexadecimal")); _hexAction->setCheckable(true); _searchOptions->setMenu(menu); hbox->addWidget(_searchOptions); hbox->addItem(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); _statusLabel = new QLabel(statusWidget); hbox->addWidget(_statusLabel); grid->addWidget(statusWidget, 1, 0, 1, 2); setWidget(widget); connect(_scrollBar, &QScrollBar::actionTriggered, _textArea, &ListerTextArea::slotActionTriggered); connect(&_searchUpdateTimer, &QTimer::timeout, this, &Lister::slotUpdate); new ListerBrowserExtension(this); enableSearch(false); _tempFile = new QTemporaryFile(this); _tempFile->setFileTemplate(QDir::tempPath() + QLatin1String("/krusader_lister.XXXXXX")); } bool Lister::openUrl(const QUrl &listerUrl) { _downloading = false; setUrl(listerUrl); _fileSize = 0; if (listerUrl.isLocalFile()) { _filePath = listerUrl.path(); if (!QFile::exists(_filePath)) return false; _fileSize = getFileSize(); } else { if (_tempFile->isOpen()) { _tempFile->close(); } _tempFile->open(); _filePath = _tempFile->fileName(); KIO::TransferJob *downloadJob = KIO::get(listerUrl, KIO::NoReload, KIO::HideProgressInfo); connect(downloadJob, &KIO::TransferJob::data, this, [=](KIO::Job*, QByteArray array) { if (array.size() != 0) { _tempFile->write(array); } }); connect(downloadJob, &KIO::TransferJob::result, this, [=](KJob *job) { _tempFile->flush(); if (job->error()) { /* any error occurred? */ auto *kioJob = qobject_cast(job); KMessageBox::error(_textArea, i18n("Error reading file %1.", kioJob->url().toDisplayString(QUrl::PreferLocalFile))); } _downloading = false; _downloadUpdateTimer.stop(); slotUpdate(); }); connect(&_downloadUpdateTimer, &QTimer::timeout, this, [&]() { slotUpdate(); }); _downloadUpdateTimer.start(500); _downloading = true; } // invalidate cache _cache.clear(); _textArea->reset(); emit started(nullptr); emit setWindowCaption(listerUrl.toDisplayString()); emit completed(); return true; } QByteArray Lister::cacheChunk(const qint64 filePos, const int maxSize) { if (filePos >= _fileSize) { return QByteArray(); } int size = maxSize; if (_fileSize - filePos < size) { size = _fileSize - filePos; } if (!_cache.isEmpty() && (filePos >= _cachePos) && (filePos + size <= _cachePos + _cache.size())) { return _cache.mid(filePos - _cachePos, size); } const int negativeOffset = CACHE_SIZE * 2 / 5; qint64 cachePos = filePos - negativeOffset; if (cachePos < 0) cachePos = 0; QFile sourceFile(_filePath); if (!sourceFile.open(QIODevice::ReadOnly)) { return QByteArray(); } if (!sourceFile.seek(cachePos)) { return QByteArray(); } const QByteArray bytes = sourceFile.read(CACHE_SIZE); if (bytes.isEmpty()) { return bytes; } _cache = bytes; _cachePos = cachePos; const qint64 cacheRefIndex = filePos - _cachePos; int newSize = bytes.size() - cacheRefIndex; if (newSize < size) size = newSize; return _cache.mid(cacheRefIndex, size); } qint64 Lister::getFileSize() { return QFile(_filePath).size(); } void Lister::guiActivateEvent(KParts::GUIActivateEvent * event) { if (event->activated()) { slotUpdate(); _textArea->redrawTextArea(true); } else { enableSearch(false); } KParts::ReadOnlyPart::guiActivateEvent(event); } void Lister::slotUpdate() { const qint64 oldSize = _fileSize; _fileSize = getFileSize(); if (oldSize != _fileSize) _textArea->sizeChanged(); int cursorX = 0, cursorY = 0; _textArea->getCursorPosition(cursorX, cursorY); bool isfirst = false; const qint64 cursor = _textArea->getCursorPosition(isfirst); const int percent = (_fileSize == 0) ? 0 : (int)((201 * cursor) / _fileSize / 2); const QString status = i18n("Column: %1, Position: %2 (%3, %4%)", cursorX, cursor, _fileSize, percent); _statusLabel->setText(status); if (_searchProgressCounter) _searchProgressCounter--; } bool Lister::isSearchEnabled() { return !_searchLineEdit->isHidden() || !_searchProgressBar->isHidden(); } void Lister::enableSearch(const bool enable) { if (enable) { _listerLabel->setText(i18n("Search:")); _searchLineEdit->show(); _searchNextButton->show(); _searchPrevButton->show(); _searchOptions->show(); if (!_searchLineEdit->hasFocus()) { _searchLineEdit->setFocus(); const QString selection = _textArea->getSelectedText(); if (!selection.isEmpty()) { _searchLineEdit->setText(selection); } _searchLineEdit->selectAll(); } } else { _listerLabel->setText(i18n("Lister:")); _searchLineEdit->hide(); _searchNextButton->hide(); _searchPrevButton->hide(); _searchOptions->hide(); _textArea->setFocus(); } } void Lister::searchNext() { search(true); } void Lister::searchPrev() { search(false); } void Lister::search(const bool forward, const bool restart) { _restartFromBeginning = restart; if (_searchInProgress || _searchLineEdit->text().isEmpty()) return; if (_searchLineEdit->isHidden()) enableSearch(true); _searchPosition = forward ? 0 : _fileSize; if (_fromCursorAction->isChecked()) { bool isfirst; qint64 cursor = _textArea->getCursorPosition(isfirst); if (cursor != 0 && !forward) cursor--; if (_searchLastFailedPosition == -1 || _searchLastFailedPosition != cursor) _searchPosition = cursor; } const bool caseSensitive = _caseSensitiveAction->isChecked(); const bool matchWholeWord = _matchWholeWordsOnlyAction->isChecked(); const bool regExp = _regExpAction->isChecked(); const bool hex = _hexAction->isChecked(); if (hex) { QString hexcontent = _searchLineEdit->text(); hexcontent.remove(QLatin1String("0x")); hexcontent.remove(' '); hexcontent.remove('\t'); hexcontent.remove('\n'); hexcontent.remove('\r'); _searchHexQuery = QByteArray(); if (hexcontent.length() & 1) { setColor(false, false); return; } while (!hexcontent.isEmpty()) { const QString hexData = hexcontent.left(2); hexcontent = hexcontent.mid(2); bool ok = true; const int c = hexData.toUInt(&ok, 16); if (!ok) { setColor(false, false); return; } _searchHexQuery.push_back((char) c); } } else { _searchQuery.setContent(_searchLineEdit->text(), caseSensitive, matchWholeWord, codec()->name(), regExp); } _searchIsForward = forward; _searchHexadecimal = hex; QTimer::singleShot(0, this, &Lister::slotSearchMore); _searchInProgress = true; _searchProgressCounter = 3; enableActions(false); } void Lister::enableActions(const bool state) { _actionSearch->setEnabled(state); _actionSearchNext->setEnabled(state); _actionSearchPrev->setEnabled(state); _actionJumpToPosition->setEnabled(state); if (state) { _searchUpdateTimer.stop(); } else { slotUpdate(); } } void Lister::slotSearchMore() { if (!_searchInProgress) return; if (!_searchUpdateTimer.isActive()) { _searchUpdateTimer.start(200); } updateProgressBar(); if (!_searchIsForward) _searchPosition--; if (_searchPosition < 0 || _searchPosition >= _fileSize) { if (_restartFromBeginning) resetSearchPosition(); else { searchFailed(); return; } } int maxCacheSize = SEARCH_CACHE_CHARS; qint64 searchPos = _searchPosition; bool setPosition = true; if (!_searchIsForward) { qint64 origSearchPos = _searchPosition; searchPos -= maxCacheSize; if (searchPos <= 0) { searchPos = 0; _searchPosition = 0; setPosition = false; } qint64 diff = origSearchPos - searchPos; if (diff < maxCacheSize) maxCacheSize = diff; } const QByteArray chunk = cacheChunk(searchPos, maxCacheSize); if (chunk.isEmpty()) { searchFailed(); return; } const int chunkSize = chunk.size(); qint64 foundAnchor = -1; qint64 foundCursor = -1; int byteCounter = 0; if (_searchHexadecimal) { const int ndx = _searchIsForward ? chunk.indexOf(_searchHexQuery) : chunk.lastIndexOf(_searchHexQuery); if (chunkSize > _searchHexQuery.length()) { if (_searchIsForward) { _searchPosition = searchPos + chunkSize; if ((_searchPosition < _fileSize) && (chunkSize > _searchHexQuery.length())) _searchPosition -= _searchHexQuery.length(); byteCounter = _searchPosition - searchPos; } else { if (_searchPosition > 0) _searchPosition += _searchHexQuery.length(); } } if (ndx != -1) { foundAnchor = searchPos + ndx; foundCursor = foundAnchor + _searchHexQuery.length(); } } else { int rowStart = 0; QString row = ""; QScopedPointer decoder(_codec->makeDecoder()); while (byteCounter < chunkSize) { const QString chr = decoder->toUnicode(chunk.mid(byteCounter++, 1)); if (chr.isEmpty() && byteCounter < chunkSize) { continue; } if (chr != "\n") row += chr; if (chr == "\n" || row.length() >= SEARCH_MAX_ROW_LEN || byteCounter >= chunkSize) { if (setPosition) { _searchPosition = searchPos + byteCounter; if (!_searchIsForward) { _searchPosition++; setPosition = false; } } if (_searchQuery.checkLine(row, !_searchIsForward)) { QByteArray cachedBuffer = chunk.mid(rowStart, chunkSize - rowStart); QTextStream stream(&cachedBuffer); stream.setCodec(_codec); stream.read(_searchQuery.matchIndex()); foundAnchor = searchPos + rowStart + stream.pos(); stream.read(_searchQuery.matchLength()); foundCursor = searchPos + rowStart + stream.pos(); if (_searchIsForward) break; } row = ""; rowStart = byteCounter; } } } if (foundAnchor != -1 && foundCursor != -1) { _textArea->setAnchorAndCursor(foundAnchor, foundCursor); searchSucceeded(); return; } if (_searchIsForward && searchPos + byteCounter >= _fileSize) { if (_restartFromBeginning) resetSearchPosition(); else { searchFailed(); return; } } else if (_searchPosition <= 0 || _searchPosition >= _fileSize) { if (_restartFromBeginning) resetSearchPosition(); else { searchFailed(); return; } } QTimer::singleShot(0, this, &Lister::slotSearchMore); } void Lister::resetSearchPosition() { _restartFromBeginning = false; _searchPosition = _searchIsForward ? 0 : _fileSize - 1; } void Lister::searchSucceeded() { _searchInProgress = false; setColor(true, false); hideProgressBar(); _searchLastFailedPosition = -1; enableActions(true); } void Lister::searchFailed() { _searchInProgress = false; setColor(false, false); hideProgressBar(); bool isfirst; _searchLastFailedPosition = _textArea->getCursorPosition(isfirst); if (!_searchIsForward) _searchLastFailedPosition--; enableActions(true); } void Lister::searchDelete() { _searchInProgress = false; setColor(false, true); hideProgressBar(); _searchLastFailedPosition = -1; enableActions(true); } void Lister::searchTextChanged() { searchDelete(); if (_fileSize < 0x10000) { // autosearch files less than 64k if (!_searchLineEdit->text().isEmpty()) { bool isfirst; const qint64 anchor = _textArea->getCursorAnchor(); const qint64 cursor = _textArea->getCursorPosition(isfirst); if (cursor > anchor && anchor != -1) { _textArea->setCursorPositionInDocument(anchor, true); } search(true, true); } } } void Lister::setColor(const bool match, const bool restore) { QColor fore, back; if (!restore) { const KConfigGroup gc(krConfig, "Colors"); QString foreground, background; const QPalette p = QGuiApplication::palette(); if (match) { foreground = "Quicksearch Match Foreground"; background = "Quicksearch Match Background"; fore = Qt::black; back = QColor(192, 255, 192); } else { foreground = "Quicksearch Non-match Foreground"; background = "Quicksearch Non-match Background"; fore = Qt::black; back = QColor(255, 192, 192); } if (gc.readEntry(foreground, QString()) == "KDE default") fore = p.color(QPalette::Active, QPalette::Text); else if (!gc.readEntry(foreground, QString()).isEmpty()) fore = gc.readEntry(foreground, fore); if (gc.readEntry(background, QString()) == "KDE default") back = p.color(QPalette::Active, QPalette::Base); else if (!gc.readEntry(background, QString()).isEmpty()) back = gc.readEntry(background, back); } else { back = _originalBackground; fore = _originalForeground; } QPalette pal = _searchLineEdit->palette(); pal.setColor(QPalette::Base, back); pal.setColor(QPalette::Text, fore); _searchLineEdit->setPalette(pal); } void Lister::hideProgressBar() { if (!_searchProgressBar->isHidden()) { _searchProgressBar->hide(); _searchStopButton->hide(); _searchLineEdit->show(); _searchNextButton->show(); _searchPrevButton->show(); _searchOptions->show(); _listerLabel->setText(i18n("Search:")); } } void Lister::updateProgressBar() { if (_searchProgressCounter) return; if (_searchProgressBar->isHidden()) { _searchProgressBar->show(); _searchStopButton->show(); _searchOptions->hide(); _searchLineEdit->hide(); _searchNextButton->hide(); _searchPrevButton->hide(); _listerLabel->setText(i18n("Search position:")); // otherwise focus is set to document tab _textArea->setFocus(); } const qint64 pcnt = (_fileSize == 0) ? 1000 : (2001 * _searchPosition) / _fileSize / 2; const auto pctInt = (int) pcnt; if (_searchProgressBar->value() != pctInt) _searchProgressBar->setValue(pctInt); } void Lister::jumpToPosition() { bool ok = true; QString res = QInputDialog::getText(_textArea, i18n("Jump to position"), i18n("Text position:"), QLineEdit::Normal, "0", &ok); if (!ok) return; res = res.trimmed(); qint64 pos = -1; if (res.startsWith(QLatin1String("0x"))) { res = res.mid(2); bool ok; const qulonglong upos = res.toULongLong(&ok, 16); if (!ok) { KMessageBox::error(_textArea, i18n("Invalid number."), i18n("Jump to position")); return; } pos = (qint64)upos; } else { bool ok; const qulonglong upos = res.toULongLong(&ok); if (!ok) { KMessageBox::error(_textArea, i18n("Invalid number."), i18n("Jump to position")); return; } pos = (qint64)upos; } if (pos < 0 || pos > _fileSize) { KMessageBox::error(_textArea, i18n("Number out of range."), i18n("Jump to position")); return; } _textArea->deleteAnchor(); _textArea->setCursorPositionInDocument(pos, true); _textArea->ensureVisibleCursor(); } void Lister::saveAs() { const QUrl url = QFileDialog::getSaveFileUrl(_textArea, i18n("Lister")); if (url.isEmpty()) return; QUrl sourceUrl; if (!_downloading) sourceUrl = QUrl::fromLocalFile(_filePath); else sourceUrl = this->url(); QList urlList; urlList << sourceUrl; KIO::Job *job = KIO::copy(urlList, url); job->setUiDelegate(new KIO::JobUiDelegate()); KIO::getJobTracker()->registerJob(job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } void Lister::saveSelected() { bool isfirst; const qint64 start = _textArea->getCursorAnchor(); const qint64 end = _textArea->getCursorPosition(isfirst); if (start == -1 || start == end) { KMessageBox::error(_textArea, i18n("Nothing is selected."), i18n("Save selection...")); return; } if (start > end) { _savePosition = end; _saveEnd = start; } else { _savePosition = start; _saveEnd = end; } const QUrl url = QFileDialog::getSaveFileUrl(_textArea, i18n("Lister")); if (url.isEmpty()) return; KIO::TransferJob *saveJob = KIO::put(url, -1, KIO::Overwrite); connect(saveJob, &KIO::TransferJob::dataReq, this, &Lister::slotDataSend); connect(saveJob, &KIO::TransferJob::result, this, &Lister::slotSendFinished); saveJob->setUiDelegate(new KIO::JobUiDelegate()); KIO::getJobTracker()->registerJob(saveJob); saveJob->uiDelegate()->setAutoErrorHandlingEnabled(true); _actionSaveSelected->setEnabled(false); } void Lister::slotDataSend(KIO::Job *, QByteArray &array) { if (_savePosition >= _saveEnd) { array = QByteArray(); return; } qint64 max = _saveEnd - _savePosition; if (max > 1000) max = 1000; array = cacheChunk(_savePosition, (int) max); _savePosition += array.size(); } void Lister::slotSendFinished(KJob *) { _actionSaveSelected->setEnabled(true); } void Lister::setCharacterSet(const QString& set) { _characterSet = set; if (_characterSet.isEmpty()) { _codec = QTextCodec::codecForLocale(); } else { _codec = KCharsets::charsets()->codecForName(_characterSet); } _textArea->redrawTextArea(true); } void Lister::print() { bool isfirst; const qint64 anchor = _textArea->getCursorAnchor(); const qint64 cursor = _textArea->getCursorPosition(isfirst); const bool hasSelection = (anchor != -1 && anchor != cursor); const QString docName = url().fileName(); QPrinter printer; printer.setDocName(docName); QScopedPointer printDialog(new QPrintDialog(&printer, _textArea)); if (hasSelection) { printDialog->addEnabledOption(QAbstractPrintDialog::PrintSelection); } if (!printDialog->exec()) { return; } if (printer.pageOrder() == QPrinter::LastPageFirst) { switch (KMessageBox::warningContinueCancel(_textArea, i18n("Reverse printing is not supported. Continue with normal printing?"))) { case KMessageBox::Continue : break; default: return; } } QPainter painter; painter.begin(&printer); const QString dateString = QDate::currentDate().toString(Qt::SystemLocaleShortDate); const QRect pageRect = printer.pageRect(); const QRect drawingRect(0, 0, pageRect.width(), pageRect.height()); const QFont normalFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); const QFontMetrics fmNormal(normalFont); const int normalFontHeight = fmNormal.height(); const QFontMetrics fmFixed(fixedFont); const int fixedFontHeight = std::max(fmFixed.height(), 1); const int fixedFontWidth = std::max(fmFixed.QFONTMETRICS_WIDTH("W"), 1); const int effPageSize = drawingRect.height() - normalFontHeight - 1; const int rowsPerPage = std::max(effPageSize / fixedFontHeight, 1); const int columnsPerPage = std::max(drawingRect.width() / fixedFontWidth, 1); bool firstPage = true; qint64 startPos = 0; qint64 endPos = _fileSize; if (printer.printRange() == QPrinter::Selection) { if (anchor > cursor) startPos = cursor, endPos = anchor; else startPos = anchor, endPos = cursor; } int page = 0; while (startPos < endPos) { page++; QStringList rows = readLines(startPos, endPos, columnsPerPage, rowsPerPage); // print since set-up fromPage number if (printer.fromPage() && page < printer.fromPage()) { continue; } // print until set-up toPage number if (printer.toPage() && printer.toPage() >= printer.fromPage() && page > printer.toPage()) break; if (!firstPage) { printer.newPage(); } firstPage = false; // Use the painter to draw on the page. painter.setFont(normalFont); painter.drawText(drawingRect, Qt::AlignLeft, dateString); painter.drawText(drawingRect, Qt::AlignHCenter, docName); painter.drawText(drawingRect, Qt::AlignRight, QString("%1").arg(page)); painter.drawLine(0, normalFontHeight, drawingRect.width(), normalFontHeight); painter.setFont(fixedFont); int yOffset = normalFontHeight + 1; foreach (const QString &row, rows) { painter.drawText(0, yOffset + fixedFontHeight, row); yOffset += fixedFontHeight; } } } QStringList Lister::readLines(qint64 &filePos, const qint64 endPos, const int columns, const int lines) { if (_textArea->hexMode()) { return readHexLines(filePos, endPos, columns, lines); } QStringList list; const int maxBytes = std::min(columns * lines * MAX_CHAR_LENGTH, (int) (endPos - filePos)); if (maxBytes <= 0) { return list; } const QByteArray chunk = cacheChunk(filePos, maxBytes); if (chunk.isEmpty()) { return list; } QScopedPointer decoder(_codec->makeDecoder()); int byteCounter = 0; QString row = ""; bool skipImmediateNewline = false; while (byteCounter < chunk.size() && list.size() < lines) { QString chr = decoder->toUnicode(chunk.mid(byteCounter++, 1)); if (chr.isEmpty()) { continue; } // replace unreadable characters if ((chr[ 0 ] < 32) && (chr[ 0 ] != '\n') && (chr[ 0 ] != '\t')) { chr = QChar(' '); } // handle newline if (chr == "\n") { if (!skipImmediateNewline) { list << row; row = ""; } skipImmediateNewline = false; continue; } skipImmediateNewline = false; // handle tab if (chr == "\t") { const int tabLength = _textArea->tabWidth() - (row.length() % _textArea->tabWidth()); if (row.length() + tabLength > columns) { list << row; row = ""; } row += QString(tabLength, QChar(' ')); } else { // normal printable character row += chr; } if (row.length() >= columns) { list << row; row = ""; skipImmediateNewline = true; } } if (list.size() < lines) { list << row; } filePos += byteCounter; return list; } int Lister::hexPositionDigits() { int positionDigits = 0; qint64 checker = _fileSize; while (checker) { positionDigits++; checker /= 16; } if (positionDigits < 8) { return 8; } return positionDigits; } int Lister::hexBytesPerLine(const int columns) { const int positionDigits = hexPositionDigits(); if (columns >= positionDigits + 5 + 128) { return 32; } if (columns >= positionDigits + 5 + 64) { return 16; } return 8; } QStringList Lister::readHexLines(qint64 &filePos, const qint64 endPos, const int columns, const int lines) { const int positionDigits = hexPositionDigits(); const int bytesPerRow = hexBytesPerLine(columns); QStringList list; const qint64 choppedPos = (filePos / bytesPerRow) * bytesPerRow; const int maxBytes = std::min(bytesPerRow * lines, (int) (endPos - choppedPos)); if (maxBytes <= 0) return list; const QByteArray chunk = cacheChunk(choppedPos, maxBytes); if (chunk.isEmpty()) return list; int cnt = 0; for (int l = 0; l < lines; l++) { if (filePos >= endPos) { break; } const qint64 printPos = (filePos / bytesPerRow) * bytesPerRow; QString pos; pos.setNum(printPos, 16); while (pos.length() < positionDigits) pos = QString("0") + pos; pos = QString("0x") + pos; pos += QString(": "); QString charData; for (int i = 0; i != bytesPerRow; ++i, ++cnt) { const qint64 currentPos = printPos + i; if (currentPos < filePos || currentPos >= endPos) { pos += QString(" "); charData += QString(" "); } else { char c = chunk.at(cnt); auto charCode = (int)c; if (charCode < 0) charCode += 256; QString hex; hex.setNum(charCode, 16); if (hex.length() < 2) hex = QString("0") + hex; pos += hex + QString(" "); if (c < 32) c = '.'; charData += QChar(c); } } pos += QString(" ") + charData; list << pos; filePos = printPos + bytesPerRow; } if (filePos > endPos) { filePos = endPos; } return list; } int Lister::hexIndexToPosition(const int columns, const int index) { const int positionDigits = hexPositionDigits(); const int bytesPerRow = hexBytesPerLine(columns); const int finalIndex = std::min(index, bytesPerRow); return positionDigits + 4 + (3*finalIndex); } int Lister::hexPositionToIndex(const int columns, const int position) { const int positionDigits = hexPositionDigits(); const int bytesPerRow = hexBytesPerLine(columns); int finalPosition = position; finalPosition -= 4 + positionDigits; if (finalPosition <= 0) return 0; finalPosition /= 3; if (finalPosition >= bytesPerRow) return bytesPerRow; return finalPosition; } void Lister::toggleHexMode() { setHexMode(!_textArea->hexMode()); } void Lister::setHexMode(const bool mode) { if (mode) { _textArea->setHexMode(true); _actionHexMode->setText(i18n("Text mode")); } else { _textArea->setHexMode(false); _actionHexMode->setText(i18n("Hex mode")); } } diff --git a/krusader/Panel/panelcontextmenu.cpp b/krusader/Panel/panelcontextmenu.cpp index 322caf18..46560e60 100644 --- a/krusader/Panel/panelcontextmenu.cpp +++ b/krusader/Panel/panelcontextmenu.cpp @@ -1,436 +1,436 @@ /***************************************************************************** * Copyright (C) 2003 Shie Erlich * * Copyright (C) 2003 Rafi Yanai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "panelcontextmenu.h" // QtGui #include #include #include #include #include #include #include #include #include #include #include #include #include "krpreviewpopup.h" #include "listpanel.h" #include "listpanelactions.h" #include "panelfunc.h" #include "PanelView/krview.h" #include "PanelView/krviewitem.h" #include "../defaults.h" #include "../icon.h" #include "../krservices.h" #include "../krslots.h" #include "../krusader.h" #include "../krusaderview.h" #include "../panelmanager.h" #include "../Archive/krarchandler.h" #include "../FileSystem/fileitem.h" #include "../FileSystem/filesystem.h" #include "../FileSystem/krtrashhandler.h" #include "../MountMan/kmountman.h" #include "../UserAction/useractionpopupmenu.h" void PanelContextMenu::run(const QPoint &pos, KrPanel *panel) { PanelContextMenu menu(panel); QAction * res = menu.exec(pos); int result = res && res->data().canConvert() ? res->data().toInt() : -1; menu.performAction(result); } /** * Copied from dolphin/src/dolphincontextmenu.cpp and modified to add only compress and extract submenus. */ void PanelContextMenu::addCompressAndExtractPluginActions() { KFileItemListProperties props(_items); QVector jsonPlugins = KPluginLoader::findPlugins("kf5/kfileitemaction", [=](const KPluginMetaData& metaData) { return metaData.pluginId() == "compressfileitemaction" || metaData.pluginId() == "extractfileitemaction"; }); foreach (const KPluginMetaData &jsonMetadata, jsonPlugins) { auto* abstractPlugin = KPluginLoader(jsonMetadata.fileName()) .factory()->create(); if (abstractPlugin) { abstractPlugin->setParent(this); addActions(abstractPlugin->actions(props, this)); } } } PanelContextMenu::PanelContextMenu(KrPanel *krPanel, QWidget *parent) : QMenu(parent), panel(krPanel) { // selected file names const QStringList fileNames = panel->gui->getSelectedNames(); // file items QList files; for (const QString& fileName : fileNames) { files.append(panel->func->files()->getFileItem(fileName)); } // KFileItems bool allFilesAreDirs = true; for (FileItem *file : files) { _items.append(KFileItem(file->getUrl(), file->getMime(), file->getMode())); allFilesAreDirs &= file->isDir(); } if (files.empty()) { addCreateNewMenu(); addSeparator(); addEmptyMenuEntries(); return; } const bool multipleSelections = files.size() > 1; QSet protocols; for (FileItem *file : files) { protocols.insert(file->getUrl().scheme()); } const bool inTrash = protocols.contains("trash"); const bool trashOnly = inTrash && protocols.count() == 1; FileItem *file = files.first(); // ------------ the OPEN/BROWSE option - open preferred service // open/run - if not only multiple dirs are selected if (!(multipleSelections && allFilesAreDirs)) { QAction *openAction = new QAction(this); openAction->setData(QVariant(OPEN_ID)); if (multipleSelections) { openAction->setText(i18n("Open/Run Files")); } else { openAction->setText(file->isExecutable() && !file->isDir() ? i18n("Run") : i18n("Open")); const KrViewItemList viewItems = panel->view->getSelectedKrViewItems(); openAction->setIcon(viewItems.first()->icon()); } addAction(openAction); } // open in a new tab (if only folder(s) are selected) if (allFilesAreDirs) { QAction *openTab = addAction(multipleSelections ? i18n("Open in New Tabs") : i18n("Open in New Tab")); openTab->setData(QVariant(OPEN_TAB_ID)); openTab->setIcon(Icon("tab-new")); } // browse archive - if one file is selected and the file can be browsed as archive... if (!multipleSelections && !panel->func->browsableArchivePath(file->getName()).isEmpty() // ...but user disabled archive browsing... && (!KConfigGroup(krConfig, "Archives").readEntry("ArchivesAsDirectories", _ArchivesAsDirectories) // ...or the file is not a standard archive (e.g. odt, docx, etc.)... || !KrArcHandler::arcSupported(file->getMime()))) { // ...it will not be browsed as a directory by default, but add an option for it QAction *browseAct = addAction(i18n("Browse Archive")); browseAct->setData(QVariant(BROWSE_ID)); browseAct->setIcon(Icon("archive-insert-directory")); } // ------------- Preview - local filesystem only ? if (panel->func->files()->isLocal()) { // create the preview popup KrPreviewPopup preview; preview.setUrls(panel->func->files()->getUrls(fileNames)); QAction *previewAction = addMenu(&preview); previewAction->setData(QVariant(PREVIEW_ID)); previewAction->setText(i18n("Preview")); previewAction->setIcon(Icon("document-print-preview")); } // -------------- Open with: try to find-out which apps can open the file QSet uniqueMimeTypes; for (FileItem *file : files) uniqueMimeTypes.insert(file->getMime()); - const QStringList mimeTypes = uniqueMimeTypes.toList(); + const QStringList mimeTypes = uniqueMimeTypes.values(); offers = mimeTypes.count() == 1 ? KMimeTypeTrader::self()->query(mimeTypes.first()) : KFileItemActions::associatedApplications(mimeTypes, QString()); if (!offers.isEmpty()) { auto *openWithMenu = new QMenu(this); for (int i = 0; i < offers.count(); ++i) { QExplicitlySharedDataPointer service = offers[i]; if (service->isValid() && service->isApplication()) { openWithMenu->addAction(Icon(service->icon()), service->name())->setData(QVariant(SERVICE_LIST_ID + i)); } } openWithMenu->addSeparator(); if (!multipleSelections && file->isDir()) { openWithMenu->addAction(Icon("utilities-terminal"), i18n("Terminal"))->setData(QVariant(OPEN_TERM_ID)); } openWithMenu->addAction(i18n("Other..."))->setData(QVariant(CHOOSE_ID)); QAction *openWithAction = addMenu(openWithMenu); openWithAction->setText(i18n("Open With")); openWithAction->setIcon(Icon("document-open")); } addSeparator(); // --------------- user actions QAction *userAction = new UserActionPopupMenu(file->getUrl(), this); userAction->setText(i18n("User Actions")); addAction(userAction); // --------------- compress/extract actions // workaround for Bug 372999: application freezes very long time if many files are selected if (_items.length() < 1000) // add compress and extract plugins (if available) addCompressAndExtractPluginActions(); // --------------- KDE file item actions // NOTE: design and usability problem here. Services disabled in kservicemenurc settings won't // be added to the menu. But Krusader does not provide a way do change these settings (only // Dolphin does). auto *fileItemActions = new KFileItemActions(this); fileItemActions->setItemListProperties(KFileItemListProperties(_items)); fileItemActions->setParentWidget(MAIN_VIEW); fileItemActions->addServiceActionsTo(this); addSeparator(); // ------------- 'create new' submenu addCreateNewMenu(); addSeparator(); // ---------- COPY addAction(panel->gui->actions()->actCopyF5); // ------- MOVE addAction(panel->gui->actions()->actMoveF6); // ------- RENAME - only one file if (!multipleSelections && !inTrash) { addAction(panel->gui->actions()->actRenameF2); } // -------- MOVE TO TRASH if (KConfigGroup(krConfig, "General").readEntry("Move To Trash", _MoveToTrash) && panel->func->files()->canMoveToTrash(fileNames)) { addAction(Icon("user-trash"), i18n("Move to Trash"))->setData(QVariant(TRASH_ID)); } // -------- DELETE addAction(Icon("edit-delete"), i18n("Delete"))->setData(QVariant(DELETE_ID)); // -------- SHRED - only one file /* if ( panel->func->files() ->getType() == filesystem:fileSystemM_NORMAL && !fileitem->isDir() && !multipleSelections ) addAction( i18n( "Shred" ) )->setData( QVariant( SHRED_ID ) );*/ // ---------- link handling // create new shortcut or redirect links - only on local directories: if (panel->func->files()->isLocal()) { addSeparator(); auto *linkMenu = new QMenu(this); linkMenu->addAction(i18n("New Symlink..."))->setData(QVariant(NEW_SYMLINK_ID)); linkMenu->addAction(i18n("New Hardlink..."))->setData(QVariant(NEW_LINK_ID)); if (file->isSymLink()) { linkMenu->addAction(i18n("Redirect Link..."))->setData(QVariant(REDIRECT_LINK_ID)); } QAction *linkAction = addMenu(linkMenu); linkAction->setText(i18n("Link Handling")); linkAction->setIcon(Icon("insert-link")); } addSeparator(); // ---------- calculate space if (panel->func->files()->isLocal() && (file->isDir() || multipleSelections)) addAction(panel->gui->actions()->actCalculate); // ---------- mount/umount/eject if (panel->func->files()->isLocal() && file->isDir() && !multipleSelections) { const QString selectedDirectoryPath = file->getUrl().path(); if (krMtMan.getStatus(selectedDirectoryPath) == KMountMan::MOUNTED) addAction(i18n("Unmount"))->setData(QVariant(UNMOUNT_ID)); else if (krMtMan.getStatus(selectedDirectoryPath) == KMountMan::NOT_MOUNTED) addAction(i18n("Mount"))->setData(QVariant(MOUNT_ID)); if (krMtMan.ejectable(selectedDirectoryPath)) addAction(i18n("Eject"))->setData(QVariant(EJECT_ID)); } // --------- send by mail if (KrServices::supportedTools().contains("MAIL") && !file->isDir()) { addAction(Icon("mail-send"), i18n("Send by Email"))->setData(QVariant(SEND_BY_EMAIL_ID)); } // --------- empty trash if (trashOnly) { addAction(i18n("Restore"))->setData(QVariant(RESTORE_TRASHED_FILE_ID)); addAction(i18n("Empty Trash"))->setData(QVariant(EMPTY_TRASH_ID)); } #ifdef SYNCHRONIZER_ENABLED // --------- synchronize if (panel->view->numSelected()) { addAction(i18n("Synchronize Selected Files..."))->setData(QVariant(SYNC_SELECTED_ID)); } #endif // --------- copy/paste addSeparator(); addAction(panel->gui->actions()->actCut); addAction(panel->gui->actions()->actCopy); addAction(panel->gui->actions()->actPaste); addSeparator(); // --------- properties addAction(panel->gui->actions()->actProperties); } void PanelContextMenu::addEmptyMenuEntries() { addAction(panel->gui->actions()->actPaste); } void PanelContextMenu::addCreateNewMenu() { auto *createNewMenu = new QMenu(this); createNewMenu->addAction(panel->gui->actions()->actNewFolderF7); createNewMenu->addAction(panel->gui->actions()->actNewFileShiftF4); QAction *newMenuAction = addMenu(createNewMenu); newMenuAction->setText(i18n("Create New")); newMenuAction->setIcon(Icon("document-new")); } void PanelContextMenu::performAction(int id) { const QUrl singleURL = _items.isEmpty() ? QUrl() : _items.first().url(); switch (id) { case - 1 : // the user clicked outside of the menu return; case OPEN_TAB_ID : for (const KFileItem &fileItem : _items) { panel->manager()->newTab(fileItem.url(), panel); } break; case OPEN_ID : for (const KFileItem &fileItem : _items) { // do not open dirs if multiple files are selected if (_items.size() == 1 || !fileItem.isDir()) { panel->func->execute(fileItem.name()); } } break; case BROWSE_ID : panel->func->goInside(singleURL.fileName()); break; case COPY_ID : panel->func->copyFiles(); break; case MOVE_ID : panel->func->moveFiles(); break; case TRASH_ID : panel->func->deleteFiles(true); break; case DELETE_ID : panel->func->deleteFiles(false); break; case EJECT_ID : krMtMan.eject(singleURL.adjusted(QUrl::StripTrailingSlash).path()); break; // case SHRED_ID : // if ( KMessageBox::warningContinueCancel( krApp, // i18n("Do you really want to shred %1? Once shred, the file is gone forever.", item->name()), // QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "Shred" ) == KMessageBox::Continue ) // KShred::shred( panel->func->files() ->getFile( item->name() ).adjusted(QUrl::RemoveTrailingSlash).path() ); // break; case OPEN_KONQ_ID : KToolInvocation::startServiceByDesktopName("konqueror", singleURL.toDisplayString(QUrl::PreferLocalFile)); break; case CHOOSE_ID : // open-with dialog panel->func->displayOpenWithDialog(_items.urlList()); break; case MOUNT_ID : krMtMan.mount(singleURL.adjusted(QUrl::StripTrailingSlash).path()); break; case NEW_LINK_ID : panel->func->krlink(false); break; case NEW_SYMLINK_ID : panel->func->krlink(true); break; case REDIRECT_LINK_ID : panel->func->redirectLink(); break; case EMPTY_TRASH_ID : KrTrashHandler::emptyTrash(); break; case RESTORE_TRASHED_FILE_ID : KrTrashHandler::restoreTrashedFiles(_items.urlList()); break; case UNMOUNT_ID : krMtMan.unmount(singleURL.adjusted(QUrl::StripTrailingSlash).path()); break; case SEND_BY_EMAIL_ID : { SLOTS->sendFileByEmail(_items.urlList()); break; } #ifdef SYNCHRONIZER_ENABLED case SYNC_SELECTED_ID : { QStringList selectedNames; for (const KFileItem& item : _items) { selectedNames.append(item.name()); } const KrViewItemList otherItems = panel->otherPanel()->view->getSelectedKrViewItems(); for (KrViewItem *otherItem : otherItems) { const QString& name = otherItem->name(); if (!selectedNames.contains(name)) { selectedNames.append(name); } } SLOTS->slotSynchronizeDirs(selectedNames); } break; #endif case OPEN_TERM_ID : SLOTS->runTerminal(singleURL.path()); break; } // check if something from the open-with-offered-services was selected if (id >= SERVICE_LIST_ID) { const QStringList names = panel->gui->getSelectedNames(); panel->func->runService(*(offers[ id - SERVICE_LIST_ID ]), panel->func->files()->getUrls(names)); } } diff --git a/krusader/Panel/panelfunc.cpp b/krusader/Panel/panelfunc.cpp index dcd2f131..07f6a822 100644 --- a/krusader/Panel/panelfunc.cpp +++ b/krusader/Panel/panelfunc.cpp @@ -1,1405 +1,1407 @@ /***************************************************************************** * Copyright (C) 2000 Shie Erlich * * Copyright (C) 2000 Rafi Yanai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "panelfunc.h" // QtCore #include #include #include #include #include #include // QtGui #include #include // QtWidgets #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dirhistoryqueue.h" #include "krcalcspacedialog.h" #include "krerrordisplay.h" #include "krsearchbar.h" #include "listpanel.h" #include "listpanelactions.h" #include "PanelView/krview.h" #include "PanelView/krviewitem.h" #include "../krglobal.h" #include "../krslots.h" #include "../kractions.h" #include "../defaults.h" #include "../abstractpanelmanager.h" #include "../krservices.h" #include "../Archive/krarchandler.h" #include "../Archive/packjob.h" #include "../FileSystem/fileitem.h" #include "../FileSystem/virtualfilesystem.h" #include "../FileSystem/krpermhandler.h" #include "../FileSystem/filesystemprovider.h" #include "../FileSystem/sizecalculator.h" #include "../Dialogs/packgui.h" #include "../Dialogs/krdialogs.h" #include "../Dialogs/krpleasewait.h" #include "../Dialogs/krspwidgets.h" #include "../Dialogs/checksumdlg.h" #include "../KViewer/krviewer.h" #include "../MountMan/kmountman.h" QPointer ListPanelFunc::copyToClipboardOrigin; ListPanelFunc::ListPanelFunc(ListPanel *parent) : QObject(parent), panel(parent), fileSystemP(nullptr), urlManuallyEntered(false), _isPaused(true), _refreshAfterPaused(true), _quickSizeCalculator(nullptr) { history = new DirHistoryQueue(panel); delayTimer.setSingleShot(true); connect(&delayTimer, &QTimer::timeout, this, &ListPanelFunc::doRefresh); } ListPanelFunc::~ListPanelFunc() { if (fileSystemP) { fileSystemP->deleteLater(); } delete history; if (_quickSizeCalculator) _quickSizeCalculator->deleteLater(); } bool ListPanelFunc::isSyncing(const QUrl &url) { if(otherFunc()->otherFunc() == this && panel->otherPanel()->gui->syncBrowseButton->isChecked() && !otherFunc()->syncURL.isEmpty() && otherFunc()->syncURL == url) return true; return false; } void ListPanelFunc::openFileNameInternal(const QString &name, bool externallyExecutable) { if (name == "..") { dirUp(); return; } FileItem *fileitem = files()->getFileItem(name); if (fileitem == nullptr) return; QUrl url = files()->getUrl(name); if (fileitem->isDir()) { panel->view->setNameToMakeCurrent(QString()); openUrl(url); return; } QString mime = fileitem->getMime(); QUrl arcPath = browsableArchivePath(name); if (!arcPath.isEmpty()) { bool browseAsDirectory = !externallyExecutable || (KConfigGroup(krConfig, "Archives").readEntry("ArchivesAsDirectories", _ArchivesAsDirectories) && (KrArcHandler::arcSupported(mime) || KrServices::isoSupported(mime))); if (browseAsDirectory) { openUrl(arcPath); return; } } if (externallyExecutable) { if (mime == QLatin1String("application/x-desktop")) { KDesktopFileActions::runWithStartup(url, url.isLocalFile(), QByteArray()); return; } if (KRun::isExecutableFile(url, mime)) { runCommand(KShell::quoteArg(url.path())); return; } KService::Ptr service = KMimeTypeTrader::self()->preferredService(mime); if(service) { runService(*service, QList() << url); return; } displayOpenWithDialog(QList() << url); } } QUrl ListPanelFunc::cleanPath(const QUrl &urlIn) { QUrl url = urlIn; url.setPath(QDir::cleanPath(url.path())); if (!url.isValid() || url.isRelative()) { if (url.url() == "~") url = QUrl::fromLocalFile(QDir::homePath()); else if (!url.url().startsWith('/')) { // possible relative URL - translate to full URL url = files()->currentDirectory(); url.setPath(url.path() + '/' + urlIn.path()); } } url.setPath(QDir::cleanPath(url.path())); return url; } void ListPanelFunc::openUrl(const QUrl &url, const QString& nameToMakeCurrent, bool manuallyEntered) { qDebug() << "URL=" << url.toDisplayString() << "; name to current=" << nameToMakeCurrent; if (panel->syncBrowseButton->isChecked()) { //do sync-browse stuff.... if(syncURL.isEmpty()) syncURL = panel->otherPanel()->virtualPath(); QString relative = QDir(panel->virtualPath().path() + '/').relativeFilePath(url.path()); syncURL.setPath(QDir::cleanPath(syncURL.path() + '/' + relative)); panel->otherPanel()->gui->setTabState(ListPanel::TabState::DEFAULT); otherFunc()->openUrlInternal(syncURL, nameToMakeCurrent, false, false); } openUrlInternal(url, nameToMakeCurrent, false, manuallyEntered); } void ListPanelFunc::immediateOpenUrl(const QUrl &url) { openUrlInternal(url, QString(), true, false); } void ListPanelFunc::openUrlInternal(const QUrl &url, const QString& nameToMakeCurrent, bool immediately, bool manuallyEntered) { const QUrl cleanUrl = cleanPath(url); if (panel->isLocked() && !files()->currentDirectory().matches(cleanUrl, QUrl::StripTrailingSlash)) { panel->_manager->newTab(url); urlManuallyEntered = false; return; } urlManuallyEntered = manuallyEntered; const QString currentItem = history->currentUrl().path() == cleanUrl.path() ? history->currentItem() : nameToMakeCurrent; panel->view->setNameToMakeCurrent(nameToMakeCurrent); history->add(cleanUrl, currentItem); if(immediately) doRefresh(); else refresh(); } void ListPanelFunc::refresh() { panel->cancelProgress(); delayTimer.start(0); // to avoid qApp->processEvents() deadlock situation } void ListPanelFunc::doRefresh() { delayTimer.stop(); if (_isPaused) { _refreshAfterPaused = true; // simulate refresh panel->slotStartUpdate(true); return; } else { _refreshAfterPaused = false; } const QUrl url = history->currentUrl(); if(!url.isValid()) { panel->slotStartUpdate(true); // refresh the panel urlManuallyEntered = false; return; } panel->cancelProgress(); // if we are not refreshing to current URL const bool isEqualUrl = files()->currentDirectory().matches(url, QUrl::StripTrailingSlash); if (!isEqualUrl) { panel->setCursor(Qt::WaitCursor); panel->view->clearSavedSelection(); } if (panel->fileSystemError) { panel->fileSystemError->hide(); } panel->setNavigatorUrl(url); // may get a new filesystem for this url FileSystem *fileSystem = FileSystemProvider::instance().getFilesystem(url, files()); fileSystem->setParentWindow(krMainWindow); connect(fileSystem, &FileSystem::aboutToOpenDir, &krMtMan, &KMountMan::autoMount, Qt::DirectConnection); if (fileSystem != fileSystemP) { panel->view->setFiles(nullptr); // disconnect older signals disconnect(fileSystemP, nullptr, panel, nullptr); fileSystemP->deleteLater(); fileSystemP = fileSystem; // v != 0 so this is safe } else { if (fileSystemP->isRefreshing()) { // TODO remove busy waiting here delayTimer.start(100); /* if filesystem is busy try refreshing later */ return; } } // (re)connect filesystem signals disconnect(files(), nullptr, panel, nullptr); connect(files(), &DirListerInterface::scanDone, panel, &ListPanel::slotStartUpdate); connect(files(), &FileSystem::fileSystemInfoChanged, panel, &ListPanel::updateFilesystemStats); connect(files(), &FileSystem::refreshJobStarted, panel, &ListPanel::slotRefreshJobStarted); connect(files(), &FileSystem::error, panel, &ListPanel::slotFilesystemError); panel->view->setFiles(files()); if (!isEqualUrl || !panel->view->getCurrentKrViewItem()) { // set current item after refresh from history, if there is none yet panel->view->setNameToMakeCurrent(history->currentItem()); } // workaround for detecting panel deletion while filesystem is refreshing QPointer panelSave = panel; // NOTE: this is blocking. Returns false on error or interruption (cancel requested or panel // was deleted) const bool scanned = fileSystemP->refresh(url); if (scanned) { // update the history and address bar, as the actual url might differ from the one requested history->setCurrentUrl(fileSystemP->currentDirectory()); panel->setNavigatorUrl(fileSystemP->currentDirectory()); } else if (!panelSave) { return; } panel->view->setNameToMakeCurrent(QString()); panel->setCursor(Qt::ArrowCursor); // on local file system change the working directory if (files()->isLocal()) QDir::setCurrent(KrServices::urlToLocalPath(files()->currentDirectory())); // see if the open url operation failed, and if so, // put the attempted url in the navigator bar and let the user change it if (!scanned) { if(isSyncing(url)) panel->otherPanel()->gui->syncBrowseButton->setChecked(false); else if(urlManuallyEntered) { panel->setNavigatorUrl(url); if(panel == ACTIVE_PANEL) panel->editLocation(); } } if(otherFunc()->otherFunc() == this) // not true if our tab is not active otherFunc()->syncURL = QUrl(); urlManuallyEntered = false; refreshActions(); } void ListPanelFunc::setPaused(bool paused) { if (paused == _isPaused) return; _isPaused = paused; // TODO: disable refresh() in local file system when paused if (!_isPaused && _refreshAfterPaused) refresh(); } void ListPanelFunc::redirectLink() { if (!files()->isLocal()) { KMessageBox::sorry(krMainWindow, i18n("You can edit links only on local file systems")); return; } FileItem *fileitem = files()->getFileItem(panel->getCurrentName()); if (!fileitem) return; QString file = fileitem->getUrl().path(); QString currentLink = fileitem->getSymDest(); if (currentLink.isEmpty()) { KMessageBox::sorry(krMainWindow, i18n("The current file is not a link, so it cannot be redirected.")); return; } // ask the user for a new destination bool ok = false; QString newLink = QInputDialog::getText(krMainWindow, i18n("Link Redirection"), i18n("Please enter the new link destination:"), QLineEdit::Normal, currentLink, &ok); // if the user canceled - quit if (!ok || newLink == currentLink) return; // delete the current link if (unlink(file.toLocal8Bit()) == -1) { KMessageBox::sorry(krMainWindow, i18n("Cannot remove old link: %1", file)); return; } // try to create a new symlink if (symlink(newLink.toLocal8Bit(), file.toLocal8Bit()) == -1) { KMessageBox:: /* --=={ Patch by Heiner }==-- */sorry(krMainWindow, i18n("Failed to create a new link: %1", file)); return; } } void ListPanelFunc::krlink(bool sym) { if (!files()->isLocal()) { KMessageBox::sorry(krMainWindow, i18n("You can create links only on local file systems")); return; } QString name = panel->getCurrentName(); // ask the new link name.. bool ok = false; QString linkName = QInputDialog::getText(krMainWindow, i18n("New Link"), i18n("Create a new link to: %1", name), QLineEdit::Normal, name, &ok); // if the user canceled - quit if (!ok || linkName == name) return; // if the name is already taken - quit if (files()->getFileItem(linkName) != nullptr) { KMessageBox::sorry(krMainWindow, i18n("A folder or a file with this name already exists.")); return; } // make link name and target absolute path if (linkName.left(1) != "/") linkName = files()->currentDirectory().path() + '/' + linkName; name = files()->getUrl(name).path(); if (sym) { if (symlink(name.toLocal8Bit(), linkName.toLocal8Bit()) == -1) KMessageBox::sorry(krMainWindow, i18n("Failed to create a new symlink '%1' to: '%2'", linkName, name)); } else { if (link(name.toLocal8Bit(), linkName.toLocal8Bit()) == -1) KMessageBox::sorry(krMainWindow, i18n("Failed to create a new link '%1' to '%2'", linkName, name)); } } void ListPanelFunc::view() { panel->searchBar->hideBarIfSearching(); QString fileName = panel->getCurrentName(); if (fileName.isNull()) return; // if we're trying to view a directory, just exit FileItem *fileitem = files()->getFileItem(fileName); if (!fileitem || fileitem->isDir()) return; if (!fileitem->isReadable()) { KMessageBox::sorry(nullptr, i18n("No permissions to view this file.")); return; } // call KViewer. KrViewer::view(files()->getUrl(fileName)); // nothing more to it! } void ListPanelFunc::viewDlg() { // ask the user for a url to view QUrl dest = KChooseDir::getFile(i18n("Enter a URL to view:"), QUrl(panel->getCurrentName()), panel->virtualPath()); if (dest.isEmpty()) return ; // the user canceled KrViewer::view(dest); // view the file } void ListPanelFunc::terminal() { SLOTS->runTerminal(panel->lastLocalPath()); } void ListPanelFunc::editFile(const QUrl &filePath) { panel->searchBar->hideBarIfSearching(); QUrl editPath; if (!filePath.isEmpty()) { editPath = filePath; } else { const QString name = panel->getCurrentName(); if (name.isNull()) return; editPath = files()->getUrl(name); } if (editPath.isLocalFile()) { const KFileItem fileToEdit = KFileItem(editPath); if (fileToEdit.isDir()) { KMessageBox::sorry(krMainWindow, i18n("You cannot edit a folder")); return; } if (!fileToEdit.isReadable()) { KMessageBox::sorry(nullptr, i18n("No permissions to edit this file.")); return; } KrViewer::edit(editPath); } else { KIO::StatJob* statJob = KIO::stat(editPath, KIO::HideProgressInfo); connect(statJob, &KIO::StatJob::result, this, &ListPanelFunc::slotStatEdit); } } void ListPanelFunc::askEditFile() { // ask the user for the filename to edit const QUrl filePath = KChooseDir::getFile(i18n("Enter the filename to edit:"), QUrl(panel->getCurrentName()), panel->virtualPath()); if (filePath.isEmpty()) { return; // the user canceled } if (filePath.isLocalFile()) { // if the file exists, edit it instead of creating a new one QFile file(filePath.toLocalFile()); if (file.exists()) { editFile(filePath); return; } else { // simply create a local file // also because KIO::CopyJob::setDefaultPermissions does not work #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) file.open(QIODevice::NewOnly); #else file.open(QIODevice::WriteOnly); #endif file.close(); slotFileCreated(nullptr, filePath); return; } } else { KIO::StatJob* statJob = KIO::stat(filePath, KIO::HideProgressInfo); connect(statJob, &KIO::StatJob::result, this, &ListPanelFunc::slotStatEdit); } } void ListPanelFunc::slotStatEdit(KJob* job) { if (!job) return; const KIO::StatJob *statJob = dynamic_cast(job); const QUrl &url = statJob->url(); if (job->error()) { if (job->error() == KIO::ERR_DOES_NOT_EXIST) { // create a new file auto *tempFile = new QTemporaryFile; tempFile->setAutoRemove(false); // done below tempFile->open(); // create file KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(tempFile->fileName()), url); job->setUiDelegate(nullptr); job->setDefaultPermissions(true); connect(job, &KIO::CopyJob::result, this, [=](KJob *job) { slotFileCreated(job, url); }); connect(job, &KIO::CopyJob::result, tempFile, &QTemporaryFile::deleteLater); return; } else { KMessageBox::error(nullptr, job->errorString()); return; } } if (statJob->statResult().isDir()) { KMessageBox::sorry(nullptr, i18n("You cannot edit a folder")); return; } KrViewer::edit(url); } void ListPanelFunc::slotFileCreated(KJob *job, const QUrl filePath) { if (!job || (!job->error() || job->error() == KIO::ERR_FILE_ALREADY_EXIST)) { KrViewer::edit(filePath); if (KIO::upUrl(filePath).matches(panel->virtualPath(), QUrl::StripTrailingSlash)) { refresh(); } if (KIO::upUrl(filePath).matches(panel->otherPanel()->virtualPath(), QUrl::StripTrailingSlash)) { otherFunc()->refresh(); } } else { KMessageBox::sorry(krMainWindow, job->errorString()); } } void ListPanelFunc::copyFiles(bool enqueue, bool move) { panel->searchBar->hideBarIfSearching(); const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return ; // safety QUrl destination = panel->otherPanel()->virtualPath(); bool fullDestPath = false; if (fileNames.count() == 1 && otherFunc()->files()->type() != FileSystem::FS_VIRTUAL) { FileItem *item = files()->getFileItem(fileNames[0]); if (item && !item->isDir()) { fullDestPath = true; // add original filename to destination destination.setPath(QDir(destination.path()).filePath(item->getUrl().fileName())); } } if (!fullDestPath) { destination = FileSystem::ensureTrailingSlash(destination); } const KConfigGroup group(krConfig, "Advanced"); const bool showDialog = move ? group.readEntry("Confirm Move", _ConfirmMove) : group.readEntry("Confirm Copy", _ConfirmCopy); if (showDialog) { QString operationText; if (move) { operationText = fileNames.count() == 1 ? i18n("Move %1 to:", fileNames.first()) : i18np("Move %1 file to:", "Move %1 files to:", fileNames.count()); } else { operationText = fileNames.count() == 1 ? i18n("Copy %1 to:", fileNames.first()) : i18np("Copy %1 file to:", "Copy %1 files to:", fileNames.count()); } // ask the user for the copy/move dest const KChooseDir::ChooseResult result = KChooseDir::getCopyDir(operationText, destination, panel->virtualPath()); destination = result.url; if (destination.isEmpty()) return ; // the user canceled enqueue = result.enqueue; } const JobMan::StartMode startMode = enqueue && krJobMan->isQueueModeEnabled() ? JobMan::Delay : !enqueue && !krJobMan->isQueueModeEnabled() ? JobMan::Start : JobMan::Enqueue; const QList fileUrls = files()->getUrls(fileNames); if (move) { // after the delete return the cursor to the first unmarked file above the current item panel->prepareToDelete(); } // make sure the user does not overwrite multiple files by mistake if (fileNames.count() > 1) { destination = FileSystem::ensureTrailingSlash(destination); } const KIO::CopyJob::CopyMode mode = move ? KIO::CopyJob::Move : KIO::CopyJob::Copy; FileSystemProvider::instance().startCopyFiles(fileUrls, destination, mode, true, startMode); if(KConfigGroup(krConfig, "Look&Feel").readEntry("UnselectBeforeOperation", _UnselectBeforeOperation)) { panel->view->saveSelection(); panel->view->unselectAll(); } } // called from SLOTS to begin the renaming process void ListPanelFunc::rename() { panel->searchBar->hideBarIfSearching(); panel->view->renameCurrentItem(); } // called by signal itemRenamed() from the view to complete the renaming process void ListPanelFunc::rename(const QString &oldname, const QString &newname) { if (oldname == newname) return ; // do nothing // set current after rename panel->view->setNameToMakeCurrent(newname); // as always - the filesystem do the job files()->rename(oldname, newname); } void ListPanelFunc::mkdir() { QDialog dialog; dialog.setModal(true); dialog.setWindowTitle(i18n("New Folder")); QVBoxLayout layout; dialog.setLayout(&layout); QLabel comboBoxLabel(i18n("Folder's name:")); layout.addWidget(&comboBoxLabel); KrHistoryComboBox comboBox(&dialog); comboBox.setMaxCount(50); comboBox.setMinimumContentsLength(30); // Ensure that the window title is fully seen layout.addWidget(&comboBox); QDialogButtonBox buttonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, &dialog); layout.addWidget(&buttonBox); layout.setSizeConstraint(QLayout::SetFixedSize); connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); connect(&comboBox, QOverload::of(&KrHistoryComboBox::activated), &comboBox, &KrHistoryComboBox::addToHistory); // ------------------------------------------------------------------------ // load the history and completion list after creating the KrHistoryComboBox // in the configuration file: the group where the configuration is saved const QString confGroup = "Private"; KConfigGroup group(krConfig, confGroup); const QString entryName = "NewFolder"; // in the configuration file: the key where the completion list is saved const QString completionListKey = entryName + " Completion list"; QStringList list = group.readEntry(completionListKey, QStringList()); comboBox.completionObject()->setItems(list); // in the configuration file: the key where the history list is saved const QString historyListKey = entryName + " History list"; list = group.readEntry(historyListKey, QStringList()); comboBox.setHistoryItems(list); // ------------------------------------------------------------------------ // the suggested name is the complete name for the folders, // while filenames are suggested without their extension QString suggestedName = panel->getCurrentName(); if (!suggestedName.isEmpty() && !files()->getFileItem(suggestedName)->isDir()) suggestedName = QFileInfo(suggestedName).completeBaseName(); comboBox.setEditText(suggestedName); comboBox.lineEdit()->selectAll(); // ask the name of the new folder const auto dialogResult = dialog.exec(); const QString dirName = comboBox.currentText(); if (dialogResult == QDialog::Accepted) comboBox.addToHistory(dirName); // save the history and completion list list = comboBox.completionObject()->items(); group.writeEntry(completionListKey, list); list = comboBox.historyItems(); group.writeEntry(historyListKey, list); if (dialogResult != QDialog::Accepted) return; const QString firstName = dirName.section('/', 0, 0, QString::SectionIncludeLeadingSep); // if the user canceled or the name was composed of slashes -> quit if (!dirName.startsWith('/') && firstName.isEmpty()) { return; } // notify the user about an existing folder if only a single directory was given if (!dirName.contains('/') && files()->getFileItem(firstName)) { // focus the existing directory panel->view->setCurrentItem(firstName); // show an error message KMessageBox::sorry(krMainWindow, i18n("A folder or a file with this name already exists.")); return; } // focus new directory when next refresh happens panel->view->setNameToMakeCurrent(firstName); // create new directory (along with underlying directories if necessary) files()->mkDir(dirName); } void ListPanelFunc::defaultOrAlternativeDeleteFiles(bool invert) { const bool trash = KConfigGroup(krConfig, "General").readEntry("Move To Trash", _MoveToTrash); deleteFiles(trash != invert); } void ListPanelFunc::deleteFiles(bool moveToTrash) { panel->searchBar->hideBarIfSearching(); const bool isVFS = files()->type() == FileSystem::FS_VIRTUAL; if (isVFS && files()->isRoot()) { // only virtual deletion possible removeVirtualFiles(); return; } // first get the selected file names list const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return; // move to trash: only if possible moveToTrash = moveToTrash && files()->canMoveToTrash(fileNames); // now ask the user if he/she is sure: const QList confirmedUrls = confirmDeletion( files()->getUrls(fileNames), moveToTrash, isVFS, false); if (confirmedUrls.isEmpty()) return; // nothing to delete // after the delete return the cursor to the first unmarked // file above the current item; panel->prepareToDelete(); // let the filesystem do the job... files()->deleteFiles(confirmedUrls, moveToTrash); } QList ListPanelFunc::confirmDeletion(const QList &urls, bool moveToTrash, bool virtualFS, bool showPath) { QStringList files; for (const QUrl& fileUrl : urls) { files.append(showPath ? fileUrl.toDisplayString(QUrl::PreferLocalFile) : fileUrl.fileName()); } const KConfigGroup advancedGroup(krConfig, "Advanced"); if (advancedGroup.readEntry("Confirm Delete", _ConfirmDelete)) { QString s; // text KGuiItem b; // continue button if (moveToTrash) { s = i18np("Do you really want to move this item to the trash?", "Do you really want to move these %1 items to the trash?", files.count()); b = KGuiItem(i18n("&Trash")); } else if (virtualFS) { s = i18np("Do you really want to delete this item physically (not just " "removing it from the virtual items)?", "Do you really want to delete these %1 items physically (not just " "removing them from the virtual items)?", files.count()); b = KStandardGuiItem::del(); } else { s = i18np("Do you really want to delete this item?", "Do you really want to delete these %1 items?", files.count()); b = KStandardGuiItem::del(); } // show message // note: i'm using continue and not yes/no because the yes/no has cancel as default button if (KMessageBox::warningContinueCancelList(krMainWindow, s, files, i18n("Warning"), b) != KMessageBox::Continue) { return QList(); } } // we want to warn the user about non empty dir const bool emptyDirVerify = advancedGroup.readEntry("Confirm Unempty Dir", _ConfirmUnemptyDir); QList toDelete; if (emptyDirVerify) { - QSet confirmedFiles = urls.toSet(); + QSet confirmedFiles; for (const QUrl& fileUrl : urls) { + confirmedFiles.insert(fileUrl); + if (!fileUrl.isLocalFile()) { continue; // TODO only local fs supported } const QString filePath = fileUrl.toLocalFile(); QFileInfo fileInfo(filePath); if (fileInfo.isDir() && !fileInfo.isSymLink()) { // read local dir... const QDir dir(filePath); if (!dir.entryList(QDir::AllEntries | QDir::System | QDir::Hidden | QDir::NoDotAndDotDot).isEmpty()) { // ...is not empty, ask user const QString fileString = showPath ? filePath : fileUrl.fileName(); const KMessageBox::ButtonCode result = KMessageBox::warningYesNoCancel( krMainWindow, i18n("

Folder %1 is not empty.

", fileString) + (moveToTrash ? i18n("

Skip this one or trash all?

") : i18n("

Skip this one or delete all?

")), QString(), KGuiItem(i18n("&Skip")), KGuiItem(moveToTrash ? i18n("&Trash All") : i18n("&Delete All"))); if (result == KMessageBox::Yes) { confirmedFiles.remove(fileUrl); // skip this dir } else if (result == KMessageBox::No) { break; // accept all remaining } else { return QList(); // cancel } } } } - toDelete = confirmedFiles.toList(); + toDelete = confirmedFiles.values(); } else { toDelete = urls; } return toDelete; } void ListPanelFunc::removeVirtualFiles() { if (files()->type() != FileSystem::FS_VIRTUAL) { qWarning() << "filesystem not virtual"; return; } const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return; const QString text = i18np("Do you really want to delete this virtual item (physical files stay untouched)?", "Do you really want to delete these %1 virtual items (physical files stay " "untouched)?", fileNames.count()); if (KMessageBox::warningContinueCancelList(krMainWindow, text, fileNames, i18n("Warning"), KStandardGuiItem::remove()) != KMessageBox::Continue) return; auto *fileSystem = dynamic_cast(files()); fileSystem->remove(fileNames); } void ListPanelFunc::goInside(const QString& name) { openFileNameInternal(name, false); } void ListPanelFunc::runCommand(const QString& cmd) { qDebug() << "command=" << cmd; const QString workdir = panel->virtualPath().isLocalFile() ? panel->virtualPath().path() : QDir::homePath(); if(!KRun::runCommand(cmd, krMainWindow, workdir)) KMessageBox::error(nullptr, i18n("Could not start %1", cmd)); } void ListPanelFunc::runService(const KService &service, const QList& urls) { qDebug() << "service name=" << service.name(); KIO::DesktopExecParser parser(service, urls); QStringList args = parser.resultingArguments(); if (!args.isEmpty()) runCommand(KShell::joinArgs(args)); else KMessageBox::error(nullptr, i18n("%1 cannot open %2", service.name(), KrServices::toStringList(urls).join(", "))); } void ListPanelFunc::displayOpenWithDialog(const QList& urls) { // NOTE: we are not using KRun::displayOpenWithDialog() because we want the commands working // directory to be the panel directory KOpenWithDialog dialog(urls, panel); dialog.hideRunInTerminal(); if (dialog.exec()) { KService::Ptr service = dialog.service(); if(!service) service = KService::Ptr(new KService(dialog.text(), dialog.text(), QString())); runService(*service, urls); } } QUrl ListPanelFunc::browsableArchivePath(const QString &filename) { FileItem *fileitem = files()->getFileItem(filename); QUrl url = files()->getUrl(filename); QString mime = fileitem->getMime(); if(url.isLocalFile()) { QString protocol = krArcMan.registeredProtocol(mime); if(!protocol.isEmpty()) { url.setScheme(protocol); return url; } } return QUrl(); } // this is done when you double click on a file void ListPanelFunc::execute(const QString& name) { openFileNameInternal(name, true); } void ListPanelFunc::pack() { const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return ; // safety if (fileNames.count() == 0) return ; // nothing to pack // choose the default name QString defaultName = panel->virtualPath().fileName(); if (defaultName.isEmpty()) defaultName = "pack"; if (fileNames.count() == 1) defaultName = fileNames.first(); // ask the user for archive name and packer new PackGUI(defaultName, panel->otherPanel()->virtualPath().toDisplayString(QUrl::PreferLocalFile | QUrl::StripTrailingSlash), fileNames.count(), fileNames.first()); if (PackGUI::type.isEmpty()) { return ; // the user canceled } // check for partial URLs if (!PackGUI::destination.contains(":/") && !PackGUI::destination.startsWith('/')) { PackGUI::destination = panel->virtualPath().toDisplayString() + '/' + PackGUI::destination; } QString destDir = PackGUI::destination; if (!destDir.endsWith('/')) destDir += '/'; bool packToOtherPanel = (destDir == FileSystem::ensureTrailingSlash(panel->otherPanel()->virtualPath()).toDisplayString(QUrl::PreferLocalFile)); QUrl destURL = QUrl::fromUserInput(destDir + PackGUI::filename + '.' + PackGUI::type, QString(), QUrl::AssumeLocalFile); if (destURL.isLocalFile() && QFile::exists(destURL.path())) { QString msg = i18n("

The archive %1.%2 already exists. Do you want to overwrite it?

All data in the previous archive will be lost.

", PackGUI::filename, PackGUI::type); if (PackGUI::type == "zip") { msg = i18n("

The archive %1.%2 already exists. Do you want to overwrite it?

Zip will replace identically named entries in the zip archive or add entries for new names.

", PackGUI::filename, PackGUI::type); } if (KMessageBox::warningContinueCancel(krMainWindow, msg, QString(), KStandardGuiItem::overwrite()) == KMessageBox::Cancel) return ; // stop operation } else if (destURL.scheme() == QStringLiteral("virt")) { KMessageBox::error(krMainWindow, i18n("Cannot pack files onto a virtual destination.")); return; } PackJob * job = PackJob::createPacker(files()->currentDirectory(), destURL, fileNames, PackGUI::type, PackGUI::extraProps); job->setUiDelegate(new KIO::JobUiDelegate()); KIO::getJobTracker()->registerJob(job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); if (packToOtherPanel) connect(job, &PackJob::result, panel->otherPanel()->func, &ListPanelFunc::refresh); } void ListPanelFunc::testArchive() { const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return ; // safety TestArchiveJob * job = TestArchiveJob::testArchives(files()->currentDirectory(), fileNames); job->setUiDelegate(new KIO::JobUiDelegate()); KIO::getJobTracker()->registerJob(job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } void ListPanelFunc::unpack() { const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return ; // safety QString s; if (fileNames.count() == 1) s = i18n("Unpack %1 to:", fileNames[0]); else s = i18np("Unpack %1 file to:", "Unpack %1 files to:", fileNames.count()); // ask the user for the copy dest QUrl dest = KChooseDir::getDir(s, panel->otherPanel()->virtualPath(), panel->virtualPath()); if (dest.isEmpty()) return ; // the user canceled bool packToOtherPanel = (dest.matches(panel->otherPanel()->virtualPath(), QUrl::StripTrailingSlash)); UnpackJob * job = UnpackJob::createUnpacker(files()->currentDirectory(), dest, fileNames); job->setUiDelegate(new KIO::JobUiDelegate()); KIO::getJobTracker()->registerJob(job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); if (packToOtherPanel) connect(job, &UnpackJob::result, panel->otherPanel()->func, &ListPanelFunc::refresh); } void ListPanelFunc::createChecksum() { if (!panel->func->files()->isLocal()) return; // only local, non-virtual files are supported const KrViewItemList items = panel->view->getSelectedKrViewItems(); QStringList fileNames; for (KrViewItem *item : items) { FileItem *file = panel->func->getFileItem(item); fileNames.append(file->getUrl().fileName()); } if (fileNames.isEmpty()) return; // nothing selected and no valid current file Checksum::startCreationWizard(panel->virtualPath().toLocalFile(), fileNames); } void ListPanelFunc::matchChecksum() { if (!panel->func->files()->isLocal()) return; // only local, non-virtual files are supported FileItem *currentItem = files()->getFileItem(panel->getCurrentName()); const QString checksumFilePath = currentItem ? currentItem->getUrl().toLocalFile() : QString(); Checksum::startVerifyWizard(panel->virtualPath().toLocalFile(), checksumFilePath); } void ListPanelFunc::calcSpace() { QStringList fileNames; panel->view->getSelectedItems(&fileNames); if (fileNames.isEmpty()) { // current file is ".." dummy file panel->view->selectAllIncludingDirs(); panel->view->getSelectedItems(&fileNames); panel->view->unselectAll(); } SizeCalculator *sizeCalculator = createAndConnectSizeCalculator(files()->getUrls(fileNames)); KrCalcSpaceDialog::showDialog(panel, sizeCalculator); } void ListPanelFunc::quickCalcSpace() { const QString currentName = panel->getCurrentName(); if (currentName.isEmpty()) { // current file is ".." dummy, do a verbose calcSpace calcSpace(); return; } if (!_quickSizeCalculator) { _quickSizeCalculator = createAndConnectSizeCalculator(QList()); panel->connectQuickSizeCalculator(_quickSizeCalculator); } _quickSizeCalculator->add(files()->getUrl(currentName)); } SizeCalculator *ListPanelFunc::createAndConnectSizeCalculator(const QList &urls) { auto *sizeCalculator = new SizeCalculator(urls); connect(sizeCalculator, &SizeCalculator::calculated, this, &ListPanelFunc::slotSizeCalculated); connect(sizeCalculator, &SizeCalculator::finished, panel, &ListPanel::slotUpdateTotals); connect(this, &ListPanelFunc::destroyed, sizeCalculator, &SizeCalculator::deleteLater); return sizeCalculator; } void ListPanelFunc::slotSizeCalculated(const QUrl &url, KIO::filesize_t size) { KrViewItem *item = panel->view->findItemByUrl(url); if (!item) return; item->setSize(size); item->redraw(); } void ListPanelFunc::FTPDisconnect() { // you can disconnect only if connected! if (files()->isRemote()) { panel->_actions->actFTPDisconnect->setEnabled(false); panel->view->setNameToMakeCurrent(QString()); openUrl(QUrl::fromLocalFile(panel->lastLocalPath())); } } void ListPanelFunc::newFTPconnection() { QUrl url = KrSpWidgets::newFTP(); // if the user canceled - quit if (url.isEmpty()) return; panel->_actions->actFTPDisconnect->setEnabled(true); qDebug() << "URL=" << url.toDisplayString(); openUrl(url); } void ListPanelFunc::properties() { const QStringList names = panel->getSelectedNames(); if (names.isEmpty()) { return ; // no names... } KFileItemList fileItems; for (const QString& name : names) { FileItem *fileitem = files()->getFileItem(name); if (!fileitem) { continue; } fileItems.push_back(KFileItem(fileitem->getEntry(), files()->getUrl(name))); } if (fileItems.isEmpty()) return; // Show the properties dialog auto *dialog = new KPropertiesDialog(fileItems, krMainWindow); connect(dialog, &KPropertiesDialog::applied, this, &ListPanelFunc::refresh); dialog->show(); } void ListPanelFunc::refreshActions() { panel->updateButtons(); if(ACTIVE_PANEL != panel) return; QString protocol = files()->currentDirectory().scheme(); krRemoteEncoding->setEnabled(protocol == "ftp" || protocol == "sftp" || protocol == "fish" || protocol == "krarc"); //krMultiRename->setEnabled( fileSystemType == FileSystem::FS_NORMAL ); // batch rename //krProperties ->setEnabled( fileSystemType == FileSystem::FS_NORMAL || fileSystemType == FileSystem::FS_FTP ); // file properties /* krUnpack->setEnabled(true); // unpack archive krTest->setEnabled(true); // test archive krSelect->setEnabled(true); // select a group by filter krSelectAll->setEnabled(true); // select all files krUnselect->setEnabled(true); // unselect by filter krUnselectAll->setEnabled( true); // remove all selections krInvert->setEnabled(true); // invert the selection krFTPConnect->setEnabled(true); // connect to an ftp krFTPNew->setEnabled(true); // create a new connection krAllFiles->setEnabled(true); // show all files in list krCustomFiles->setEnabled(true); // show a custom set of files krRoot->setEnabled(true); // go all the way up krExecFiles->setEnabled(true); // show only executables */ panel->_actions->setViewActions[panel->panelType]->setChecked(true); panel->_actions->actFTPDisconnect->setEnabled(files()->isRemote()); // allow disconnecting a network session panel->_actions->actCreateChecksum->setEnabled(files()->isLocal()); panel->_actions->actDirUp->setEnabled(!files()->isRoot()); panel->_actions->actRoot->setEnabled(!panel->virtualPath().matches(QUrl::fromLocalFile(ROOT_DIR), QUrl::StripTrailingSlash)); panel->_actions->actHome->setEnabled(!atHome()); panel->_actions->actHistoryBackward->setEnabled(history->canGoBack()); panel->_actions->actHistoryForward->setEnabled(history->canGoForward()); panel->view->op()->emitRefreshActions(); } FileSystem* ListPanelFunc::files() { if (!fileSystemP) fileSystemP = FileSystemProvider::instance().getFilesystem(QUrl::fromLocalFile(ROOT_DIR)); return fileSystemP; } QUrl ListPanelFunc::virtualDirectory() { return _isPaused ? history->currentUrl() : files()->currentDirectory(); } FileItem *ListPanelFunc::getFileItem(const QString &name) { return files()->getFileItem(name); } FileItem *ListPanelFunc::getFileItem(KrViewItem *item) { return files()->getFileItem(item->name()); } void ListPanelFunc::clipboardChanged(QClipboard::Mode mode) { if (mode == QClipboard::Clipboard && this == copyToClipboardOrigin) { disconnect(QApplication::clipboard(), nullptr, this, nullptr); copyToClipboardOrigin = nullptr; } } void ListPanelFunc::copyToClipboard(bool move) { const QStringList fileNames = panel->getSelectedNames(); if (fileNames.isEmpty()) return ; // safety QList fileUrls = files()->getUrls(fileNames); auto *mimeData = new QMimeData; mimeData->setData("application/x-kde-cutselection", move ? "1" : "0"); mimeData->setUrls(fileUrls); if (copyToClipboardOrigin) disconnect(QApplication::clipboard(), nullptr, copyToClipboardOrigin, nullptr); copyToClipboardOrigin = this; QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); connect(QApplication::clipboard(), &QClipboard::changed, this, &ListPanelFunc::clipboardChanged); } void ListPanelFunc::pasteFromClipboard() { QClipboard * cb = QApplication::clipboard(); ListPanelFunc *origin = nullptr; if (copyToClipboardOrigin) { disconnect(QApplication::clipboard(), nullptr, copyToClipboardOrigin, nullptr); origin = copyToClipboardOrigin; copyToClipboardOrigin = nullptr; } bool move = false; const QMimeData *data = cb->mimeData(); if (data->hasFormat("application/x-kde-cutselection")) { QByteArray a = data->data("application/x-kde-cutselection"); if (!a.isEmpty()) move = (a.at(0) == '1'); // true if 1 } QList urls = data->urls(); if (urls.isEmpty()) return; if(origin && KConfigGroup(krConfig, "Look&Feel").readEntry("UnselectBeforeOperation", _UnselectBeforeOperation)) { origin->panel->view->saveSelection(); for(KrViewItem *item = origin->panel->view->getFirst(); item != nullptr; item = origin->panel->view->getNext(item)) { if (urls.contains(item->getFileItem()->getUrl())) item->setSelected(false); } } files()->addFiles(urls, move ? KIO::CopyJob::Move : KIO::CopyJob::Copy); } ListPanelFunc* ListPanelFunc::otherFunc() { return panel->otherPanel()->func; } void ListPanelFunc::historyGotoPos(int pos) { if(history->gotoPos(pos)) refresh(); } void ListPanelFunc::historyBackward() { if(history->goBack()) refresh(); } void ListPanelFunc::historyForward() { if(history->goForward()) refresh(); } void ListPanelFunc::dirUp() { openUrl(KIO::upUrl(files()->currentDirectory()), files()->currentDirectory().fileName()); } void ListPanelFunc::home() { openUrl(QUrl::fromLocalFile(QDir::homePath())); } void ListPanelFunc::root() { openUrl(QUrl::fromLocalFile(ROOT_DIR)); } void ListPanelFunc::cdToOtherPanel() { openUrl(panel->otherPanel()->virtualPath()); } void ListPanelFunc::syncOtherPanel() { otherFunc()->openUrl(panel->virtualPath()); } bool ListPanelFunc::atHome() { return QUrl::fromLocalFile(QDir::homePath()).matches(panel->virtualPath(), QUrl::StripTrailingSlash); } diff --git a/krusader/Search/krsearchmod.h b/krusader/Search/krsearchmod.h index ac0fcaec..ef865395 100644 --- a/krusader/Search/krsearchmod.h +++ b/krusader/Search/krsearchmod.h @@ -1,82 +1,83 @@ /***************************************************************************** * Copyright (C) 2001 Shie Erlich * * Copyright (C) 2001 Rafi Yanai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #ifndef KRSEARCHMOD_H #define KRSEARCHMOD_H // QtCore #include #include #include #include #include +#include #include class KrQuery; class FileItem; class FileSystem; class DefaultFileSystem; class VirtualFileSystem; /** * Search for files based on a search query. * * Subdirectories are included if query->isRecursive() is true. */ class KrSearchMod : public QObject { Q_OBJECT public: explicit KrSearchMod(const KrQuery *query); ~KrSearchMod() override; void start(); void stop(); private: void scanUrl(const QUrl &url); void scanDirectory(const QUrl &url); FileSystem *getFileSystem(const QUrl &url); signals: void searching(const QString &url); void found(const FileItem &file, const QString &foundText); // NOTE: unused void error(const QUrl &url); void finished(); private slots: void slotProcessEvents(bool &stopped); private: KrQuery *m_query; DefaultFileSystem *m_defaultFileSystem; VirtualFileSystem *m_virtualFileSystem; bool m_stopSearch; QStack m_scannedUrls; QStack m_unScannedUrls; - QTime m_timer; + QElapsedTimer m_timer; }; #endif diff --git a/krusader/Synchronizer/synchronizer.cpp b/krusader/Synchronizer/synchronizer.cpp index acb66a23..37e3b27b 100644 --- a/krusader/Synchronizer/synchronizer.cpp +++ b/krusader/Synchronizer/synchronizer.cpp @@ -1,1448 +1,1448 @@ /***************************************************************************** * Copyright (C) 2003 Csaba Karai * * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "synchronizer.h" #include "synchronizerdirlist.h" #include "../krglobal.h" #include "../krservices.h" #include "../FileSystem/filesystem.h" #include "../FileSystem/krquery.h" #include // QtCore #include #include #include #include #include #include #include // QtWidgets #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_POSIX_ACL #include #ifdef HAVE_NON_POSIX_ACL_EXTENSIONS #include #endif #endif #define DISPLAY_UPDATE_PERIOD 2 Synchronizer::Synchronizer() : displayUpdateCount(0), markEquals(true), markDiffers(true), markCopyToLeft(true), markCopyToRight(true), markDeletable(true), parentWidget(nullptr), resultListIt(resultList) { } Synchronizer::~Synchronizer() { clearLists(); } QUrl Synchronizer::fsUrl(const QString& strUrl) { QUrl result = QUrl::fromUserInput(strUrl, QString(), QUrl::AssumeLocalFile); return KrServices::escapeFileUrl(result); } void Synchronizer::clearLists() { QListIterator i1(resultList); while (i1.hasNext()) delete i1.next(); resultList.clear(); QListIterator i2(stack); while (i2.hasNext()) delete i2.next(); stack.clear(); temporaryList.clear(); } void Synchronizer::reset() { displayUpdateCount = 0; markEquals = markDiffers = markCopyToLeft = markCopyToRight = markDeletable = true; stopped = false; recurseSubDirs = followSymLinks = ignoreDate = asymmetric = cmpByContent = ignoreCase = autoScroll = false; markEquals = markDiffers = markCopyToLeft = markCopyToRight = markDeletable = markDuplicates = markSingles = false; leftCopyEnabled = rightCopyEnabled = deleteEnabled = overWrite = autoSkip = paused = false; leftCopyNr = rightCopyNr = deleteNr = 0; leftCopySize = rightCopySize = deleteSize = 0; comparedDirs = fileCount = 0; leftBaseDir.clear(); rightBaseDir.clear(); clearLists(); } int Synchronizer::compare(QString leftURL, QString rightURL, KrQuery *query, bool subDirs, bool symLinks, bool igDate, bool asymm, bool cmpByCnt, bool igCase, bool autoSc, QStringList &selFiles, int equThres, int timeOffs, int parThreads, bool hiddenFiles) { clearLists(); recurseSubDirs = subDirs; followSymLinks = symLinks; ignoreDate = igDate; asymmetric = asymm; cmpByContent = cmpByCnt; autoScroll = autoSc; ignoreCase = igCase; selectedFiles = selFiles; equalsThreshold = equThres; timeOffset = timeOffs; parallelThreads = parThreads; ignoreHidden = hiddenFiles; stopped = false; this->query = query; leftURL = KUrlCompletion::replacedPath(leftURL, true, true); rightURL = KUrlCompletion::replacedPath(rightURL, true, true); if (!leftURL.endsWith('/')) leftURL += '/'; if (!rightURL.endsWith('/')) rightURL += '/'; excludedPaths = KrServices::toStringList(query->dontSearchInDirs()); for (int i = 0; i != excludedPaths.count(); i++) if (excludedPaths[ i ].endsWith('/')) excludedPaths[ i ].truncate(excludedPaths[ i ].length() - 1); comparedDirs = fileCount = 0; stack.append(new CompareTask(nullptr, leftBaseDir = leftURL, rightBaseDir = rightURL, "", "", ignoreHidden)); compareLoop(); QListIterator it(temporaryList); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); if (item->isTemporary()) delete item; } temporaryList.clear(); if (!autoScroll) refresh(true); emit statusInfo(i18n("Number of files: %1", fileCount)); return fileCount; } void Synchronizer::compareLoop() { while (!stopped && !stack.isEmpty()) { for (int thread = 0; thread < (int)stack.count() && thread < parallelThreads; thread++) { SynchronizerTask * entry = stack.at(thread); if (entry->state() == ST_STATE_NEW) entry->start(parentWidget); if (entry->inherits("CompareTask")) { if (entry->state() == ST_STATE_READY) { auto *ctentry = qobject_cast( entry); if (ctentry->isDuplicate()) compareDirectory(ctentry->parent(), ctentry->leftDirList(), ctentry->rightDirList(), ctentry->leftDir(), ctentry->rightDir()); else addSingleDirectory(ctentry->parent(), ctentry->dirList(), ctentry->dir(), ctentry->isLeft()); } if (entry->state() == ST_STATE_READY || entry->state() == ST_STATE_ERROR) comparedDirs++; } switch (entry->state()) { case ST_STATE_STATUS: emit statusInfo(entry->status()); break; case ST_STATE_READY: case ST_STATE_ERROR: emit statusInfo(i18n("Number of compared folders: %1", comparedDirs)); stack.removeAll(entry); delete entry; continue; default: break; } } if (!stack.isEmpty()) qApp->processEvents(); } QListIterator it(stack); while (it.hasNext()) delete it.next(); stack.clear(); } void Synchronizer::compareDirectory(SynchronizerFileItem *parent, SynchronizerDirList * left_directory, SynchronizerDirList * right_directory, const QString &leftDir, const QString &rightDir) { const QString &leftURL = left_directory->url(); const QString &rightURL = right_directory->url(); FileItem *left_file; FileItem *right_file; QString file_name; bool checkIfSelected = false; if (leftDir.isEmpty() && rightDir.isEmpty() && selectedFiles.count()) checkIfSelected = true; /* walking through in the left directory */ for (left_file = left_directory->first(); left_file != nullptr && !stopped; left_file = left_directory->next()) { if (isDir(left_file)) continue; file_name = left_file->getName(); if (checkIfSelected && !selectedFiles.contains(file_name)) continue; if (!query->match(left_file)) continue; if ((right_file = right_directory->search(file_name, ignoreCase)) == nullptr) addLeftOnlyItem(parent, file_name, leftDir, left_file->getSize(), left_file->getModificationTime(), readLink(left_file), left_file->getOwner(), left_file->getGroup(), left_file->getMode(), left_file->getACL()); else { if (isDir(right_file)) continue; addDuplicateItem(parent, file_name, right_file->getName(), leftDir, rightDir, left_file->getSize(), right_file->getSize(), left_file->getModificationTime(), right_file->getModificationTime(), readLink(left_file), readLink(right_file), left_file->getOwner(), right_file->getOwner(), left_file->getGroup(), right_file->getGroup(), left_file->getMode(), right_file->getMode(), left_file->getACL(), right_file->getACL()); } } /* walking through in the right directory */ for (right_file = right_directory->first(); right_file != nullptr && !stopped; right_file = right_directory->next()) { if (isDir(right_file)) continue; file_name = right_file->getName(); if (checkIfSelected && !selectedFiles.contains(file_name)) continue; if (!query->match(right_file)) continue; if (left_directory->search(file_name, ignoreCase) == nullptr) addRightOnlyItem(parent, file_name, rightDir, right_file->getSize(), right_file->getModificationTime(), readLink(right_file), right_file->getOwner(), right_file->getGroup(), right_file->getMode(), right_file->getACL()); } /* walking through the subdirectories */ if (recurseSubDirs) { for (left_file = left_directory->first(); left_file != nullptr && !stopped; left_file = left_directory->next()) { if (left_file->isDir() && (followSymLinks || !left_file->isSymLink())) { QString left_file_name = left_file->getName(); if (checkIfSelected && !selectedFiles.contains(left_file_name)) continue; if (excludedPaths.contains(leftDir.isEmpty() ? left_file_name : leftDir + '/' + left_file_name)) continue; if (!query->matchDirName(left_file_name)) continue; if ((right_file = right_directory->search(left_file_name, ignoreCase)) == nullptr) { SynchronizerFileItem *me = addLeftOnlyItem(parent, left_file_name, leftDir, 0, left_file->getModificationTime(), readLink(left_file), left_file->getOwner(), left_file->getGroup(), left_file->getMode(), left_file->getACL(), true, !query->match(left_file)); stack.append(new CompareTask(me, leftURL + left_file_name + '/', leftDir.isEmpty() ? left_file_name : leftDir + '/' + left_file_name, true, ignoreHidden)); } else { QString right_file_name = right_file->getName(); SynchronizerFileItem *me = addDuplicateItem(parent, left_file_name, right_file_name, leftDir, rightDir, 0, 0, left_file->getModificationTime(), right_file->getModificationTime(), readLink(left_file), readLink(right_file), left_file->getOwner(), right_file->getOwner(), left_file->getGroup(), right_file->getGroup(), left_file->getMode(), right_file->getMode(), left_file->getACL(), right_file->getACL(), true, !query->match(left_file)); stack.append(new CompareTask(me, leftURL + left_file_name + '/', rightURL + right_file_name + '/', leftDir.isEmpty() ? left_file_name : leftDir + '/' + left_file_name, rightDir.isEmpty() ? right_file_name : rightDir + '/' + right_file_name, ignoreHidden)); } } } /* walking through the right side subdirectories */ for (right_file = right_directory->first(); right_file != nullptr && !stopped; right_file = right_directory->next()) { if (right_file->isDir() && (followSymLinks || !right_file->isSymLink())) { file_name = right_file->getName(); if (checkIfSelected && !selectedFiles.contains(file_name)) continue; if (excludedPaths.contains(rightDir.isEmpty() ? file_name : rightDir + '/' + file_name)) continue; if (!query->matchDirName(file_name)) continue; if (left_directory->search(file_name, ignoreCase) == nullptr) { SynchronizerFileItem *me = addRightOnlyItem(parent, file_name, rightDir, 0, right_file->getModificationTime(), readLink(right_file), right_file->getOwner(), right_file->getGroup(), right_file->getMode(), right_file->getACL(), true, !query->match(right_file)); stack.append(new CompareTask(me, rightURL + file_name + '/', rightDir.isEmpty() ? file_name : rightDir + '/' + file_name, false, ignoreHidden)); } } } } } QString Synchronizer::getTaskTypeName(TaskType taskType) { static QString names[] = {"=", "!=", "<-", "->", "DEL", "?", "?", "?", "?", "?"}; return names[taskType]; } SynchronizerFileItem * Synchronizer::addItem(SynchronizerFileItem *parent, const QString &leftFile, const QString &rightFile, const QString &leftDir, const QString &rightDir, bool existsLeft, bool existsRight, KIO::filesize_t leftSize, KIO::filesize_t rightSize, time_t leftDate, time_t rightDate, const QString &leftLink, const QString &rightLink, const QString &leftOwner, const QString &rightOwner, const QString &leftGroup, const QString &rightGroup, mode_t leftMode, mode_t rightMode, const QString &leftACL, const QString &rightACL, TaskType tsk, bool isDir, bool isTemp) { bool marked = autoScroll ? !isTemp && isMarked(tsk, existsLeft && existsRight) : false; auto *item = new SynchronizerFileItem(leftFile, rightFile, leftDir, rightDir, marked, existsLeft, existsRight, leftSize, rightSize, leftDate, rightDate, leftLink, rightLink, leftOwner, rightOwner, leftGroup, rightGroup, leftMode, rightMode, leftACL, rightACL, tsk, isDir, isTemp, parent); if (!isTemp) { while (parent && parent->isTemporary()) setPermanent(parent); bool doRefresh = false; if (marked) { fileCount++; if (autoScroll && markParentDirectories(item)) doRefresh = true; } resultList.append(item); emit comparedFileData(item); if (doRefresh) refresh(true); if (marked && (displayUpdateCount++ % DISPLAY_UPDATE_PERIOD == (DISPLAY_UPDATE_PERIOD - 1))) qApp->processEvents(); } else temporaryList.append(item); return item; } void Synchronizer::compareContentResult(SynchronizerFileItem * item, bool res) { item->compareContentResult(res); bool marked = autoScroll ? isMarked(item->task(), item->existsInLeft() && item->existsInRight()) : false; item->setMarked(marked); if (marked) { markParentDirectories(item); fileCount++; emit markChanged(item, true); } } void Synchronizer::setPermanent(SynchronizerFileItem *item) { if (item->parent() && item->parent()->isTemporary()) setPermanent(item->parent()); item->setPermanent(); resultList.append(item); emit comparedFileData(item); } SynchronizerFileItem * Synchronizer::addLeftOnlyItem(SynchronizerFileItem *parent, const QString &file_name, const QString &dir, KIO::filesize_t size, time_t date, const QString &link, const QString &owner, const QString &group, mode_t mode, const QString &acl, bool isDir, bool isTemp) { return addItem(parent, file_name, file_name, dir, dir, true, false, size, 0, date, 0, link, QString(), owner, QString(), group, QString(), mode, (mode_t) - 1, acl, QString(), asymmetric ? TT_DELETE : TT_COPY_TO_RIGHT, isDir, isTemp); } SynchronizerFileItem * Synchronizer::addRightOnlyItem(SynchronizerFileItem *parent, const QString &file_name, const QString &dir, KIO::filesize_t size, time_t date, const QString &link, const QString &owner, const QString &group, mode_t mode, const QString &acl, bool isDir, bool isTemp) { return addItem(parent, file_name, file_name, dir, dir, false, true, 0, size, 0, date, QString(), link, QString(), owner, QString(), group, (mode_t) - 1, mode, QString(), acl, TT_COPY_TO_LEFT, isDir, isTemp); } SynchronizerFileItem * Synchronizer::addDuplicateItem(SynchronizerFileItem *parent, const QString &leftName, const QString &rightName, const QString &leftDir, const QString &rightDir, KIO::filesize_t leftSize, KIO::filesize_t rightSize, time_t leftDate, time_t rightDate, const QString &leftLink, const QString &rightLink, const QString &leftOwner, const QString &rightOwner, const QString &leftGroup, const QString &rightGroup, mode_t leftMode, mode_t rightMode, const QString &leftACL, const QString &rightACL, bool isDir, bool isTemp) { TaskType task; int checkedRightDate = rightDate - timeOffset; int uncertain = 0; do { if (isDir) { task = TT_EQUALS; break; } if (leftSize == rightSize) { if (!leftLink.isNull() || !rightLink.isNull()) { if (leftLink == rightLink) { task = TT_EQUALS; break; } } else if (cmpByContent) uncertain = TT_UNKNOWN; else { if (ignoreDate || leftDate == checkedRightDate) { task = TT_EQUALS; break; } time_t diff = (leftDate > checkedRightDate) ? leftDate - checkedRightDate : checkedRightDate - leftDate; if (diff <= equalsThreshold) { task = TT_EQUALS; break; } } } if (ignoreDate) task = TT_DIFFERS; else if (asymmetric) { // To avoid that the default action is // to overwrite a file by an old version of it if (leftDate > checkedRightDate) task = TT_DIFFERS; else task = TT_COPY_TO_LEFT; } else if (leftDate > checkedRightDate) task = TT_COPY_TO_RIGHT; else if (leftDate < checkedRightDate) task = TT_COPY_TO_LEFT; else task = TT_DIFFERS; } while (false); SynchronizerFileItem * item = addItem(parent, leftName, rightName, leftDir, rightDir, true, true, leftSize, rightSize, leftDate, rightDate, leftLink, rightLink, leftOwner, rightOwner, leftGroup, rightGroup, leftMode, rightMode, leftACL, rightACL, (TaskType)(task + uncertain), isDir, isTemp); if (uncertain == TT_UNKNOWN) { QUrl leftURL = Synchronizer::fsUrl(leftDir.isEmpty() ? leftBaseDir + leftName : leftBaseDir + leftDir + '/' + leftName); QUrl rightURL = Synchronizer::fsUrl(rightDir.isEmpty() ? rightBaseDir + rightName : rightBaseDir + rightDir + '/' + rightName); stack.append(new CompareContentTask(this, item, leftURL, rightURL, leftSize)); } return item; } void Synchronizer::addSingleDirectory(SynchronizerFileItem *parent, SynchronizerDirList *directory, const QString &dirName, bool isLeft) { const QString &url = directory->url(); FileItem *file; QString file_name; /* walking through the directory files */ for (file = directory->first(); file != nullptr && !stopped; file = directory->next()) { if (isDir(file)) continue; file_name = file->getName(); if (!query->match(file)) continue; if (isLeft) addLeftOnlyItem(parent, file_name, dirName, file->getSize(), file->getModificationTime(), readLink(file), file->getOwner(), file->getGroup(), file->getMode(), file->getACL()); else addRightOnlyItem(parent, file_name, dirName, file->getSize(), file->getModificationTime(), readLink(file), file->getOwner(), file->getGroup(), file->getMode(), file->getACL()); } /* walking through the subdirectories */ for (file = directory->first(); file != nullptr && !stopped; file = directory->next()) { if (file->isDir() && (followSymLinks || !file->isSymLink())) { file_name = file->getName(); if (excludedPaths.contains(dirName.isEmpty() ? file_name : dirName + '/' + file_name)) continue; if (!query->matchDirName(file_name)) continue; SynchronizerFileItem *me; if (isLeft) me = addLeftOnlyItem(parent, file_name, dirName, 0, file->getModificationTime(), readLink(file), file->getOwner(), file->getGroup(), file->getMode(), file->getACL(), true, !query->match(file)); else me = addRightOnlyItem(parent, file_name, dirName, 0, file->getModificationTime(), readLink(file), file->getOwner(), file->getGroup(), file->getMode(), file->getACL(), true, !query->match(file)); stack.append(new CompareTask(me, url + file_name + '/', dirName.isEmpty() ? file_name : dirName + '/' + file_name, isLeft, ignoreHidden)); } } } void Synchronizer::setMarkFlags(bool left, bool equal, bool differs, bool right, bool dup, bool sing, bool del) { markEquals = equal; markDiffers = differs; markCopyToLeft = left; markCopyToRight = right; markDeletable = del; markDuplicates = dup; markSingles = sing; } bool Synchronizer::isMarked(TaskType task, bool isDuplicate) { if ((isDuplicate && !markDuplicates) || (!isDuplicate && !markSingles)) return false; switch (task) { case TT_EQUALS: return markEquals; case TT_DIFFERS: return markDiffers; case TT_COPY_TO_LEFT: return markCopyToLeft; case TT_COPY_TO_RIGHT: return markCopyToRight; case TT_DELETE: return markDeletable; default: return false; } } bool Synchronizer::markParentDirectories(SynchronizerFileItem *item) { if (item->parent() == nullptr || item->parent()->isMarked()) return false; markParentDirectories(item->parent()); item->parent()->setMarked(true); fileCount++; emit markChanged(item->parent(), false); return true; } int Synchronizer::refresh(bool nostatus) { fileCount = 0; QListIterator it(resultList); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); bool marked = isMarked(item->task(), item->existsInLeft() && item->existsInRight()); item->setMarked(marked); if (marked) { markParentDirectories(item); fileCount++; } } it.toFront(); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); emit markChanged(item, false); } if (!nostatus) emit statusInfo(i18n("Number of files: %1", fileCount)); return fileCount; } void Synchronizer::operate(SynchronizerFileItem *item, void (*executeOperation)(SynchronizerFileItem *)) { executeOperation(item); if (item->isDir()) { QString leftDirName = (item->leftDirectory().isEmpty()) ? item->leftName() : item->leftDirectory() + '/' + item->leftName(); QString rightDirName = (item->rightDirectory().isEmpty()) ? item->rightName() : item->rightDirectory() + '/' + item->rightName(); QListIterator it(resultList); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); if (item->leftDirectory() == leftDirName || item->leftDirectory().startsWith(leftDirName + '/') || item->rightDirectory() == rightDirName || item->rightDirectory().startsWith(rightDirName + '/')) executeOperation(item); } } } void Synchronizer::excludeOperation(SynchronizerFileItem *item) { item->setTask(TT_DIFFERS); } void Synchronizer::exclude(SynchronizerFileItem *item) { if (!item->parent() || item->parent()->task() != TT_DELETE) operate(item, excludeOperation); /* exclude only if the parent task is not DEL */ } void Synchronizer::restoreOperation(SynchronizerFileItem *item) { item->restoreOriginalTask(); } void Synchronizer::restore(SynchronizerFileItem *item) { operate(item, restoreOperation); while ((item = item->parent()) != nullptr) /* in case of restore, the parent directories */ { /* must be changed for being consistent */ if (item->task() != TT_DIFFERS) break; if (item->originalTask() == TT_DELETE) /* if the parent original task is delete */ break; /* don't touch it */ item->restoreOriginalTask(); /* restore */ } } void Synchronizer::reverseDirectionOperation(SynchronizerFileItem *item) { if (item->existsInRight() && item->existsInLeft()) { if (item->task() == TT_COPY_TO_LEFT) item->setTask(TT_COPY_TO_RIGHT); else if (item->task() == TT_COPY_TO_RIGHT) item->setTask(TT_COPY_TO_LEFT); } } void Synchronizer::reverseDirection(SynchronizerFileItem *item) { operate(item, reverseDirectionOperation); } void Synchronizer::deleteLeftOperation(SynchronizerFileItem *item) { if (!item->existsInRight() && item->existsInLeft()) item->setTask(TT_DELETE); } void Synchronizer::deleteLeft(SynchronizerFileItem *item) { operate(item, deleteLeftOperation); } void Synchronizer::copyToLeftOperation(SynchronizerFileItem *item) { if (item->existsInRight()) { if (!item->isDir()) item->setTask(TT_COPY_TO_LEFT); else { if (item->existsInLeft() && item->existsInRight()) item->setTask(TT_EQUALS); else if (!item->existsInLeft() && item->existsInRight()) item->setTask(TT_COPY_TO_LEFT); } } } void Synchronizer::copyToLeft(SynchronizerFileItem *item) { operate(item, copyToLeftOperation); while ((item = item->parent()) != nullptr) { if (item->task() != TT_DIFFERS) break; if (item->existsInLeft() && item->existsInRight()) item->setTask(TT_EQUALS); else if (!item->existsInLeft() && item->existsInRight()) item->setTask(TT_COPY_TO_LEFT); } } void Synchronizer::copyToRightOperation(SynchronizerFileItem *item) { if (item->existsInLeft()) { if (!item->isDir()) item->setTask(TT_COPY_TO_RIGHT); else { if (item->existsInLeft() && item->existsInRight()) item->setTask(TT_EQUALS); else if (item->existsInLeft() && !item->existsInRight()) item->setTask(TT_COPY_TO_RIGHT); } } } void Synchronizer::copyToRight(SynchronizerFileItem *item) { operate(item, copyToRightOperation); while ((item = item->parent()) != nullptr) { if (item->task() != TT_DIFFERS && item->task() != TT_DELETE) break; if (item->existsInLeft() && item->existsInRight()) item->setTask(TT_EQUALS); else if (item->existsInLeft() && !item->existsInRight()) item->setTask(TT_COPY_TO_RIGHT); } } bool Synchronizer::totalSizes(int * leftCopyNr, KIO::filesize_t *leftCopySize, int * rightCopyNr, KIO::filesize_t *rightCopySize, int *deleteNr, KIO::filesize_t *deletableSize) { bool hasAnythingToDo = false; *leftCopySize = *rightCopySize = *deletableSize = 0; *leftCopyNr = *rightCopyNr = *deleteNr = 0; QListIterator it(resultList); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); if (item->isMarked()) { switch (item->task()) { case TT_COPY_TO_LEFT: *leftCopySize += item->rightSize(); (*leftCopyNr)++; hasAnythingToDo = true; break; case TT_COPY_TO_RIGHT: *rightCopySize += item->leftSize(); (*rightCopyNr)++; hasAnythingToDo = true; break; case TT_DELETE: *deletableSize += item->leftSize(); (*deleteNr)++; hasAnythingToDo = true; break; default: break; } } } return hasAnythingToDo; } void Synchronizer::swapSides() { QString leftTmp = leftBaseDir; leftBaseDir = rightBaseDir; rightBaseDir = leftTmp; QListIterator it(resultList); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); item->swap(asymmetric); } } void Synchronizer::setScrolling(bool scroll) { autoScroll = scroll; if (autoScroll) { int oldFileCount = fileCount; refresh(true); fileCount = oldFileCount; } } void Synchronizer::synchronize(QWidget *syncWdg, bool leftCopyEnabled, bool rightCopyEnabled, bool deleteEnabled, bool overWrite, int parThreads) { this->leftCopyEnabled = leftCopyEnabled; this->rightCopyEnabled = rightCopyEnabled; this->deleteEnabled = deleteEnabled; this->overWrite = overWrite; this->parallelThreads = parThreads; this->syncDlgWidget = syncWdg; autoSkip = paused = disableNewTasks = false; leftCopyNr = rightCopyNr = deleteNr = 0; leftCopySize = rightCopySize = deleteSize = 0; inTaskFinished = 0; lastTask = nullptr; jobMap.clear(); receivedMap.clear(); resultListIt = resultList; synchronizeLoop(); } void Synchronizer::synchronizeLoop() { if (disableNewTasks) { if (!resultListIt.hasNext() && jobMap.count() == 0) emit synchronizationFinished(); return; } while ((int)jobMap.count() < parallelThreads) { SynchronizerFileItem *task = getNextTask(); if (task == nullptr) { if (jobMap.count() == 0) emit synchronizationFinished(); return; } executeTask(task); if (disableNewTasks) break; } } SynchronizerFileItem * Synchronizer::getNextTask() { TaskType task; SynchronizerFileItem * currentTask; do { if (!resultListIt.hasNext()) return nullptr; currentTask = resultListIt.next(); if (currentTask->isMarked()) { task = currentTask->task(); if (leftCopyEnabled && task == TT_COPY_TO_LEFT) break; else if (rightCopyEnabled && task == TT_COPY_TO_RIGHT) break; else if (deleteEnabled && task == TT_DELETE) break; } } while (true); return lastTask = currentTask; } void Synchronizer::executeTask(SynchronizerFileItem * task) { QString leftDirName = task->leftDirectory(); if (!leftDirName.isEmpty()) leftDirName += '/'; QString rightDirName = task->rightDirectory(); if (!rightDirName.isEmpty()) rightDirName += '/'; QUrl leftURL = Synchronizer::fsUrl(leftBaseDir + leftDirName + task->leftName()); QUrl rightURL = Synchronizer::fsUrl(rightBaseDir + rightDirName + task->rightName()); switch (task->task()) { case TT_COPY_TO_LEFT: if (task->isDir()) { KIO::SimpleJob *job = KIO::mkdir(leftURL); connect(job, &KIO::MkdirJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; disableNewTasks = true; } else { QUrl destURL(leftURL); if (!task->destination().isNull()) destURL = Synchronizer::fsUrl(task->destination()); if (task->rightLink().isNull()) { KIO::FileCopyJob *job = KIO::file_copy(rightURL, destURL, -1, ((overWrite || task->overWrite()) ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo); connect(job, SIGNAL(processedSize(KJob*,qulonglong)), this, SLOT(slotProcessedSize(KJob*,qulonglong))); connect(job, &KIO::FileCopyJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; } else { KIO::SimpleJob *job = KIO::symlink(task->rightLink(), destURL, ((overWrite || task->overWrite()) ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo); connect(job, &KIO::SimpleJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; } } break; case TT_COPY_TO_RIGHT: if (task->isDir()) { KIO::SimpleJob *job = KIO::mkdir(rightURL); connect(job, &KIO::SimpleJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; disableNewTasks = true; } else { QUrl destURL(rightURL); if (!task->destination().isNull()) destURL = Synchronizer::fsUrl(task->destination()); if (task->leftLink().isNull()) { KIO::FileCopyJob *job = KIO::file_copy(leftURL, destURL, -1, ((overWrite || task->overWrite()) ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo); connect(job, SIGNAL(processedSize(KJob*,qulonglong)), this, SLOT(slotProcessedSize(KJob*,qulonglong))); connect(job, &KIO::FileCopyJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; } else { KIO::SimpleJob *job = KIO::symlink(task->leftLink(), destURL, ((overWrite || task->overWrite()) ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo); connect(job, &KIO::SimpleJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; } } break; case TT_DELETE: { KIO::DeleteJob *job = KIO::del(leftURL, KIO::DefaultFlags); connect(job, &KIO::DeleteJob::result, this, &Synchronizer::slotTaskFinished); jobMap[ job ] = task; } break; default: break; } } void Synchronizer::slotTaskFinished(KJob *job) { inTaskFinished++; SynchronizerFileItem * item = jobMap[ job ]; jobMap.remove(job); KIO::filesize_t receivedSize = 0; if (receivedMap.contains(job)) { receivedSize = receivedMap[ job ]; receivedMap.remove(job); } if (disableNewTasks && item == lastTask) disableNewTasks = false; // the blocker task finished QString leftDirName = item->leftDirectory().isEmpty() ? "" : item->leftDirectory() + '/'; QString rightDirName = item->rightDirectory().isEmpty() ? "" : item->rightDirectory() + '/'; QUrl leftURL = Synchronizer::fsUrl(leftBaseDir + leftDirName + item->leftName()); QUrl rightURL = Synchronizer::fsUrl(rightBaseDir + rightDirName + item->rightName()); do { if (!job->error()) { switch (item->task()) { case TT_COPY_TO_LEFT: if (leftURL.isLocalFile()) { struct utimbuf timestamp; timestamp.actime = time(nullptr); timestamp.modtime = item->rightDate() - timeOffset; utime((const char *)(leftURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), ×tamp); auto newOwnerID = (uid_t) - 1; // chown(2) : -1 means no change if (!item->rightOwner().isEmpty()) { struct passwd* pw = getpwnam(QFile::encodeName(item->rightOwner())); if (pw != nullptr) newOwnerID = pw->pw_uid; } auto newGroupID = (gid_t) - 1; // chown(2) : -1 means no change if (!item->rightGroup().isEmpty()) { struct group* g = getgrnam(QFile::encodeName(item->rightGroup())); if (g != nullptr) newGroupID = g->gr_gid; } int status1 = chown((const char *)(leftURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), newOwnerID, (gid_t) - 1); int status2 = chown((const char *)(leftURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), (uid_t) - 1, newGroupID); if (status1 < 0 || status2 < 0) { // synchronizer currently ignores chown errors } chmod((const char *)(leftURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), item->rightMode() & 07777); #ifdef HAVE_POSIX_ACL if (!item->rightACL().isNull()) { acl_t acl = acl_from_text(item->rightACL().toLatin1()); if (acl && !acl_valid(acl)) acl_set_file(leftURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit(), ACL_TYPE_ACCESS, acl); if (acl) acl_free(acl); } #endif } break; case TT_COPY_TO_RIGHT: if (rightURL.isLocalFile()) { struct utimbuf timestamp; timestamp.actime = time(nullptr); timestamp.modtime = item->leftDate() + timeOffset; utime((const char *)(rightURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), ×tamp); auto newOwnerID = (uid_t) - 1; // chown(2) : -1 means no change if (!item->leftOwner().isEmpty()) { struct passwd* pw = getpwnam(QFile::encodeName(item->leftOwner())); if (pw != nullptr) newOwnerID = pw->pw_uid; } auto newGroupID = (gid_t) - 1; // chown(2) : -1 means no change if (!item->leftGroup().isEmpty()) { struct group* g = getgrnam(QFile::encodeName(item->leftGroup())); if (g != nullptr) newGroupID = g->gr_gid; } int status1 = chown((const char *)(rightURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), newOwnerID, (uid_t) - 1); int status2 = chown((const char *)(rightURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), (uid_t) - 1, newGroupID); if (status1 < 0 || status2 < 0) { // synchronizer currently ignores chown errors } chmod((const char *)(rightURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit()), item->leftMode() & 07777); #ifdef HAVE_POSIX_ACL if (!item->leftACL().isNull()) { acl_t acl = acl_from_text(item->leftACL().toLatin1()); if (acl && !acl_valid(acl)) acl_set_file(rightURL.adjusted(QUrl::StripTrailingSlash).path().toLocal8Bit(), ACL_TYPE_ACCESS, acl); if (acl) acl_free(acl); } #endif } break; default: break; } } else { if (job->error() == KIO::ERR_FILE_ALREADY_EXIST && item->task() != TT_DELETE) { KIO::RenameDialog_Result result; QString newDest; if (autoSkip) break; auto *ui = dynamic_cast(job->uiDelegate()); ui->setWindow(syncDlgWidget); if (item->task() == TT_COPY_TO_LEFT) { result = ui->askFileRename(job, i18n("File Already Exists"), rightURL, leftURL, KIO::RenameDialog_Overwrite | KIO::RenameDialog_Skip | KIO::RenameDialog_MultipleItems, newDest, item->rightSize(), item->leftSize(), QDateTime(), QDateTime(), QDateTime::fromTime_t(item->rightDate()), QDateTime::fromTime_t(item->leftDate())); } else { result = ui->askFileRename(job, i18n("File Already Exists"), leftURL, rightURL, KIO::RenameDialog_Overwrite | KIO::RenameDialog_Skip | KIO::RenameDialog_MultipleItems, newDest, item->leftSize(), item->rightSize(), QDateTime(), QDateTime(), QDateTime::fromTime_t(item->leftDate()), QDateTime::fromTime_t(item->rightDate())); } switch (result) { case KIO::Result_Rename: item->setDestination(newDest); executeTask(item); inTaskFinished--; return; case KIO::Result_Overwrite: item->setOverWrite(); executeTask(item); inTaskFinished--; return; case KIO::Result_OverwriteAll: overWrite = true; executeTask(item); inTaskFinished--; return; case KIO::Result_AutoSkip: autoSkip = true; case KIO::Result_Skip: default: break; } break; } if (job->error() != KIO::ERR_DOES_NOT_EXIST || item->task() != TT_DELETE) { if (autoSkip) break; QString error; switch (item->task()) { case TT_COPY_TO_LEFT: error = i18n("Error at copying file %1 to %2.", rightURL.toDisplayString(QUrl::PreferLocalFile), leftURL.toDisplayString(QUrl::PreferLocalFile)); break; case TT_COPY_TO_RIGHT: error = i18n("Error at copying file %1 to %2.", leftURL.toDisplayString(QUrl::PreferLocalFile), rightURL.toDisplayString(QUrl::PreferLocalFile)); break; case TT_DELETE: error = i18n("Error at deleting file %1.", leftURL.toDisplayString(QUrl::PreferLocalFile)); break; default: break; } auto *ui = dynamic_cast(job->uiDelegate()); ui->setWindow(syncDlgWidget); KIO::SkipDialog_Result result = ui->askSkip(job, KIO::SkipDialog_MultipleItems, error); switch (result) { case KIO::Result_Cancel: executeTask(item); /* simply retry */ inTaskFinished--; return; case KIO::Result_AutoSkip: autoSkip = true; default: break; } } } } while (false); switch (item->task()) { case TT_COPY_TO_LEFT: leftCopyNr++; leftCopySize += item->rightSize() - receivedSize; break; case TT_COPY_TO_RIGHT: rightCopyNr++; rightCopySize += item->leftSize() - receivedSize; break; case TT_DELETE: deleteNr++; deleteSize += item->leftSize() - receivedSize; break; default: break; } emit processedSizes(leftCopyNr, leftCopySize, rightCopyNr, rightCopySize, deleteNr, deleteSize); if (--inTaskFinished == 0) { if (paused) emit pauseAccepted(); else synchronizeLoop(); } } void Synchronizer::slotProcessedSize(KJob * job , qulonglong size) { KIO::filesize_t dl = 0, dr = 0, dd = 0; SynchronizerFileItem * item = jobMap[ job ]; KIO::filesize_t lastProcessedSize = 0; if (receivedMap.contains(job)) lastProcessedSize = receivedMap[ job ]; receivedMap[ job ] = size; switch (item->task()) { case TT_COPY_TO_LEFT: dl = size - lastProcessedSize; break; case TT_COPY_TO_RIGHT: dr = size - lastProcessedSize; break; case TT_DELETE: dd = size - lastProcessedSize; break; default: break; } emit processedSizes(leftCopyNr, leftCopySize += dl, rightCopyNr, rightCopySize += dr, deleteNr, deleteSize += dd); } void Synchronizer::pause() { paused = true; } void Synchronizer::resume() { paused = false; synchronizeLoop(); } QString Synchronizer::leftBaseDirectory() { return leftBaseDir; } QString Synchronizer::rightBaseDirectory() { return rightBaseDir; } KgetProgressDialog::KgetProgressDialog(QWidget *parent, const QString &caption, const QString &text, bool modal) : QDialog(parent) { if (caption.isEmpty()) setWindowTitle(caption); setModal(modal); auto *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(new QLabel(text)); mProgressBar = new QProgressBar; mainLayout->addWidget(mProgressBar); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); mPauseButton = new QPushButton(i18n("Pause")); buttonBox->addButton(mPauseButton, QDialogButtonBox::ActionRole); buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); connect(mPauseButton, &QPushButton::clicked, this, &KgetProgressDialog::slotPause); connect(buttonBox, &QDialogButtonBox::rejected, this, &KgetProgressDialog::slotCancel); mCancelled = mPaused = false; } void KgetProgressDialog::slotPause() { if ((mPaused = !mPaused) == false) mPauseButton->setText(i18n("Pause")); else mPauseButton->setText(i18n("Resume")); } void KgetProgressDialog::slotCancel() { mCancelled = true; reject(); } void Synchronizer::synchronizeWithKGet() { bool isLeftLocal = QUrl::fromUserInput(leftBaseDirectory(), QString(), QUrl::AssumeLocalFile).isLocalFile(); KgetProgressDialog *progDlg = nullptr; int processedCount = 0, totalCount = 0; QListIterator it(resultList); while (it.hasNext()) if (it.next()->isMarked()) totalCount++; it.toFront(); while (it.hasNext()) { SynchronizerFileItem * item = it.next(); if (item->isMarked()) { QUrl downloadURL; QUrl destURL; QString destDir; QString leftDirName = item->leftDirectory().isEmpty() ? "" : item->leftDirectory() + '/'; QString rightDirName = item->rightDirectory().isEmpty() ? "" : item->rightDirectory() + '/'; if (progDlg == nullptr) { progDlg = new KgetProgressDialog(krMainWindow, i18n("Krusader::Synchronizer"), i18n("Feeding the URLs to KGet"), true); progDlg->progressBar()->setMaximum(totalCount); progDlg->show(); qApp->processEvents(); } if (item->task() == TT_COPY_TO_RIGHT && !isLeftLocal) { downloadURL = Synchronizer::fsUrl(leftBaseDirectory() + leftDirName + item->leftName()); destDir = rightBaseDirectory() + rightDirName; destURL = Synchronizer::fsUrl(destDir + item->rightName()); if (item->isDir()) destDir += item->leftName(); } if (item->task() == TT_COPY_TO_LEFT && isLeftLocal) { downloadURL = Synchronizer::fsUrl(rightBaseDirectory() + rightDirName + item->rightName()); destDir = leftBaseDirectory() + leftDirName; destURL = Synchronizer::fsUrl(destDir + item->leftName()); if (item->isDir()) destDir += item->rightName(); } // creating the directory system for (int i = 0; i >= 0 ; i = destDir.indexOf('/', i + 1)) if (!QDir(destDir.left(i)).exists()) QDir().mkdir(destDir.left(i)); if (!item->isDir() && !downloadURL.isEmpty()) { if (QFile(destURL.path()).exists()) QFile(destURL.path()).remove(); QString source = downloadURL.toDisplayString(); if (source.indexOf('@') >= 2) { /* is this an ftp proxy URL? */ int lastAt = source.lastIndexOf('@'); QString startString = source.left(lastAt); QString endString = source.mid(lastAt); startString.replace('@', "%40"); source = startString + endString; } KProcess p; p << KrServices::fullPathName("kget") << source << destURL.path(); if (!p.startDetached()) KMessageBox::error(parentWidget, i18n("Error executing %1.", KrServices::fullPathName("kget"))); } progDlg->progressBar()->setValue(++processedCount); - QTime t; + QElapsedTimer t; t.start(); bool canExit = false; do { qApp->processEvents(); if (progDlg->wasCancelled()) break; canExit = (t.elapsed() > 100); if (progDlg->isPaused() || !canExit) usleep(10000); } while (progDlg->isPaused() || !canExit); if (progDlg->wasCancelled()) break; } } if (progDlg) delete progDlg; } bool Synchronizer::isDir(const FileItem *file) { if (followSymLinks) { return file->isDir(); } else { return file->isDir() && !file->isSymLink(); } } QString Synchronizer::readLink(const FileItem *file) { if (file->isSymLink()) return file->getSymDest(); else return QString(); } SynchronizerFileItem *Synchronizer::getItemAt(unsigned ndx) { if (ndx < (unsigned)resultList.count()) return resultList.at(ndx); else return nullptr; } diff --git a/krusader/Synchronizer/synchronizertask.cpp b/krusader/Synchronizer/synchronizertask.cpp index 72b3a7d2..f170756e 100644 --- a/krusader/Synchronizer/synchronizertask.cpp +++ b/krusader/Synchronizer/synchronizertask.cpp @@ -1,341 +1,342 @@ /***************************************************************************** * Copyright (C) 2006 Csaba Karai * * Copyright (C) 2006-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #include "synchronizertask.h" // QtCore #include #include +#include #include #include #include "synchronizer.h" #include "synchronizerfileitem.h" #include "synchronizerdirlist.h" #include "../FileSystem/filesystem.h" CompareTask::CompareTask(SynchronizerFileItem *parentIn, const QString &leftURL, const QString &rightURL, const QString &leftDir, const QString &rightDir, bool hidden) : m_parent(parentIn), m_url(leftURL), m_dir(leftDir), m_otherUrl(rightURL), m_otherDir(rightDir), m_duplicate(true), m_dirList(nullptr), m_otherDirList(nullptr) { ignoreHidden = hidden; } CompareTask::CompareTask(SynchronizerFileItem *parentIn, const QString &urlIn, const QString &dirIn, bool isLeftIn, bool hidden) : m_parent(parentIn), m_url(urlIn), m_dir(dirIn), m_isLeft(isLeftIn), m_duplicate(false), m_dirList(nullptr), m_otherDirList(nullptr) { ignoreHidden = hidden; } CompareTask::~CompareTask() { if (m_dirList) { delete m_dirList; m_dirList = nullptr; } if (m_otherDirList) { delete m_otherDirList; m_otherDirList = nullptr; } } void CompareTask::start() { if (m_state == ST_STATE_NEW) { m_state = ST_STATE_PENDING; m_loadFinished = m_otherLoadFinished = false; m_dirList = new SynchronizerDirList(parentWidget, ignoreHidden); connect(m_dirList, &SynchronizerDirList::finished, this, &CompareTask::slotFinished); m_dirList->load(m_url, false); if (m_duplicate) { m_otherDirList = new SynchronizerDirList(parentWidget, ignoreHidden); connect(m_otherDirList, &SynchronizerDirList::finished, this, &CompareTask::slotOtherFinished); m_otherDirList->load(m_otherUrl, false); } } } void CompareTask::slotFinished(bool result) { if (!result) { m_state = ST_STATE_ERROR; return; } m_loadFinished = true; if (m_otherLoadFinished || !m_duplicate) m_state = ST_STATE_READY; } void CompareTask::slotOtherFinished(bool result) { if (!result) { m_state = ST_STATE_ERROR; return; } m_otherLoadFinished = true; if (m_loadFinished) m_state = ST_STATE_READY; } CompareContentTask::CompareContentTask(Synchronizer *syn, SynchronizerFileItem *itemIn, const QUrl &leftURLIn, const QUrl &rightURLIn, KIO::filesize_t sizeIn) : leftURL(leftURLIn), rightURL(rightURLIn), size(sizeIn), errorPrinted(false), leftReadJob(nullptr), rightReadJob(nullptr), compareArray(), owner(-1), item(itemIn), timer(nullptr), leftFile(nullptr), rightFile(nullptr), received(0), sync(syn) { } CompareContentTask::~CompareContentTask() { abortContentComparing(); if (timer) delete timer; if (leftFile) delete leftFile; if (rightFile) delete rightFile; } void CompareContentTask::start() { m_state = ST_STATE_PENDING; if (leftURL.isLocalFile() && rightURL.isLocalFile()) { leftFile = new QFile(leftURL.path()); if (!leftFile->open(QIODevice::ReadOnly)) { KMessageBox::error(parentWidget, i18n("Error at opening %1.", leftURL.path())); m_state = ST_STATE_ERROR; return; } rightFile = new QFile(rightURL.path()); if (!rightFile->open(QIODevice::ReadOnly)) { KMessageBox::error(parentWidget, i18n("Error at opening %1.", rightURL.path())); m_state = ST_STATE_ERROR; return; } timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &CompareContentTask::sendStatusMessage); timer->setSingleShot(true); timer->start(1000); localFileCompareCycle(); } else { leftReadJob = KIO::get(leftURL, KIO::NoReload, KIO::HideProgressInfo); rightReadJob = KIO::get(rightURL, KIO::NoReload, KIO::HideProgressInfo); connect(leftReadJob, &KIO::TransferJob::data, this, &CompareContentTask::slotDataReceived); connect(rightReadJob, &KIO::TransferJob::data, this, &CompareContentTask::slotDataReceived); connect(leftReadJob, &KIO::TransferJob::result, this, &CompareContentTask::slotFinished); connect(rightReadJob, &KIO::TransferJob::result, this, &CompareContentTask::slotFinished); rightReadJob->suspend(); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &CompareContentTask::sendStatusMessage); timer->setSingleShot(true); timer->start(1000); } } void CompareContentTask::localFileCompareCycle() { bool different = false; char leftBuffer[ 1440 ]; char rightBuffer[ 1440 ]; - QTime timer; + QElapsedTimer timer; timer.start(); int cnt = 0; while (!leftFile->atEnd() && !rightFile->atEnd()) { int leftBytes = leftFile->read(leftBuffer, sizeof(leftBuffer)); int rightBytes = rightFile->read(rightBuffer, sizeof(rightBuffer)); if (leftBytes != rightBytes) { different = true; break; } if (leftBytes <= 0) break; received += leftBytes; if (memcmp(leftBuffer, rightBuffer, leftBytes)) { different = true; break; } if ((++cnt % 16) == 0 && timer.elapsed() >= 250) break; } if (different) { sync->compareContentResult(item, false); m_state = ST_STATE_READY; return; } if (leftFile->atEnd() && rightFile->atEnd()) { sync->compareContentResult(item, true); m_state = ST_STATE_READY; return; } QTimer::singleShot(0, this, &CompareContentTask::localFileCompareCycle); } void CompareContentTask::slotDataReceived(KIO::Job *job, const QByteArray &data) { int jobowner = (job == leftReadJob) ? 1 : 0; int bufferLen = compareArray.size(); int dataLen = data.size(); if (job == leftReadJob) received += dataLen; if (jobowner == owner) { compareArray.append(data); return; } do { if (bufferLen == 0) { compareArray = QByteArray(data.data(), dataLen); owner = jobowner; break; } int minSize = (dataLen < bufferLen) ? dataLen : bufferLen; for (int i = 0; i != minSize; i++) if (data[i] != compareArray[i]) { abortContentComparing(); return; } if (minSize == bufferLen) { compareArray = QByteArray(data.data() + bufferLen, dataLen - bufferLen); if (dataLen == bufferLen) { owner = -1; return; } owner = jobowner; break; } else { compareArray = QByteArray(compareArray.data() + dataLen, bufferLen - dataLen); return; } } while (false); KIO::TransferJob *otherJob = (job == leftReadJob) ? rightReadJob : leftReadJob; if (otherJob == nullptr) { if (compareArray.size()) abortContentComparing(); } else { if (!(qobject_cast(job))->isSuspended()) { (qobject_cast(job))->suspend(); otherJob->resume(); } } } void CompareContentTask::slotFinished(KJob *job) { KIO::TransferJob *otherJob = (job == leftReadJob) ? rightReadJob : leftReadJob; if (job == leftReadJob) leftReadJob = nullptr; else rightReadJob = nullptr; if (otherJob) otherJob->resume(); if (job->error()) { timer->stop(); abortContentComparing(); } if (job->error() && job->error() != KIO::ERR_USER_CANCELED && !errorPrinted) { errorPrinted = true; KMessageBox::error(parentWidget, i18n("I/O error while comparing file %1 with %2.", leftURL.toDisplayString(QUrl::PreferLocalFile), rightURL.toDisplayString(QUrl::PreferLocalFile))); } if (leftReadJob == nullptr && rightReadJob == nullptr) { if (!compareArray.size()) sync->compareContentResult(item, true); else sync->compareContentResult(item, false); m_state = ST_STATE_READY; } } void CompareContentTask::abortContentComparing() { if (timer) timer->stop(); if (leftReadJob) leftReadJob->kill(KJob::EmitResult); if (rightReadJob) rightReadJob->kill(KJob::EmitResult); if (item->task() >= TT_UNKNOWN) sync->compareContentResult(item, false); m_state = ST_STATE_READY; } void CompareContentTask::sendStatusMessage() { double perc = (size == 0) ? 1. : (double)received / (double)size; auto percent = (int)(perc * 10000. + 0.5); QString statstr = QString("%1.%2%3").arg(percent / 100).arg((percent / 10) % 10).arg(percent % 10) + '%'; setStatusMessage(i18n("Comparing file %1 (%2)...", leftURL.fileName(), statstr)); timer->setSingleShot(true); timer->start(500); } diff --git a/krusader/compat.h b/krusader/compat.h index bd3eac10..d65dab0f 100644 --- a/krusader/compat.h +++ b/krusader/compat.h @@ -1,45 +1,51 @@ /***************************************************************************** * Copyright (C) 2019-2020 Krusader Krew [https://krusader.org] * * * * This file is part of Krusader [https://krusader.org]. * * * * Krusader is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 2 of the License, or * * (at your option) any later version. * * * * Krusader is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Krusader. If not, see [http://www.gnu.org/licenses/]. * *****************************************************************************/ #ifndef _COMPAT_H_ #define _COMPAT_H_ #include #if KIO_VERSION >= QT_VERSION_CHECK(5, 48, 0) #define UDS_ENTRY_INSERT(A, B) UDSEntry::fastInsert((A), (B)); #else #define UDS_ENTRY_INSERT(A, B) UDSEntry::insert((A), (B)); #endif /** * QFontMetrics::width(const QString&, int) was made obsoleted in QT 5.11 in * favor of QFontMetrics::horizontalAdvance(const QString &, int) * * https://doc.qt.io/archives/qt-5.11/qfontmetrics-obsolete.html#width * * This can be removed when the qt minimum version required will be >= 5.11 */ #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) #define QFONTMETRICS_WIDTH(A) horizontalAdvance(A) #else #define QFONTMETRICS_WIDTH(A) width(A) #endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + #define SET_TAB_STOP_DISTANCE(X) setTabStopDistance(X) +#else + #define SET_TAB_STOP_DISTANCE(X) setTabStopWidth(X) +#endif + #endif