Source code for vdat.gui.menus_actions

# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2016, 2017, 2018  "The HETDEX collaboration"
#
# 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 <https://www.gnu.org/licenses/>.
'''Module that implements custom menus and action'''
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import glob
import os
from textwrap import dedent

from qtpy import QtCore, QtGui, QtWidgets

from vdat import config
import vdat.database as vdb
import vdat.utilities as vutils
from . import help_window
from .utils import help_collection


[docs]class RemoveExposuresMenu(QtWidgets.QMenu): '''A menu that removes the exposures matching the ``path`` and create a list of actions .. list-table:: **Custom signals** :header-rows: 1 * - Name - Signature - Description * - :attr:`sig_exposure_removed` - str - emitted when an exposure is removed; the value is the ``expname`` from the database .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`enable_on_selection` - str, str - save the first string into a ``path`` attribute * - :meth:`about_to_show_slot` - - dynamically adds actions to the menu just before showing it * - :meth:`remove_exposure` - :class:`PyQt5.QtWidgets.QAction` - receive the triggered action, using its object name find the basename, remove files with that basename and the corresponding entry in the database and in the exposure file .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - :attr:`RemoveExposuresMenu.aboutToShow` - :meth:`about_to_show_slot` * - :attr:`RemoveExposuresMenu.triggered` - :meth:`remove_exposure` ''' sig_exposure_removed = QtCore.Signal(str) def __init__(self, parent=None): super(RemoveExposuresMenu, self).__init__('Remove exposure', parent=parent) self._path = None self.setEnabled(False) self.setIcon(QtGui.QIcon.fromTheme('edit-delete')) # map action object names to corresponding basenames self._basenames = {} self.aboutToShow.connect(self.about_to_show_slot) self.triggered.connect(self.remove_exposure) @QtCore.Property(str) def path(self): '''Path where the exposures to remove are located. When setting a non ``None`` path, the menu is activated; when setting a ``None`` path or the path gets deleted, the menu is deactivated''' return self._path @path.setter def path(self, value): self._path = value self.setEnabled(value is not None) # NOTE: the Property.deleter is present in PyQt4/5 but not on # PySide/PySide2. The PyQt4 documentation hints the fact that the deleter # annotation is ignored by Qt, so probably is something added only on the # python side only by PyQt: # http://pyqt.sourceforge.net/Docs/PyQt4/qt_properties.html # NOTE (FM: 20180613): as far as I can tell, ``path`` deleter is used only # in the tests, so it can be safely removed @path.deleter def path(self): self._path = None self.setEnabled(False)
[docs] @QtCore.Slot(str, str) def enable_on_selection(self, path, typ): '''Save the path and the type and enable disabled actions when the path is non-null. Parameters ---------- path : string a path, usually the directory selected in three view typ : string type of the path, ignored ''' self.path = path
[docs] @QtCore.Slot() def about_to_show_slot(self): '''Slot to connect with the :attr:`aboutToShow` signal. If no path has been selected, nothing happens, otherwise dynamically create actions in the menu''' # first clear the actions and the _basenames dictionary self.clear() self._basenames.clear() if self.path is not None: with vdb.connect(): exposures = (vdb.VDATExposures.select() .where(vdb.VDATExposures.path == self.path) .order_by(vdb.VDATExposures.original_type, vdb.VDATExposures.expname)) for i, exp in enumerate(exposures): title = '{} of type {}'.format(exp.expname, exp.original_type) action = self.addAction(QtGui.QIcon .fromTheme('edit-delete'), title) key = title + str(i) action.setObjectName(key) self._basenames[key] = exp.basename
[docs] @QtCore.Slot(QtWidgets.QAction) def remove_exposure(self, action): '''Get the triggered action, remove the files for the given exposure. Then remove the exposure from the VDATExposures database and the metadata file in the directory. Parameters ---------- action : :class:`PyQt5.QtWidgets.QAction` action triggered ''' import vdat.gui.utils as gutils with gutils.wait_cursor(): basename = self._basenames[action.objectName()] thread = gutils.FuncQThread(gutils.delete_files, [self.path, os.path.join(self.path, gutils.THUMB_DIR)], ['*' + basename + '*', ], parent=self) thread.start() with vdb.connect(): # delete the exposure from the database exposure = (vdb.VDATExposures.select() .where((vdb.VDATExposures.path == self.path) & (vdb.VDATExposures.basename == basename))) exposure = exposure.get() expname = exposure.expname exposure.delete_instance() # remove the exposure from the exposure file append = False for line in vutils.read_exps_file(self.path): if line['basename'] == basename: continue else: vutils.write_to_exps_file(self.path, append=append, **line) append = True thread.wait() thread.deleteLater() self.sig_exposure_removed.emit(expname)
[docs]class QuitAction(QtWidgets.QAction): '''Quit action to plug in menus or toolbars. Set the icon to ``window-close``, the text to ``Quit`` and the shortcut to ``Ctrl+Q``. .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - ``triggered`` - ``connect_to``, if present Parameters ---------- connect_to : signal or slot, optional if present connect the ``triggered`` parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the menu' bar ''' def __init__(self, connect_to=None, parent=None): icon = QtGui.QIcon.fromTheme('window-close') super(QuitAction, self).__init__(icon, "Quit", parent) self.setStatusTip('Exit application') self.setShortcut('Ctrl+Q') if connect_to: self.triggered.connect(connect_to)
[docs]class HelpMenu(QtWidgets.QMenu): '''Create a menu with the various help actions. Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the menu help_parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the windows opened by the actions in this menu ''' def __init__(self, parent=None, windows_parent=None): super(HelpMenu, self).__init__("Help", parent=parent) self.setObjectName('help-menu') # add the actions # handbook if help_collection(): self.handbook = HandbookAction(parent=self, window_parent=windows_parent) self.addAction(self.handbook) # online resources self.links = VDATLinksAction(parent=self, window_parent=windows_parent) self.addAction(self.links) # about message self.about = VDATAboutAction(parent=self, window_parent=windows_parent) self.addAction(self.about) # about Qt message self.about_qt = QtWidgets.QAction(QtGui.QIcon.fromTheme('help-about'), "About Qt", parent) self.about_qt.triggered.connect(QtWidgets.QApplication.aboutQt) self.addAction(self.about_qt)
[docs]class HandbookAction(QtWidgets.QAction): '''"Show Handbook" action. It Shows :class:`vdat.gui.help_window.HelpWindow` .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`show_handbook` - - Create a :class:`vdat.gui.help_window.HelpWindow` and show it .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - :attr:`triggered` - :meth:`show_handbook` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the action window_parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the help window ''' def __init__(self, parent=None, window_parent=None): icon = QtGui.QIcon.fromTheme('help-browser') super(HandbookAction, self).__init__(icon, "VDAT Handbook", parent) self._window_parent = window_parent self.triggered.connect(self.show_handbook)
[docs] @QtCore.Slot() def show_handbook(self): '''open the VDAT handbook in a new window This method is also a PyQt slot ''' help_win = help_window.HelpWindow(parent=self._window_parent) help_win.show()
[docs]class VDATLinksAction(QtWidgets.QAction): '''Show links with the online VDAT documentation. .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`show_links` - - Create a :class:`~PyQt5.QtWidgets.QMessageBox` showing the links .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - :attr:`triggered` - :meth:`show_links` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the action window_parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the help window ''' def __init__(self, parent=None, window_parent=None): icon = QtGui.QIcon.fromTheme('help-browser') super(VDATLinksAction, self).__init__(icon, "VDAT Online Resources", parent) self._window_parent = window_parent self.triggered.connect(self.show_links)
[docs]class VDATAboutAction(QtWidgets.QAction): '''Show a short "about" VDAT. It Shows :class:`vdat.gui.help_window.HelpWindow` .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`show_about` - - Create a :class:`~PyQt5.QtWidgets.QMessageBox` showing some information about authors and such .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - :attr:`triggered` - :meth:`show_about` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the action window_parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the help window ''' def __init__(self, parent=None, window_parent=None): icon = QtGui.QIcon.fromTheme('help-about') super(VDATAboutAction, self).__init__(icon, "About VDAT", parent) self._window_parent = window_parent self.triggered.connect(self.show_about)
[docs] @QtCore.Slot() def show_about(self): '''Open the VDAT about in a message box. This method is also a PyQt slot ''' import vdat msg = dedent(""" VDAT The Virus Data Analysis Tool was lovingly crafted in Garching, near the Bavarian Alps, out of entirely hand-picked materials. Version: {} Created by our 100% bio programmers: Daniel Farrow Francesco Montypython Jan Snigula """.format(vdat.__version__)) about = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Information, "About VDAT", msg, parent=self._window_parent) about.exec_()
[docs]class LogAction(QtWidgets.QAction): '''Action about the log files Parameters ---------- fname : string name of the file to open parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the menu Attributes ---------- fname : string name of the file to open ''' def __init__(self, fname, parent=None): self.fname = fname _text = os.path.splitext(os.path.basename(fname))[0] super(LogAction, self).__init__(_text, parent)
[docs]class LogViewerWindow(QtWidgets.QMainWindow): '''Editor window used to display log files. .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`load_file` - - Load the file and disable the refresh action * - :meth:`enable_refresh` - string - Enable the refresh button .. list-table:: **Connections between custom signals and/or slots**. :header-rows: 1 * - Signal - Slot * - :attr:`refresh.triggered` - :meth:`load_file` * - :attr:`watcher.fileChanged` - :meth:`enable_refresh` Parameters ---------- file_names : string file to display parent : :class:`PyQt5.QtWidgets.QWidget` or derivate parent object of the tree view model Attributes ---------- file_name : string name of the file to display text_edit : :class:`PyQt5.QtWidgets.QTextEdit` display the text refresh : :class:`PyQt5.QtWidgets.QAction` action to refresh the content watcher : :class:`PyQt5.QtCore.QFileSystemWatcher` file watcher ''' def __init__(self, file_name, parent=None): super(LogViewerWindow, self).__init__(parent=parent) self.file_name = file_name self.resize(600, 500) self.setWindowTitle(os.path.basename(file_name)) self.make_common_actions() self.setMenuBar(self.make_menubar()) self.addToolBar(self.make_toolbar()) self.text_edit = self.make_text_edit() self.setCentralWidget(self.text_edit) # load the file self.load_file() # add a file watcher to enable to refresh button self.watcher = QtCore.QFileSystemWatcher(parent=self) self.watcher.addPath(file_name) self.watcher.fileChanged.connect(self.enable_refresh)
[docs] def make_text_edit(self): '''Create a QTextEdit edit and returns it ''' text_edit = QtWidgets.QTextEdit(self) text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) text_edit.setReadOnly(True) return text_edit
[docs] @QtCore.Slot() def load_file(self): '''Load the file in the :attr:`text_edit` widget and disable the refresh action This method is also a PyQt slot ''' with open(self.file_name) as f: file_content = f.read() self.text_edit.setPlainText(file_content) self.refresh.setEnabled(False)
[docs] @QtCore.Slot(str) def enable_refresh(self, fname): '''Enable the refresh button''' self.refresh.setEnabled(True)
[docs] def make_common_actions(self): '''Create actions. ''' refresh_icon = QtGui.QIcon.fromTheme('view-refresh') self.refresh = QtWidgets.QAction(refresh_icon, 'Reload', self) self.refresh.setStatusTip('Reload the document') self.refresh.setShortcut('Ctrl+R') self.refresh.triggered.connect(self.load_file)
[docs] def make_menubar(self): '''Create and return the menu''' menu_bar = QtWidgets.QMenuBar(self) file_menu = menu_bar.addMenu("File") file_menu.addAction(self.refresh) file_menu.addSeparator() quit_action = QuitAction(connect_to=self.close, parent=self) file_menu.addAction(quit_action) menu_bar.addMenu(HelpMenu(parent=menu_bar, windows_parent=self)) return menu_bar
[docs] def make_toolbar(self): '''Create and return the toolbar''' tool_bar = QtWidgets.QToolBar('Options', self) tool_bar.addAction(self.refresh) # tool_bar.addSeparator() return tool_bar
[docs]class LogMenu(QtWidgets.QMenu): '''Create a menu with actions related with log files. .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`open_log_file` - :class:`LogAction` - open the selected log file in a read-only window .. list-table:: **Connections between custom signals and/or slots**. :header-rows: 1 * - Signal - Slot * - :attr:`triggered` of the log files menu - :meth:`open_log_file` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the menu ''' def __init__(self, parent=None): super(LogMenu, self).__init__("Log", parent=parent) self.setObjectName('log-menu') # add the actions # clear the log widget self.clear_log = QtWidgets.QAction(QtGui.QIcon.fromTheme('edit-clear'), "Clear log panel", parent) self.addAction(self.clear_log) self.addSeparator() log_files_menu = QtWidgets.QMenu('Open log file', parent=self) self.addMenu(log_files_menu) # add actions to open log files conf = config.get_config('main') logdir = conf['logging']['logdir'] action_group = QtWidgets.QActionGroup(self) for file_name in sorted(glob.glob(os.path.join(logdir, '*'))): action_group.addAction(LogAction(file_name, action_group)) action_group.triggered.connect(self.open_log_file) log_files_menu.addActions(action_group.actions())
[docs] @QtCore.Slot(LogAction) def open_log_file(self, action): '''Open the given log files''' log_window = LogViewerWindow(action.fname, parent=self) log_window.show()
[docs]class TreeViewMenu(QtWidgets.QMenu): """Menu to collapse/expand the tree view Create the menu with the entries to expand/collapse the menu .. list-table:: **Custom signals** :header-rows: 1 * - Name - Signature - Description * - :attr:`sig_collapse` - - emitted when the "Collapse" action is triggered * - :attr:`sig_expand` - - emitted when the "Expand" action is triggered Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` instance, optional parent of the menu """ sig_collapse = QtCore.Signal() sig_expand = QtCore.Signal() def __init__(self, parent=None): super(TreeViewMenu, self).__init__('Reduction Browser', parent=parent) col_action = self.addAction('Collapse all') col_action.triggered.connect(self.sig_collapse) exp_action = self.addAction('Expand all') exp_action.triggered.connect(self.sig_expand)