1import os
2import wx
3import time
4import fnmatch
5
6import abipy.gui.awx as awx
7import wx.lib.agw.flatnotebook as fnb
8
9from collections import OrderedDict
10from monty.dev import get_ncpus
11from abipy.gui.events import AbinitEventsFrame, AbinitEventsNotebookFrame
12from abipy.gui.timer import MultiTimerFrame, AbinitTimerFrame
13from abipy.gui.browser import FileListFrame, DirBrowserFrame, frame_from_filepath, frameclass_from_filepath
14from abipy.gui.editor import TextNotebookFrame, SimpleTextViewer
15import abipy.flowtk as flowtk
16
17
18def yaml_manager_dialog(parent):
19    """
20    Open a dialog that allows the user to select a YAML file with the taskmanager.
21    Returns the new manager or None if the user clicked CANCEL or the specifed file is not valid.
22    """
23    dialog = wx.FileDialog(parent, message="Choose a taskmanager.yml file", defaultDir=os.getcwd(),
24                           wildcard="YAML files (*.yml)|*.yml",
25                           style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR)
26
27    # Show the dialog and retrieve the user response.
28    # If it is the OK response, process the data.
29    if dialog.ShowModal() == wx.ID_CANCEL: return None
30    filepath = dialog.GetPath()
31    dialog.Destroy()
32
33    try:
34        return flowtk.TaskManager.from_file(filepath)
35    except:
36        awx.showErrorMessage(parent)
37        return None
38
39
40class FlowViewerFrame(awx.Frame):
41    VERSION = "0.1"
42
43    # Time in second after which we check the status of the tasks.
44    REFRESH_INTERVAL = 120
45
46    HELP_MSG = """\
47Quick help:
48
49  Task list:
50
51     Left-Click:   Open directory with output files.
52     Right-Click:  display popup menu with choices.
53
54Also, these key bindings can be used
55(For Mac OSX, replace 'Ctrl' with 'Apple'):
56
57  Ctrl-Q:     quit"""
58
59    def __init__(self, parent, flow, **kwargs):
60        """
61        Args:
62            parent:
63                Parent window.
64            flow:
65                `AbinitFlow` with the list of `Workflow` objects.
66        """
67        if "title" not in kwargs:
68            kwargs["title"] = self.codename
69
70        super(FlowViewerFrame, self).__init__(parent, -1, **kwargs)
71
72        # This combination of options for config seems to work on my Mac.
73        self.config = wx.FileConfig(appName=self.codename, localFilename=self.codename + ".ini",
74                                    style=wx.CONFIG_USE_LOCAL_FILE)
75
76        # Build menu, toolbar and status bar.
77        self.SetMenuBar(self.makeMenu())
78        self.makeToolBar()
79        self.statusbar = self.CreateStatusBar()
80        self.Centre()
81
82        self.flow = flow
83
84        # Disable launch mode if we already executing the flow with the scheduler.
85        self.check_launcher_file()
86
87        # Build UI
88        self.panel = panel = wx.Panel(self, -1)
89        self.main_sizer = main_sizer = wx.BoxSizer(wx.VERTICAL)
90
91        # Here we create a panel and a notebook on the panel
92        self.notebook = FlowNotebook(panel, self.flow)
93        main_sizer.Add(self.notebook, 1, wx.EXPAND, 5)
94
95        submit_button = wx.Button(panel, -1, label='Submit')
96        submit_button.Bind(wx.EVT_BUTTON, self.OnSubmitButton)
97
98        text = wx.StaticText(panel, -1, "Max nlaunch:")
99        text.Wrap(-1)
100        text.SetToolTipString("Maximum number of tasks that can be submitted. Use -1 for unlimited launches.")
101        self.max_nlaunch = wx.SpinCtrl(panel, -1, value=str(get_ncpus()), min=-1)
102
103        hsizer = wx.BoxSizer(wx.HORIZONTAL)
104        hsizer.Add(submit_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
105        hsizer.Add(text, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
106        hsizer.Add(self.max_nlaunch, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
107        main_sizer.Add(hsizer, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)
108
109        panel.SetSizerAndFit(main_sizer)
110
111        # Register this event when the GUI is IDLE
112        # Not used anymore since the Yaml parser
113        # is very slow when we use the pure python version.
114        self.last_refresh = time.time()
115        #self.Bind(wx.EVT_IDLE, self.OnIdle)
116
117    @property
118    def codename(self):
119        """String with the code name."""
120        return "Flow Viewer"
121
122    def makeToolBar(self):
123        """Create toolbar."""
124        self.toolbar = toolbar = self.CreateToolBar()
125        toolbar.SetToolBitmapSize(wx.Size(48, 48))
126
127        def bitmap(path):
128            return wx.Bitmap(awx.path_img(path))
129
130        self.ID_SHOW_INPUTS = wx.NewId()
131        self.ID_SHOW_OUTPUTS = wx.NewId()
132        self.ID_SHOW_LOGS = wx.NewId()
133        self.ID_SHOW_JOB_SCRIPTS = wx.NewId()
134        self.ID_SHOW_JOB_OUTERRS = wx.NewId()
135        self.ID_BROWSE = wx.NewId()
136        self.ID_SHOW_MAIN_EVENTS = wx.NewId()
137        self.ID_SHOW_LOG_EVENTS = wx.NewId()
138        self.ID_SHOW_TIMERS = wx.NewId()
139        self.ID_CHECK_STATUS = wx.NewId()
140
141        toolbar.AddSimpleTool(self.ID_SHOW_INPUTS, bitmap("in.png"), "Visualize the input file(s) of the workflow.")
142        toolbar.AddSimpleTool(self.ID_SHOW_OUTPUTS, bitmap("out.png"), "Visualize the output file(s) of the workflow..")
143        toolbar.AddSimpleTool(self.ID_SHOW_LOGS, bitmap("log.png"), "Visualize the log file(s) of the workflow.")
144        toolbar.AddSimpleTool(self.ID_SHOW_JOB_SCRIPTS, bitmap("script.png"), "Visualize the script(s).")
145        toolbar.AddSimpleTool(self.ID_SHOW_JOB_OUTERRS, bitmap("script.png"), "Visualize the script(s).")
146        toolbar.AddSimpleTool(self.ID_BROWSE, bitmap("browse.png"), "Browse all the files of the workflow.")
147        toolbar.AddSimpleTool(self.ID_SHOW_MAIN_EVENTS, bitmap("out_evt.png"), "Show the ABINIT events reported in the main output files.")
148        toolbar.AddSimpleTool(self.ID_SHOW_LOG_EVENTS, bitmap("log_evt.png"), "Show the ABINIT events reported in the log files.")
149        toolbar.AddSimpleTool(self.ID_SHOW_TIMERS, bitmap("timer.png"), "Show the ABINIT timers in the abo files.")
150
151        toolbar.AddSeparator()
152
153        toolbar.AddSimpleTool(self.ID_CHECK_STATUS, bitmap("refresh.png"), "Check the status of the workflow(s).")
154
155        #toolbar.AddSeparator()
156        #self.file_selector = FileSelector(toolbar, self)
157        #toolbar.AddControl(control=self.file_selector)
158
159        toolbar.Realize()
160
161        # Associate menu/toolbar items with their handlers.
162        menu_handlers = [
163            (self.ID_SHOW_INPUTS, self.OnShowInputs),
164            (self.ID_SHOW_OUTPUTS, self.OnShowOutputs),
165            (self.ID_SHOW_LOGS, self.OnShowLogs),
166            (self.ID_SHOW_JOB_SCRIPTS, self.OnShowJobScripts),
167            (self.ID_SHOW_JOB_OUTERRS, self.OnShowJobOutErrs),
168            (self.ID_BROWSE, self.OnBrowse),
169            (self.ID_SHOW_MAIN_EVENTS, self.OnShowMainEvents),
170            (self.ID_SHOW_LOG_EVENTS, self.OnShowLogEvents),
171            (self.ID_SHOW_TIMERS, self.OnShowTimers),
172            (self.ID_CHECK_STATUS, self.OnCheckStatusButton),
173        ]
174
175        for combo in menu_handlers:
176            mid, handler = combo[:2]
177            self.Bind(wx.EVT_MENU, handler, id=mid)
178
179    def makeMenu(self):
180        """Make the menu bar."""
181        menu_bar = wx.MenuBar()
182
183        file_menu = wx.Menu()
184        file_menu.Append(wx.ID_OPEN, "&Open", help="Open an existing flow in a new frame")
185        file_menu.Append(wx.ID_CLOSE, "&Close", help="Close the file associated to the active tab")
186        file_menu.Append(wx.ID_EXIT, "&Quit", help="Exit the application")
187
188        file_history = self.file_history = wx.FileHistory(8)
189        file_history.Load(self.config)
190        recent = wx.Menu()
191        file_history.UseMenu(recent)
192        file_history.AddFilesToMenu()
193        file_menu.AppendMenu(-1, "&Recent Files", recent)
194        self.Bind(wx.EVT_MENU_RANGE, self.OnFileHistory, id=wx.ID_FILE1, id2=wx.ID_FILE9)
195        menu_bar.Append(file_menu, "File")
196
197        flow_menu = wx.Menu()
198
199        #self.ID_FLOW_CHANGE_MANAGER = wx.NewId()
200        #flow_menu.Append(self.ID_FLOW_CHANGE_MANAGER, "Change TaskManager", help="")
201
202        self.ID_FLOW_OPEN_OUTFILES = wx.NewId()
203        flow_menu.Append(self.ID_FLOW_OPEN_OUTFILES, "Open output files", help="")
204
205        self.ID_FLOW_TREE_VIEW = wx.NewId()
206        flow_menu.Append(self.ID_FLOW_TREE_VIEW, "Tree view", help="Tree view of the tasks")
207
208        menu_bar.Append(flow_menu, "Flow")
209
210        help_menu = wx.Menu()
211        help_menu.Append(wx.ID_ABOUT, "About " + self.codename, help="Info on the application")
212        menu_bar.Append(help_menu, "Help")
213
214        self.ID_HELP_QUICKREF = wx.NewId()
215        help_menu.Append(self.ID_HELP_QUICKREF, "Quick Reference ", help="Quick reference for " + self.codename)
216        help_menu.Append(wx.ID_ABOUT, "About " + self.codename, help="Info on the application")
217
218        # Associate menu/toolbar items with their handlers.
219        menu_handlers = [
220            (wx.ID_OPEN, self.OnOpen),
221            (wx.ID_CLOSE, self.OnClose),
222            (wx.ID_EXIT, self.OnExit),
223            (wx.ID_ABOUT, self.OnAboutBox),
224            #
225            #(self.ID_FLOW_CHANGE_MANAGER, self.onChangeManager),
226            (self.ID_FLOW_OPEN_OUTFILES, self.onOpenOutFiles),
227            (self.ID_FLOW_TREE_VIEW, self.onTaskTreeView),
228            #
229            (self.ID_HELP_QUICKREF, self.onQuickRef),
230        ]
231
232        for combo in menu_handlers:
233            mid, handler = combo[:2]
234            self.Bind(wx.EVT_MENU, handler, id=mid)
235
236        return menu_bar
237
238    def check_launcher_file(self, with_dialog=True):
239        """
240        Disable the launch button if we have a sheduler running,
241        since we don't want to have processes modifying the flow.
242        """
243        self.disabled_launcher = False
244        pid_files = fnmatch.filter(os.listdir(self.flow.workdir), "*.pid")
245
246        if pid_files:
247            pid_file = os.path.join(self.flow.workdir, pid_files[0])
248            if not os.path.exists(pid_file): return
249            self.disabled_launcher = True
250
251            with open(pid_file, "r") as fh:
252                pid = int(fh.readline())
253
254            message = ("Found pid file %s associated to an already running scheduler with pid %d. "
255                       "Job submission has been disabled." % (pid_file, pid))
256
257            if with_dialog:
258                dialog = wx.MessageDialog(None, message=message, caption='Flow is being executed by a scheduler',
259                                          style=wx.OK | wx.ICON_EXCLAMATION)
260                dialog.ShowModal()
261                dialog.Destroy()
262
263            else:
264                self.statusbar.PushStatusText(message)
265
266    def OnSubmitButton(self, event):
267        """Submit up to max_nlauch tasks."""
268        if self.disabled_launcher: return
269        max_nlaunch = int(self.max_nlaunch.GetValue())
270        nlaunch = flowtk.PyLauncher(self.flow).rapidfire(max_nlaunch=max_nlaunch)
271
272        self.statusbar.PushStatusText("Submitted %d tasks" % nlaunch)
273
274    def GetSelectedWork(self):
275        """
276        Return the selected workflow namely that the
277        workflow associated to the active tab. None if list is empty.
278        """
279        return self.notebook.GetSelectedWork()
280
281    def OnIdle(self, event):
282        """Function executed when the GUI is idle."""
283        now = time.time()
284        if (now - self.last_refresh) > self.REFRESH_INTERVAL:
285            self.check_launcher_file(with_dialog=False)
286            self.CheckStatusAndRedraw()
287            self.last_refresh = time.time()
288
289    def OnCheckStatusButton(self, event):
290        """Callback triggereed by the checkstatus button."""
291        self.CheckStatusAndRedraw()
292
293    def CheckStatusAndRedraw(self):
294        """Check the status of all the workflows and redraw the panel."""
295        self.statusbar.PushStatusText("Checking status...")
296
297        self.flow.check_status()
298
299        # Count the number of tasks with given status.
300        counter = self.flow.status_counter
301
302        # Save the active tab so that we can set it afterwards.
303        old_selection = self.notebook.GetSelection()
304
305        # Build new notebook and redraw the panel
306        main_sizer = self.main_sizer
307        main_sizer.Hide(0)
308        main_sizer.Remove(0)
309        new_notebook = FlowNotebook(self, self.flow)
310        main_sizer.Insert(0, new_notebook, 1, wx.EXPAND, 5)
311        self.notebook = new_notebook
312
313        self.panel.Layout()
314
315        # Reinstate the old selection
316        self.notebook.SetSelection(old_selection)
317
318        # Write number of jobs with given status.
319        message = ", ".join("%s: %s" % (k, v) for (k, v) in counter.items())
320        self.statusbar.PushStatusText(message)
321
322    def read_file(self, filepath):
323        self.statusbar.PushStatusText("Reading %s" % filepath)
324
325        try:
326            flow = flowtk.AbinitFlow.pickle_load(filepath)
327            self.AddFileToHistory(filepath)
328            return flow
329        except:
330            awx.showErrorMessage(self)
331            return None
332
333    def OnOpen(self, event):
334        dialog = wx.FileDialog(
335            self, message="Choose a __workflow__.pickle file", defaultDir=os.getcwd(),
336            wildcard="pickle files (*.pickle)|*.pickle",
337            style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR)
338
339        # Show the dialog and retrieve the user response.
340        # If it is the OK response, process the data.
341        if dialog.ShowModal() == wx.ID_CANCEL: return
342
343        filepath = dialog.GetPath()
344        dialog.Destroy()
345
346        # Add to the history.
347        self.file_history.AddFileToHistory(filepath)
348        self.file_history.Save(self.config)
349        self.config.Flush()
350        flow = self.read_file(filepath)
351
352        if flow is not None:
353            # Open new frame.
354            FlowViewerFrame(self, flow).Show()
355
356    def AddFileToHistory(self, filepath):
357        """Add the absolute filepath to the file history."""
358        self.file_history.AddFileToHistory(filepath)
359        self.file_history.Save(self.config)
360        self.config.Flush()
361
362    def OnFileHistory(self, event):
363        fileNum = event.GetId() - wx.ID_FILE1
364        filepath = self.file_history.GetHistoryFile(fileNum)
365        self.file_history.AddFileToHistory(filepath)
366        self.read_file(filepath)
367
368    def OnClose(self, event):
369        print("onclose")
370        #self.flow.pickle_dump()
371        #self.Close()
372
373    def OnExit(self, event):
374        """Save the status of the flow in the database and exit the GUI."""
375        print("onexit")
376        #self.flow.pickle_dump()
377        self.Close()
378
379    def OnAboutBox(self, event):
380        """"Info on the application."""
381        awx.makeAboutBox(codename=self.codename, version=self.VERSION,
382                         description="", developers="M. Giantomassi")
383
384    def onQuickRef(self, event=None):
385        dialog = wx.MessageDialog(self, self.HELP_MSG, self.codename + " Quick Reference",
386                               wx.OK | wx.ICON_INFORMATION)
387        dialog.ShowModal()
388        dialog.Destroy()
389
390    def OnShowInputs(self, event):
391        """Show all the input files of the selected `Workflow`."""
392        work = self.GetSelectedWork()
393        if work is None: return
394        TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.abi").Show()
395
396    def OnShowOutputs(self, event):
397        """Show all the output files of the selected `Workflow`."""
398        work = self.GetSelectedWork()
399        if work is None: return
400        TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.abo").Show()
401
402    def OnShowJobScripts(self, event):
403        """Show all the job script files of the selected `Workflow`."""
404        work = self.GetSelectedWork()
405        if work is None: return
406        TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.sh").Show()
407
408    def OnShowJobOutErrs(self, event):
409        """Show all the queue output/error files files of the selected `Workflow`."""
410        work = self.GetSelectedWork()
411        if work is None: return
412        TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.qout|*.qerr").Show()
413
414    def OnShowLogs(self, event):
415        """Show all the log files of the selected `Workflow`."""
416        work = self.GetSelectedWork()
417        if work is None: return
418        TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.log").Show()
419
420    def OnBrowse(self, event):
421        """Browse all the output files produced by the selected `Workflow`."""
422        work = self.GetSelectedWork()
423        if work is None: return
424        FileListFrame(self, dirpaths=work.workdir).Show()
425        #DirBrowserFrame(self, dir=work.workdir).Show()
426
427    def OnShowMainEvents(self, event):
428        """Browse all the main events of the tasks in the selected `Workflow`."""
429        work = self.GetSelectedWork()
430        if work is None: return
431        AbinitEventsNotebookFrame(self, filenames=[task.output_file.path for task in work]).Show()
432
433    def OnShowLogEvents(self, event):
434        """Browse all the log events of the tasks in the selected `Workflow`."""
435        work = self.GetSelectedWork()
436        if work is None: return
437        AbinitEventsNotebookFrame(self, [task.log_file.path for task in work]).Show()
438
439    def OnShowTimers(self, event):
440        """Analyze the timing data of all the output files of the selected `Workflow`."""
441        work = self.GetSelectedWork()
442        if work is None: return
443        timers = work.parse_timers()
444        # Build the frame for analyzing multiple timers.
445        MultiTimerFrame(self, timers).Show()
446
447    def onTaskTreeView(self, event):
448        TaskTreeView(self, self.flow).Show()
449
450    #def onChangeManager(self, event):
451    #    #ChangeTaskManager(self, self.flow).Show()
452    #    new_manager = yaml_manager_dialog(self)
453    #    if new_manager is None: return
454    #    print(new_manager)
455    #    #status_selected =  upper()
456    #    #status = None if status_selected == "ALL" else status_selected
457    #    # Change the manager of the errored tasks.
458    #    #for task in flow.iflat_tasks(status="S_ERROR"):
459    #    #    task.reset()
460    #    #    task.set_manager(new_manager)
461
462    def onOpenOutFiles(self, event):
463        FileSelectorFrame(parent=self, viewer=self).Show()
464
465
466class FlowNotebook(fnb.FlatNotebook):
467    """
468    Notebook class
469    """
470    def __init__(self, parent, flow):
471        try:
472            style = fnb.FNB_NO_X_BUTTON | fnb.FNB_NAV_BUTTONS_WHEN_NEEDED
473        except AttributeError:
474            style = fnb.FNB_NO_X_BUTTON
475
476        super(FlowNotebook, self).__init__(parent, id=-1, style=style)
477
478        self.flow = flow
479        for work in flow:
480            tab = TabPanel(self, work)
481            self.AddPage(tab, text=os.path.basename(work.workdir))
482
483    def GetSelectedWork(self):
484        """
485        Return the selected workflow namely that the workflow associated to the
486        active tab. None is list is empy.
487        """
488        # FIXME: If we want to allow the user to remove some tab we have
489        # to remove the corresponding work from workflows.
490        # Easy if workflow_viewer can only read data but it might be source
491        # of bugs if we want to modify the object e.g. by submitting the calculation.
492        # For the time-being, use this assertion to prevent users from removing pages.
493        if self.GetPageCount() != len(self.flow):
494            return awx.showErrorMessage(self, message="Bad user has removed pages from the notebook!")
495
496        idx = self.GetSelection()
497        if idx == -1: return None
498        try:
499            return self.flow[idx]
500        except IndexError:
501            return None
502
503
504class TabPanel(wx.Panel):
505    """
506    Notebook tab.
507    """
508    def __init__(self, parent, work, **kwargs):
509        wx.Panel.__init__(self, parent=parent, id=-1, **kwargs)
510
511        # List Control with the individual tasks of the workflow.
512        task_listctrl = TaskListCtrl(self, work)
513
514        label = wx.StaticText(self, -1, "Workflow class %s, status: %s, finalized: %s" % (
515            work.__class__.__name__, work.status, work.finalized))
516        label.Wrap(-1)
517
518        main_sizer = wx.BoxSizer(wx.VERTICAL)
519        main_sizer.Add(label, 0, wx.ALIGN_LEFT, 5)
520        main_sizer.Add(task_listctrl, 1, wx.EXPAND, 5)
521
522        self.SetSizerAndFit(main_sizer)
523
524
525class TaskListCtrl(wx.ListCtrl):
526    """
527    ListCtrl with the list of task in the `Workflow`.
528    """
529    def __init__(self, parent, work, **kwargs):
530        """
531        Args:
532            parent:
533                Parent window.
534            work:
535                `Workflow` containig the List of `Task` instances.
536        """
537        super(TaskListCtrl, self).__init__(parent, id=-1, style=wx.LC_REPORT | wx.BORDER_SUNKEN, **kwargs)
538
539        self.work = work
540
541        columns = ["Task", "Status", "Queue_id",
542                   "Errors", "Warnings", "Comments",
543                   "MPI", "OMP",
544                   "num_restarts", "Task Class",
545                   ]
546
547        for index, col in enumerate(columns):
548            self.InsertColumn(index, col)
549
550        # Used to store the Max width in pixels for the data in the column.
551        column_widths = [awx.get_width_height(self, s)[0] for s in columns]
552
553        for task in work:
554            events = map(str, 3*["N/A"])
555
556            try:
557                report = task.get_event_report()
558                if report is not None:
559                    events = map(str, [report.num_errors, report.num_warnings, report.num_comments])
560            except:
561                pass
562
563            cpu_info = [task.mpi_procs, task.omp_threads]
564            entry = map(str, [task.name, str(task.status), task.queue_id] +
565                              events +
566                              cpu_info +
567                              [task.num_restarts, task.__class__.__name__]
568                        )
569            w = [awx.get_width_height(self, s)[0] for s in entry]
570            column_widths = map(max, zip(w, column_widths))
571
572            self.Append(entry)
573
574        # Set the width in pixel for each column.
575        for index, col in enumerate(columns):
576            self.SetColumnWidth(index, column_widths[index])
577
578        self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnRightClick)
579        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated)
580
581    def OnRightClick(self, event):
582        currentItem = event.m_itemIndex
583        if currentItem == -1: return
584
585        # Open the popup menu then destroy it to avoid mem leak.
586        task = self.work[currentItem]
587        menu = TaskPopupMenu(parent=self, task=task)
588        self.PopupMenu(menu, event.GetPoint())
589        menu.Destroy()
590
591    def OnItemActivated(self, event):
592        """
593        Browse the outdir of the task
594        """
595        currentItem = event.m_itemIndex
596        task = self.work[currentItem]
597        FileListFrame(self, dirpaths=task.outdir.path, title=task.outdir.relpath).Show()
598
599
600# Callbacks
601
602def show_task_main_output(parent, task):
603    """Show the main output file of the task."""
604    file = task.output_file
605
606    if file.exists:
607        frame_from_filepath(parent, file.path).Show()
608    else:
609        awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path)
610
611
612def show_task_log(parent, task):
613    """Show the log file of the task."""
614    file = task.log_file
615
616    if file.exists:
617        frame_from_filepath(parent, file.path).Show()
618    else:
619        awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path)
620
621
622def show_task_main_events(parent, task):
623    """Show the events reported in the main output file."""
624    file = task.output_file
625
626    if file.exists:
627        AbinitEventsFrame(parent, file.path).Show()
628    else:
629        awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path)
630
631
632def show_task_log_events(parent, task):
633    """Show the events reported in the log file."""
634    file = task.log_file
635
636    if file.exists:
637        AbinitEventsFrame(parent, file.path).Show()
638    else:
639        awx.showErrorMessage(parent=parent, message="Log file %s does not exist" % file.path)
640
641
642def browse_indir(parent, task):
643    """Open a window that allows the user to browse the input files in indir."""
644    FileListFrame(parent, dirpaths=task.indir.path).Show()
645
646
647def browse_outdir(parent, task):
648    """Open a window that allows the user to browse the output files in outdir."""
649    FileListFrame(parent, dirpaths=task.outdir.path).Show()
650
651
652def browse_tmpdir(parent, task):
653    """Open a window that allows the user to browse the output files in outdir."""
654    FileListFrame(parent, dirpaths=task.tmpdir.path).Show()
655
656
657def show_history(parent, task):
658    """Show the history of the task."""
659    text = "\n".join(task.history)
660    SimpleTextViewer(parent, text).Show()
661
662
663def show_timer(parent, task):
664    """Show timing data of the k."""
665    try:
666        frame = AbinitTimerFrame(parent, task.output_file.path)
667        frame.Show()
668    except awx.Error as exc:
669        awx.showErrorMessage(parent, str(exc))
670
671
672def check_status_and_pickle(task):
673    """Check the status of the task and update the pickle database."""
674    task.flow.check_status()
675    task.flow.pickle_dump()
676
677
678def task_restart(parent, task):
679    """Restart the task."""
680    task.restart()
681    check_status_and_pickle(task)
682
683
684def task_reset(parent, task):
685    """Reset the status of the task."""
686    task.reset()
687    check_status_and_pickle(task)
688
689
690def task_set_status(parent, task):
691    """Reset the status of the task."""
692    choices = [str(s) for s in flowtk.Node.ALL_STATUS]
693    dialog = wx.SingleChoiceDialog(parent, message="Select new status", caption="", choices=choices)
694    if dialog.ShowModal() == wx.ID_CANCEL: return None
695    status = choices[dialog.GetSelection()]
696    dialog.Destroy()
697
698    task.set_status(status, info_msg="Status changed by user on %s" % time.asctime())
699    #task.reset()
700    check_status_and_pickle(task)
701
702
703def task_show_deps(parent, task):
704    """Show the dependencies of the task."""
705    text = task.str_deps()
706    SimpleTextViewer(parent, text).Show()
707
708
709def task_inspect(parent, task):
710    """Inspect the results at runtime."""
711    if hasattr(task, "inspect"):
712        task.inspect()
713
714
715class TaskPopupMenu(wx.Menu):
716    """
717    A `TaskPopupMenu` has a list of callback functions indexed by the menu title.
718    The signature of the callback function is func(parent, task) where parent is
719    the wx Window that will become the parent of the new frame created by the callback.
720    and task is a `Task` instance.
721    """
722    MENU_TITLES = OrderedDict([
723        ("output", show_task_main_output),
724        ("log", show_task_log),
725        ("main events", show_task_main_events),
726        ("log events",  show_task_log_events),
727        ("browse indir", browse_indir),
728        ("browse outdir", browse_outdir),
729        ("browse tmpdir", browse_tmpdir),
730        ("history", show_history),
731        ("restart", task_restart),
732        ("reset", task_reset),
733        ("dependencies", task_show_deps),
734        ("inspect", task_inspect),
735        ("set status", task_set_status),
736        ("timer", show_timer),
737    ])
738
739    def __init__(self, parent, task):
740        super(TaskPopupMenu, self).__init__()
741        self.parent, self.task = parent, task
742
743        self._make_menu()
744
745    def _make_menu(self):
746        """Build the menu"""
747        self.menu_title_by_id = OrderedDict()
748
749        for title in self.MENU_TITLES:
750            self.menu_title_by_id[wx.NewId()] = title
751
752        for (id, title) in self.menu_title_by_id.items():
753            # Register menu handlers with EVT_MENU, on the menu.
754            self.Append(id, title)
755            wx.EVT_MENU(self, id, self.OnMenuSelection)
756
757    def _get_callback(self, title):
758        return self.MENU_TITLES[title]
759
760    def OnMenuSelection(self, event):
761        title = self.menu_title_by_id[event.GetId()]
762        callback = self._get_callback(title)
763        #print("Calling callback %s on task %s" % (callback, self.task))
764        try:
765            callback(self.parent, self.task)
766        except:
767            awx.showErrorMessage(parent=self.parent)
768
769#class ChangeTaskManager(awx.Frame):
770#    def __init__(self, parent, flow, **kwargs)
771#        super(ChangeTaskManager, self).__init__(self, parent, **kwargs)
772#
773#
774#    def onOkButton(self, event):
775#        new_manager = yaml_manager_dialog(self)
776#        if new_manager is None: return
777#        print(new_manager)
778#        #status_selected =  upper()
779#        #status = None if status_selected == "ALL" else status_selected
780#        # Change the manager of the errored tasks.
781#        #for task in flow.iflat_tasks(status="S_ERROR"):
782#        #    task.reset()
783#        #    task.set_manager(new_manager)
784
785
786class TaskStatusTreePanel(awx.Panel):
787    """
788    Panel with a TreeCtrl that allows the user to navigate the tasks grouped by status.
789    """
790    def __init__(self, parent, flow, **kwargs):
791        super(TaskStatusTreePanel, self).__init__(parent, -1, **kwargs)
792
793        main_sizer = wx.BoxSizer(wx.VERTICAL)
794        vbox = wx.BoxSizer(wx.VERTICAL)
795        panel1 = awx.Panel(self, -1)
796        panel2 = awx.Panel(self, -1)
797
798        self.tree = tree = wx.TreeCtrl(panel1, 1, wx.DefaultPosition, (-1, -1), wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS)
799
800        root = self.tree.AddRoot('Task Status')
801
802        # entry = collections.namedtuple("Entry", "task wi ti")
803        #print(status2entries)
804        self.status2entries = flow.groupby_status()
805
806        self.status_nodes = []
807        for status, entries in self.status2entries.items():
808            node = tree.AppendItem(root, "%d %s tasks" % (len(entries), str(status)), data=wx.TreeItemData(status))
809            self.status_nodes.append(node)
810            for entry in entries:
811                tree.AppendItem(node, "Task: " + str(entry.task), data=wx.TreeItemData(entry))
812
813        tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onSelChanged)
814        tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.onItemRightClick)
815
816        self.display = wx.StaticText(panel2, -1, '', (10, 10), style=wx.ALIGN_LEFT)
817
818        vbox.Add(self.tree, 1, wx.EXPAND)
819        main_sizer.Add(panel1, 1, wx.EXPAND)
820        main_sizer.Add(panel2, 1, wx.EXPAND)
821        panel1.SetSizerAndFit(vbox)
822
823        self.SetSizerAndFit(main_sizer)
824        self.Centre()
825
826    def onSelChanged(self, event):
827        node = event.GetItem()
828        if node in self.status_nodes: return
829
830        proxy = self.tree.GetItemData(node)
831        if proxy is None: return
832        entry = proxy.GetData()
833
834        task = entry.task
835        s = str(task)
836        s += "\nHistory:\n" + task.str_history
837
838        self.display.SetLabel(s)
839
840    def onItemRightClick(self, event):
841        node = event.GetItem()
842        proxy = self.tree.GetItemData(node)
843        if proxy is None: return
844
845        if node in self.status_nodes:
846            status = proxy.GetData()
847            print("received set of tasks with status %s" % status)
848            popup_menu = self.makePopupMenuStatus()
849            self.PopupMenu(popup_menu, event.GetPoint())
850            popup_menu.Destroy()
851
852        #print("click")
853        #print("event", dir(event))
854
855    def makePopupMenuStatus(self):
856        self.ID_POPUP_CHANGE_MANAGER = wx.NewId()
857        menu = wx.Menu()
858        menu.Append(self.ID_POPUP_CHANGE_MANAGER, "Change manager")
859
860        # Associate menu/toolbar items with their handlers.
861        menu_handlers = [
862            (self.ID_POPUP_CHANGE_MANAGER, self.onChangeManager),
863        ]
864
865        for combo in menu_handlers:
866            mid, handler = combo[:2]
867            self.Bind(wx.EVT_MENU, handler, id=mid)
868
869        return menu
870
871    def onChangeManager(self, event):
872        print("changer manager")
873        node = self.tree.GetSelection()
874        status = self.tree.GetItemData(node).GetData()
875        print("status", status)
876
877        entries = self.status2entries[status]
878
879        new_manager = yaml_manager_dialog(self)
880        for e in entries:
881            e.task.reset()
882            e.task.set_manager(new_manager)
883
884        #self.flow.build_and_pickle_dump()
885
886
887class TaskTreeView(awx.Frame):
888    """
889    A frame with a TaskStatusTreePanel.
890    """
891    def __init__(self, parent, flow, **kwargs):
892        if "title" not in kwargs:
893            kwargs["title"] = "Task tree view: %s" % flow.workdir
894
895        super(TaskTreeView, self).__init__(parent, **kwargs)
896
897        panel = TaskStatusTreePanel(self, flow)
898
899
900class FileSelector(awx.Panel):
901#class FileSelector(wx.Control):
902    """
903    Control that allows the user to select multiple output files of the same type (either inside
904    a `Workflow` on in the entire `Flow`. The open button will open a viewer to analyze
905    the multiple files selected.
906    """
907    def __init__(self, parent, viewer, **kwargs):
908        super(FileSelector, self).__init__(parent, -1, **kwargs)
909        self.viewer = viewer
910        panel = self #wx.Panel(self, -1)
911
912        wcards = ["*GSR.nc", "*WFK-etsf.nc", "*SIGRES.nc", "*MDF.nc"]
913
914        self.wcards_cbox = wx.ComboBox(panel, id=-1, name='File type', choices=wcards, value=wcards[0], style=wx.CB_READONLY)
915
916        smodes = ["Selected Workflow", "Entire Flow"]
917        self.select_rbox = wx.RadioBox(panel, id=1, name="Selection Mode", choices=smodes, style=wx.RA_SPECIFY_ROWS)
918
919        open_button = wx.Button(panel, -1, label='Open files')
920        open_button.Bind(wx.EVT_BUTTON, self.onOpenButton)
921
922        #main_sizer = wx.BoxSizer(wx.HORIZONTAL)
923        main_sizer = wx.GridBagSizer(10, 10)
924
925        #vsizer = wx.BoxSizer(wx.VERTICAL)
926        #vsizer.Add(self.wcards_cbox, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5)
927        #vsizer.Add(open_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5)
928        #main_sizer.Add(vsizer)
929        #main_sizer.Add(self.select_rbox, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5)
930
931        main_sizer.Add(self.wcards_cbox, (0, 0), (1,1), wx.ALIGN_CENTER)
932        main_sizer.Add(open_button, (1, 0), (1,1), wx.ALIGN_CENTER)
933        main_sizer.Add(self.select_rbox, (0, 1), (3,3), wx.EXPAND)
934
935        panel.SetSizerAndFit(main_sizer)
936
937    def getWildCard(self):
938        """Returns a string with the Abinit extension selected by the user."""
939        return self.wcards_cbox.GetValue()
940
941    def getSelectionMode(self):
942        """Returns a string with the selection mode selected by the user."""
943        index = self.select_rbox.GetSelection()
944        return self.select_rbox.GetString(index)
945
946    def onOpenButton(self, event):
947        wcard = self.getWildCard()
948        smode = self.getSelectionMode()
949        print("wcard", wcard, "smode", smode)
950
951        # Find the filepaths according to smode.
952        filepaths = []
953
954        if smode == "Selected Workflow":
955            work = self.viewer.GetSelectedWork()
956            for task in work:
957                filepaths.extend(task.outdir.list_filepaths(wildcard=wcard))
958
959        elif smode == "Entire Flow":
960            for work in self.viewer.flow:
961                for task in work:
962                    filepaths.extend(task.outdir.list_filepaths(wildcard=wcard))
963
964        else:
965            return awx.showErrorMessage(self, "Wrong value of smode: %s" % smode)
966
967        if not filepaths:
968            return awx.showErrorMessage(self, "Cannot find any file matching the specified shell pattern")
969
970        print("filepaths", filepaths)
971
972        # Get the viewer class associated to these files, build the frame and show it.
973        frame_class = frameclass_from_filepath(filepaths[0])
974        if frame_class is None: return
975        frame_class(self, filepaths).Show()
976
977
978class FileSelectorFrame(wx.Frame):
979    def __init__(self, parent, viewer, **kwargs):
980        super(FileSelectorFrame, self).__init__(parent, -1, **kwargs)
981
982        panel = wx.Panel(self, -1)
983        file_selector = FileSelector(panel, viewer)
984
985        main_sizer = wx.BoxSizer(wx.VERTICAL)
986        main_sizer.Add(file_selector, 1, wx.EXPAND , 5)
987        panel.SetSizerAndFit(main_sizer)
988
989
990#class StatusSelectorFrame(wx.Frame):
991#    def __init__(self, parent, viewer, **kwargs):
992#        super(StatusSelectorFrame, self).__init__(parent, -1, **kwargs)
993#
994#        panel = wx.Panel(self, -1)
995#
996#        #choices = ["S_OK", "*WFK-etsf.nc", "*SIGRES.nc", "*MDF.nc"]
997#
998#        #self.status_cbox = wx.ComboBox(panel, id=-1, name='File type', choices=choices, value=choices[0], style=wx.CB_READONLY)
999#
1000#        #smodes = ["Selected Workflow", "Entire Flow"]
1001#        #self.select_rbox = wx.RadioBox(panel, id=1, name="Selection Mode", choices=smodes, style=wx.RA_SPECIFY_ROWS)
1002#
1003#        #open_button = wx.Button(panel, -1, label='Open files')
1004#        #open_button.Bind(wx.EVT_BUTTON, self.onOpenButton)
1005#
1006#        main_sizer = wx.BoxSizer(wx.VERTICAL)
1007#        main_sizer.Add(file_selector, 1, wx.EXPAND , 5)
1008#        panel.SetSizerAndFit(main_sizer)
1009#
1010#    def getSelectedStatus(self):
1011#        return self.choices_cbox.GetValue()
1012
1013
1014def wxapp_flow_viewer(works):
1015    """Standalone application for `FlowViewerFrame`."""
1016    app = awx.App()
1017    FlowViewerFrame(None, works).Show()
1018    return app
1019