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] @QtCore.Slot()
def show_links(self):
'''Display the links to the VDAT documentation and the coverage
reports, both for the latest and the development version.
This method is also a PyQt slot
'''
msg = dedent("""
<h2>VDAT online documentations</h2>
<ul>
<li><a
href="http://www.mpe.mpg.de/~montefra/documentation/vdat/latest">latest</a></li>
<li><a
href="http://www.mpe.mpg.de/~montefra/documentation/vdat/devel">development
</a></li>
</ul>
<h3>Coverage reports</h3>
<ul>
<li><a
href="http://www.mpe.mpg.de/~montefra/documentation/vdat_cover/latest">latest</a></li>
<li><a
href="http://www.mpe.mpg.de/~montefra/documentation/vdat_cover/devel">development
</a></li>
</ul>
""")
instructions = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Information,
"VDAT online documentation", msg,
parent=self._window_parent)
instructions.exec_()
[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_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)