Skip to content
Snippets Groups Projects
tfiledialog.cpp 17.6 KiB
Newer Older
/***************************************************************************
 *   Copyright (C) 2015-2021 by Tomasz Bojczuk                             *
 *   seelook@gmail.com                                                     *
 *                                                                         *
 *   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 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   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/>.  *
 ***************************************************************************/
#include <QtWidgets/QtWidgets>
#include <qtr.h>
#include <tpath.h>
class TiconProvider : public QFileIconProvider
{
public:
    TiconProvider(QFileIconProvider *realProv)
        : QFileIconProvider()
        , m_realProvider(realProv)
    {
    }
    virtual QIcon icon(const QFileInfo &info) const
    {
        if (info.isDir())
            return QIcon(QLatin1String(":/mobile/dir.png"));
        else if (info.suffix().contains(QLatin1String("nel")))
            return QIcon(Tpath::img("nootka-level"));
        else if (info.suffix().contains(QLatin1String("noo")))
            return QIcon(Tpath::img("nootka-exam"));
        else if (info.suffix().contains(QLatin1String("xml"))) // either *.xml or *.musicxml
            return QIcon(Tpath::img("melody"));
        else if (info.suffix().contains(QLatin1String("mxl")))
            return QIcon(Tpath::img("melody"));
        else
            return m_realProvider->icon(info);
    }

    virtual QIcon icon(IconType type) const
    {
        switch (type) {
        case Folder:
            return qApp->style()->standardIcon(QStyle::SP_DirIcon);
        default:
            return qApp->style()->standardIcon(QStyle::SP_FileIcon);
        }
    QFileIconProvider *m_realProvider;
// #################################################################################################
// ###################     class   TnewDirMessage       ############################################
// #################################################################################################
/**
 * Subclass of QDialog to get name of new directory
 */
class TnewDirMessage : public QDialog
{
public:
    explicit TnewDirMessage(QWidget *parent = nullptr)
        : QDialog(parent)
    {
        auto label = new QLabel(qTR("QFileDialog", "Create New Folder"), this);
        m_edit = new QLineEdit(this);
        m_edit->setPlaceholderText(qTR("QFileSystemModel", "Name"));
        m_edit->setMinimumWidth(qMin<int>(Tmtr::longScreenSide() / 3, fontMetrics().width(QStringLiteral("w")) * 20));

        QSize iconS(Tmtr::fingerPixels() * 0.7, Tmtr::fingerPixels() * 0.7);
        m_createButt =
            new QPushButton(QIcon(QLatin1String(":/mobile/newDir.png")), qTR("QFileDialog", "&New Folder").replace(QLatin1String("&"), QString()), this);
        m_createButt->setIconSize(iconS);
        m_createButt->setDisabled(true);
        m_createButt->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
        m_createButt->setDefault(true);
        auto cancelButt = new QPushButton(style()->standardIcon(QStyle::SP_DialogCancelButton), qTR("QPlatformTheme", "Cancel"), this);
        cancelButt->setIconSize(iconS);
        cancelButt->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);

        auto lay = new QVBoxLayout;
        lay->addWidget(label);
        lay->addWidget(m_edit);
        lay->addSpacing(Tmtr::fingerPixels() / 2);
        auto buttonLay = new QHBoxLayout;
        buttonLay->addWidget(m_createButt);
        buttonLay->addWidget(cancelButt);
        buttonLay->setContentsMargins(0, 0, 0, 0);
        lay->addLayout(buttonLay);
        lay->setContentsMargins(0, 0, 0, 0);
        setLayout(lay);
        connect(m_createButt, &QPushButton::clicked, this, &QDialog::accept);
        connect(cancelButt, &QPushButton::clicked, this, &QDialog::reject);
        connect(m_edit, &QLineEdit::textChanged, this, &TnewDirMessage::textChangedSlot);
    }
    QString getName() { return m_edit->text(); }
    static QString dirName(QWidget *parent = 0)
    {
        TnewDirMessage newDir(parent);
        if (newDir.exec() == Accepted && !newDir.getName().isEmpty())
            return newDir.getName();
        else
            return QString();
    }
    void textChangedSlot(const QString &t) { m_createButt->setDisabled(t.isEmpty()); }
    QLineEdit *m_edit;
    QPushButton *m_createButt;
QString TfileDialog::getOpenFileName(const QString &directory, const QString &filter)
{
    TfileDialog openDialog(directory, filter, TfileDialog::e_acceptOpen);
    if (openDialog.exec() == QFileDialog::Accepted)
        return openDialog.selectedFile();
    else
        return QString();
QString TfileDialog::getSaveFileName(const QString &directory, const QString &filter)
{
    TfileDialog saveDialog(directory, filter, TfileDialog::e_acceptSave);
    if (saveDialog.exec() == QFileDialog::Accepted)
        return saveDialog.selectedFile();
    else
        return QString();
// #################################################################################################
// #################################################################################################
// ###################           class TfileDialog      ############################################
// #################################################################################################
// #################################################################################################
TfileDialog::TfileDialog(const QString &directory, const QString &filter, EacceptMode mode)
    : QDialog(nullptr)
    , m_acceptMode(mode)
    , m_newDirItem(nullptr)
    showMaximized();

    Tandroid::askForWriteAcces();
    // left side menu
    m_menu = new QListWidget(this);
    m_menu->setIconSize(QSize(60, 60));
    m_menu->setMaximumWidth(80);
    m_menu->setViewMode(QListView::IconMode);
    m_menu->setMovement(QListView::Static);
    m_menu->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_menu->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    int bSize = qBound<int>(Tmtr::fingerPixels() * 1.1, Tmtr::longScreenSide() / 12, Tmtr::fingerPixels() * 1.6);
    m_menu->setIconSize(QSize(bSize, bSize));
    m_menu->setMaximumWidth(bSize + 10);
    QFont f = font();
    f.setPixelSize(qMin<int>(bSize / 5, fontMetrics().height()));
    m_menu->setFont(f);
    m_menu->setObjectName(QLatin1String("fileMenu")); // revert colors
    m_menu->setStyleSheet(m_menu->styleSheet() + QLatin1String(" QListWidget#fileMenu { background: palette(base); color: palette(text); }"));
    QScroller::grabGesture(m_menu->viewport(), QScroller::LeftMouseButtonGesture);

    QStringList filters = filter.split(QLatin1String("|"));

    QIcon fileIcon;
    if (filters.size() == 1) { // if filter has only one entry - check is it *.nel or *.noo and use its icon
        if (filters.first().contains(QLatin1String("nel")))
            fileIcon = QIcon(Tpath::img("nootka-level"));
        else if (filters.first().contains(QLatin1String("noo")))
            fileIcon = QIcon(Tpath::img("nootka-exam"));
    }
    if (filters.size() != 1 || fileIcon.isNull()) { // if more or none filters or other file types use standard icons
        if (mode == e_acceptSave)
            fileIcon = QIcon(Tpath::img("save"));
        else
            fileIcon = QIcon(Tpath::img("open"));
    }
    m_acceptItem = addMenuItem(fileIcon, mode == e_acceptSave ? qTR("QShortcut", "Save") : qTR("QShortcut", "Open"));

    QString space = QLatin1String(" ");
    QString newLine = QLatin1String("\n");
    m_dirUpItem = addMenuItem(QIcon(QLatin1String(":/mobile/dirUp.png")), qTR("QFileDialog", "Parent Directory").replace(space, newLine));
        m_newDirItem = addMenuItem(QIcon(QLatin1String(":/mobile/newDir.png")),
                                   qTR("QFileDialog", "&New Folder").replace(space, newLine).replace(QLatin1String("&"), QString()));
    if (Tandroid::getAPIlevelNr() < 19) // display SD card shortcut only below Kitkat
        addMenuItem(QIcon(QLatin1String(":/mobile/card.png")), tr("Memory card").replace(space, newLine));
    m_cancelItem = addMenuItem(QIcon(Tpath::img("exit")), qTR("QShortcut", "Close"));

    // upper location label, file name edit, extension combo
    m_locationLab = new QLabel(this);
    m_locationLab->setAlignment(Qt::AlignRight);
    m_locationLab->setFixedWidth(qApp->fontMetrics().height() * 7);

    m_editName = new QLineEdit(this);
    m_editName->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
    if (mode == e_acceptOpen)
        m_editName->setReadOnly(true);

    m_extensionCombo = new QComboBox(this);
    if (!filter.isEmpty()) {
        int filterCharsLen = 0, longestId = 0;
        for (int f = 0; f < filters.size(); ++f) {
            if (filters[f].length() > filterCharsLen) {
                filterCharsLen = filters[f].length();
                longestId = f;
            }
        }
        m_extensionCombo->setFixedWidth(qApp->fontMetrics().boundingRect(filters[longestId]).width() + qApp->fontMetrics().height() * 1.5);

    // file list
    m_list = new QListView(this);
    int is = Tmtr::fingerPixels();
    m_list->setIconSize(QSize(is, is));
    m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
    m_list->setSelectionBehavior(QAbstractItemView::SelectItems);
    m_list->setSelectionMode(QAbstractItemView::SingleSelection);
    m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    QScroller::grabGesture(m_list->viewport(), QScroller::LeftMouseButtonGesture);

    // Layout
    m_lay = new QBoxLayout(QBoxLayout::LeftToRight);
    m_lay->addWidget(m_menu);
    auto innLay = new QVBoxLayout;
    auto fileLay = new QHBoxLayout;
    fileLay->addWidget(m_locationLab);
    fileLay->addWidget(m_editName);
    fileLay->addWidget(m_extensionCombo);
    innLay->addLayout(fileLay);
    innLay->addWidget(m_list);
    m_lay->addLayout(innLay);
    setLayout(m_lay);
    innLay->setContentsMargins(0, m_lay->contentsMargins().top(), 0, 0);
    m_lay->setContentsMargins(0, 0, m_lay->contentsMargins().right(), 0);

    // Determine root path from given parameter 'directory'
    m_fileModel = new QFileSystemModel(this);
    QFileInfo fi(directory);
    // Directory rather exists (checked at the very beginning) - if not it is treated as a file
    if (fi.isDir())
        m_fileModel->setRootPath(directory);
    else {
        m_fileModel->setRootPath(fi.absolutePath());
        m_editName->setText(fi.fileName());
    // Set filters
    if (filters.size()) {
        for (int i = 0; i < filters.size(); ++i) {
            filters[i].prepend(QLatin1String("."));
            m_extensionCombo->addItem(filters[i]);
            filters[i].prepend(QLatin1String("*"));
        }
        m_extensionCombo->setCurrentIndex(0);
        m_fileModel->setNameFilters(filters);
        m_fileModel->setNameFilterDisables(false);
    }
    m_list->setModel(m_fileModel);
    m_list->setRootIndex(m_fileModel->index(m_fileModel->rootPath()));
    m_fileModel->setIconProvider(new TiconProvider(m_fileModel->iconProvider()));
    connect(m_menu, &QListWidget::itemClicked, this, &TfileDialog::menuClickedSlot);
    connect(m_list, &QListView::clicked, this, &TfileDialog::itemClickedSlot);
    // Adjust width of menu list according to widest text
    QTimer::singleShot(100, this, [=] {
        m_menu->setFixedWidth(m_menu->sizeHintForColumn(0) + 2 * m_menu->frameWidth());
    });
    QString externalStorage = Tandroid::getExternalPath();
    if (mode == e_acceptSave && externalStorage == m_fileModel->rootPath()) {
        // Ask to create Nootka folder but only when file dialog is called with external storage path (first launch)
        if (!externalStorage.isEmpty() && !QFileInfo::exists(externalStorage + QLatin1String("/Nootka")))
            QTimer::singleShot(200, this, [=] {
                createNootkaDir();
            });
    }
}
// #################################################################################################
// ###################              PROTECTED           ############################################
// #################################################################################################
void TfileDialog::itemClickedSlot(const QModelIndex &index)
{
    if (m_fileModel->isDir(index)) {
        m_list->setRootIndex(index);
        m_fileModel->setRootPath(m_fileModel->filePath(index));
        updateLocationLabel();
        if (acceptMode() == e_acceptOpen) {
            m_editName->setText(QString());
            m_selectedFile.clear(); // reset previously selected file
        }
    } else {
        m_selectedFile = m_fileModel->fileInfo(index).absoluteFilePath();
        m_editName->setText(QFileInfo(m_selectedFile).fileName());
    }
void TfileDialog::performAction()
{
    if (acceptMode() == e_acceptSave) {
        if (m_editName->text().isEmpty()) {
            QMessageBox::warning(this, QString(), qTR("QFileSystemModel", "Invalid filename"));
            return;
        } else {
            m_selectedFile = m_fileModel->rootPath() + QLatin1String("/") + m_editName->text();
            QFileInfo fi(m_selectedFile);
            bool addExt = false; // Add current file extension
            if (!fi.suffix().isEmpty()) {
                if (m_extensionCombo->currentIndex() > -1 && m_extensionCombo->currentText() != m_selectedFile.right(4))
                    addExt = true;
            } else
                addExt = true;
            if (addExt) {
                m_selectedFile.append(m_extensionCombo->currentText());
                fi.setFile(m_selectedFile);
            }
            if (fi.exists()) {
                if (QMessageBox::question(this, QString(), qTR("QFileDialog", "%1 already exists.\nDo you want to replace it?").arg(m_selectedFile))
                    != QMessageBox::Ok) {
                    m_selectedFile.clear();
                    return;
                }
            }
void TfileDialog::menuClickedSlot(QListWidgetItem *item)
{
    if (item == m_cancelItem)
        reject();
    else if (item == m_acceptItem)
        performAction();
    else if (item == m_dirUpItem)
        dirUpSlot();
    else if (item == m_newDirItem)
        newDirSlot();
    else if (m_menu->currentRow() == 3) {
        itemClickedSlot(m_fileModel->index(Tandroid::getExternalPath()));
    }
void TfileDialog::dirUpSlot()
{
    QDir dir(m_fileModel->rootDirectory());
    if (dir.cdUp()) {
        m_fileModel->setRootPath(dir.absolutePath());
        m_list->setRootIndex(m_fileModel->index(dir.absolutePath()));
        updateLocationLabel();
    }
void TfileDialog::newDirSlot()
{
    createNewDir(TnewDirMessage::dirName(this));
void TfileDialog::createNewDir(const QString &newDir)
{
    if (!newDir.isEmpty()) {
        auto dirIndex = m_fileModel->mkdir(m_list->rootIndex(), newDir);
        if (dirIndex.isValid())
            itemClickedSlot(dirIndex);
        else
            QMessageBox::warning(this, m_newDirItem->text(), qTR("QFtp", "Creating directory failed:\n%1").arg(newDir));
    }
QListWidgetItem *TfileDialog::addMenuItem(const QIcon &icon, const QString &text)
{
    auto menuButton = new QListWidgetItem(m_menu);
    menuButton->setIcon(icon);
    if (!text.isEmpty())
        menuButton->setText(text);
    menuButton->setTextAlignment(Qt::AlignHCenter);
    menuButton->setFlags(Qt::ItemIsEnabled | Qt::ItemNeverHasChildren);
    return menuButton;
void TfileDialog::updateLocationLabel()
{
    m_locationLab->setText(
        fontMetrics().elidedText(m_fileModel->rootPath() + QLatin1String("/"), Qt::ElideMiddle, m_locationLab->width(), Qt::TextShowMnemonic));
void TfileDialog::createNootkaDir()
{
    if (QMessageBox::question(
            this,
            QString(),
            tr("Directory named <b>Nootka</b> will be created in<br>%1<br>Application files will be written there.").arg(m_fileModel->rootPath()))
        == QMessageBox::Yes)
        createNewDir(QLatin1String("Nootka"));
QString TfileDialog::getOpenFileName(const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
{
    QString fN;
    auto f = qApp->font();
    qApp->setFont(Tmtr::systemFont);
    fN = QFileDialog::getOpenFileName(nullptr, caption, dir, filter, selectedFilter, options);
    qApp->setFont(f);
    return fN;
QString TfileDialog::getSaveFileName(const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
{
    QString fN;
    auto f = qApp->font();
    qApp->setFont(Tmtr::systemFont);
    fN = QFileDialog::getSaveFileName(nullptr, caption, dir, filter, selectedFilter, options);
    qApp->setFont(f);
    return fN;