Source code for mda2idd_gui

#!/usr/bin/env python

"""
GUI for :mod:`mda2idd_report`

Objectives
------------

Provide GUI tools to browse a file system and select
MDA files.  Process them with :mod:`mda2idd_report`.

Instructions
------------

Browse to a directory containing MDA files.
Select one.  A summary will be shown.
Choose:
* File --> Save  (or ^S)  to convert selected MDA file to an ASCII text file.
* File --> Convert entire Directory (^D) to convert all MDA files

For now, only `*.mda` files may be browsed.
ASCII text files will be written to directory: ../ASCII/
(relative to the MDA file directory)

Features
-----------

* presents file system list
* adds directory picker dialog and text entry box
* preview brief header or full summary of MDA file (^B)
* convert one selected MDA file to ASCII (^S)
* convert entire directory of MDA files to ASCII (^D)

---------------

Source Code Documentation
-------------------------

.. autosummary::

    ~MainWindow

--------------

"""


import optparse
import datetime
import glob
import platform
import os
import sys
import traceback
import wx
from xml.etree import ElementTree
from xml.dom import minidom
import mda2idd_report
import mda2idd_summary


__description__ = "GUI for mda2idd_report"
__version__ = "2016-04"
__author__ = "Pete Jemian"
__author_email__ = "jemian@anl.gov"
__url__ = "https://github.com/BCDA-APS/mda2idd_report"


RC_FILE = ".mda2idd_gui_rc.xml"


[docs]class MainWindow(wx.Frame): """ Manage the application through the main window """ def __init__(self, parent=None, start_fresh=False): wx.Frame.__init__(self, parent, wx.ID_ANY, u'mda2idd_gui', wx.DefaultPosition, wx.Size(200, 100), name=u'root', style=wx.DEFAULT_FRAME_STYLE) self.startup_complete = False self.selectedMdaFile = None self.preferences_file = self.GetDefaultPreferencesFileName() self.mrud = [] # most-recently-used directories self.getPreferences(start_fresh) self._init_menus() self._init_contents() # apply preferences self.SetSize(wx.Size(self.prefs['size_h'], self.prefs['size_v'])) self.SetPosition(wx.Point(self.prefs['pos_h'], self.prefs['pos_v'])) self.splitter1.SetSashPosition(self.prefs['sash_pos'], True) self.menu_file.Check(self.id_menu_report, self.prefs['short_summary']) self.update_mrud_menus() self.setStatusText('preferences file: ' + self.preferences_file) self.setSummaryText('') self.startup_complete = True def _init_menus(self): self.menu_file = wx.Menu(title='') item = self.menu_file.Append(text=u'&Save\tCtrl+S', id=wx.ID_ANY, help=u'Save MDA data to ASCII text file') self.Bind(wx.EVT_MENU, self.OnMenuFileItemSave, id=item.GetId()) item = self.menu_file.Append( text=u'Convert entire &Directory\tCtrl+D', id=wx.ID_ANY, help=u'Convert all MDA files in current directory to ASCII text files') self.Bind(wx.EVT_MENU, self.OnConvertAll, id=item.GetId()) self.menu_file.AppendSeparator() item = self.menu_file.AppendCheckItem( text=u'Brief &Report\tCtrl+R', id=wx.ID_ANY, help=u'Show a brief summary report of the selected MDA file') self.Bind(wx.EVT_MENU, self.OnMenuFileItemReportStyle, id=item.GetId()) self.id_menu_report = item.GetId() # TODO: provide a control to let user edit self.preferences_file # TODO: provide a control to let user edit self.prefs #self.menu_file.Append(text=u'&Preferences ...', id=id_menu_prefs, # help=u'Edit program preferences ...') #self.Bind(wx.EVT_MENU, self.OnMenuFileItemPrefs, id=id_menu_prefs) self.menu_file.AppendSeparator() item = self.menu_file.Append(text=u'MRUD list', id=wx.ID_ANY, help=u'Most Recently Used Directories') self.menu_file.Enable(item.GetId(), False) self.mrud_insertion_pos = self.menu_file.GetMenuItemCount() self.menu_file.AppendSeparator() item = self.menu_file.Append(text=u'E&xit', id=wx.ID_ANY, help=u'Quit this application') self.Bind(wx.EVT_MENU, self.OnMenuFileItemExit, id=item.GetId()) self.menu_edit = wx.Menu(title='') self.menu_help = wx.Menu(title='') item = self.menu_help.Append(text=u'&About ...', id=wx.ID_ANY, help=u'About this application') self.Bind(wx.EVT_MENU, self.OnAbout, id=item.GetId()) self.menuBar1 = wx.MenuBar() self.menuBar1.Append(menu=self.menu_file, title=u'&File') self.menuBar1.Append(menu=self.menu_edit, title=u'&Edit') self.menuBar1.Append(menu=self.menu_help, title=u'&Help') self.SetMenuBar(self.menuBar1) def _init_contents(self): self.statusBar = self.CreateStatusBar() sizer = wx.BoxSizer(orient=wx.VERTICAL) self.dirPicker = wx.DirPickerCtrl (self, id=wx.ID_ANY, style=wx.DIRP_DIR_MUST_EXIST | wx.DIRP_USE_TEXTCTRL) sizer.Add( self.dirPicker, 0, # make vertically unstretchable wx.EXPAND | # make horizontally stretchable wx.ALL, # and make border all around ) self.splitter1 = wx.SplitterWindow(self, id=wx.ID_ANY, style=wx.SP_3D) sizer.Add( self.splitter1, 1, # make vertically stretchable wx.EXPAND | # make horizontally stretchable wx.ALL, # and make border all around ) self.textCtrl1 = wx.TextCtrl (self.splitter1, id=wx.ID_ANY, style=wx.TE_READONLY|wx.TE_MULTILINE) self.setSummaryText('(empty)') self.dir = wx.GenericDirCtrl(self.splitter1, wx.ID_ANY, dir=self.prefs['start_dir'], filter=self.prefs['file_filter'], ) # Select the starting folder and expand to it self.setCurrentDirectory(self.prefs['start_dir']) self.splitter1.SplitVertically(self.dir, self.textCtrl1) tree = self.dir.GetTreeCtrl() wx.EVT_TREE_SEL_CHANGED(self, tree.GetId(), self.OnSelectTreeCtrlItem) wx.EVT_SPLITTER_SASH_POS_CHANGED(self, self.splitter1.GetId(), self.OnSashMoved) #self.Bind(wx.EVT_SIZE, self.OnWindowGeometryChanged) self.Bind(wx.EVT_MOVE, self.OnWindowGeometryChanged) self.Bind(wx.EVT_DIRPICKER_CHANGED, self.OnSelectDirPicker) self.SetSizerAndFit(sizer)
[docs] def GetDefaultPreferencesFileName(self): ''' return the name of the preferences file for this session The preferences file, an XML file that contains recent program settings for specific program features, is saved in the HOME (or USERPROFILE on Windows) directory for the user account under the ``.mda2idd_gui_rc.xml`` file name. Here is an example from a Windows 7 system:: <?xml version="1.0" encoding="UTF-8"?> <mda2idd_gui datetime="2013-03-06 13:09:17.593000" version="2013-02"> <preferences_file>C:\Users\Pete\.mda2idd_gui_rc.xml</preferences_file> <written_by program="C:\Users\Pete\Documents\eclipse\mda2idd_report\src\mda2idd_gui.py"/> <subversion id="$Id$"/> <window> <size h="1212" v="561"/> <position h="114" v="232"/> <sash pos="300"/> </window> <file_filter>*.mda</file_filter> <starting_directory>C:\Users\Pete\Documents\eclipse\mda2idd_report\data\mda</starting_directory> <short_summary>False</short_summary> <mrud max_directories="9"> <!--MRUD: Most-Recently-Used Directory--> <dir>C:\Users\Pete\Documents\eclipse\mda2idd_report\data\mda</dir> <dir>C:\Users\Pete\Apps\epics\synAppsSVN\support\sscan\documentation</dir> <dir>C:\Temp\mdalib</dir> <dir>C:\Users\Pete\Desktop\scanSee3.1\DATA</dir> <dir>C:\Users\Pete\Documents\eclipse\dc2mda\src</dir> <dir>C:\Users\Pete\Documents\eclipse\dc2mda\src\topo</dir> </mrud> </mda2idd_gui> Items remembered between program sessions include: * window size and position * position of the sash thats plits the file list from the summary output * the list of most-recently-used directories (MRUD) * the first directory to show (the last directory from which an MDA file was selected) .. note:: If more than one copy of this program is run by the same user at the same time, the content of the preferences file will be that of the latest action that forced an update to the file content. ''' known_os = { 'Windows': 'USERPROFILE', 'Linux': 'HOME', 'SunOS': 'HOME', 'Darwin': 'HOME', } this_os = platform.system() if this_os not in known_os.keys(): raise Exception, "Unknown OS, cannot identify preferences" key = known_os[this_os] prefs_dir = os.environ[key] prefs_file = os.path.join(prefs_dir, RC_FILE) return prefs_file
[docs] def OnSashMoved(self, event): '''user moved the sash''' self.prefs['sash_pos'] = self.splitter1.GetSashPosition() self.writePreferences()
[docs] def OnWindowGeometryChanged(self, event): '''user changed the window size or position''' self.writePreferences()
[docs] def OnSelectTreeCtrlItem(self, event): '''user selected something in the directory list tree control''' if not isinstance(event, wx.Event): self.setStatusText( "Not an event: %s" % str(event) ) event.Skip() return selectedItem = self.dir.GetPath() self.setStatusText( 'selected: ' + selectedItem ) if os.path.exists(selectedItem): if os.path.isfile(selectedItem): checked = self.menu_file.IsChecked(self.id_menu_report) summary = mda2idd_summary.summaryMda(selectedItem, checked) self.setSummaryText(summary) self.selectedMdaFile = selectedItem self.update_mrud(os.path.dirname(selectedItem)) path = os.path.dirname(selectedItem) if path != self.prefs['start_dir']: self.prefs['start_dir'] = path self.update_mrud(path) self.dirPicker.SetPath( path ) if os.path.isdir(selectedItem): # must select a valid MDA file to join the MRUD list! #self.prefs['start_dir'] = selectedItem #self.update_mrud(selectedItem) self.dirPicker.SetPath( selectedItem ) self.writePreferences()
[docs] def OnSelectDirPicker(self, event): '''user changed the text or browsed to a directory in the picker''' if not isinstance(event, wx.Event): self.setStatusText( "Not an event: %s" % str(event) ) event.Skip() return selectedItem = self.dirPicker.GetPath() if os.path.exists(selectedItem): if os.path.isdir(selectedItem): self.prefs['start_dir'] = selectedItem self.update_mrud(selectedItem) self.dir.ExpandPath(selectedItem)
[docs] def OnMenuFileItemSave(self, event): '''save the selected MDA file as ASCII''' if self.selectedMdaFile is not None and os.path.exists(self.selectedMdaFile): self.setStatusText("converting MDA file %s to ASCII text" % self.selectedMdaFile) converted = mda2idd_report.report(self.selectedMdaFile) if self.selectedMdaFile in converted: msg = "converted MDA file " + self.selectedMdaFile num = len(converted[self.selectedMdaFile]) msg += " to %d ASCII text file" % num if num > 1: msg += "s" # plural self.setStatusText(msg) else: self.setStatusText("No ASCII files written from " + self.selectedMdaFile)
[docs] def OnMenuFileItemPrefs(self, event): '''save the preferences to a file''' # TODO: edit preferences dialog self.writePreferences() # TODO: allow user to change file name?
[docs] def OnMenuFileItemReportStyle(self, event): if self.selectedMdaFile is not None and os.path.exists(self.selectedMdaFile): checked = self.menu_file.IsChecked(self.id_menu_report) summary = mda2idd_summary.summaryMda(self.selectedMdaFile, checked) self.setSummaryText(summary)
[docs] def OnMenuFileItemExit(self, event): ''' User requested to quit the application :param event: wxPython event object ''' # TODO: does not get here in RHEL5 self.writePreferences() self.Close()
[docs] def setCurrentDirectory(self, directory): '''set the current directory''' self.dir.ExpandPath(directory) self.dirPicker.SetPath(directory)
[docs] def setSummaryText(self, text): '''post new text to the summary TextCtrl, clearing any existing text''' self.textCtrl1.ChangeValue(str(text))
[docs] def appendSummaryText(self, text): '''post new text to the summary TextCtrl, appending to any existing text''' self.textCtrl1.AppendText(str(text))
# FIXME: self.textCtrl1.Refresh() # Refresh() won't work here since the caller does not let the window get redrawn # Refactor to update as loop through MDA files progresses.
[docs] def setStatusText(self, text): '''post new text to the status bar''' self.statusBar.SetStatusText(text)
[docs] def getPreferences(self, start_fresh=False): ''' set program preferences: default (start_fresh) and then optionally override from a file ''' self.prefs = { # define default prefs here as a dictionary 'size_h': 700, 'size_v': 320, 'pos_h': 80, 'pos_v': 20, 'sash_pos': 200, 'start_dir': os.path.dirname(self.preferences_file), 'short_summary': True, 'file_filter': '*.mda', 'mrud': [ os.path.dirname(self.preferences_file), ], 'mrud_max_directories': 9, } if not start_fresh and os.path.exists(self.preferences_file): self.readPreferences()
[docs] def readPreferences(self): '''read program prefs from a file''' if self.preferences_file is None: return if not os.path.exists(self.preferences_file): return tree = ElementTree.parse(self.preferences_file) root = tree.getroot() window = root.find('window') node = window.find('size') self.prefs['size_h'] = int(node.attrib['h']) self.prefs['size_v'] = int(node.attrib['v']) node = window.find('position') self.prefs['pos_h'] = int(node.attrib['h']) self.prefs['pos_v'] = int(node.attrib['v']) node = window.find('sash') self.prefs['sash_pos'] = int(node.attrib['pos']) node = root.find('mrud') if node is not None: self.prefs['mrud_max_directories'] = int(node.attrib['max_directories']) self.mrud = [subnode.text.strip() for subnode in node.findall('dir')] self.prefs['file_filter'] = root.find('file_filter').text.strip() node = root.find('short_summary') self.prefs['short_summary'] = node is None or 'true' == node.text.strip().lower() self.prefs['start_dir'] = root.find('starting_directory').text.strip()
[docs] def writePreferences(self): '''save program prefs to a file''' if self.preferences_file is None: return if not os.path.exists(os.path.dirname(self.preferences_file)): return if not self.startup_complete: return self.prefs['size_h'], self.prefs['size_v'] = self.GetSize() self.prefs['pos_h'], self.prefs['pos_v'] = self.GetPosition() self.prefs['short_summary'] = self.menu_file.IsChecked(self.id_menu_report) root = ElementTree.Element("mda2idd_gui") root.set("version", __version__) root.set("datetime", str(datetime.datetime.now())) node = ElementTree.SubElement(root, "preferences_file") node.text = self.preferences_file node = ElementTree.SubElement(root, "written_by") node.set("program", sys.argv[0]) window = ElementTree.SubElement(root, "window") node = ElementTree.SubElement(window, "size") node.set("h", str(self.prefs['size_h'])) node.set("v", str(self.prefs['size_v'])) node = ElementTree.SubElement(window, "position") node.set("h", str(self.prefs['pos_h'])) node.set("v", str(self.prefs['pos_v'])) node = ElementTree.SubElement(window, "sash") node.set("pos", str(self.prefs['sash_pos'])) node = ElementTree.SubElement(root, "file_filter") node.text = self.prefs['file_filter'] node = ElementTree.SubElement(root, "starting_directory") node.text = self.prefs['start_dir'] node = ElementTree.SubElement(root, "short_summary") node.text = str(self.prefs['short_summary']) mrud = ElementTree.SubElement(root, "mrud") mrud.append(ElementTree.Comment('MRUD: Most-Recently-Used Directory')) mrud.set("max_directories", str(self.prefs['mrud_max_directories'])) for item in self.mrud: ElementTree.SubElement(mrud, "dir").text = item doc = minidom.parseString(ElementTree.tostring(root)) xmlText = doc.toprettyxml(indent = " ", encoding='UTF-8') f = open(self.preferences_file, 'w') f.write(xmlText) f.close()
[docs] def update_mrud(self, newdir): '''MRUD: list of most-recently-used directories''' if newdir in self.mrud: if self.mrud[0] == newdir: return self.mrud.remove(newdir) fileList = self.listMdaFiles(newdir) if len(fileList) == 0: # no MDA files here, do not add this dir to MRUD list return self.mrud.insert(0, newdir) if len(self.mrud) >= self.prefs['mrud_max_directories']: self.mrud = self.mrud[:self.prefs['mrud_max_directories']] self.update_mrud_menus()
[docs] def update_mrud_menus(self): '''manage the MRUD menu items''' if len(self.mrud) == 0: return # remove old MRUD items # look for items just after "MRUD list" until the separator item = self.menu_file.FindItemByPosition(self.mrud_insertion_pos) while item.GetKind() != wx.ITEM_SEPARATOR: #label = item.GetLabel() self.menu_file.Delete(item.GetId()) item = self.menu_file.FindItemByPosition(self.mrud_insertion_pos) # add new MRUD items counter = 0 for path in self.mrud: if os.path.exists(path): text = '%s\tCtrl+%d' % (path, counter+1) position = self.mrud_insertion_pos + counter item = self.menu_file.Insert(position, wx.ID_ANY, text=text) self.Bind(wx.EVT_MENU, self.OnMrudItem, id=item.GetId()) counter += 1
[docs] def OnMrudItem(self, event): '''handle MRUD menu items''' label = self.menu_file.GetLabelText(event.GetId()) self.setCurrentDirectory(label)
[docs] def OnAbout(self, event): '''show the "About" box''' # derived from http://wiki.wxpython.org/Using%20wxPython%20Demo%20Code # First we create and fill the info object info = wx.AboutDialogInfo() info.SetName( os.path.basename(sys.argv[0]) ) info.SetVersion( __version__ ) info.SetDescription( sys.argv[0] + '\n\n' + __doc__ ) info.SetWebSite( __url__ ) info.SetDevelopers( ( 'main author: ' + __author__ + " <" + __author_email__ + ">", 'MDA file support: Tim Mooney <mooney@aps.anl.gov>', __version__, ) ) wx.AboutBox(info)
[docs] def OnConvertAll(self, event): '''selected the "ConvertAll" menu item''' # use path from preferences path = self.prefs['start_dir'] # use path from self.dirPicker widget #path = self.dirPicker.GetPath() self.setStatusText('Converting all MDA files to ASCII in directory: ' + path) self.convertMdaDir(path)
[docs] def convertMdaDir(self, path): '''convert all MDA files in a given directory''' fileList = self.listMdaFiles(path) if len(fileList) == 0: self.setSummaryText('No MDA files to convert in directory: ' + path) return self.setSummaryText('Converting these files:\n') known_exceptions = ( mda2idd_report.ReadMdaException, # some problem reading the MDA file mda2idd_report.RankException, # only handle 1-D and 2-D scans IndexError, # requested array index is not available OSError, # 1 case: could not create ../ASCII directory Exception, # anything at all ) for mdaFile in sorted(fileList): try: msg = '' answer = mda2idd_report.report(mdaFile, allowException=True) for k, v in answer.items(): msg += '\n* ' + k + ' --> ' # + str(v) msg += "\n " + "\n ".join(v) except known_exceptions as answer: problem = mdaFile + '\n' + traceback.format_exc() # self.messageDialog('problem', problem) # return msg += '\n* ' + problem self.appendSummaryText(msg)
[docs] def listMdaFiles(self, path): '''return a list of all MDA files in the path directory''' if not os.path.exists(path): self.setSummaryText('non-existent path: ' + path) return None # assumes self.prefs['file_filter'] is just '*.mda' return glob.glob(os.path.join(path, self.prefs['file_filter']))
[docs] def messageDialog(self, description, text): ''' Present a dialog asking user to acknowledge something :param str description: short description of message :param str text: message to be shown :param bool yes_and_no: if False (default), does not show a <No> button ''' # confirm this step self.SetStatusText('Request Acknowledgment') dlg = wx.MessageDialog(self, text, description, wx.CANCEL) result = dlg.ShowModal() dlg.Destroy() # destroy first return result
[docs]def main(): '''presents the GUI''' parser = optparse.OptionParser(description=__description__) parser.add_option('-f', '--fresh', action='store_true', default=False, dest='start_fresh', help='start fresh (ignore / replace the prefs file)') parser.add_option('-v', '--version', action='version') # also: -h gets a help / usage message options = parser.parse_args()[0] # ignore any args fresh_start = options.start_fresh app = wx.App() win = MainWindow(None, fresh_start) win.Show() app.MainLoop()
if __name__ == '__main__': main()