eric6/Project/QuickFindFileDialog.py

Tue, 02 Mar 2021 17:17:09 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 02 Mar 2021 17:17:09 +0100
changeset 8143
2c730d5fd177
parent 7988
c4c17121eff8
child 8205
4a0f1f896341
permissions
-rw-r--r--

Changed the use of PyQt enums because the way they were used previously is deprecated since two years and replaced some deprecated Qt stuff.

# -*- coding: utf-8 -*-

# Copyright (c) 2004 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a quick search for files.

This is basically the FindFileNameDialog modified to support faster
interactions.
"""

import os

from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent
from PyQt5.QtWidgets import (
    QWidget, QHeaderView, QApplication, QDialogButtonBox, QTreeWidgetItem
)

from .Ui_QuickFindFile import Ui_QuickFindFile


class QuickFindFileDialog(QWidget, Ui_QuickFindFile):
    """
    Class implementing the Quick Find File by Name Dialog.
    
    This dialog provides a slightly more streamlined behaviour
    than the standard FindFileNameDialog in that it tries to
    match any name in the project against (fragmentary) bits of
    file names.
    
    @signal sourceFile(str) emitted to open a file in the editor
    @signal designerFile(str) emitted to open a Qt-Designer file
    @signal linguistFile(str) emitted to open a Qt translation file
    """
    sourceFile = pyqtSignal(str)
    designerFile = pyqtSignal(str)
    linguistFile = pyqtSignal(str)
    
    def __init__(self, project, parent=None):
        """
        Constructor
        
        @param project reference to the project object
        @type Project
        @param parent parent widget of this dialog
        @type QWidget
        """
        super(QuickFindFileDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.fileList.headerItem().setText(self.fileList.columnCount(), "")
        self.fileNameEdit.returnPressed.connect(
            self.on_fileNameEdit_returnPressed)
        self.installEventFilter(self)

        self.stopButton = self.buttonBox.addButton(
            self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
        self.project = project
    
    def eventFilter(self, source, event):
        """
        Public method to handle event for another object.
        
        @param source object to handle events for
        @type QObject
        @param event event to handle
        @type QEvent
        @return flag indicating that the event was handled
        @rtype bool
        """
        if event.type() == QEvent.Type.KeyPress:
            
            # Anywhere in the dialog, make hitting escape cancel it
            if event.key() == Qt.Key.Key_Escape:
                self.close()
            
            # Anywhere in the dialog, make hitting up/down choose next item
            # Note: This doesn't really do anything, as other than the text
            #       input there's nothing that doesn't handle up/down already.
            elif (
                event.key() == Qt.Key.Key_Up or
                event.key() == Qt.Key.Key_Down
            ):
                current = self.fileList.currentItem()
                index = self.fileList.indexOfTopLevelItem(current)
                if event.key() == Qt.Key.Key_Up:
                    if index != 0:
                        self.fileList.setCurrentItem(
                            self.fileList.topLevelItem(index - 1))
                else:
                    if index < (self.fileList.topLevelItemCount() - 1):
                        self.fileList.setCurrentItem(
                            self.fileList.topLevelItem(index + 1))
        return QWidget.eventFilter(self, source, event)

    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.stopButton:
            self.shouldStop = True
        elif (
            button ==
            self.buttonBox.button(QDialogButtonBox.StandardButton.Open)
        ):
            self.__openFile()
    
    def __openFile(self, itm=None):
        """
        Private slot to open a file.
        
        It emits the signal sourceFile or designerFile depending on the
        file extension.
        
        @param itm item to be opened
        @type QTreeWidgetItem
        @return flag indicating a file was opened
        @rtype bool
        """
        if itm is None:
            itm = self.fileList.currentItem()
        if itm is not None:
            filePath = itm.text(1)
            fileName = itm.text(0)
            fullPath = os.path.join(self.project.ppath, filePath, fileName)
            
            if fullPath.endswith('.ui'):
                self.designerFile.emit(fullPath)
            elif fullPath.endswith(('.ts', '.qm')):
                self.linguistFile.emit(fullPath)
            else:
                self.sourceFile.emit(fullPath)
            return True
        
        return False
    
    def __generateLocations(self):
        """
        Private method to generate a set of locations that can be searched.
        
        @yield set of files in our project
        @ytype str
        """
        for typ in ["SOURCES", "FORMS", "INTERFACES", "PROTOCOLS", "RESOURCES",
                    "TRANSLATIONS", "OTHERS"]:
            entries = self.project.pdata.get(typ)
            for entry in entries[:]:
                yield entry
    
    def __sortedMatches(self, items, searchTerm):
        """
        Private method to find the subset of items which match a search term.
        
        @param items list of items to be scanned for the search term
        @type list of str
        @param searchTerm search term to be searched for
        @type str
        @return sorted subset of items which match searchTerm in
            relevance order (i.e. the most likely match first)
        @rtype list of tuple of bool, int and str
        """
        fragments = searchTerm.split()
        
        possible = [
            # matches, in_order, file name
        ]
        
        for entry in items:
            count = 0
            match_order = []
            for fragment in fragments:
                index = entry.find(fragment)
                if index == -1:
                    # try case-insensitive match
                    index = entry.lower().find(fragment.lower())
                if index != -1:
                    count += 1
                    match_order.append(index)
            if count:
                record = (count, match_order == sorted(match_order), entry)
                if possible and count < possible[0][0]:
                    # ignore...
                    continue
                elif possible and count > possible[0][0]:
                    # better than all previous matches, discard them and
                    # keep this
                    del possible[:]
                possible.append(record)
        
        ordered = []
        for (_, in_order, name) in possible:
            try:
                age = os.stat(os.path.join(self.project.ppath, name)).st_mtime
            except OSError:
                # skipping, because it doesn't appear to exist...
                continue
            ordered.append((
                in_order,    # we want closer match first
                - age,       # then approximately "most recently edited"
                name
            ))
        ordered.sort()
        return ordered

    def __searchFile(self):
        """
        Private slot to handle the search.
        """
        fileName = self.fileNameEdit.text().strip()
        if not fileName:
            self.fileList.clear()
            return
        
        ordered = self.__sortedMatches(self.__generateLocations(), fileName)
        
        found = False
        self.fileList.clear()
        locations = {}

        for _in_order, _age, name in ordered:
            found = True
            QTreeWidgetItem(self.fileList, [os.path.basename(name),
                                            os.path.dirname(name)])
        QApplication.processEvents()
            
        del locations
        self.stopButton.setEnabled(False)
        self.fileList.header().resizeSections(
            QHeaderView.ResizeMode.ResizeToContents)
        self.fileList.header().setStretchLastSection(True)
        
        if found:
            self.fileList.setCurrentItem(self.fileList.topLevelItem(0))

    def on_fileNameEdit_textChanged(self, text):
        """
        Private slot to handle the textChanged signal of the file name edit.
        
        @param text (ignored)
        """
        self.__searchFile()

    def on_fileNameEdit_returnPressed(self):
        """
        Private slot to handle enter being pressed on the file name edit box.
        """
        if self.__openFile():
            self.close()
    
    def on_fileList_itemActivated(self, itm, column):
        """
        Private slot to handle the double click on a file item.
        
        It emits the signal sourceFile or designerFile depending on the
        file extension.
        
        @param itm the double clicked listview item (QTreeWidgetItem)
        @param column column that was double clicked (integer) (ignored)
        """
        self.__openFile(itm)
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_fileList_currentItemChanged(self, current, previous):
        """
        Private slot handling a change of the current item.
        
        @param current current item (QTreeWidgetItem)
        @param previous prevoius current item (QTreeWidgetItem)
        """
        self.buttonBox.button(QDialogButtonBox.StandardButton.Open).setEnabled(
            current is not None)

    def show(self):
        """
        Public method to enable/disable the project checkbox.
        """
        self.fileNameEdit.selectAll()
        self.fileNameEdit.setFocus()
        
        super(QuickFindFileDialog, self).show()

eric ide

mercurial