diff --git a/src/fileTree.h b/src/fileTree.h index dc4ee28..bdc4b40 100644 --- a/src/fileTree.h +++ b/src/fileTree.h @@ -1,162 +1,170 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * Copyright 2017 Harald Sitter * * This program 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 this program. If not, see . ***********************************************************************/ #ifndef FILETREE_H #define FILETREE_H #include //qstrdup #include //decodeName() #include #include #include typedef quint64 FileSize; typedef quint64 Dirsize; //**** currently unused class Folder; class File { public: friend class Folder; public: - File(const char *name, FileSize size) : m_parent(nullptr), m_name(qstrdup(name)), m_size(size) {} + const bool isHardlink = false; + + File(const char *name, FileSize size, const bool hardlink = false) : + isHardlink(hardlink), + m_parent(nullptr), + m_name(qstrdup(name)), + m_size(size) + {} + virtual ~File() { delete [] m_name; } Folder *parent() const { return m_parent; } /** Do not use for user visible strings. Use name instead. */ const char *name8Bit() const { return m_name; } /** Decoded name. Use when you need a QString. */ QString decodedName() const { return QFile::decodeName(m_name); } /** * Human readable name (including native separators where applicable). * Only use for display. */ QString displayName() const; FileSize size() const { return m_size; } virtual bool isFolder() const { return false; } /** * Human readable path for display (including native separators where applicable. * Only use for display. */ QString displayPath(const Folder * = nullptr) const; QString humanReadableSize() const { return KFormat().formatByteSize(m_size); } /** Builds a complete QUrl by walking up to root. */ QUrl url(const Folder *root = nullptr) const; protected: - File(const char *name, FileSize size, Folder *parent) : m_parent(parent), m_name(qstrdup(name)), m_size(size) {} + File(const char *name, FileSize size, const bool hardlink, Folder *parent) : isHardlink(hardlink), m_parent(parent), m_name(qstrdup(name)), m_size(size) {} Folder *m_parent; //0 if this is treeRoot char *m_name; // partial path name (e.g. 'boot/' or 'foo.svg') FileSize m_size; // in units of bytes; sum of all children's sizes private: File(const File&); void operator=(const File&); }; class Folder : public File { public: - Folder(const char *name) : File(name, 0), m_children(0) {} //DON'T pass the full path! + Folder(const char *name) : File(name, 0, false), m_children(0) {} //DON'T pass the full path! uint children() const { return m_children; } bool isFolder() const override { return true; } ///appends a Folder void append(Folder *d, const char *name=nullptr) { if (name) { delete [] d->m_name; d->m_name = qstrdup(name); } //directories that had a fullpath copy just their names this way m_children += d->children(); //doesn't include the dir itself d->m_parent = this; append((File*)d); //will add 1 to filecount for the dir itself } ///appends a File - void append(const char *name, FileSize size) + void append(const char *name, FileSize size, const bool isHardlink = false) { - append(new File(name, size, this)); + append(new File(name, size, isHardlink, this)); } /// removes a file void remove(const File *f) { files.removeAll(const_cast(f)); for (Folder *d = this; d; d = d->parent()) { d->m_size -= f->size(); d->m_children--; } } QList files; private: void append(File *p) { // This is also called by append(Folder), but only once all its children // were scanned. We do not need to forward the size change to our parent // since in turn we too only are added to our parent when we are have // been scanned already. m_children++; m_size += p->size(); files.append(p); } uint m_children; private: Folder(const Folder&); //undefined void operator=(const Folder&); //undefined }; #endif diff --git a/src/localLister.cpp b/src/localLister.cpp index 82aef35..ac81a66 100644 --- a/src/localLister.cpp +++ b/src/localLister.cpp @@ -1,262 +1,260 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 this program. If not, see . ***********************************************************************/ #include "localLister.h" #include "Config.h" #include "fileTree.h" #include "scan.h" #include "filelight_debug.h" #include #include #include //postEvent() #include #include #include #include #include #include #ifdef HAVE_MNTENT_H #include #endif namespace Filelight { QStringList LocalLister::s_remoteMounts; QStringList LocalLister::s_localMounts; LocalLister::LocalLister(const QString &path, QList *cachedTrees, ScanManager *parent) : QThread() , m_path(path) , m_trees(cachedTrees) , m_parent(parent) { //add empty directories for any mount points that are in the path //TODO empty directories is not ideal as adds to fileCount incorrectly QStringList list(Config::skipList); if (!Config::scanAcrossMounts) list += s_localMounts; if (!Config::scanRemoteMounts) list += s_remoteMounts; for (const QString &ignorePath : qAsConst(list)) { if (ignorePath.startsWith(path)) { QString folderName = ignorePath; if (!folderName.endsWith(QLatin1Char('/'))) { folderName += QLatin1Char('/'); } m_trees->append(new Folder(folderName.toLocal8Bit().constData())); } } } void LocalLister::run() { QElapsedTimer timer; timer.start(); //recursively scan the requested path const QByteArray path = QFile::encodeName(m_path); Folder *tree = scan(path, path); qCDebug(FILELIGHT_LOG) << "Scan completed in" << (timer.elapsed()/1000); //delete the list of trees useful for this scan, //in a successful scan the contents would now be transferred to 'tree' delete m_trees; if (m_parent->m_abort) //scan was cancelled { qCDebug(FILELIGHT_LOG) << "Scan successfully aborted"; delete tree; tree = nullptr; } qCDebug(FILELIGHT_LOG) << "Emitting signal to cache results ..."; emit branchCompleted(tree); qCDebug(FILELIGHT_LOG) << "Thread terminating ..."; } #ifndef S_BLKSIZE #define S_BLKSIZE 512 #endif #include static void outputError(const QByteArray &path) { ///show error message that stat or opendir may give #define out(s) qWarning() << s ": " << path; break switch (errno) { case EACCES: out("Inadequate access permissions"); case EMFILE: out("Too many file descriptors in use by Filelight"); case ENFILE: out("Too many files are currently open in the system"); case ENOENT: out("A component of the path does not exist, or the path is an empty string"); case ENOMEM: out("Insufficient memory to complete the operation"); case ENOTDIR: out("A component of the path is not a folder"); case EBADF: out("Bad file descriptor"); case EFAULT: out("Bad address"); #ifndef Q_OS_WIN case ELOOP: //NOTE shouldn't ever happen out("Too many symbolic links encountered while traversing the path"); #endif case ENAMETOOLONG: out("File name too long"); } #undef out } Folder* LocalLister::scan(const QByteArray &path, const QByteArray &dirname) { Folder *cwd = new Folder(dirname.constData()); DIR *dir = opendir(path.constData()); if (!dir) { outputError(path); return cwd; } #ifdef _MSC_VER //use a wider struct on win for nice handling of files larger than 2 GB #undef KDE_struct_stat #undef KDE_lstat #define KDE_struct_stat struct _stat64 #define KDE_lstat kdewin32_stat64 #endif struct stat statbuf; dirent *ent; - while ((ent = readdir(dir))) - { - if (m_parent->m_abort) - { + while ((ent = readdir(dir))) { + if (m_parent->m_abort) { closedir(dir); return cwd; } - if (qstrcmp(ent->d_name, ".") == 0 || qstrcmp(ent->d_name, "..") == 0) + if (qstrcmp(ent->d_name, ".") == 0 || qstrcmp(ent->d_name, "..") == 0) { continue; + } // QStringBuilder is used here. It assumes ent->d_name is char[NAME_MAX + 1], // and thus copies only first NAME_MAX + 1 chars. // Actually, while it's not fully POSIX-compatible, current behaviour may return d_name longer than NAME_MAX. // Make full copy of this string. QByteArray new_path = path + static_cast(ent->d_name); //get file information if (lstat(new_path.constData(), &statbuf) == -1) { outputError(new_path); continue; } if (S_ISLNK(statbuf.st_mode) || S_ISCHR(statbuf.st_mode) || S_ISBLK(statbuf.st_mode) || S_ISFIFO(statbuf.st_mode)|| S_ISSOCK(statbuf.st_mode)) { continue; } - if (S_ISREG(statbuf.st_mode)) //file + if (S_ISREG(statbuf.st_mode)) { //file #ifndef Q_OS_WIN - cwd->append(ent->d_name, (statbuf.st_blocks * S_BLKSIZE)); + const bool isHardlink = statbuf.st_nlink > 1; + cwd->append(ent->d_name, (statbuf.st_blocks * S_BLKSIZE), isHardlink); #else cwd->append(ent->d_name, statbuf.st_size); #endif - - else if (S_ISDIR(statbuf.st_mode)) //folder - { + } else if (S_ISDIR(statbuf.st_mode)) { //folder Folder *d = nullptr; const QByteArray new_dirname = QByteArray(ent->d_name) + QByteArrayLiteral("/"); new_path += '/'; //check to see if we've scanned this section already - for (Folder *folder : *m_trees) - { - if (new_path == folder->name8Bit()) - { + for (Folder *folder : *m_trees) { + if (new_path == folder->name8Bit()) { qCDebug(FILELIGHT_LOG) << "Tree pre-completed: " << folder->decodedName(); d = folder; m_trees->removeAll(folder); m_parent->m_files += folder->children(); cwd->append(folder, new_dirname.constData()); } } - if (!d) //then scan - if ((d = scan(new_path, new_dirname))) //then scan was successful + if (!d) {//then scan + if ((d = scan(new_path, new_dirname))) {//then scan was successful cwd->append(d); + } + } } ++m_parent->m_files; } closedir(dir); std::sort(cwd->files.begin(), cwd->files.end(), [](File *a, File*b) { return a->size() > b->size(); }); return cwd; } void LocalLister::readMounts() { static const QSet remoteFsTypes = { "smbfs", "nfs", "afs" }; for (const QStorageInfo &storage : QStorageInfo::mountedVolumes()) { if (storage.isRoot()) { continue; } QString path = storage.rootPath(); if (!path.endsWith(QLatin1Char('/'))) { path += QLatin1Char('/'); } if (remoteFsTypes.contains(storage.fileSystemType()) && !s_remoteMounts.contains(path)) { s_remoteMounts.append(path); } else if (!s_localMounts.contains(path)) { s_localMounts.append(path); } } qCDebug(FILELIGHT_LOG) << "Found the following remote filesystems: " << s_remoteMounts; qCDebug(FILELIGHT_LOG) << "Found the following local filesystems: " << s_localMounts; } }//namespace Filelight diff --git a/src/radialMap/labels.cpp b/src/radialMap/labels.cpp index f64936e..15bc88b 100644 --- a/src/radialMap/labels.cpp +++ b/src/radialMap/labels.cpp @@ -1,317 +1,317 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 this program. If not, see . ***********************************************************************/ #include #include #include #include #include "Config.h" #include "fileTree.h" #include "radialMap.h" #include "sincos.h" #include "widget.h" namespace RadialMap { class Label { public: Label(const RadialMap::Segment *s, int l) : segment(s), level(l), angle(segment->start() + (segment->length() / 2)) { } bool tooClose(const int otherAngle) const { return (angle > otherAngle - LABEL_ANGLE_MARGIN && angle < otherAngle + LABEL_ANGLE_MARGIN); } const RadialMap::Segment *segment; const unsigned int level; const int angle; int targetX, targetY, middleX, startY, startX; int textX, textY, tw, th; QString qs; }; void RadialMap::Widget::paintExplodedLabels(QPainter &paint) const { //we are a friend of RadialMap::Map QVector list; unsigned int startLevel = 0; //1. Create list of labels sorted in the order they will be rendered if (m_focus && m_focus->file() != m_tree) { //separate behavior for selected vs unselected segments //don't bother with files if (m_focus && m_focus->file() && !m_focus->file()->isFolder()) { return; } //find the range of levels we will be potentially drawing labels for //startLevel is the level above whatever m_focus is in for (const Folder *p = (const Folder*)m_focus->file(); p != m_tree; ++startLevel) { p = p->parent(); } //range=2 means 2 levels to draw labels for const uint start = m_focus->start(); const uint end = m_focus->end(); //boundary angles const uint minAngle = int(m_focus->length() * LABEL_MIN_ANGLE_FACTOR); //**** Levels should be on a scale starting with 0 //**** range is a useless parameter //**** keep a topblock var which is the lowestLevel OR startLevel for indentation purposes for (unsigned int i = startLevel; i <= m_map.m_visibleDepth; ++i) { for (const Segment *segment : m_map.m_signature[i]) { if (segment->start() >= start && segment->end() <= end) { if (segment->length() > minAngle) { list.append(new Label(segment, i)); } } } } } else { for (Segment *segment : *m_map.m_signature) { if (segment->length() > 288) { list.append(new Label(segment, 0)); } } } std::sort(list.begin(), list.end(), [](Label *item1, Label *item2) { //you add 1440 to work round the fact that later you want the circle split vertically //and as it is you start at 3 o' clock. It's to do with rightPrevY, stops annoying bug int angle1 = (item1)->angle + 1440; int angle2 = (item2)->angle + 1440; // Also sort by level if (angle1 == angle2) { return (item1->level > item2->level); } if (angle1 > 5760) angle1 -= 5760; if (angle2 > 5760) angle2 -= 5760; return (angle1 < angle2); }); //2. Check to see if any adjacent labels are too close together // if so, remove it (the least significant labels, since we sort by level too). int pos = 0; while (pos < list.size() - 1) { if (list[pos]->tooClose(list[pos+1]->angle)) { delete list.takeAt(pos+1); } else { ++pos; } } //used in next two steps bool varySizes; //**** should perhaps use doubles int *sizes = new int [ m_map.m_visibleDepth + 1 ]; //**** make sizes an array of floats I think instead (or doubles) // If the minimum is larger than the default it fucks up further down if (paint.font().pointSize() < 0 || paint.font().pointSize() < Config::minFontPitch) { QFont font = paint.font(); font.setPointSize(Config::minFontPitch); paint.setFont(font); } QVector::iterator it; do { //3. Calculate font sizes { //determine current range of levels to draw for uint range = 0; for (Label *label : list) { range = qMax(range, label->level); //**** better way would just be to assign if nothing is range } range -= startLevel; //range 0 means 1 level of labels varySizes = Config::varyLabelFontSizes && (range != 0); if (varySizes) { //create an array of font sizes for various levels //will exceed normal font pitch automatically if necessary, but not minPitch //**** this needs to be checked lots //**** what if this is negative (min size gtr than default size) uint step = (paint.font().pointSize() - Config::minFontPitch) / range; if (step == 0) { step = 1; } for (uint x = range + startLevel, y = Config::minFontPitch; x >= startLevel; y += step, --x) { sizes[x] = y; } } } //4. determine label co-ordinates const int preSpacer = int(m_map.m_ringBreadth * 0.5) + m_map.m_innerRadius; const int fullStrutLength = (m_map.width() - m_map.MAP_2MARGIN) / 2 + LABEL_MAP_SPACER; //full length of a strut from map center int prevLeftY = 0; int prevRightY = height(); QFont font; for (it = list.begin(); it != list.end(); ++it) { Label *label = *it; //** bear in mind that text is drawn with QPoint param as BOTTOM left corner of text box QString string = label->segment->file()->displayName(); if (varySizes) { font.setPointSize(sizes[label->level]); } QFontMetrics fontMetrics(font); - const int minTextWidth = fontMetrics.width(QStringLiteral("M...")) + LABEL_TEXT_HMARGIN; // Fully elided string + const int minTextWidth = fontMetrics.horizontalAdvance(QStringLiteral("M...")) + LABEL_TEXT_HMARGIN; // Fully elided string const int fontHeight = fontMetrics.height() + LABEL_TEXT_VMARGIN; //used to ensure label texts don't overlap const int lineSpacing = fontHeight / 4; const bool rightSide = (label->angle < 1440 || label->angle > 4320); double sinra, cosra; const double ra = M_PI/2880 * label->angle; //convert to radians sincos(ra, &sinra, &cosra); const int spacer = preSpacer + m_map.m_ringBreadth * label->level; const int centerX = m_map.width() / 2 + m_offset.x(); //centre relative to canvas const int centerY = m_map.height() / 2 + m_offset.y(); int targetX = centerX + cosra * spacer; int targetY = centerY - sinra * spacer; int startX = targetX + cosra * (fullStrutLength - spacer + m_map.m_ringBreadth / 2); int startY = targetY - sinra * (fullStrutLength - spacer); if (rightSide) { //righthand side, going upwards if (startY > prevRightY /*- fmh*/) { //then it is too low, needs to be drawn higher startY = prevRightY /*- fmh*/; } } else {//lefthand side, going downwards if (startY < prevLeftY/* + fmh*/) { //then we're too high, need to be drawn lower startY = prevLeftY /*+ fmh*/; } } int middleX = targetX - (startY - targetY) / tan(ra); int textY = startY + lineSpacing; int textX; - const int textWidth = fontMetrics.width(string) + LABEL_TEXT_HMARGIN; + const int textWidth = fontMetrics.horizontalAdvance(string) + LABEL_TEXT_HMARGIN; if (rightSide) { if (startX + minTextWidth > width() || textY < fontHeight || middleX < targetX) { //skip this strut //**** don't duplicate this code list.erase(it); //will delete the label and set it to list.current() which _should_ be the next ptr break; } prevRightY = textY - fontHeight - lineSpacing; //must be after above's "continue" if (m_offset.x() + m_map.width() + textWidth < width()) { startX = m_offset.x() + m_map.width(); } else { startX = qMax(width() - textWidth, startX); string = fontMetrics.elidedText(string, Qt::ElideMiddle, width() - startX); } textX = startX + LABEL_TEXT_HMARGIN; } else { // left side if (startX - minTextWidth < 0 || textY > height() || middleX > targetX) { //skip this strut list.erase(it); //will delete the label and set it to list.current() which _should_ be the next ptr break; } prevLeftY = textY + fontHeight - lineSpacing; if (m_offset.x() - textWidth > 0) { startX = m_offset.x(); textX = startX - textWidth - LABEL_TEXT_HMARGIN; } else { textX = 0; string = fontMetrics.elidedText(string, Qt::ElideMiddle, startX); - startX = fontMetrics.width(string) + LABEL_TEXT_HMARGIN; + startX = fontMetrics.horizontalAdvance(string) + LABEL_TEXT_HMARGIN; } } label->targetX = targetX; label->targetY = targetY; label->middleX = middleX; label->startY = startY; label->startX = startX; label->textX = textX; label->textY = textY; label->qs = string; } //if an element is deleted at this stage, we need to do this whole //iteration again, thus the following loop //**** in rare case that deleted label was last label in top level // and last in labelList too, this will not work as expected (not critical) } while (it != list.end()); //5. Render labels QFont font; for (Label *label : list) { if (varySizes) { //**** how much overhead in making new QFont each time? // (implicate sharing remember) font.setPointSize(sizes[label->level]); paint.setFont(font); } paint.drawLine(label->targetX, label->targetY, label->middleX, label->startY); paint.drawLine(label->middleX, label->startY, label->startX, label->startY); paint.drawText(label->textX, label->textY, label->qs); } qDeleteAll(list); delete [] sizes; } } diff --git a/src/radialMap/map.cpp b/src/radialMap/map.cpp index f40723a..ed6229b 100644 --- a/src/radialMap/map.cpp +++ b/src/radialMap/map.cpp @@ -1,483 +1,483 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 this program. If not, see . ***********************************************************************/ #include //make() #include //make() & paint() #include //ctor #include //ctor #include #include #include "filelight_debug.h" #include //make() #include #include "radialMap.h" // defines #include "Config.h" #include "fileTree.h" #define SINCOS_H_IMPLEMENTATION (1) #include "sincos.h" #include "widget.h" RadialMap::Map::Map(bool summary) : m_signature(nullptr) , m_visibleDepth(DEFAULT_RING_DEPTH) , m_ringBreadth(MIN_RING_BREADTH) , m_innerRadius(0) , m_summary(summary) { //FIXME this is all broken. No longer is a maximum depth! const int fmh = QFontMetrics(QFont()).height(); const int fmhD4 = fmh / 4; MAP_2MARGIN = 2 * (fmh - (fmhD4 - LABEL_MAP_SPACER)); //margin is dependent on fitting in labels at top and bottom } RadialMap::Map::~Map() { delete [] m_signature; } void RadialMap::Map::invalidate() { delete [] m_signature; m_signature = nullptr; m_visibleDepth = Config::defaultRingDepth; } void RadialMap::Map::make(const Folder *tree, bool refresh) { //slow operation so set the wait cursor QApplication::setOverrideCursor(Qt::WaitCursor); //build a signature of visible components { //**** REMOVE NEED FOR the +1 with MAX_RING_DEPTH uses //**** add some angle bounds checking (possibly in Segment ctor? can I delete in a ctor?) //**** this is a mess delete [] m_signature; m_signature = new QList[m_visibleDepth + 1]; m_root = tree; if (!refresh) { m_minSize = (tree->size() * 3) / (PI * height() - MAP_2MARGIN); findVisibleDepth(tree); } setRingBreadth(); // Calculate ring size limits m_limits.resize(m_visibleDepth + 1); const double size = m_root->size(); const double pi2B = M_PI * 4 * m_ringBreadth; for (uint depth = 0; depth <= m_visibleDepth; ++depth) { m_limits[depth] = uint(size / double(pi2B * (depth + 1))); //min is angle that gives 3px outer diameter for that depth } build(tree); } //colour the segments colorise(); m_centerText = tree->humanReadableSize(); //paint the pixmap paint(); QApplication::restoreOverrideCursor(); } void RadialMap::Map::setRingBreadth() { //FIXME called too many times on creation m_ringBreadth = (height() - MAP_2MARGIN) / (2 * m_visibleDepth + 4); m_ringBreadth = qBound(MIN_RING_BREADTH, m_ringBreadth, MAX_RING_BREADTH); } void RadialMap::Map::findVisibleDepth(const Folder *dir, uint currentDepth) { //**** because I don't use the same minimumSize criteria as in the visual function // this can lead to incorrect visual representation //**** BUT, you can't set those limits until you know m_depth! //**** also this function doesn't check to see if anything is actually visible // it just assumes that when it reaches a new level everything in it is visible // automatically. This isn't right especially as there might be no files in the // dir provided to this function! static uint stopDepth = 0; if (dir == m_root) { stopDepth = m_visibleDepth; m_visibleDepth = 0; } if (m_visibleDepth < currentDepth) m_visibleDepth = currentDepth; if (m_visibleDepth >= stopDepth) return; for (File *file : dir->files) { if (file->isFolder() && file->size() > m_minSize) { findVisibleDepth((Folder *)file, currentDepth + 1); //if no files greater than min size the depth is still recorded } } } //**** segments currently overlap at edges (i.e. end of first is start of next) bool RadialMap::Map::build(const Folder * const dir, const uint depth, uint a_start, const uint a_end) { //first iteration: dir == m_root if (dir->children() == 0) //we do fileCount rather than size to avoid chance of divide by zero later return false; FileSize hiddenSize = 0; uint hiddenFileCount = 0; for (File *file : dir->files) { if (file->size() < m_limits[depth] * 6) { // limit is half a degree? we want at least 3 degrees hiddenSize += file->size(); if (file->isFolder()) { //**** considered virtual, but dir wouldn't count itself! hiddenFileCount += static_cast(file)->children(); //need to add one to count the dir as well } ++hiddenFileCount; continue; } unsigned int a_len = (unsigned int)(5760 * ((double)file->size() / (double)m_root->size())); Segment *s = new Segment(file, a_start, a_len); m_signature[depth].append(s); if (file->isFolder()) { if (depth != m_visibleDepth) { //recurse s->m_hasHiddenChildren = build((Folder*)file, depth + 1, a_start, a_start + a_len); } else { s->m_hasHiddenChildren = true; } } a_start += a_len; //**** should we add 1? } if (hiddenFileCount == dir->children() && !Config::showSmallFiles) { return true; } if ((depth == 0 || Config::showSmallFiles) && hiddenSize >= m_limits[depth] && hiddenFileCount > 0) { //append a segment for unrepresented space - a "fake" segment const QString s = i18np("1 file, with an average size of %2", "%1 files, with an average size of %2", hiddenFileCount, KFormat().formatByteSize(hiddenSize/hiddenFileCount)); - (m_signature + depth)->append(new Segment(new File(s.toUtf8().constData(), hiddenSize), a_start, a_end - a_start, true)); + (m_signature + depth)->append(new Segment(new File(s.toUtf8().constData(), hiddenSize), a_start, a_end - a_start, true)); // todo hardlinks } return false; } bool RadialMap::Map::resize(const QRect &rect) { //there's a MAP_2MARGIN border #define mw width() #define mh height() #define cw rect.width() #define ch rect.height() if (cw < mw || ch < mh || (cw > mw && ch > mh)) { uint size = ((cw < ch) ? cw : ch) - MAP_2MARGIN; //this also causes uneven sizes to always resize when resizing but map is small in that dimension //size -= size % 2; //even sizes mean less staggered non-antialiased resizing { const uint minSize = MIN_RING_BREADTH * 2 * (m_visibleDepth + 2); if (size < minSize) size = minSize; //this QRect is used by paint() m_rect.setRect(0,0,size,size); } m_pixmap = QPixmap(m_rect.size()); //resize the pixmap size += MAP_2MARGIN; if (m_signature != nullptr) { setRingBreadth(); paint(); } return true; } #undef mw #undef mh #undef cw #undef ch return false; } void RadialMap::Map::colorise() { if (!m_signature || m_signature->isEmpty()) { qCDebug(FILELIGHT_LOG) << "no signature yet"; return; } QColor cp, cb; double darkness = 1; double contrast = (double)Config::contrast / (double)100; int h, s1, s2, v1, v2; QPalette palette; const QColor kdeColour[2] = { palette.windowText().color(), palette.window().color() }; double deltaRed = (double)(kdeColour[0].red() - kdeColour[1].red()) / 2880; //2880 for semicircle double deltaGreen = (double)(kdeColour[0].green() - kdeColour[1].green()) / 2880; double deltaBlue = (double)(kdeColour[0].blue() - kdeColour[1].blue()) / 2880; if (m_summary) { // Summary view has its own colors, special cased. cp = Qt::gray; cb = Qt::white; m_signature[0][0]->setPalette(cp, cb); // need to check in case there's no free space if (m_signature[0].size() > 1) { cb = QApplication::palette().highlight().color(); cb.getHsv(&h, &s1, &v1); if (s1 > 80) { s1 = 80; } v2 = v1 - int(contrast * v1); s2 = s1 + int(contrast * (255 - s1)); cb.setHsv(h, s1, v1); cp.setHsv(h, s2, v2); m_signature[0][1]->setPalette(cp, cb); } return; } for (uint i = 0; i <= m_visibleDepth; ++i, darkness += 0.04) { for (Segment *segment : m_signature[i]) { switch (Config::scheme) { case Filelight::KDE: { //gradient will work by figuring out rgb delta values for 360 degrees //then each component is angle*delta int a = segment->start(); if (a > 2880) a = 2880 - (a - 2880); h = (int)(deltaRed * a) + kdeColour[1].red(); s1 = (int)(deltaGreen * a) + kdeColour[1].green(); v1 = (int)(deltaBlue * a) + kdeColour[1].blue(); cb.setRgb(h, s1, v1); cb.getHsv(&h, &s1, &v1); break; } case Filelight::HighContrast: cp.setHsv(0, 0, 0); //values of h, s and v are irrelevant cb.setHsv(180, 0, int(255.0 * contrast)); segment->setPalette(cp, cb); continue; default: h = int(segment->start() / 16); s1 = 160; v1 = (int)(255.0 / darkness); //****doing this more often than once seems daft! } v2 = v1 - int(contrast * v1); s2 = s1 + int(contrast * (255 - s1)); if (s1 < 80) s1 = 80; //can fall too low and makes contrast between the files hard to discern if (segment->isFake()) { //multi-file cb.setHsv(h, s2, (v2 < 90) ? 90 : v2); //too dark if < 100 cp.setHsv(h, 17, v1); } else if (!segment->file()->isFolder()) { //file cb.setHsv(h, 17, v1); cp.setHsv(h, 17, v2); } else { //folder cb.setHsv(h, s1, v1); //v was 225 cp.setHsv(h, s2, v2); //v was 225 - delta } segment->setPalette(cp, cb); //TODO: //**** may be better to store KDE colours as H and S and vary V as others //**** perhaps make saturation difference for s2 dependent on contrast too //**** fake segments don't work with highContrast //**** may work better with cp = cb rather than Qt::white //**** you have to ensure the grey of files is sufficient, currently it works only with rainbow (perhaps use contrast there too) //**** change v1,v2 to vp, vb etc. //**** using percentages is not strictly correct as the eye doesn't work like that //**** darkness factor is not done for kde_colour scheme, and also value for files is incorrect really for files in this scheme as it is not set like rainbow one is } } } void RadialMap::Map::paint(bool antialias) { KColorScheme scheme(QPalette::Active, KColorScheme::View); QPainter paint; QRect rect = m_rect; rect.adjust(5, 5, -5, -5); m_pixmap.fill(scheme.background().color()); //m_rect.moveRight(1); // Uncommenting this breaks repainting when recreating map from cache //**** best option you can think of is to make the circles slightly less perfect, // ** i.e. slightly eliptic when resizing inbetween if (m_pixmap.isNull()) return; if (!paint.begin(&m_pixmap)) { qWarning() << "Filelight::RadialMap Failed to initialize painting, returning..."; return; } if (antialias && Config::antialias) { paint.translate(0.7, 0.7); paint.setRenderHint(QPainter::Antialiasing); } int step = m_ringBreadth; int excess = -1; //do intelligent distribution of excess to prevent nasty resizing if (m_ringBreadth != MAX_RING_BREADTH && m_ringBreadth != MIN_RING_BREADTH) { excess = rect.width() % m_ringBreadth; ++step; } for (int x = m_visibleDepth; x >= 0; --x) { int width = rect.width() / 2; //clever geometric trick to find largest angle that will give biggest arrow head uint a_max = int(acos((double)width / double((width + 5))) * (180*16 / M_PI)); for (Segment *segment : m_signature[x]) { //draw the pie segments, most of this code is concerned with drawing the little //arrows on the ends of segments when they have hidden files paint.setPen(segment->pen()); if (segment->hasHiddenChildren()) { //draw arrow head to indicate undisplayed files/directories QPolygon pts(3); QPoint pos, cpos = rect.center(); uint a[3] = { segment->start(), segment->length(), 0 }; a[2] = a[0] + (a[1] / 2); //assign to halfway between if (a[1] > a_max) { a[1] = a_max; a[0] = a[2] - a_max / 2; } a[1] += a[0]; for (int i = 0, radius = width; i < 3; ++i) { double ra = M_PI/(180*16) * a[i], sinra, cosra; if (i == 2) radius += 5; sincos(ra, &sinra, &cosra); pos.rx() = cpos.x() + static_cast(cosra * radius); pos.ry() = cpos.y() - static_cast(sinra * radius); pts.setPoint(i, pos); } paint.setBrush(segment->pen()); paint.drawPolygon(pts); } paint.setBrush(segment->brush()); paint.drawPie(rect, segment->start(), segment->length()); if (segment->hasHiddenChildren()) { //**** code is bloated! paint.save(); QPen pen = paint.pen(); int width = 2; pen.setWidth(width); paint.setPen(pen); QRect rect2 = rect; width /= 2; rect2.adjust(width, width, -width, -width); paint.drawArc(rect2, segment->start(), segment->length()); paint.restore(); } } if (excess >= 0) { //excess allows us to resize more smoothly (still crud tho) if (excess < 2) //only decrease rect by more if even number of excesses left --step; excess -= 2; } rect.adjust(step, step, -step, -step); } // if(excess > 0) rect.addCoords(excess, excess, 0, 0); //ugly paint.setPen(scheme.foreground().color()); paint.setBrush(scheme.background().color()); paint.drawEllipse(rect); paint.drawText(rect, Qt::AlignCenter, m_centerText); m_innerRadius = rect.width() / 2; //rect.width should be multiple of 2 paint.end(); } diff --git a/src/radialMap/widgetEvents.cpp b/src/radialMap/widgetEvents.cpp index 8534d27..d286c33 100644 --- a/src/radialMap/widgetEvents.cpp +++ b/src/radialMap/widgetEvents.cpp @@ -1,389 +1,389 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 this program. If not, see . ***********************************************************************/ #include "fileTree.h" #include "Config.h" #include "radialMap.h" //class Segment #include "widget.h" #include //::mouseMoveEvent() #include #include //::mousePressEvent() #include #include #include //::mousePressEvent() #include //::mousePressEvent() #include //::mousePressEvent() #include #include #include #include #include #include //QApplication::setOverrideCursor() #include #include #include //::resizeEvent() #include #include #include #include #include #include #include #include #include //::segmentAt() void RadialMap::Widget::resizeEvent(QResizeEvent*) { if (m_map.resize(rect())) m_timer.setSingleShot(true); m_timer.start(500); //will cause signature to rebuild for new size //always do these as they need to be initialised on creation m_offset.rx() = (width() - m_map.width()) / 2; m_offset.ry() = (height() - m_map.height()) / 2; } void RadialMap::Widget::paintEvent(QPaintEvent*) { QPainter paint; paint.begin(this); if (!m_map.isNull()) paint.drawPixmap(m_offset, m_map.pixmap()); else { paint.drawText(rect(), 0, i18nc("We messed up, the user needs to initiate a rescan.", "Internal representation is invalid,\nplease rescan.")); return; } //exploded labels if (!m_map.isNull() && !m_timer.isActive()) { if (Config::antialias) { paint.setRenderHint(QPainter::Antialiasing); //make lines appear on pixel boundaries paint.translate(0.5, 0.5); } paintExplodedLabels(paint); } } const RadialMap::Segment* RadialMap::Widget::segmentAt(QPoint &e) const { //determine which segment QPoint e is above e -= m_offset; if (!m_map.m_signature) return nullptr; if (e.x() <= m_map.width() && e.y() <= m_map.height()) { //transform to cartesian coords e.rx() -= m_map.width() / 2; //should be an int e.ry() = m_map.height() / 2 - e.y(); double length = hypot(e.x(), e.y()); if (length >= m_map.m_innerRadius) //not hovering over inner circle { uint depth = ((int)length - m_map.m_innerRadius) / m_map.m_ringBreadth; if (depth <= m_map.m_visibleDepth) //**** do earlier since you can //** check not outside of range { //vector calculation, reduces to simple trigonometry //cos angle = (aibi + ajbj) / albl //ai = x, bi=1, aj=y, bj=0 //cos angle = x / (length) uint a = (uint)(acos((double)e.x() / length) * 916.736); //916.7324722 = #radians in circle * 16 //acos only understands 0-180 degrees if (e.y() < 0) a = 5760 - a; for (Segment *segment : m_map.m_signature[depth]) { if (segment->intersects(a)) return segment; } } } else return m_rootSegment; //hovering over inner circle } return nullptr; } void RadialMap::Widget::mouseMoveEvent(QMouseEvent *e) { //set m_focus to what we hover over, update UI if it's a new segment Segment const * const oldFocus = m_focus; QPoint p = e->pos(); m_focus = segmentAt(p); //NOTE p is passed by non-const reference if (m_focus) { m_tooltip.move(e->globalX() + 20, e->globalY() + 20); if (m_focus != oldFocus) //if not same as last time { setCursor(Qt::PointingHandCursor); QString string; if (isSummary()) { if (strcmp("used", m_focus->file()->name8Bit()) == 0) { string = i18nc("Tooltip of used space on the partition, %1 is path, %2 is size", "%1\nUsed: %2", m_focus->file()->parent()->displayPath(), m_focus->file()->humanReadableSize()); } else if (strcmp("free", m_focus->file()->name8Bit()) == 0) { string = i18nc("Tooltip of free space on the partition, %1 is path, %2 is size", "%1\nFree: %2", m_focus->file()->parent()->displayPath(), m_focus->file()->humanReadableSize()); } else { string = i18nc("Tooltip of file/folder, %1 is path, %2 is size", "%1\n%2", m_focus->file()->displayPath(), m_focus->file()->humanReadableSize()); } } else { string = i18nc("Tooltip of file/folder, %1 is path, %2 is size", "%1\n%2", m_focus->file()->displayPath(), m_focus->file()->humanReadableSize()); if (m_focus->file()->isFolder()) { int files = static_cast(m_focus->file())->children(); const uint percent = uint((100 * files) / (double)m_tree->children()); string += QLatin1Char('\n'); if (percent > 0) { string += i18ncp("Tooltip of folder, %1 is number of files", "%1 File (%2%)", "%1 Files (%2%)", files, percent); } else { string += i18ncp("Tooltip of folder, %1 is number of files", "%1 File", "%1 Files", files); } } const QUrl url = Widget::url(m_focus->file()); if (m_focus == m_rootSegment && url != KIO::upUrl(url)) { string += i18n("\nClick to go up to parent directory"); } } // Calculate a semi-sane size for the tooltip QFontMetrics fontMetrics(font()); int tooltipWidth = 0; int tooltipHeight = 0; for (const QString &part : string.split(QLatin1Char('\n'))) { tooltipHeight += fontMetrics.height(); - tooltipWidth = qMax(tooltipWidth, fontMetrics.width(part)); + tooltipWidth = qMax(tooltipWidth, fontMetrics.horizontalAdvance(part)); } // Limit it to the window size, probably should find something better tooltipWidth = qMin(tooltipWidth, window()->width()); tooltipWidth += 10; tooltipHeight += 10; m_tooltip.resize(tooltipWidth, tooltipHeight); m_tooltip.setText(string); m_tooltip.show(); emit mouseHover(m_focus->file()->displayPath()); update(); } } else if (oldFocus && oldFocus->file() != m_tree) { m_tooltip.hide(); unsetCursor(); update(); emit mouseHover(QString()); } } void RadialMap::Widget::enterEvent(QEvent *) { if (!m_focus) return; setCursor(Qt::PointingHandCursor); emit mouseHover(m_focus->file()->displayPath()); update(); } void RadialMap::Widget::leaveEvent(QEvent *) { m_tooltip.hide(); } void RadialMap::Widget::mousePressEvent(QMouseEvent *e) { if (!isEnabled()) return; //m_focus is set correctly (I've been strict, I assure you it is correct!) if (!m_focus || m_focus->isFake()) { return; } const QUrl url = Widget::url(m_focus->file()); const bool isDir = m_focus->file()->isFolder(); // Open file if (e->button() == Qt::MidButton || (e->button() == Qt::LeftButton && !isDir)) { new KRun(url, this, true); return; } if (e->button() == Qt::LeftButton) { if (m_focus->file() != m_tree) { emit activated(url); //activate first, this will cause UI to prepare itself createFromCache((Folder *)m_focus->file()); } else if (KIO::upUrl(url) != url) { emit giveMeTreeFor(KIO::upUrl(url)); } return; } if (e->button() != Qt::RightButton) { // Ignore other mouse buttons return; } // Actions in the right click menu QAction* openFileManager = nullptr; QAction* openTerminal = nullptr; QAction* centerMap = nullptr; QAction* openFile = nullptr; QAction* copyClipboard = nullptr; QAction* deleteItem = nullptr; QMenu popup; popup.setTitle(m_focus->file()->displayPath(m_tree)); if (isDir) { openFileManager = popup.addAction(QIcon::fromTheme(QStringLiteral("system-file-manager")), i18n("Open &File Manager Here")); if (url.scheme() == QLatin1String("file")) { openTerminal = popup.addAction(QIcon::fromTheme(QStringLiteral( "utilities-terminal" )), i18n("Open &Terminal Here")); } if (m_focus->file() != m_tree) { popup.addSeparator(); centerMap = popup.addAction(QIcon::fromTheme(QStringLiteral( "zoom-in" )), i18n("&Center Map Here")); } } else { openFile = popup.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("Scan/open the path of the selected element", "&Open")); } popup.addSeparator(); copyClipboard = popup.addAction(QIcon::fromTheme(QStringLiteral( "edit-copy" )), i18n("&Copy to clipboard")); if (m_focus->file() != m_tree) { popup.addSeparator(); deleteItem = popup.addAction(QIcon::fromTheme(QStringLiteral( "edit-delete" )), i18n("&Delete")); } QAction* clicked = popup.exec(e->globalPos(), nullptr); if (openFileManager && clicked == openFileManager) { KRun::runUrl(url, QStringLiteral("inode/directory"), this #if KIO_VERSION >= QT_VERSION_CHECK(5, 31, 0) , KRun::RunFlags() #endif ); } else if (openTerminal && clicked == openTerminal) { KToolInvocation::invokeTerminal(QString(),url.path()); } else if (centerMap && clicked == centerMap) { emit activated(url); //activate first, this will cause UI to prepare itself createFromCache((Folder *)m_focus->file()); } else if (openFile && clicked == openFile) { new KRun(url, this, true); } else if (clicked == copyClipboard) { QMimeData* mimedata = new QMimeData(); mimedata->setUrls(QList() << url); QApplication::clipboard()->setMimeData(mimedata , QClipboard::Clipboard); } else if (clicked == deleteItem && m_focus->file() != m_tree) { m_toBeDeleted = m_focus; const QUrl url = Widget::url(m_toBeDeleted->file()); const QString message = m_toBeDeleted->file()->isFolder() ? i18n("The folder at '%1' will be recursively and permanently deleted.", url.toString()) : i18n("'%1' will be permanently deleted.", url.toString()); const int userIntention = KMessageBox::warningContinueCancel( this, message, QString(), KGuiItem(i18n("&Delete"), QStringLiteral("edit-delete"))); if (userIntention == KMessageBox::Continue) { KIO::Job *job = KIO::del(url); connect(job, &KJob::finished, this, &RadialMap::Widget::deleteJobFinished); QApplication::setOverrideCursor(Qt::BusyCursor); setEnabled(false); } } else { //ensure m_focus is set for new mouse position sendFakeMouseEvent(); } } void RadialMap::Widget::deleteJobFinished(KJob *job) { QApplication::restoreOverrideCursor(); setEnabled(true); if (!job->error() && m_toBeDeleted) { m_toBeDeleted->file()->parent()->remove(m_toBeDeleted->file()); delete m_toBeDeleted->file(); m_toBeDeleted = nullptr; m_focus = nullptr; m_map.make(m_tree, true); repaint(); } else KMessageBox::error(this, job->errorString(), i18n("Error while deleting")); } void RadialMap::Widget::dropEvent(QDropEvent *e) { QList uriList = KUrlMimeData::urlsFromMimeData(e->mimeData()); if (!uriList.isEmpty()) emit giveMeTreeFor(uriList.first()); } void RadialMap::Widget::dragEnterEvent(QDragEnterEvent *e) { QList uriList = KUrlMimeData::urlsFromMimeData(e->mimeData()); e->setAccepted(!uriList.isEmpty()); } void RadialMap::Widget::changeEvent(QEvent *e) { if (e->type() == QEvent::ApplicationPaletteChange || e->type() == QEvent::PaletteChange) m_map.paint(); }