diff --git a/src/main.cpp b/src/main.cpp index 15c2a43..5ddfec6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,91 +1,93 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2014 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 "define.h" #include "mainWindow.h" #include #include #include #include #include #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); KLocalizedString::setApplicationDomain("filelight"); Kdelibs4ConfigMigrator migrate(QStringLiteral("filelight")); migrate.setConfigFiles(QStringList() << QStringLiteral("filelightrc")); migrate.setUiFiles(QStringList() << QStringLiteral("filelightui.rc")); migrate.migrate(); using Filelight::MainWindow; KAboutData about( QStringLiteral(APP_NAME), i18n(APP_PRETTYNAME), QStringLiteral(APP_VERSION), i18n("Graphical disk-usage information"), KAboutLicense::GPL, i18n("(C) 2006 Max Howell\n" "(C) 2008-2014 Martin Sandsmark"), QString(), QStringLiteral("https://utils.kde.org/projects/filelight") ); about.addAuthor(i18n("Martin Sandsmark"), i18n("Maintainer"), QStringLiteral("martin.sandsmark@kde.org"), QStringLiteral("https://iskrembilen.com/")); about.addAuthor(i18n("Max Howell"), i18n("Original author"), QStringLiteral("max.howell@methylblue.com"), QStringLiteral("https://www.methylblue.com/")); about.addCredit(i18n("Lukas Appelhans"), i18n("Help and support")); about.addCredit(i18n("Steffen Gerlach"), i18n("Inspiration"), QString(), QStringLiteral("http://www.steffengerlach.de/")); about.addCredit(i18n("Mike Diehl"), i18n("Original documentation")); about.addCredit(i18n("Sune Vuorela"), i18n("Icon")); about.addCredit(i18n("Nuno Pinheiro"), i18n("Icon")); KAboutData::setApplicationData(about); app.setOrganizationName(QStringLiteral("KDE")); app.setWindowIcon(QIcon::fromTheme(QStringLiteral(APP_NAME))); + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); + QCommandLineParser options; options.addHelpOption(); options.addVersionOption(); options.addPositionalArgument(QStringLiteral("url"), i18n("Path or URL to scan"), i18n("[url]")); about.setupCommandLine(&options); options.process(app); about.processCommandLine(&options); if (!app.isSessionRestored()) { MainWindow *mw = new MainWindow(); QStringList args = options.positionalArguments(); if (args.count() > 0) { mw->scan(QUrl::fromUserInput(args.at(0), QDir::currentPath(), QUrl::AssumeLocalFile)); } mw->show(); } else RESTORE(MainWindow); return app.exec(); } diff --git a/src/progressBox.cpp b/src/progressBox.cpp index df01e9a..c9093c6 100644 --- a/src/progressBox.cpp +++ b/src/progressBox.cpp @@ -1,131 +1,131 @@ /*********************************************************************** * 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 "progressBox.h" #include "scan.h" #include "mainWindow.h" #include #include #include #include #include #include ProgressBox::ProgressBox(QWidget *parent, Filelight::MainWindow *mainWindow, Filelight::ScanManager *scanManager) : QWidget(parent) , m_manager(scanManager) , m_colorScheme(QPalette::Active, KColorScheme::Tooltip) { hide(); setObjectName(QStringLiteral( "ProgressBox" )); setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); setText(999999); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); setMinimumSize(300, 300); connect(&m_timer, &QTimer::timeout, this, &ProgressBox::report); connect(mainWindow, &Filelight::MainWindow::started, this, &ProgressBox::start); connect(mainWindow, &Filelight::MainWindow::completed, this, &ProgressBox::stop); connect(mainWindow, &Filelight::MainWindow::canceled, this, &ProgressBox::halt); } void ProgressBox::start() //slot { m_timer.start(50); //20 times per second - very smooth report(); show(); } void ProgressBox::report() //slot { setText(m_manager->files()); update(); //repaint(); } void ProgressBox::stop() { m_timer.stop(); } void ProgressBox::halt() { // canceled by stop button m_timer.stop(); QTimer::singleShot(2000, this, &QWidget::hide); } void ProgressBox::setText(int files) { m_text = i18np("%1 File", "%1 Files", files); m_textWidth = fontMetrics().boundingRect(m_text).width(); m_textHeight = fontMetrics().height(); } #define PIECES_NUM 4 static const float angleFactor[] = { -0.25, 0.9, -1.0, 0.3 }; static const float length[] = { 1.0, 1.0, 1.0, 1.0 }; static const int aLength[] = { 2000, 2000, 2000, 2000 }; void ProgressBox::paintEvent(QPaintEvent*) { QPainter paint(this); paint.setPen(Qt::transparent); paint.setRenderHint(QPainter::Antialiasing); static int tick = 0; tick+=16; for (int i=0; i * 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(QFile::encodeName(s).constData(), hiddenSize), a_start, a_end - a_start, true)); } return false; } -bool RadialMap::Map::resize(const QRect &newRect) +bool RadialMap::Map::resize(const QRectF &newRect) { //there's a MAP_2MARGIN border if (newRect.width() < width() && newRect.height() < height() && !newRect.contains(m_rect)) { return false; } uint size = qMin(newRect.width(), newRect.height()) - 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() + //this QRectF is used by paint() m_rect.setRect(0,0,size,size); - m_pixmap = QPixmap(m_rect.size()); + m_pixmap = QPixmap(m_rect.width() * m_dpr, m_rect.height() * m_dpr); + m_pixmap.setDevicePixelRatio(m_dpr); //resize the pixmap size += MAP_2MARGIN; if (m_signature) { setRingBreadth(); paint(); } return true; } 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; + QRectF rect = m_rect; rect.adjust(MAP_HIDDEN_TRIANGLE_SIZE, MAP_HIDDEN_TRIANGLE_SIZE, -MAP_HIDDEN_TRIANGLE_SIZE, -MAP_HIDDEN_TRIANGLE_SIZE); m_pixmap.fill(Qt::transparent); //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; + excess = int(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 + MAP_HIDDEN_TRIANGLE_SIZE))) * (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()); paint.setPen(segment->pen()); paint.setBrush(segment->brush()); paint.drawPie(rect, segment->start(), segment->length()); if (!segment->hasHiddenChildren()) { continue; } //draw arrow head to indicate undisplayed files/directories - QPolygon pts(3); - QPoint pos, cpos = rect.center(); + QPolygonF pts; + QPointF 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 += MAP_HIDDEN_TRIANGLE_SIZE; } sincos(ra, &sinra, &cosra); - pos.rx() = cpos.x() + static_cast(cosra * radius); - pos.ry() = cpos.y() - static_cast(sinra * radius); - pts.setPoint(i, pos); + pos.rx() = cpos.x() + cosra * radius; + pos.ry() = cpos.y() - sinra * radius; + pts << pos; } paint.setBrush(segment->pen()); paint.drawPolygon(pts); // Draw outline on the arc for hidden children QPen pen = paint.pen(); pen.setCapStyle(Qt::FlatCap); int width = 2; pen.setWidth(width); paint.setPen(pen); - QRect rect2 = rect; + QRectF rect2 = rect; width /= 2; rect2.adjust(width, width, -width, -width); paint.drawArc(rect2, segment->start(), segment->length()); } 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/map.h b/src/radialMap/map.h index a914496..37f540a 100644 --- a/src/radialMap/map.h +++ b/src/radialMap/map.h @@ -1,87 +1,88 @@ /*********************************************************************** * 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 . ***********************************************************************/ #ifndef MAP_H #define MAP_H #include "fileTree.h" #include #include -#include +#include #include namespace RadialMap { class Segment; class Map { public: explicit Map(bool summary); ~Map(); void make(const Folder *, bool = false); - bool resize(const QRect&); + bool resize(const QRectF&); bool isNull() const { return (m_signature == nullptr); } void invalidate(); - int height() const { + qreal height() const { return m_rect.height(); } - int width() const { + qreal width() const { return m_rect.width(); } QPixmap pixmap() const { return m_pixmap; } friend class Widget; private: void paint(bool antialias = true); void colorise(); void setRingBreadth(); void findVisibleDepth(const Folder *dir, uint currentDepth = 0); bool build(const Folder* const dir, const uint depth =0, uint a_start =0, const uint a_end =5760); QList *m_signature; const Folder *m_root; uint m_minSize; QVector m_limits; - QRect m_rect; + QRectF m_rect; uint m_visibleDepth; ///visible level depth of system QPixmap m_pixmap; int m_ringBreadth; uint m_innerRadius; ///radius of inner circle QString m_centerText; bool m_summary; + qreal m_dpr; uint MAP_2MARGIN; }; } #endif diff --git a/src/radialMap/widget.cpp b/src/radialMap/widget.cpp index 28f0320..f535e3c 100644 --- a/src/radialMap/widget.cpp +++ b/src/radialMap/widget.cpp @@ -1,223 +1,224 @@ /*********************************************************************** * 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 "widget.h" #include "Config.h" #include "fileTree.h" #include "radialMap.h" //constants #include "map.h" #include //ctor #include #include //sendEvent #include //ctor - finding cursor size #include //slotPostMouseEvent() #include //member #include RadialMap::Widget::Widget(QWidget *parent, bool isSummary) : QWidget(parent) , m_tree(nullptr) , m_focus(nullptr) , m_map(isSummary) , m_rootSegment(nullptr) //TODO we don't delete it, *shrug* , m_isSummary(isSummary) , m_toBeDeleted(nullptr) { setAcceptDrops(true); setMinimumSize(350, 250); connect(this, &Widget::folderCreated, this, &Widget::sendFakeMouseEvent); connect(&m_timer, &QTimer::timeout, this, &Widget::resizeTimeout); m_tooltip.setFrameShape(QFrame::StyledPanel); m_tooltip.setWindowFlags(Qt::ToolTip | Qt::WindowTransparentForInput); + m_map.m_dpr = devicePixelRatioF(); } RadialMap::Widget::~Widget() { delete m_rootSegment; } QString RadialMap::Widget::path() const { return m_tree->displayPath(); } QUrl RadialMap::Widget::url(File const * const file) const { return file ? file->url() : m_tree->url(); } void RadialMap::Widget::invalidate() { if (isValid()) { //**** have to check that only way to invalidate is this function frankly //**** otherwise you may get bugs.. //disable mouse tracking setMouseTracking(false); // Get this before reseting m_tree below QUrl invalidatedUrl(url()); //ensure this class won't think we have a map still m_tree = nullptr; m_focus = nullptr; delete m_rootSegment; m_rootSegment = nullptr; //FIXME move this disablement thing no? // it is confusing in other areas, like the whole createFromCache() thing m_map.invalidate(); update(); //tell rest of Filelight emit invalidated(invalidatedUrl); } } void RadialMap::Widget::create(const Folder *tree) { //it is not the responsibility of create() to invalidate first //skip invalidation at your own risk //FIXME make it the responsibility of create to invalidate first if (tree) { m_focus = nullptr; //generate the filemap image m_map.make(tree); //this is the inner circle in the center m_rootSegment = new Segment(tree, 0, 16*360); setMouseTracking(true); } m_tree = tree; //tell rest of Filelight emit folderCreated(tree); } void RadialMap::Widget::createFromCache(const Folder *tree) { //no scan was necessary, use cached tree, however we MUST still emit invalidate invalidate(); create(tree); } void RadialMap::Widget::sendFakeMouseEvent() //slot { // If we're not the focused window (or on another desktop), don't pop up our tooltip if (!qApp->focusWindow()) { return; } QMouseEvent me(QEvent::MouseMove, mapFromGlobal(QCursor::pos()), Qt::NoButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(this, &me); update(); } void RadialMap::Widget::resizeTimeout() //slot { // the segments are about to erased! // this was a horrid bug, and proves the OO programming should be obeyed always! m_focus = nullptr; if (m_tree) m_map.make(m_tree, true); update(); } void RadialMap::Widget::refresh(int filth) { //TODO consider a more direct connection if (!m_map.isNull()) { switch (filth) { case 1: m_focus=nullptr; m_map.make(m_tree, true); //true means refresh only break; case 2: m_map.paint(true); //antialiased painting break; case 3: m_map.colorise(); //FALL THROUGH! case 4: m_map.paint(); default: break; } update(); } } void RadialMap::Widget::zoomIn() //slot { if (m_map.m_visibleDepth > MIN_RING_DEPTH) { --m_map.m_visibleDepth; m_focus = nullptr; m_map.make(m_tree); Config::defaultRingDepth = m_map.m_visibleDepth; update(); } } void RadialMap::Widget::zoomOut() //slot { m_focus = nullptr; ++m_map.m_visibleDepth; m_map.make(m_tree); if (m_map.m_visibleDepth > Config::defaultRingDepth) Config::defaultRingDepth = m_map.m_visibleDepth; update(); } RadialMap::Segment::~Segment() { if (isFake()) delete m_file; //created by us in Builder::build() } diff --git a/src/radialMap/widget.h b/src/radialMap/widget.h index 2d9c6c5..17ec23f 100644 --- a/src/radialMap/widget.h +++ b/src/radialMap/widget.h @@ -1,124 +1,124 @@ /*********************************************************************** * 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 . ***********************************************************************/ #ifndef WIDGET_H #define WIDGET_H #include #include #include #include #include #include #include #include #include #include #include "map.h" class Folder; class File; namespace KIO { class Job; } namespace RadialMap { class Segment; class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget* = nullptr, bool = false); ~Widget() override; QString path() const; QUrl url(File const * const = nullptr) const; bool isValid() const { return m_tree != nullptr; } bool isSummary() const { return m_isSummary; } friend class Label; //FIXME badness public Q_SLOTS: void zoomIn(); void zoomOut(); void create(const Folder*); void invalidate(); void refresh(int); private Q_SLOTS: void resizeTimeout(); void sendFakeMouseEvent(); void deleteJobFinished(KJob*); void createFromCache(const Folder*); Q_SIGNALS: void activated(const QUrl&); void invalidated(const QUrl&); void folderCreated(const Folder*); void mouseHover(const QString&); void giveMeTreeFor(const QUrl&); protected: void changeEvent(QEvent*) override; void dragEnterEvent(QDragEnterEvent*) override; void dropEvent(QDropEvent*) override; void mouseMoveEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void paintEvent(QPaintEvent*) override; void resizeEvent(QResizeEvent*) override; void enterEvent(QEvent*) override; void leaveEvent(QEvent*) override; protected: - const Segment *segmentAt(QPoint position) const; //FIXME const reference for a library others can use + const Segment *segmentAt(QPointF position) const; //FIXME const reference for a library others can use const Segment *rootSegment() const { return m_rootSegment; ///never == 0 } const Segment *focusSegment() const { return m_focus; ///0 == nothing in focus } private: void paintExplodedLabels(QPainter&) const; const Folder *m_tree; const Segment *m_focus; - QPoint m_offset; + QPointF m_offset; QTimer m_timer; Map m_map; Segment *m_rootSegment; const bool m_isSummary; const Segment *m_toBeDeleted; QLabel m_tooltip; }; } #endif diff --git a/src/radialMap/widgetEvents.cpp b/src/radialMap/widgetEvents.cpp index 5f8c181..e2c7a6c 100644 --- a/src/radialMap/widgetEvents.cpp +++ b/src/radialMap/widgetEvents.cpp @@ -1,410 +1,410 @@ /*********************************************************************** * 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 //QApplication::setOverrideCursor() #include #include #include //::resizeEvent() #include #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 +const RadialMap::Segment* RadialMap::Widget::segmentAt(QPointF e) const { - //determine which segment QPoint e is above + //determine which segment QPointF 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; m_focus = segmentAt(e->pos()); if (!m_focus) { if (oldFocus && oldFocus->file() != m_tree) { m_tooltip.hide(); unsetCursor(); update(); emit mouseHover(QString()); } return; } - const QRect screenRect = window()->windowHandle()->screen()->availableGeometry(); + const QRectF screenRect = window()->windowHandle()->screen()->availableGeometry(); QPoint tooltipPosition = e->globalPos() + QPoint(20, 20); - QRect tooltipRect(tooltipPosition, m_tooltip.size()); + QRectF tooltipRect(tooltipPosition, m_tooltip.size()); // Same content as before if (m_focus == oldFocus) { if (tooltipRect.right() > screenRect.right()) { tooltipPosition.setX(screenRect.width() - m_tooltip.width()); } if (tooltipRect.bottom() > screenRect.bottom()) { tooltipPosition.setY(screenRect.height() - m_tooltip.height()); } m_tooltip.move(tooltipPosition); return; } 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.boundingRect(part).width()); } tooltipWidth += 10; tooltipHeight += 10; m_tooltip.resize(tooltipWidth, tooltipHeight); m_tooltip.setText(string); // Make sure we're visible on screen tooltipRect.setSize(QSize(tooltipWidth, tooltipHeight)); if (tooltipRect.right() > screenRect.right()) { tooltipPosition.setX(screenRect.width() - m_tooltip.width()); } if (tooltipRect.bottom() > screenRect.bottom()) { tooltipPosition.setY(screenRect.height() - m_tooltip.height()); } m_tooltip.move(tooltipPosition); m_tooltip.show(); emit mouseHover(m_focus->file()->displayPath()); update(); } 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); update(); } 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(); }