#!/usr/bin/env python
# Copyright (c) 2009-2019, UChicago Argonne, LLC.
# See LICENSE.txt file for details.
'''
BcdaMenu: Creates a GUI menu button to start common software
.. autosummary::
~MainButtonWindow
~CommandThread
~read_settings
~gui
~timestamp
~main
'''
import argparse
from collections import OrderedDict
import datetime
from functools import partial
import os
import sys
import threading
try:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
try:
import subprocess32 as subprocess
except:
import subprocess
from . import config_file_parser
MAIN_SECTION_LABEL = 'BcdaMenu'
DEBUG = False
#DEBUG = True
DEBUG_COLOR_OFF = "white"
DEBUG_COLOR_ON = "#fec"
[docs]class MainButtonWindow(QMainWindow):
'''
the widget that holds the menu button
.. autosummary::
~receiver
~reload_settings_file
~build_user_menus
~showStatus
~historyUpdate
~toggleAutoScroll
~toggleDebug
~toggleEcho
~hide_history_window
~about_box
~closeEvent
'''
process_responded = pyqtSignal(str)
def __init__(self, parent=None, settingsfilename=None):
QMainWindow.__init__(self, parent)
self.settingsfilename = settingsfilename
if settingsfilename is None:
raise ValueError('settings file name must be given')
self.command_number = 0
self.command_echo = True
self._init_gui()
self.reload_settings_file()
def _init_gui(self):
self.statusbar = QStatusBar()
self.setStatusBar(self.statusbar)
self.menubar = QMenuBar()
self.setMenuBar(self.menubar)
self.menubar.setNativeMenuBar(False) # keep menubar in the window
self.historyPane = QPlainTextEdit()
self.setCentralWidget(self.historyPane)
self.historyPane.setLineWrapMode(False)
self.historyPane.setReadOnly(True)
self.toggleDebug(DEBUG)
self.auto_scroll = True
if self.debug:
self.resize(500,300)
self.historyPane.setStyleSheet("background: " + DEBUG_COLOR_ON)
else:
self.hide_history_window()
self.resize(400,0)
self.historyPane.setStyleSheet("background: " + DEBUG_COLOR_OFF)
self.process_responded.connect(self.historyUpdate)
self.showStatus('starting %s ...' % sys.argv[0])
self.admin_menu = QMenu('Help')
self.menubar.addMenu(self.admin_menu)
self.admin_menu.addAction('About ...', self.about_box)
self.admin_menu.addSeparator()
self.admin_menu.addAction('&Reload User Menus', self.reload_settings_file)
self.admin_menu.addSeparator()
self.admin_menu.addAction('(Un)hide &History panel', self.hide_history_window)
# keyboard shortcuts
def shortcut(sequence, action):
cut = QShortcut(QKeySequence(sequence), self)
cut.activated.connect(action)
return cut
shortcut("Ctrl+R", self.reload_settings_file)
shortcut("Ctrl+H", self.hide_history_window)
shortcut("Ctrl+D", self.toggleDebug)
action = self.admin_menu.addAction('scroll to new output', self.toggleAutoScroll)
action.setCheckable(True)
action.setChecked(self.auto_scroll)
action = self.admin_menu.addAction('command echo', self.toggleEcho)
action.setCheckable(True)
action.setChecked(self.command_echo)
action = self.admin_menu.addAction('toggle &Debug flag', self.toggleDebug)
action.setCheckable(True)
action.setChecked(self.debug)
self.user_menus = OrderedDict()
[docs] def receiver(self, label, command):
'''handle commands from menu button'''
msg = MAIN_SECTION_LABEL + ' (' + timestamp() + '), ' + label
if command is None:
msg += ': '
else:
command = os.path.normpath(command)
msg += ': ' + str(command)
self.showStatus(msg, isCommand=True)
if command is not None:
self.command_number += 1
process_name = "id_" + str(self.command_number)
# ref: https://docs.python.org/3.3/library/subprocess.html
process = CommandThread()
process.setName(process_name)
process.setDebug(self.debug)
process.setSignal(self.process_responded)
process.setCommand(command)
process.start()
[docs] @pyqtSlot()
def toggleAutoScroll(self):
"""change whether (or not) to keep new output in view"""
self.auto_scroll = not self.auto_scroll
state = {True: "on", False: "off"}[self.auto_scroll]
self.process_responded.emit("auto scroll: " + state)
[docs] @pyqtSlot()
def toggleDebug(self, debug_state = None):
"""change whether (or not) to output diagnostic information"""
if debug_state is not None:
self.debug = debug_state
else:
self.debug = not self.debug
color = {True: DEBUG_COLOR_ON, False: DEBUG_COLOR_OFF}[self.debug]
self.historyPane.setStyleSheet("background: " + color)
[docs] @pyqtSlot()
def toggleEcho(self):
"""change whether (or not) to echo command before running it"""
self.command_echo = not self.command_echo
state = {True: "on", False: "off"}[self.command_echo]
self.process_responded.emit("command echo: " + state)
[docs] def about_box(self):
'''display an About box'''
from .__init__ import (__version__, __url__, __author__,
__issues__, __copyright__, __license_url__)
from . import about
print("DEBUG: about file:" + about.__file__)
summary = __doc__.strip().splitlines()[0] # 1st line only
msg = summary
msg += '\n version: ' + __version__
msg += '\n URL: ' + __url__
self.showStatus(msg)
ui = about.InfoBox(self)
ui.setTodoURL(__issues__)
ui.setDocumentationURL(__url__)
ui.setLicenseURL(__license_url__)
ui.setTitle(config_file_parser.MAIN_SECTION_LABEL)
ui.setVersionText("software version: " + __version__)
ui.setSummaryText(summary)
ui.setAuthorText(__author__)
ui.setCopyrightText(__copyright__)
ui.show()
[docs] def showStatus(self, text, isCommand=False):
"""write to the status bar"""
self.statusbar.showMessage(text.splitlines()[0])
if not isCommand and self.command_echo:
self.historyUpdate(text)
[docs] def historyUpdate(self, text):
"""record history where user can see it"""
if self.historyPane is not None:
self.historyPane.appendPlainText(text)
if self.auto_scroll:
self.historyPane.ensureCursorVisible()
scroll = self.historyPane.verticalScrollBar()
scroll.setValue(scroll.maximum())
[docs] def hide_history_window(self):
"""toggle the visibility of the history panel"""
self.historyPane.setHidden(not self.historyPane.isHidden())
[docs] def reload_settings_file(self):
'''(re)load the settings file and (re)create the menu(s)'''
self.showStatus('(re)load settings: ' + self.settingsfilename)
# read the settings file (again)
self.config = read_settings(self.settingsfilename)
# install the new user popup menu buttons
self.menubar.clear()
self.user_menus = OrderedDict()
self.build_user_menus(self.config)
self.menubar.addMenu(self.admin_menu)
self.setWindowTitle(self.config['title'])
def _build_menu(self, menu, widget):
for k, v in menu.itemDict.items():
if isinstance(v, config_file_parser.MenuItem):
action = widget.addAction(
v.label,
partial(self.receiver, v.label, v.command))
elif isinstance(v, config_file_parser.MenuSeparator):
widget.addSeparator()
elif isinstance(v, config_file_parser.Menu):
subwidget = QMenu(v.title)
self.user_menus[v.sectionName] = subwidget
self._build_menu(v, subwidget)
widget.addMenu(subwidget)
else:
raise RuntimeError("unexpected: %s : " % k + str(v))
[docs] def build_user_menus(self, config):
"""build the user menus"""
for menu in config['menus']:
widget = QMenu(menu.title)
self.user_menus[menu.sectionName] = widget
self._build_menu(menu, widget)
self.menubar.addMenu(widget)
[docs] @pyqtSlot(QCloseEvent)
def closeEvent(self, event):
# TODO: dispose any threads and timers
pass
[docs]class CommandThread(threading.Thread):
"""
run the command as a subprocess in its own thread, report any output
**Usage**
::
process = CommandThread()
process.setName(process_name)
process.setDebug(self.debug)
process.setSignal(self.process_responded)
process.setCommand(command)
process.start()
:see: https://docs.python.org/3.3/library/subprocess.html
**Methods**
.. autosummary::
~run
~execute
~setCommand
~setDebug
~setSignal
"""
kwargs = {
'bufsize': 1,
'shell': True,
'stderr': subprocess.STDOUT,
'stdout': subprocess.PIPE,
'universal_newlines': True,
}
def __init__(self):
self.stdout = None
self.stderr = None
threading.Thread.__init__(self)
self.signal = None
self.debug = False
self.command = None
if os.name == 'nt': # Windows
self.kwargs['shell'] = False
[docs] def setCommand(self, command):
"""user's command to be run"""
self.command = command
[docs] def setDebug(self, value):
"""`True` to output more diagnostics"""
self.debug = value
[docs] def setSignal(self, signal):
"""designate the signal to use when subprocess output has been received"""
self.signal = signal
[docs] def run(self):
"""print any/all output when command is run"""
if self.debug:
self.signal.emit("thread %s starting" % self.name)
for line in self.execute():
if self.debug:
line = " ".join([self.name, timestamp(), ":", line])
self.signal.emit(line)
if self.debug:
self.signal.emit("thread %s ended" % self.name)
[docs] def execute(self):
"""run the command in a shell, reporting its output as it comes in"""
if self.debug:
yield self.name + " started"
process = subprocess.Popen(self.command, **self.kwargs)
while not process.stdout.closed:
stdoutdata, stderrdata = process.communicate()
if stdoutdata is not None:
for line in stdoutdata.splitlines():
yield line
if self.debug:
yield self.name + " finished"
# try:
# with subprocess.Popen(self.command, **self.kwargs) as process:
# if self.debug:
# yield self.name + " started"
# for buffer in iter(process.stdout.readline, ""):
# for line in buffer.splitlines():
# yield line
# if self.debug:
# yield self.name + " finished"
# except AttributeError: # happens on Windows
# pass
[docs]def read_settings(ini_file):
'''
read the user menu settings from the .ini file
'''
if not os.path.exists(ini_file):
raise ValueError('settings file not found: ' + ini_file)
settings = config_file_parser.readConfigFile(ini_file)
return settings
[docs]def gui(settingsfilename = None):
'''display the main widget'''
app = QApplication(sys.argv)
the_gui = MainButtonWindow(settingsfilename=settingsfilename)
the_gui.show()
sys.exit(app.exec_())
[docs]def timestamp():
"""ISO8601-compliant date & time string"""
return str(datetime.datetime.now())
[docs]def main():
'''process any command line options before starting the GUI'''
from .__init__ import __version__
version = __version__
doc = __doc__.strip().splitlines()[0]
doc += '\n v' + version
parser = argparse.ArgumentParser(prog=MAIN_SECTION_LABEL, description=doc)
parser.add_argument('settingsfile', help="Settings file (.ini)")
parser.add_argument('-v', '--version', action='version', version=version)
params = parser.parse_args()
if not os.path.exists(params.settingsfile):
raise IOError('file not found: ' + params.settingsfile)
gui(settingsfilename = params.settingsfile)
if __name__ == '''__main__''':
main()