Basic file handling with QDirModel and QTreeView



  • Hi,

    I am building a kind of file explorer but I have issues with controlling the move and rename actions. To modify my files I have a custom context menu:

    void MainWindow::showDirContextMenu(const QPoint &pos){
        QModelIndex index=ui->treeView->indexAt(pos);
        bool isRoot = false;
    
        if(!index.isValid()){
            index = dirModel->index(projects[ui->projectSelector->currentText()]);
            isRoot = true;
        }
    
        bool isDir = dirModel->isDir(index);
    
        QAction *pOpen = new QAction(QIcon(":/icon/icons/openEnable.png"),tr("open"), this);
        QAction *pRename = new QAction(tr("rename"), this);
        pRename->setShortcut(QKeySequence(Qt::Key_F2));
        QAction *pDel = new QAction(tr("delete"), this);
        pDel->setShortcut(QKeySequence(Qt::Key_Delete));
        QAction *pCopy =new QAction(QIcon(":/icon/icons/copyEnable.png"),tr("&copy"), this);
        pCopy->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_C));
        QAction *pCut = new QAction(QIcon(":/icon/icons/cutEnable.png"), tr("cut"), this);
        pCut->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_X));
        QAction *pPast = new QAction(QIcon(":/icon/icons/pastEnable.png"), tr("past"), this);
        QAction *pAddFolder = new QAction(tr("Create Folder"), this);
        QAction *pAddFile = new QAction(tr("Add new file"), this);
    
        QMenu *menu=new QMenu(this);
        menu->addAction(pOpen);
        if(!isRoot)
            menu->addAction(pRename);
        menu->addSeparator();
        if(!isRoot){
            menu->addAction(pCopy);
            menu->addAction(pCut);
        }
        menu->addAction(pPast);
        menu->addSeparator();
        menu->addAction(pAddFile);
        if(isDir){
            menu->addAction(pAddFolder);
        }
        menu->addSeparator();
        if(!isRoot)
            menu->addAction(pDel);
    
        QAction* selectedItem = menu->exec(ui->treeView->viewport()->mapToGlobal(pos));
    
        if(selectedItem == pOpen){
            on_treeView_doubleClicked(index);
        }else if(selectedItem == pRename){
            ui->treeView->edit(index); // Update 2017-03-21
        }else if(selectedItem == pDel){
            int ret = QMessageBox::question(this, tr("Delete file?"), tr("Are you sure you want to delete this file? This cannot be undone."), QMessageBox::Cancel | QMessageBox::Ok);
            if(ret == QMessageBox::Ok){
                bool deleted;
                // files are not moved to trash! change!
                if(isDir)
                    deleted = dirModel->rmdir(index);
                else
                    deleted = dirModel->remove(index);
    
                if(!deleted)
                    QMessageBox::warning(this, tr("File error"), tr("The file could not be removed."));
            }
        }else if(selectedItem == pAddFolder){
            QString mName = QInputDialog::getText(this, tr("Create Folder"), tr("Folder Name"));
            if(!mName.isEmpty()){
                dirModel->mkdir(index, mName);
            }
        }else if(selectedItem == pAddFile){
            QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), dirModel->filePath(index), tr("PHP file (*.php, *php3)"));
    
            if(!fileName.isEmpty()){
                QFile sFile(fileName);
                if(!sFile.open(QFile::WriteOnly | QFile::Text)){
                    qDebug() << "we have an error =( " << " " << sFile.errorString();
                }else{
                    QTextStream out(&sFile);
                    out << "";
    
                    sFile.flush();
                    sFile.close();
    
                    dirModel->refresh();
                    ui->treeView->expand(index);
                    addEditor(fileName, true);
                }
            }
        }
    }
    
    // update 2017-03-21
    void MainWindow::fileRenamed(QString path, QString oldName, QString newName)
    {
        // Handle the rename
    }
    

    If I press the F2 key the file name can be edited but my rename function is not used (no output from qDebug). If I choose the rename action from the menu the rename function is used but I do not know how I can trigger the rename action as F2 does? Apparently there is no ui->treeView->rename(index).
    Ideally I could control the entire rename process from that rename function. In case the file is opened I need to tell the file that is has a new name and therefore path.

    Also I am currently reading up on drag and drop. Is there really no standard signal dropped, which would give me a start and end path?

    Thanks for any help in advance!


  • Moderators

    @Sikarjan What is the value of isRoot? Did you try to debug to see what happens?



  • In case you click on an empty space (no file or folder) the root folder is selected. The folder root folder itself is not visible in the tree view. The rename action is only active if a file or folder is selected.


  • Lifetime Qt Champion

    Hi,

    Just a side note, this class has been obsoleted. You should consider using QFileSystemModel.



  • Thanks for the hint. Did not see that QDirModel is obsolet but QFileSystemModel does not seem to be a very good replacement. It has a fileChanged signal but I cannot refresh it. I did a quick search but looks like it is not possible to refresh a QFileSystemModel.


  • Lifetime Qt Champion

    What exactly do you mean by refresh ?



  • QDirModel has a function refresh() to update the view in case a file was added or removed.


  • Lifetime Qt Champion

    That should happen automatically with QFileSystemModel.



  • Okay, need to try that. But it will probably not work if a file is changed outside of the app. There is a resetInternalData and modelReset function, inherited from QAbstractModel. Not sure if it is a good idea to use them but I will give it a try.

    Thanks for the help so far but does really no one know how to trigger the rename function by code?



  • Hi,

    I had a success regarding the renaming. The rename function is not part of the DirModel but the treeView!

    ui->treeView->edit(index);
    

    Did the trick. And with switching to QFileSystemModel gave me a fileRenamed signal that is emitted when the changes are accepted. So the renaming part is done but I still have aquestion:
    I noticed that my shortcuts I defined for the actions do not work. They only work if they are already implemented by the treeView. What did I do wrong here?


  • Lifetime Qt Champion

    AFAICS, you are re-creating every QAction every time you generate your contextual menu. So before you trigger that once, you likely don't have any shortcut for your application.

    What is usually done is to create your QAction once in a central place (usually the constructor) and then re-use them as necessary to build menus/contextual menus etc.



  • Thanks for the hint. I did that. I moved the QAction block into the constructor of MainWindow but my shortcuts still do not work. I've been reading the QAction help but I do not see what I am doing differently from the example...


  • Lifetime Qt Champion

    Did you took a look at the Application Example ? They show how to create QAction and use them from several places.



  • My shortcuts cannot work because the triggered action is not connect to a slot! I was handling everything in one function and this function could not be entered by the shortcut. So now I am redoing my entire code and define the action, connect it so a slot and handle the action in the slot. But it still does not work! While compiling I get an error:

    QMetaObject::connectSlotsByName: No matching signal for on_pDel_triggered()
    
    pDel = new QAction(tr("delete"), this);
    pDel->setShortcut(QKeySequence(Qt::Key_Delete));
    connect(pDel, QAction::triggered, this, MainWindow::on_pDel_triggered);
    

    Why is this just not working?

    EDIT:
    Apparently on_ is not allowed for a custom slot... I changed the slot name to pDel_triggered() and now the app complies but the shortcut is still not working =( Also if I use the context menu now the slot is called twice.


  • Lifetime Qt Champion

    The on_WidgetName_SignalName pattern is used by the connectSlotsByName method to automatically create the connection between the signal SignalName from the widget named WidgetName to that slot. The most common use case is designer based widgets.

    Can you show the complete code where you setup your action ?



  • @SGaist Thanks a lot for taking so much interest in my problem.
    Here is what I currently have. I solved the issue with the signal being emitted twice over the context menu but I am still not getting anywhere with my shortcuts. Currently I am focussing on pDel.

    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
        isCtrlPressed = false;
        pCopyPath = "";
        isCutAction = false;
        fileContextMenu = new QMenu(ui->treeView);
    
        // Define actions for explorer
        pOpen = new QAction(QIcon(":/icon/icons/openEnable.png"),tr("open"), this);
        pRename = new QAction(tr("rename"), this);
        pRename->setShortcut(QKeySequence(Qt::Key_F2));
        pDel = new QAction(tr("delete"), fileContextMenu);
        pDel->setShortcut(QKeySequence(Qt::Key_Delete));
        connect(pDel, SIGNAL(triggered()), this, SLOT(pDel_triggered()));
        pCopy =new QAction(QIcon(":/icon/icons/copyEnable.png"),tr("&copy"), this);
        pCopy->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_C));
        pCut = new QAction(QIcon(":/icon/icons/cutEnable.png"), tr("cut"), this);
        pCut->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_X));
        pPast = new QAction(QIcon(":/icon/icons/pastEnable.png"), tr("past"), this);
        pAddFolder = new QAction(tr("Create Folder"), this);
        pAddFile = new QAction(QIcon(":/icon/icons/newEnable.png"),tr("Add new file"), this);
    
        fileContextMenu->addAction(pOpen);
        fileContextMenu->addAction(pRename);
        fileContextMenu->addSeparator();
        fileContextMenu->addAction(pCopy);
        fileContextMenu->addAction(pCut);
        fileContextMenu->addAction(pPast);
        fileContextMenu->addSeparator();
        fileContextMenu->addAction(pAddFile);
        fileContextMenu->addAction(pAddFolder);
        fileContextMenu->addSeparator();
        fileContextMenu->addAction(pDel);
    
        [...]
    
        dirModel = new QFileSystemModel(this);
        dirModel->setReadOnly(false);
        connect(dirModel, SIGNAL(fileRenamed(QString,QString,QString)), this, SLOT(fileRenamed(QString,QString,QString)));
        ui->treeView->setModel(dirModel);
        ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(ui->treeView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showDirContextMenu(QPoint)));
    }
    
    void MainWindow::showDirContextMenu(const QPoint &pos){
        QModelIndex index=ui->treeView->indexAt(pos);
        bool isRoot = false;
    
        if(!index.isValid()){
            index = dirModel->index(projects[ui->projectSelector->currentText()]);
            isRoot = true;
        }
    
        bool isDir = dirModel->isDir(index);
    
        if(isRoot){
            pRename->setEnabled(false);
            pDel->setEnabled(false);
            pCopy->setEnabled(false);
            pCut->setEnabled(false);
        }else{
            pRename->setEnabled(true);
            pDel->setEnabled(true);
            pCopy->setEnabled(true);
            pCut->setEnabled(true);
        }
    
        if(pCopyPath != "" && isDir){
            pPast->setEnabled(true);
            pPast->setStatusTip(tr("Insert")+ ": " + pCopyPath.section("/",-1,-1));
        }else{
            pPast->setEnabled(false);
        }
    
        QAction* selectedItem = fileContextMenu->exec(ui->treeView->viewport()->mapToGlobal(pos));
    
        if(selectedItem == pOpen){
            on_treeView_doubleClicked(index);
        }else if [...]
    }
    
    void MainWindow::pDel_triggered()
    {
        QModelIndex index = ui->treeView->currentIndex();
        qDebug() << dirModel->filePath(index);
    }
    

    Action without a shortcut I plan to handle in the showDirContextMenu function. The other I plan to handle by signal and slots but currently the function pDel_triggered is only entered if I use the context menu. The shortcut is doing nothing.

    Last thing I tried was to change the parents for the contectMenu and the actions.



  • @Sikarjan

    hi,

    a quick look into the docu, and I found this paragraph:

    Once a QAction has been created it should be added to the relevant menu and toolbar, then connected to the slot which will perform the action.
    

    You did not post the connects in your code-example, so did you keep to that order, did you mix it up? :-)



  • Hi @J-Hilk,

    I did connect the action, see above:

        pDel = new QAction(tr("delete"), fileContextMenu);
        pDel->setShortcut(QKeySequence(Qt::Key_Delete));
        connect(pDel, SIGNAL(triggered()), this, SLOT(pDel_triggered()));
    

    So I believe I did this correct. The connect works because I see an output when I use the context menu.



  • @Sikarjan
    ah,

    Oh! Sorry I obviously missed that, you should use more linebreaks/paragraphs in your code :-)

    But, at least Qt::Key_Delete should work, as it is set up correctly.

    I'm sorry, but I have not much experience with QActions, but you should be in good hands with @SGaist .



  • Found the error! =) I got a hint on Stackoverflow. It is not enough to add the action to the menu, you also have to add the action to the widget. In my case the treeView.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.