Example #1
0
File: dyr.py Project: iacopy/dyr
class MainWindow(QMainWindow):

    def __init__(self, parent=None, viewtype=None):
        """
        viewtype option specifies the file browser type of view ('tree' and 'table' are accepted, 'tree' is the default)
        :type viewtype: str
        """
        super(MainWindow, self).__init__(parent)
        #
        # MODEL (is the default PyQt QFileSystemModel)
        #
        model = QFileSystemModel()
        model.setRootPath(QDir.currentPath())  # needed to fetch files
        model.setReadOnly(False)

        #
        # VIEW
        #
        # Select the view QWidget based on command prompt parameter
        # (table and tree view supported as far as now)
        if viewtype == 'table':
            view = TableView(self)
        elif viewtype == 'tree':
            view = TreeView(self)
        else:
            raise ValueError(u"'{}' view is not recognized. 'tree' or 'table' expected.".format(viewtype))
        # passed self to use MainWindow's methods (see signal's TODO)
        logging.info(u'{}View'.format(viewtype.capitalize()))

        view.setModel(model)
        # If you set the root of the tree to the current path
        # you start much more quickly than leaving the default, that cause 
        # detection of all drives on Windows (slow)
        if last_visited_directory:
            startpath = last_visited_directory
        else:
            startpath = QDir.currentPath()
        self.curpath = startpath
        view.setRootIndex(model.index(startpath))

        # A generic 'view' attribute name is used. Don't care about the view
        # is a TreeView or a TableView or...
        self.view = view

        self.setCentralWidget(self.view)

        # STATUS BAR
        status = self.statusBar()
        status.setSizeGripEnabled(False)
        status.showMessage(tr('SB', "Ready"), 2000)
        self.status_bar = status
        # TODO: add message to the log

        wintitle = u'{} {}'.format(__title__, __date__.replace(u'-', u'.'))
        self.setWindowTitle(wintitle)

        # Selection behavior: SelectRows 
        # (important if the view is a QTableView)
        self.view.setSelectionBehavior(view.SelectRows)
        # Set extended selection mode (ExtendedSelection is a standard)
        self.view.setSelectionMode(QTreeView.ExtendedSelection)
        # Enable the context menu
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.connect(self.view, 
                     SIGNAL('customContextMenuRequested(const QPoint&)'), 
                     self.on_context_menu)

        # Double click on the view
        self.view.doubleClicked.connect(self.doubleclick)

        # Selection changed
        self.connect(self.view, SIGNAL('sel_changed(PyQt_PyObject)'), 
                     self.selChanged)

        # Creating window to show the textual content of files
        self.text_window = QTextEdit()
        # hidden for now (no method show() called)

        self.addOns = []
        # let's populate self.addOns list with dynamically created AddOn instances of scripts
        for script in py_addons:
            modulename = path.splitext(path.split(script)[-1])[0]
            exec 'from scripts import {}'.format(modulename)
            try:
                exec 'self.addOns.append({}.AddOn(self))'.format(modulename)
            except BaseException as err:
                message = u'Error on script {}: {}'.format(err, modulename)
                print(message, args)
                self.status_bar.showMessage(message)
                logging.error(message)
            else:
                print(u"Loaded add-on '{}'".format(modulename))

    @property
    def selection(self):
        """
        Property that returns selected objects in a dict like this:
            {filePath1: {u'Name': fileName,
                         u'Type': u'Drive',
                         u'Size': u'4,3 MB',
                         u'Date Modified': u'13/12/2011 15:35:46'}}
        So, to have only the selected filepaths:
        a = self.selection
        a.keys() -> filepaths

        This property works well with all types of selection behaviors.
        Type examples: u'Drive', u'File Folder', u'png File'
        """
        # Get "polymorphically" the selection from 'view' 
        indexes = self.view.selectedIndexes()
        return Selection(self._indexes2fileinfos(indexes))

    def _indexes2fileinfos(self, indexes):
        """
        from indexes extracts the meta informations of files in a dict where
        the keys are the file paths and the values are dictionaries like
         {PyQt4.QtCore.QString(u'Date Modified'): PyQt4.QtCore.QString(u'03/09/2012 14:22:10'),
          PyQt4.QtCore.QString(u'Name'): PyQt4.QtCore.QString(u'ba.dna'),
          PyQt4.QtCore.QString(u'Size'): PyQt4.QtCore.QString(u'58 KB'),
          PyQt4.QtCore.QString(u'Type'): PyQt4.QtCore.QString(u'dna File')},
        """
        res = dict()
        for index in indexes:
            col = index.column()
            fpath = self.view.model().filePath(index)
            res.setdefault(fpath, dict())
            value = index.data().toString()
            header_col = self.view.model().headerData(col, Qt.Horizontal)
            hval = header_col.toString()
            res[fpath][hval] = value
        return res

    def createAction(self, text, slot=None, shortcut=None, icon=None,
                     tip=None, checkable=False, signal="triggered()"):
        action = QAction(text, self)
        if icon is not None:
            icon_path = "./icons/{0}.png".format(icon)
            if path.exists(icon_path):
                action.setIcon(QIcon(icon_path))
            else:
                logging.warning('Warning: icon {} does not exist'.format(icon_path))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def doubleclick(self, index):
        # double click handler
        fpath = self.view.model().filePath(index)
        self.open_path(fpath)

    #============================================== CONTEXT MENU =======================================================
    def on_context_menu(self, point):
        sel = self.selection
        popMenu = QMenu()
        # describing objects
        desc = desc_objects(sel)

        # Copy selected objects
        editCopyAction = self.createAction(u"&Copy {}".format(desc), self.edit_copy,
            QKeySequence.Copy, icon='icon_copy', tip=u'Copy selected files to clipboard')
        popMenu.addAction(editCopyAction)

        # AddOns actions
        #popMenu.addSeparator()
        for addon in self.addOns:
            addon.on_context_menu(popMenu, sel)

        # Python command-line scripts
        popMenu.addSeparator()
        for py_fpath in py_scripts:
            dirpath, filename = path.split(py_fpath)
            name, ext = path.splitext(filename)
            menuItemText = u'Run {}'.format(py_fpath)
            scriptAction = self.createAction(menuItemText, slot=self.run_script, icon='icon_python',
                tip=u'Run {} on {}'.format(py_fpath, desc))
            popMenu.addAction(scriptAction)
        popMenu.exec_(self.view.mapToGlobal(point))
    #===================================================================================================================

    def selChanged(self, newSel, oldSel):
        """
        Method called from view when the selection changes.
        Give some information about currently selected objects
        """
        # ignoring newSel and oldSel
        sel = self.selection
        desc = desc_objects(sel)
        #print(n, 'oggetti', newSelection.__class__.__name__, type(newSelection))
        msg = u'{} selected'.format(desc)
        # Update the status bar with the new selection
        self.status_bar.showMessage(tr('SB', msg))

        self.connect(self.view, SIGNAL('sel_changed(PyQt_PyObject)'), self.selChanged)

    def keyPressEvent(self, event):
        if type(event) == QKeyEvent and event.key() == Qt.Key_Backspace:
            # Backspace -> upper directory
            self.go_up()

    # =========================================================================
    # Methods not directly called from GUI
    # =========================================================================
    def open_path(self, path):
        qfi = QFileInfo(path)
        if qfi.isDir():
            self.open_dir(path)
        elif qfi.isFile():
            self.open_file(path)

    def open_dir(self, path):
        index = self.view.model().index(path)
        message = u'Opening directory {}'.format(path)
        logging.info(message)
        self.view.model().setRootPath(path)
        self.view.setRootIndex(index)
        self.curpath = path

    def open_file(self, filePath):
        """
        Try to read the currently selected file and print it out to the stdout.
        """
        fileInfo = QFileInfo(filePath)
        fname = unicode(fileInfo.fileName())
        name, ext = path.splitext(fname)
        if ext.lower() in u'.bmp .gif .ico .jpg .png .tif .xbm'.split():
            # apertura come immagine
            message = u'Opening image {}...'.format(filePath)
            logging.info(message)
            self.status_bar.showMessage(message)
            pixmap = QPixmap(filePath)
            self.image_window = QScrollArea()
            self.image_label = QLabel()
            self.image_label.setPixmap(pixmap)
            self.image_window.setWidget(self.image_label)
            self.image_window.show()
            self.image_label.show()
            message = u'Image size: {}x{}'.format(pixmap.width(), pixmap.height())
            self.status_bar.showMessage(message)
        else:
            message = u'Opening file {}...'.format(filePath)
            logging.info(message)
            self.status_bar.showMessage(message)
            nchars = 1000000  # number of first characters showed (temp)
            try:
                fobj = open(unicode(filePath), 'rb')
            except Exception as err:
                # [Errno 22] invalid mode ('r') or filename: PyQt4.QtCore.QString(u'F:/\u65e5\u672c.txt')
                print('open error: {}'.format(err))
                logging.error(err)
                self.status_bar.showMessage(str(err))
            else:
                try:
                    data = fobj.read(nchars)
                except Exception as err:
                    print('read error: {}'.format(err))
                    logging.error(err)
                    self.status_bar.showMessage(str(err))
                else:
                    #sts, old_output = getstatusoutput('chardetect.py "{}"'.format(filePath))
                    dec = data.decode('utf-8')
                    # override existing text window
                    self.text_window.setPlainText(dec)
                    if not self.text_window.isVisible():
                        self.text_window.show()
                    status_msg = u"'{}' file read.".format(fname)
                    if len(data) == nchars:
                        status_msg += u' (NB: showed {:,} first bytes only!)'.format(nchars)
                    self.status_bar.showMessage(status_msg)
                finally:
                    fobj.close()

    def go_up(self):
        """
        Open upper directory.
        """
        upper_path = upper_dir(self.curpath)
        if upper_path != self.curpath:
            self.open_dir(upper_path)
        else:
            if windows:
                # Windows: get drives when the root of unit is reached
                self.open_dir(u'')

    def edit_copy(self):
        """
        Copy selected objects to clipboard
        """
        # Options
        sep = ';'  # list of paths separator

        sel = self.selection
        sel_desc = desc_objects(sel)
        #selpaths = sel['paths']
        self.clipboard = sel

        # Copy to the system clipboard
        sys_clipboard = QApplication.clipboard()
        sys_clipboard.setText(QStringList(sel.paths).join(sep))
        # example: "F:/Dropbox/progetti/dyr/dyr.py;F:/Dropbox/progetti/dyr/dyr.pyc;F:/Dropbox/progetti/dyr/README.rst"
        self.status_bar.showMessage(u'{} copied'.format(sel_desc))

    def run_script(self):
        """
        Run on selection a Python script without importing it 
        (no interactions except capturing its output) and printing its
        output on the shell.
        """
        def menuItemText2scriptPath(mit):
            # mit = menu item text (es. 'Run script/path.py')
            return u'{}'.format(mit[4:])

        menuItem = self.sender()  # metodo fondamentale per sapere chi ha
        # inviato il segnale e quindi quale script da eseguire รจ stato selezionato
        # (risale allo script a partire dal testo della voce di menu)
        py_path = menuItemText2scriptPath(menuItem.text())
        selpaths = self.selection.keys()
        args = ' '.join(['"{}"'.format(sel_path) for sel_path in selpaths])
        # TODO: editable script options
        cmd = u'python {} {}'.format(py_path, args)
        message = u'Running {} on {} object(s)...'.format(py_path, len(selpaths))
        self.status_bar.showMessage(message)
        logging.info(message)  # temp: automatically log when updating status bar

        # RUN
        t0 = crono()
        sts, output = getstatusoutput(cmd)
        t = crono() - t0

        output_lines = output.splitlines()
        last_output_line = output_lines[-1]
        withOrWithout = {False:'out', True:''}[sts is not 0]
        message = u'{} completed with{} errors in {:.03f} s'.format(py_path, withOrWithout, t)
        logging.info(message)  # temp: automatically log when updating status bar
        self.status_bar.showMessage(message)
        try:
            print(u'Output captured -> [{}]'.format(output))
        except UnicodeError:
            print(repr(output))
        print(u'Exit status: {}'.format(sts))
        if sts:
            # Report the error in a warning dialog, displaying the exit code and the last output line
            QMessageBox.warning(self, u'Script error: exit status {}'.format(sts), u'{} says:\n{}'.format(py_path, last_output_line), 'bah?')
        else:
            QMessageBox.information(self, u'{} output'.format(py_path), '\n'.join(output_lines[-10:]), 'bah?')