def __init__(self): super(CoreBrowser, self).__init__(parent=None, id=wx.ID_ANY, title='CScience', size=(540, 380)) #hide the frame until the initial repo is loaded, to prevent flicker. self.Show(False) self.browser_view = SampleBrowserView() self.core = None self.CreateStatusBar() self.create_menus() self.create_widgets() self.Bind(events.EVT_REPO_CHANGED, self.on_repository_altered) self.Bind(wx.EVT_CLOSE, self.quit)
class CoreBrowser(MemoryFrame): framename = 'samplebrowser' def __init__(self): super(CoreBrowser, self).__init__(parent=None, id=wx.ID_ANY, title='CScience', size=(540, 380)) #hide the frame until the initial repo is loaded, to prevent flicker. self.Show(False) self.browser_view = SampleBrowserView() self.core = None self.CreateStatusBar() self.create_menus() self.create_widgets() self.Bind(events.EVT_REPO_CHANGED, self.on_repository_altered) self.Bind(wx.EVT_CLOSE, self.quit) def create_menus(self): menu_bar = wx.MenuBar() #Build File menu #Note: on a mac, the 'Quit' option is moved for platform nativity automatically file_menu = wx.Menu() item = file_menu.Append(wx.ID_OPEN, "Switch Repository\tCtrl-O", "Switch to a different CScience Repository") self.Bind(wx.EVT_MENU, self.change_repository, item) file_menu.AppendSeparator() item = file_menu.Append(wx.ID_SAVE, "Save Repository\tCtrl-S", "Save changes to current CScience Repository") self.Bind(wx.EVT_MENU, self.save_repository, item) file_menu.AppendSeparator() item = file_menu.Append(wx.ID_EXIT, "Quit CScience\tCtrl-Q", "Quit CScience") self.Bind(wx.EVT_MENU, self.quit, item) edit_menu = wx.Menu() item = edit_menu.Append(wx.ID_COPY, "Copy\tCtrl-C", "Copy selected samples.") self.Bind(wx.EVT_MENU, self.OnCopy, item) tool_menu = wx.Menu() def bind_editor(name, edclass, menuname, tooltip): menuitem = tool_menu.Append(wx.ID_ANY, menuname, tooltip) hid_name = ''.join(('_', name)) def del_editor(event, *args, **kwargs): setattr(self, hid_name, None) def create_editor(): editor = getattr(self, hid_name, None) if not editor: #TODO: fix this hack! editor = getattr(edclass, edclass.__name__.rpartition('.')[2])(self) self.Bind(wx.EVT_CLOSE, del_editor, editor) setattr(self, hid_name, editor) return editor def raise_editor(event, *args, **kwargs): editor = create_editor() editor.Show() editor.Raise() self.Bind(wx.EVT_MENU, raise_editor, menuitem) return menuitem bind_editor('filter_editor', FilterEditor, "Filter Editor\tCtrl-1", "Create and Edit CScience Filters for use in the Sample Browser") bind_editor('view_editor', ViewEditor, "View Editor\tCtrl-2", "Edit the list of views that can filter the display of samples in CScience") tool_menu.AppendSeparator() bind_editor('attribute_editor', AttEditor, "Attribute Editor\tCtrl-3", "Edit the list of attributes that can appear on samples in CScience") tool_menu.AppendSeparator() bind_editor('template_editor', TemplateEditor, "Template Editor\tCtrl-4", "Edit the list of templates for the CScience Paleobase") bind_editor('milieu_browser', MilieuBrowser, "Milieu Browser\tCtrl-5", "Browse and Import Paleobase Entries") tool_menu.AppendSeparator() bind_editor('cplan_browser', ComputationPlanBrowser, "Computation Plan Browser\tCtrl-6", "Browse Existing Computation Plans and Create New Computation Plans") help_menu = wx.Menu() item = help_menu.Append(wx.ID_ABOUT, "About CScience", "View Credits") self.Bind(wx.EVT_MENU, self.show_about, item) #Disallow save unless there's something to save :) file_menu.Enable(wx.ID_SAVE, False) #Disable copy when no rows are selected edit_menu.Enable(wx.ID_COPY, False) menu_bar.Append(file_menu, "&File") menu_bar.Append(edit_menu, "&Edit") menu_bar.Append(tool_menu, "&Tools") menu_bar.Append(help_menu, "&Help") self.SetMenuBar(menu_bar) def create_action_buttons(self): #TODO: These would make a lot more sense as menu & toolbar thingies. self.button_panel = wx.Panel(self, wx.ID_ANY) button_sizer = wx.BoxSizer(wx.HORIZONTAL) imp_button = wx.Button(self.button_panel, wx.ID_ANY, "Import Samples...") self.Bind(wx.EVT_BUTTON, self.import_samples, imp_button) button_sizer.Add(imp_button, border=5, flag=wx.ALL) calc_button = wx.Button(self.button_panel, wx.ID_APPLY, "Do Calculations...") self.Bind(wx.EVT_BUTTON, self.OnDating, calc_button) button_sizer.Add(calc_button, border=5, flag=wx.ALL) calv_button = wx.Button(self.button_panel, wx.ID_ANY, "Analyze Ages...") self.Bind(wx.EVT_BUTTON, self.OnRunCalvin, calv_button) button_sizer.Add(calv_button, border=5, flag=wx.ALL) self.del_button = wx.Button(self.button_panel, wx.ID_DELETE, "Delete Sample...") self.Bind(wx.EVT_BUTTON, self.OnDeleteSample, self.del_button) button_sizer.Add(self.del_button, border=5, flag=wx.ALL) self.del_button.Disable() self.strip_button = wx.Button(self.button_panel, wx.ID_ANY, "Strip Calculated Data...") self.Bind(wx.EVT_BUTTON, self.OnStripExperiment, self.strip_button) button_sizer.Add(self.strip_button, border=5, flag=wx.ALL) self.strip_button.Disable() exp_button = wx.Button(self.button_panel, wx.ID_ANY, "Export Samples...") self.Bind(wx.EVT_BUTTON, self.OnExportView, exp_button) button_sizer.Add(exp_button, border=5, flag=wx.ALL) self.button_panel.SetSizer(button_sizer) return self.button_panel def create_widgets(self): #NOTE: we can stick these in a panel if needed to prevent them showing #up while asking to open the repository. #TODO: investigate whether all these ComboBoxes should actually be Choices #TODO: save & load these values using PersistentControls self.selected_core = wx.ComboBox(self, wx.ID_ANY, choices=['No Core Selected'], style=wx.CB_READONLY) self.selected_view = wx.ComboBox(self, wx.ID_ANY, choices=['All'], style=wx.CB_READONLY) self.selected_filter = wx.ComboBox(self, wx.ID_ANY, choices=['<No Filter>'], style=wx.CB_READONLY) self.filter_desc = wx.StaticText(self, wx.ID_ANY, "No Filter Selected") self.Bind(wx.EVT_COMBOBOX, self.select_core, self.selected_core) self.Bind(wx.EVT_COMBOBOX, self.select_view, self.selected_view) self.Bind(wx.EVT_COMBOBOX, self.select_filter, self.selected_filter) self.sselect_prim = wx.ComboBox(self, wx.ID_ANY, choices=["Not Sorted"], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SORT) self.sselect_sec = wx.ComboBox(self, wx.ID_ANY, choices=["Not Sorted"], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SORT) self.sdir_select = wx.ComboBox(self, wx.ID_ANY, value=self.browser_view.get_direction(), choices=["Ascending", "Descending"], style=wx.CB_DROPDOWN | wx.CB_READONLY) self.plotbutton = wx.Button(self, wx.ID_ANY, "Plot Attributes...") self.Bind(wx.EVT_COMBOBOX, self.OnChangeSort, self.sselect_prim) self.Bind(wx.EVT_COMBOBOX, self.OnChangeSort, self.sselect_sec) self.Bind(wx.EVT_COMBOBOX, self.OnSortDirection, self.sdir_select) self.Bind(wx.EVT_BUTTON, self.do_plot, self.plotbutton) self.search_box = wx.TextCtrl(self, wx.ID_ANY, size=(300, -1)) self.exact_box = wx.CheckBox(self, wx.ID_ANY, "Use Exact Match") self.Bind(wx.EVT_TEXT, self.OnTextSearchUpdate, self.search_box) self.Bind(wx.EVT_CHECKBOX, self.OnTextSearchUpdate, self.exact_box) self.grid = grid.LabelSizedGrid(self, wx.ID_ANY) self.table = SampleGridTable(self.grid) self.grid.SetSelectionMode(wx.grid.Grid.SelectRows) self.grid.AutoSize() self.grid.EnableEditing(False) self.create_action_buttons() sizer = wx.BoxSizer(wx.VERTICAL) row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Viewing Core:"), border=5, flag=wx.ALL) row_sizer.Add(self.selected_core, border=5, flag=wx.ALL) row_sizer.AddStretchSpacer() row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "View:"), border=5, flag=wx.ALL) row_sizer.Add(self.selected_view, border=5, flag=wx.ALL) row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Filter:"), border=5, flag=wx.ALL) row_sizer.Add(self.selected_filter, border=5, flag=wx.ALL) row_sizer.Add(self.filter_desc, border=5, flag=wx.ALL) sizer.Add(row_sizer, flag=wx.EXPAND) row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Sort by"), border=5, flag=wx.ALL) row_sizer.Add(self.sselect_prim, border=5, flag=wx.ALL) row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "and then by"), border=5, flag=wx.ALL) row_sizer.Add(self.sselect_sec, border=5, flag=wx.ALL) row_sizer.Add(self.sdir_select, border=5, flag=wx.ALL) row_sizer.Add(self.plotbutton, border=5, flag=wx.ALL) sizer.Add(row_sizer, flag=wx.EXPAND) sizer.Add(self.grid, proportion=1, flag=wx.EXPAND) row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Search:"), border=5, flag=wx.ALL) row_sizer.Add(self.search_box, border=5, flag=wx.ALL) row_sizer.Add(self.exact_box, border=5, flag=wx.ALL) sizer.Add(row_sizer, flag=wx.EXPAND) sizer.Add(self.button_panel) self.SetSizer(sizer) def show_about(self, event): dlg = AboutBox(self) dlg.ShowModal() dlg.Destroy() def quit(self, event): self.close_repository() wx.Exit() def on_repository_altered(self, event): """ Used to cause the File->Save Repo menu option to be enabled only if there is new data to save. """ if 'views' in event.changed: view_name = self.browser_view.get_view() # get list of views self.selected_view.SetItems(datastore.views.keys()) # if current view has been deleted, then switch to "All" view if view_name not in datastore.views: self.selected_view.SetValue('All') else: self.selected_view.SetValue(view_name) if event.value and view_name == event.value: #update the currently viewed view, if that's the one that #was changed. self.select_view(None) elif 'filters' in event.changed: filter_name = self.browser_view.get_filter() # get list of filters self.selected_filter.SetItems(['<No Filter>'] + sorted(datastore.filters.keys())) # if current filter has been deleted, then switch to "None" filter if filter_name not in datastore.filters: self.selected_filter.SetValue('<No Filter>') else: self.selected_filter.SetValue(filter_name) if event.value and filter_name == event.value: #if we changed the currently selected filter, we should #re-filter the current view. self.select_filter(None) else: #TODO: select new core on import, & stuff. self.show_new_core() datastore.data_modified = True self.GetMenuBar().Enable(wx.ID_SAVE, True) event.Skip() def change_repository(self, event): self.close_repository() #Close all other editors, as the repository is changing... for window in self.Children: if window.IsTopLevel(): window.Close() self.open_repository() self.SetTitle(' '.join(('CScience:', datastore.data_source))) def open_repository(self, repo_dir=None): if not repo_dir: dialog = wx.DirDialog(None, "Choose a Repository", style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST | wx.DD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: repo_dir = dialog.GetPath() dialog.Destroy() else: #end the app, if the user doesn't want to open a repo dir self.Close() return elif not os.path.exists(repo_dir): raise datastore.RepositoryException('Previously saved repository no longer exists.') try: datastore.set_data_source(repo_dir) except Exception as e: import traceback print repr(e) print traceback.format_exc() raise datastore.RepositoryException('Error while loading selected repository.') else: self.selected_core.SetItems(sorted(datastore.cores.keys()) or ['No Cores -- Import Samples to Begin']) self.selected_core.SetSelection(0) self.selected_view.SetItems([v for v in datastore.views]) self.selected_view.SetStringSelection(self.browser_view.get_view()) self.selected_filter.SetItems(['<No Filter>'] + sorted(datastore.filters.keys())) self.selected_filter.SetStringSelection(self.browser_view.get_filter()) self.show_new_core() wx.CallAfter(self.Raise) def close_repository(self): if datastore.data_modified: if wx.MessageBox('You have modified this repository. ' 'Would you like to save your changes?', "Unsaved Changes", wx.YES_NO | wx.ICON_EXCLAMATION) == wx.YES: self.save_repository(None) #just in case, for now datastore.data_modified = False def save_repository(self, event): datastore.save_datastore() def OnCopy(self, event): samples = [self.displayed_samples[index] for index in self.grid.SelectedRowset] view = datastore.views[self.browser_view.get_view()] #views are guaranteed to give attributes as id, then computation_plan, then #remaining atts in order when iterated. result = os.linesep.join(['\t'.join([ datastore.sample_attributes.format_value(att, sample[att]) for att in view]) for sample in samples]) result = os.linesep.join(['\t'.join(view), result]) data = wx.TextDataObject() data.SetText(result) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(data) wx.TheClipboard.Close() def show_new_core(self): self.samples = [] if self.core is not None: for vc in self.core.virtualize(): self.samples.extend(vc) self.filter_samples() def filter_samples(self): self.displayed_samples = None filter_name = self.browser_view.get_filter() try: filt = datastore.filters[filter_name] except KeyError: self.browser_view.set_filter('<No Filter>') self.selected_filter.SetStringSelection('<No Filter>') self.filter_desc.SetLabel('No Filter Selected') filtered_samples = self.samples[:] else: self.filter_desc.SetLabel(filt.description) filtered_samples = filter(filt.apply, self.samples) self.search_samples(filtered_samples) def search_samples(self, filtered_samples=[]): value = self.search_box.GetValue() if value: if not self.exact_box.IsChecked() and self.displayed_samples and \ self.previous_query in value: samples_to_search = self.displayed_samples else: samples_to_search = filtered_samples self.previous_query = value view = datastore.views[self.browser_view.get_view()] self.displayed_samples = [s for s in samples_to_search if s.search(value, view, self.exact_box.IsChecked())] else: self.displayed_samples = filtered_samples self.previous_query = '' self.display_samples() def display_samples(self): def sort_none_last(x, y): def cp_none(x, y): if x is None and y is None: return 0 elif x is None: return 1 elif y is None: return -1 else: return cmp(x, y) for a, b in zip(x, y): val = cp_none(a, b) if val: return val return 0 self.displayed_samples.sort(cmp=sort_none_last, key=lambda s: (s[self.browser_view.get_primary()], s[self.browser_view.get_secondary()]), reverse=self.GetSortDirection()) self.table.view = datastore.views[self.browser_view.get_view()] self.table.samples = self.displayed_samples def OnTextSearchUpdate(self, event): self.search_samples() def OnExportView(self, event): view_name = self.browser_view.get_view() view = datastore.views[view_name] # add header labels -- need to use iterator to get computation_plan/id correct rows = [att for att in view] rows.extend([[sample[att] for att in view] for sample in self.displayed_samples]) wildcard = "CSV Files (*.csv)|*.csv|" \ "All files (*.*)|*.*" dlg = wx.FileDialog(self, message="Save view in ...", defaultDir=os.getcwd(), defaultFile="view.csv", wildcard=wildcard, style=wx.SAVE | wx.CHANGE_DIR | wx.OVERWRITE_PROMPT) dlg.SetFilterIndex(0) if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() tmp = open(path, "wb") writer = csv.writer(tmp) writer.writerows(rows) tmp.flush() tmp.close() the_dir = os.path.dirname(path) os.chdir(the_dir) dlg.Destroy() def do_plot(self, event): #TODO: let user select all those pretty plotting options! options = PlotOptions('depth') pw = PlotWindow(self, self.displayed_samples, options) pw.Show() pw.Raise() def import_samples(self, event): dialog = wx.FileDialog(None, "Please select a CSV File containing Samples to be Imported or Updated:", defaultDir=os.getcwd(), wildcard="CSV Files (*.csv)|*.csv|All Files|*.*", style=wx.OPEN | wx.DD_CHANGE_DIR) result = dialog.ShowModal() path = dialog.GetPath() #destroy the dialog now so no problems happen on early return dialog.Destroy() if result == wx.ID_OK: with open(path, 'rU') as input_file: #allow whatever sane csv formats we can manage, here sniffer = csv.Sniffer() dialect = sniffer.sniff(input_file.read(1024)) dialect.skipinitialspace = True input_file.seek(0) reader = csv.DictReader(input_file, dialect=dialect) if not reader.fieldnames: wx.MessageBox("Selected file is empty.", "Operation Cancelled", wx.OK | wx.ICON_INFORMATION) return #strip extra spaces, since that was apparently a problem before? reader.fieldnames = [name.strip() for name in reader.fieldnames] if 'depth' not in reader.fieldnames: wx.MessageBox("Selected file is missing the required attribute 'depth'.", "Operation Cancelled", wx.OK | wx.ICON_INFORMATION) return rows = [] for index, line in enumerate(reader, 1): #do appropriate type conversions... for key, value in line.iteritems(): try: line[key] = datastore.sample_attributes.convert_value(key, value) except ValueError: wx.MessageBox("%s on row %i has an incorrect type." "Please update the csv file and try again." % (key, index), "Operation Cancelled", wx.OK | wx.ICON_INFORMATION) return rows.append(line) if not rows: wx.MessageBox("Selected file appears to contain no data.", "Operation Cancelled", wx.OK | wx.ICON_INFORMATION) return dialog = DisplayImportedSamples(self, os.path.basename(path), reader.fieldnames, rows) if dialog.ShowModal() == wx.ID_OK: if dialog.source_name: for item in rows: item['source'] = dialog.source_name cname = dialog.core_name core = datastore.cores.get(cname, None) if core is None: core = Core(cname) datastore.cores[cname] = core for item in rows: s = Sample('input', item) core.add(s) wx.MessageBox('Core %s imported/updated' % cname, "Import Results", wx.OK | wx.CENTRE) events.post_change(self, 'samples') dialog.Destroy() def OnRunCalvin(self, event): """ Runs Calvin on all highlighted samples, or all samples if none are highlighted. """ if not self.grid.SelectedRowset: samples = self.displayed_samples else: indexes = list(self.grid.SelectedRowset) samples = [self.displayed_samples[index] for index in indexes] calvin.argue.analyzeSamples(samples) def select_core(self, event): try: self.core = datastore.cores[self.selected_core.GetStringSelection()] except KeyError: self.core = None self.show_new_core() def select_filter(self, event): self.browser_view.set_filter(self.selected_filter.GetStringSelection()) self.filter_samples() def select_view(self, event): view_name = self.selected_view.GetStringSelection() try: view = datastore.views[view_name] except KeyError: view_name = 'All' view = datastore.views['All'] self.selected_view.SetStringSelection('All') self.browser_view.set_view(view_name) self.sselect_prim.SetItems(view) self.sselect_sec.SetItems(view) previous_primary = self.browser_view.get_primary() previous_secondary = self.browser_view.get_secondary() if previous_primary in view: self.sselect_prim.SetStringSelection(previous_primary) else: self.sselect_prim.SetStringSelection("depth") self.browser_view.set_primary("depth") if previous_secondary in view: self.sselect_sec.SetStringSelection(previous_secondary) else: self.sselect_sec.SetStringSelection("computation plan") self.browser_view.set_secondary("computation plan") self.filter_samples() def GetSortDirection(self): # return true for descending, else return false # this corresponds to the expected value for the reverse parameter of the sort() method if self.browser_view.get_direction() == "Descending": return True return False def OnSortDirection(self, event): self.browser_view.set_direction(self.sdir_select.GetStringSelection()) self.display_samples() def OnChangeSort(self, event): self.browser_view.set_primary(self.sselect_prim.GetStringSelection()) self.browser_view.set_secondary(self.sselect_sec.GetStringSelection()) self.display_samples() def OnDating(self, event): dlg = ComputationDialog(self, self.core) ret = dlg.ShowModal() plan = dlg.plan # depths = dlg.depths dlg.Destroy() if ret != wx.ID_OK: return computation_plan = datastore.computation_plans[plan] workflow = datastore.workflows[computation_plan['workflow']] vcore = self.core.new_computation(plan) aborting = wx.lib.delayedresult.AbortEvent() self.button_panel.Disable() self.plotbutton.Disable() dialog = WorkflowProgress(self, "Applying Computation '%s'" % plan) wx.lib.delayedresult.startWorker(self.OnDatingDone, workflow.execute, wargs=(computation_plan, vcore, aborting), cargs=(plan, self.core, dialog)) if dialog.ShowModal() != wx.ID_OK: aborting.set() self.core.strip_experiment(plan) dialog.Destroy() def OnDatingDone(self, dresult, planname, core, dialog): try: result = dresult.get() except Exception as exc: core.strip_experiment(planname) print exc wx.MessageBox("There was an error running the requested computation." " Please contact support.") else: dialog.EndModal(wx.ID_OK) events.post_change(self, 'samples') finally: self.button_panel.Enable() self.plotbutton.Enable() def OnStripExperiment(self, event): indexes = list(self.grid.SelectedRowset) samples = [self.displayed_samples[index] for index in indexes] dialog = wx.MessageDialog(None, 'This operation will strip all performed computations from the selected samples. (Note: Input cannot be deleted.) Are you sure you want to do this?', "Are you sure?", wx.YES_NO | wx.ICON_EXCLAMATION) if dialog.ShowModal() == wx.ID_YES: for sample in samples: for exp in sample.keys(): if exp != 'input': del sample[exp] self.grid.ClearSelection() events.post_change(self, 'samples') def OnDeleteSample(self, event): indexes = self.grid.SelectedRowset samples = [self.displayed_samples[index] for index in indexes] ids = [sample['id'] for sample in samples] dialog = wx.MessageDialog(None, 'Are you sure that you want to delete the following samples: %s' % (ids), "Are you sure?", wx.YES_NO | wx.ICON_EXCLAMATION) if dialog.ShowModal() == wx.ID_YES: for s_id in ids: del self.core[depth] self.grid.ClearSelection() events.post_change(self, 'samples')