widgetEvents.cpp 14.3 KB
Newer Older
1 2
/***********************************************************************
* Copyright 2003-2004  Max Howell <max.howell@methylblue.com>
3
* Copyright 2008-2009  Martin Sandsmark <martin.sandsmark@kde.org>
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
*
* 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 <http://www.gnu.org/licenses/>.
***********************************************************************/

22 23
#include "fileTree.h"
#include "Config.h"
24 25 26
#include "radialMap.h"   //class Segment
#include "widget.h"

27
#include <KCursor>     //::mouseMoveEvent()
28
#include <KJob>
29
#include <KIO/Job>     //::mousePressEvent()
30
#include <KIO//DeleteJob>
31 32
#include <KIO/JobUiDelegate>
#include <KMessageBox> //::mousePressEvent()
33
#include <QMenu>  //::mousePressEvent()
34
#include <KRun>        //::mousePressEvent()
35
#include <KToolInvocation>
36
#include <QUrl>
37
#include <KLocalizedString>
38
#include <kio_version.h>
39 40 41 42 43 44 45 46 47 48 49

#include <QApplication> //QApplication::setOverrideCursor()
#include <QClipboard>
#include <QPainter>
#include <QTimer>      //::resizeEvent()
#include <QDropEvent>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QMouseEvent>
#include <QDragEnterEvent>
#include <QMimeData>
Laurent Montel's avatar
Laurent Montel committed
50
#include <KUrlMimeData>
51 52
#include <QWindow>
#include <QScreen>
53 54

#include <cmath>         //::segmentAt()
55

56
void RadialMap::Widget::resizeEvent(QResizeEvent*)
57
{
58 59 60
    if (m_map.resize(rect()))
        m_timer.setSingleShot(true);
    m_timer.start(500); //will cause signature to rebuild for new size
61 62 63 64 65 66

    //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;
}

67
void RadialMap::Widget::paintEvent(QPaintEvent*)
68
{
69 70 71
    QPainter paint;
    paint.begin(this);

72
    if (!m_map.isNull())
73
        paint.drawPixmap(m_offset, m_map.pixmap());
74
    else {
75
        paint.drawText(rect(), 0, i18nc("We messed up, the user needs to initiate a rescan.", "Internal representation is invalid,\nplease rescan."));
76 77
        return;
    }
78 79

    //exploded labels
80
    if (!m_map.isNull() && !m_timer.isActive())
81
    {
82 83 84 85 86
        if (Config::antialias) {
            paint.setRenderHint(QPainter::Antialiasing);
            //make lines appear on pixel boundaries
            paint.translate(0.5, 0.5);
        }
87
        paintExplodedLabels(paint);
88
    }
89 90
}

Yunhe Guo's avatar
Yunhe Guo committed
91
const RadialMap::Segment* RadialMap::Widget::segmentAt(QPointF e) const
92
{
Yunhe Guo's avatar
Yunhe Guo committed
93
    //determine which segment QPointF e is above
94 95 96

    e -= m_offset;

97
    if (m_map.m_signature.isEmpty())
98
        return nullptr;
99

100
    if (e.x() <= m_map.width() && e.y() <= m_map.height())
101 102 103 104 105
    {
        //transform to cartesian coords
        e.rx() -= m_map.width() / 2; //should be an int
        e.ry()  = m_map.height() / 2 - e.y();

106
        double length = hypot(e.x(), e.y());
107

108
        if (length >= m_map.m_innerRadius) //not hovering over inner circle
109 110 111
        {
            uint depth  = ((int)length - m_map.m_innerRadius) / m_map.m_ringBreadth;

112
            if (depth <= m_map.m_visibleDepth) //**** do earlier since you can //** check not outside of range
113 114 115 116 117 118
            {
                //vector calculation, reduces to simple trigonometry
                //cos angle = (aibi + ajbj) / albl
                //ai = x, bi=1, aj=y, bj=0
                //cos angle = x / (length)

119
                uint a  = (uint)(acos((double)e.x() / length) * 916.736);  //916.7324722 = #radians in circle * 16
120 121

                //acos only understands 0-180 degrees
122
                if (e.y() < 0) a = 5760 - a;
123

124 125 126 127
                for (Segment *segment : m_map.m_signature[depth]) {
                    if (segment->intersects(a))
                        return segment;
                }
128 129 130 131 132
            }
        }
        else return m_rootSegment; //hovering over inner circle
    }

133
    return nullptr;
134 135
}

136
void RadialMap::Widget::mouseMoveEvent(QMouseEvent *e)
137
{
138
    //set m_focus to what we hover over, update UI if it's a new segment
139

140
    Segment const * const oldFocus = m_focus;
141
    m_focus = segmentAt(e->pos());
142

143 144 145 146 147
    if (!m_focus) {
        if (oldFocus && oldFocus->file() != m_tree) {
            m_tooltip.hide();
            unsetCursor();
            update();
148

Laurent Montel's avatar
Laurent Montel committed
149
            Q_EMIT mouseHover(QString());
150
        }
151

152 153 154
        return;
    }

Yunhe Guo's avatar
Yunhe Guo committed
155
    const QRectF screenRect = window()->windowHandle()->screen()->availableGeometry();
156 157

    QPoint tooltipPosition = e->globalPos() + QPoint(20, 20);
Yunhe Guo's avatar
Yunhe Guo committed
158
    QRectF tooltipRect(tooltipPosition, m_tooltip.size());
159 160 161 162 163 164 165 166 167 168 169 170 171 172

    // 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);
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
    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<const Folder*>(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);
212
            }
213 214 215 216 217
        }

        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");
218 219
        }
    }
220

221 222 223 224 225 226
    // 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();
Laurent Montel's avatar
Laurent Montel committed
227
        tooltipWidth = qMax(tooltipWidth, fontMetrics.boundingRect(part).width());
228
    }
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    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();

Laurent Montel's avatar
Laurent Montel committed
247
    Q_EMIT mouseHover(m_focus->file()->displayPath());
248
    update();
249
}
250

251
void RadialMap::Widget::enterEvent(QEvent *)
252 253 254 255
{
    if (!m_focus) return;

    setCursor(Qt::PointingHandCursor);
Laurent Montel's avatar
Laurent Montel committed
256
    Q_EMIT mouseHover(m_focus->file()->displayPath());
257 258 259
    update();
}

260 261 262 263 264
void RadialMap::Widget::leaveEvent(QEvent *)
{
    m_tooltip.hide();
}

265 266
void RadialMap::Widget::mousePressEvent(QMouseEvent *e)
{
267 268
    if (!isEnabled())
        return;
269

270
    //m_focus is set correctly (I've been strict, I assure you it is correct!)
271

272 273 274
    if (!m_focus || m_focus->isFake()) {
        return;
    }
275

276 277
    const QUrl url   = Widget::url(m_focus->file());
    const bool isDir = m_focus->file()->isFolder();
278

279
    // Open file
280
    if (e->button() == Qt::MiddleButton || (e->button() == Qt::LeftButton && !isDir)) {
281 282 283 284 285 286 287
        new KRun(url, this, true);

        return;
    }

    if (e->button() == Qt::LeftButton) {
        if (m_focus->file() != m_tree) {
Laurent Montel's avatar
Laurent Montel committed
288
            Q_EMIT activated(url); //activate first, this will cause UI to prepare itself
289 290
            createFromCache((Folder *)m_focus->file());
        } else if (KIO::upUrl(url) != url) {
Laurent Montel's avatar
Laurent Montel committed
291
            Q_EMIT giveMeTreeFor(KIO::upUrl(url));
292 293 294 295 296 297 298 299 300 301
        }

        return;
    }

    if (e->button() != Qt::RightButton) {
        // Ignore other mouse buttons
        return;
    }

302
    // Actions in the right click menu
303 304 305 306 307 308
    QAction* openFileManager = nullptr;
    QAction* openTerminal = nullptr;
    QAction* centerMap = nullptr;
    QAction* openFile = nullptr;
    QAction* copyClipboard = nullptr;
    QAction* deleteItem = nullptr;
309
    QAction* doNotScanItem = nullptr;
310

311
    QMenu popup;
312
    popup.setTitle(m_focus->file()->displayPath(m_tree));
313

314
    if (isDir) {
Laurent Montel's avatar
Laurent Montel committed
315
        openFileManager = popup.addAction(QIcon::fromTheme(QStringLiteral("system-file-manager")), i18n("Open &File Manager Here"));
316

317
        if (url.scheme() == QLatin1String("file")) {
Laurent Montel's avatar
Laurent Montel committed
318
            openTerminal = popup.addAction(QIcon::fromTheme(QStringLiteral( "utilities-terminal" )), i18n("Open &Terminal Here"));
319 320 321 322
        }

        if (m_focus->file() != m_tree) {
            popup.addSeparator();
Laurent Montel's avatar
Laurent Montel committed
323
            centerMap = popup.addAction(QIcon::fromTheme(QStringLiteral( "zoom-in" )), i18n("&Center Map Here"));
324
        }
325 326 327
        
        popup.addSeparator();
        doNotScanItem = popup.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Do &Not Scan This Folder"));
328
    } else {
Laurent Montel's avatar
Laurent Montel committed
329
        openFile = popup.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("Scan/open the path of the selected element", "&Open"));
330
    }
331

332
    popup.addSeparator();
Laurent Montel's avatar
Laurent Montel committed
333
    copyClipboard = popup.addAction(QIcon::fromTheme(QStringLiteral( "edit-copy" )), i18n("&Copy to clipboard"));
334

335 336
    if (m_focus->file() != m_tree) {
        popup.addSeparator();
Laurent Montel's avatar
Laurent Montel committed
337
        deleteItem = popup.addAction(QIcon::fromTheme(QStringLiteral( "edit-delete" )), i18n("&Delete"));
338 339
    }

340
    QAction* clicked = popup.exec(e->globalPos(), nullptr);
341 342

    if (openFileManager && clicked == openFileManager) {
Laurent Montel's avatar
Laurent Montel committed
343
        KRun::runUrl(url, QStringLiteral("inode/directory"), this
344 345 346 347
             #if KIO_VERSION >= QT_VERSION_CHECK(5, 31, 0)
                     , KRun::RunFlags()
             #endif
                     );
348 349 350
    } else if (openTerminal && clicked == openTerminal) {
        KToolInvocation::invokeTerminal(QString(),url.path());
    } else if (centerMap && clicked == centerMap) {
Laurent Montel's avatar
Laurent Montel committed
351
        Q_EMIT activated(url); //activate first, this will cause UI to prepare itself
352
        createFromCache((Folder *)m_focus->file());
353 354 355 356 357
    } else if (doNotScanItem && clicked == doNotScanItem) {
        if (!Config::skipList.contains(Widget::url(m_focus->file()).toLocalFile())) {
            Config::skipList.append(Widget::url(m_focus->file()).toLocalFile());
            Config::write();
        }
358 359 360 361 362 363 364 365 366 367 368 369 370 371
    } else if (openFile && clicked == openFile) {
        new KRun(url, this, true);
    } else if (clicked == copyClipboard) {
        QMimeData* mimedata = new QMimeData();
        mimedata->setUrls(QList<QUrl>() << 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("<qt>The folder at <i>'%1'</i> will be <b>recursively</b> and <b>permanently</b> deleted.</qt>", url.toString())
                : i18n("<qt><i>'%1'</i> will be <b>permanently</b> deleted.</qt>", url.toString());
        const int userIntention = KMessageBox::warningContinueCancel(
                    this, message,
Laurent Montel's avatar
Laurent Montel committed
372
                    QString(), KGuiItem(i18n("&Delete"), QStringLiteral("edit-delete")));
373 374 375 376 377 378

        if (userIntention == KMessageBox::Continue) {
            KIO::Job *job = KIO::del(url);
            connect(job, &KJob::finished, this, &RadialMap::Widget::deleteJobFinished);
            QApplication::setOverrideCursor(Qt::BusyCursor);
            setEnabled(false);
379
        }
380 381 382
    } else {
        //ensure m_focus is set for new mouse position
        sendFakeMouseEvent();
383
    }
384 385
}

386
void RadialMap::Widget::deleteJobFinished(KJob *job)
387
{
388
    QApplication::restoreOverrideCursor();
389
    setEnabled(true);
390
    if (!job->error() && m_toBeDeleted) {
391
        m_toBeDeleted->file()->parent()->remove(m_toBeDeleted->file());
392 393
        m_toBeDeleted = nullptr;
        m_focus = nullptr;
394
        m_map.make(m_tree, true);
395
        update();
396
    } else
397
        KMessageBox::error(this, job->errorString(), i18n("Error while deleting"));
398 399
}

400
void RadialMap::Widget::dropEvent(QDropEvent *e)
401
{
402
    QList<QUrl> uriList = KUrlMimeData::urlsFromMimeData(e->mimeData());
403
    if (!uriList.isEmpty())
Laurent Montel's avatar
Laurent Montel committed
404
        Q_EMIT giveMeTreeFor(uriList.first());
405
}
406

407
void RadialMap::Widget::dragEnterEvent(QDragEnterEvent *e)
408
{
409
    QList<QUrl> uriList = KUrlMimeData::urlsFromMimeData(e->mimeData());
410
    e->setAccepted(!uriList.isEmpty());
411
}
412 413 414

void RadialMap::Widget::changeEvent(QEvent *e)
{
415
    if (e->type() == QEvent::ApplicationPaletteChange ||
416 417 418
        e->type() == QEvent::PaletteChange)
        m_map.paint();
}