class SchemaEditor(Frame): """ An editor for a label schema """ def __init__(self, master, label_schema=None, **kwargs): """ Parameters ---------- master : tkinter.Tk|tkinter.TopLevel label_schema : None|str|LabelSchema kwargs keyword arguments for Frame """ self.variables = AppVariables() self.master = master Frame.__init__(self, master, **kwargs) self.frame1 = Frame(self, borderwidth=1, relief=tkinter.RIDGE) self.version_label = Label(self.frame1, text='Version Number:', relief=tkinter.RIDGE, justify=tkinter.LEFT, padding=5, width=18) self.version_label.grid(row=0, column=0, sticky='NEW') self.version_entry = Entry(self.frame1, text='') self.version_entry.grid(row=0, column=1, sticky='NEW') self.version_date_label = Label(self.frame1, text='Version Date:', relief=tkinter.RIDGE, justify=tkinter.LEFT, padding=5, width=18) self.version_date_label.grid(row=1, column=0, sticky='NEW') self.version_date_entry = Entry(self.frame1, text='') self.version_date_entry.grid(row=1, column=1, sticky='NEW') self.classification_label = Label(self.frame1, text='Classification:', relief=tkinter.RIDGE, justify=tkinter.LEFT, padding=5, width=18) self.classification_label.grid(row=2, column=0, sticky='NEW') self.classification_entry = Entry(self.frame1, text='') self.classification_entry.grid(row=2, column=1, sticky='NEW') self.confidence_label = Label(self.frame1, text='Confidence Values:', relief=tkinter.RIDGE, justify=tkinter.LEFT, padding=5, width=18) self.confidence_label.grid(row=3, column=0, sticky='NEW') self.confidence_entry = Entry(self.frame1, text='') self.confidence_entry.grid(row=3, column=1, sticky='NEW') self.geometries_label = Label(self.frame1, text='Geometries:', relief=tkinter.RIDGE, justify=tkinter.LEFT, padding=5, width=18) self.geometries_label.grid(row=4, column=0, sticky='NEW') self.geometries_entry = Entry(self.frame1, text='') self.geometries_entry.grid(row=4, column=1, sticky='NEW') self.frame1.grid_columnconfigure(1, weight=1) self.frame1.pack(side=tkinter.TOP, fill=tkinter.X) self.frame2 = Frame(self, borderwidth=1, relief=tkinter.RIDGE) self.new_button = Button(self.frame2, text='New Entry') self.new_button.pack(side=tkinter.LEFT, padx=5, pady=5) self.edit_button = Button(self.frame2, text='Edit Entry') self.edit_button.pack(side=tkinter.LEFT, padx=5, pady=5) self.delete_button = Button(self.frame2, text='Delete Entry') self.delete_button.pack(side=tkinter.LEFT, padx=5, pady=5) self.move_up_button = Button(self.frame2, text='Move Entry Up') self.move_up_button.pack(side=tkinter.LEFT, padx=5, pady=5) self.move_down_button = Button(self.frame2, text='Move Entry Down') self.move_down_button.pack(side=tkinter.LEFT, padx=5, pady=5) self.frame2.pack(side=tkinter.TOP, fill=tkinter.X) self.schema_viewer = SchemaViewer(self) self.schema_viewer.frame.pack(side=tkinter.BOTTOM, expand=tkinter.TRUE, fill=tkinter.BOTH) # NB: it's already packed inside a frame self.pack(expand=tkinter.YES, fill=tkinter.BOTH) # set up the menu bar menu = tkinter.Menu() filemenu = tkinter.Menu(menu, tearoff=0) filemenu.add_command(label="Open Schema", command=self.callback_open) filemenu.add_command(label="New Schema", command=self.callback_new_schema) filemenu.add_separator() filemenu.add_command(label="Save", command=self.save) filemenu.add_command(label="Save As", command=self.callback_save_as) filemenu.add_separator() filemenu.add_command(label="Exit", command=self.exit) menu.add_cascade(label="File", menu=filemenu) self.master.config(menu=menu) # setup entry configs and some validation callbacks self.schema_viewer.bind('<<TreeviewSelect>>', self.item_selected_on_viewer) self.version_entry.config(validate='focusout', validatecommand=self.register( self._version_entry_validate)) self.version_date_entry.config(state='disabled') self.classification_entry.config(validate='focusout', validatecommand=self.register( self._classification_validate)) self.confidence_entry.config(validate='focusout', validatecommand=self.register( self._confidence_validate)) self.geometries_entry.config( validate='focusout', validatecommand=(self.register(self._geometry_validate), ), invalidcommand=(self.register(self._geometry_invalid), )) self.edit_button.config(command=self.callback_edit_entry) self.new_button.config(command=self.callback_new_entry) self.delete_button.config(command=self.callback_delete_entry) self.move_up_button.config(command=self.callback_move_up) self.move_down_button.config(command=self.callback_move_down) # setup the entry panel self.entry_popup = tkinter.Toplevel(self.master) self.entry = LabelEntryPanel(self.entry_popup, self.variables) self.entry.hide_on_close() self.entry_popup.withdraw() self.entry.save_button.config(command=self.callback_save_entry) self.set_label_schema(label_schema) @property def label_schema(self): """ None|LabelSchema: The label schema """ return self.variables.label_schema # some entry validation methods def _version_entry_validate(self): the_value = self.version_entry.get().strip() if the_value != '' and self.label_schema.version != the_value: self.variables.unsaved_edits = True self.label_schema._version = the_value self.label_schema.update_version_date(value=None) self.version_date_entry.set_text(self.label_schema.version_date) return True def _classification_validate(self): the_value = self.classification_entry.get().strip() if self.label_schema.classification != the_value: self.variables.unsaved_edits = True self.label_schema._classification = the_value return True def _confidence_validate(self): the_value = self.confidence_entry.get().strip() if the_value == '': the_values = None else: if ',' in the_value: temp_values = [entry.strip() for entry in the_value.split(',')] else: temp_values = the_value.split() # noinspection PyBroadException try: the_values = [int(entry) for entry in temp_values] except Exception: the_values = temp_values # reformat the contents self.confidence_entry.delete(0, len(the_value)) self.confidence_entry.insert( 0, ', '.join('{}'.format(entry) for entry in the_values)) if self.label_schema.confidence_values != the_values: self.variables.unsaved_edits = True self.label_schema.confidence_values = the_values return True def _geometry_validate(self): the_value = self.geometries_entry.get().strip() if the_value == '': the_values = None else: if ',' in the_value: the_values = [ entry.strip().lower() for entry in the_value.split(',') ] else: the_values = [entry.lower() for entry in the_value.split()] if (self.label_schema.permitted_geometries == the_values) or \ (self.label_schema.permitted_geometries is None and the_values is None): return True try: self.label_schema.permitted_geometries = the_values self.variables.unsaved_edits = True # reformat the contents self.geometries_entry.delete(0, len(the_value)) self.geometries_entry.insert( 0, ', '.join(self.label_schema.permitted_geometries)) return True except ValueError: return False def _geometry_invalid(self): showinfo( 'geometry type guidance', message='The contents for geometry should be a space delimited\n' 'combination of {"point", "line", "polygon"}') self.geometries_entry.focus_set() temp = self.geometries_entry.get() self.geometries_entry.delete(0, len(temp)) if self.label_schema is None or self.label_schema.permitted_geometries is None: self.geometries_entry.insert(0, '') else: self.geometries_entry.insert( 0, ' '.join(self.label_schema.permitted_geometries)) # some helper methods def _set_focus_on_entry_popup(self): self.entry_popup.deiconify() self.entry_popup.focus_set() self.entry_popup.lift() self.entry_popup.grab_set() def _verify_selected(self): if self.variables.current_id is None: showinfo('No Element Selected', message='Choose element from Viewer') return False return True def _check_save_state(self): """ Checks the save state. Returns ------- bool Continue (True) or abort (False) """ if not self.variables.unsaved_edits: return True result = askyesnocancel( title="Unsaved Edits", message="There are unsaved edits. Save before opening a new file?") if result is None: return False if result is True: self.save() return True def _update_schema_display(self): if self.variables.label_schema is None: self.version_entry.config(state='disabled') self.version_entry.set_text('') self.version_date_entry.set_text('') self.classification_entry.config(state='disabled') self.classification_entry.set_text('') self.confidence_entry.config(state='disabled') self.confidence_entry.set_text('') self.geometries_entry.config(state='disabled') self.geometries_entry.set_text('') else: self.version_entry.config(validate='none') self.version_entry.set_text(self.label_schema.version) self.version_entry.config(validate='focusout', state='normal') self.version_date_entry.set_text(self.label_schema.version_date) self.classification_entry.config(validate='none') self.classification_entry.set_text( self.label_schema.classification) self.classification_entry.config(validate='focusout', state='normal') self.confidence_entry.config(validate='none') if self.label_schema.confidence_values is None: self.confidence_entry.set_text('') else: self.confidence_entry.set_text(' '.join( '{}'.format(entry) for entry in self.label_schema.confidence_values)) self.confidence_entry.config(validate='focusout', state='normal') self.geometries_entry.config(validate='none') if self.label_schema.permitted_geometries is None: self.geometries_entry.set_text('') else: self.geometries_entry.set_text(' '.join( self.label_schema.permitted_geometries)) self.geometries_entry.config(validate='focusout', state='normal') self.schema_viewer.fill_from_label_schema(self.variables.label_schema) self.entry.update_label_schema() def prompt_for_filename(self): schema_file = asksaveasfilename( initialdir=self.variables.browse_directory, filetypes=[file_filters.json_files, file_filters.all_files]) if schema_file == '' or schema_file == (): # closed or cancelled return False self.variables.browse_directory = os.path.split(schema_file)[0] self.variables.label_file_name = schema_file # todo: what if this file name exists? It should prompt already? return True def set_label_schema(self, label_schema): """ Sets the label schema value. Parameters ---------- label_schema : None|str|LabelSchema """ if label_schema is None: self.variables.label_file_name = None self.variables.label_schema = LabelSchema() elif isinstance(label_schema, str): the_file = label_schema label_schema = LabelSchema.from_file(the_file) self.variables.label_file_name = the_file self.variables.label_schema = label_schema browse_dir = os.path.split(os.path.abspath(the_file))[0] self.variables.browse_directory = browse_dir elif isinstance(label_schema, LabelSchema): self.variables.label_file_name = None self.variables.label_schema = label_schema else: raise TypeError( 'input must be the path for a label schema file or a LabelSchema instance' ) self.variables.unsaved_edits = False self.variables.current_id = None self._update_schema_display() def set_current_id(self, value): if value == '': value = None if (value is None and self.variables.current_id is None) or \ (value == self.variables.current_id): return self.variables.current_id = value self.entry.update_current_id() if value is not None: self.schema_viewer.set_selection_with_expansion(value) # callbacks and bound methods def save(self): """ Save any current progress. """ if self.variables.label_file_name is None: if not self.prompt_for_filename(): return # they opted to not pick a file self.label_schema.to_file(self.variables.label_file_name) self.variables.unsaved_edits = False def exit(self): """ Exit the application. Returns ------- None """ if self.variables.unsaved_edits: save_state = askyesno('Save Progress', message='There are unsaved edits. Save?') if save_state is True: self.save() self.master.destroy() def callback_open(self): if not self._check_save_state(): return schema_file = askopenfilename( initialdir=self.variables.browse_directory, filetypes=[file_filters.json_files, file_filters.all_files]) if schema_file == '' or schema_file == (): # closed or cancelled return self.set_label_schema(schema_file) def callback_new_schema(self): if not self._check_save_state(): return self.set_label_schema(LabelSchema()) def callback_save_as(self): if self.prompt_for_filename(): # they chose the new filename self.save() def callback_edit_entry(self): if not self._verify_selected(): return self._set_focus_on_entry_popup() def callback_new_entry(self): self.variables.current_id = None self.entry.update_current_id() self._set_focus_on_entry_popup() def callback_delete_entry(self): if not self._verify_selected(): return selected = self.variables.current_id # does the selected entry have any children? children = self.label_schema.subtypes.get(selected, None) if children is not None and len(children) > 0: response = askyesnocancel( 'Delete Children?', message='Selected entry has children. Delete all children?') if response is not True: return self.schema_viewer.delete_entry(selected) self.variables.unsaved_edits = True self.set_current_id(None) def callback_move_up(self): if not self._verify_selected(): return selected = self.variables.current_id result = self.label_schema.reorder_child_element(selected, spaces=-1) if result: self.schema_viewer.rerender_entry(selected) self.variables.unsaved_edits = True def callback_move_down(self): if not self._verify_selected(): return selected = self.variables.current_id result = self.label_schema.reorder_child_element(selected, spaces=1) if result: self.schema_viewer.rerender_entry(selected) self.variables.unsaved_edits = True def callback_save_entry(self): if self.entry.save_function(): self.schema_viewer.rerender_entry(self.entry.id_changed) self.set_current_id(self.entry.id_changed) self.entry.close_window() self.entry_popup.grab_release() # noinspection PyUnusedLocal def item_selected_on_viewer(self, event): item_id = self.schema_viewer.focus() if item_id == '': return self.set_current_id(item_id)
class PulseExplorer(Frame, WidgetWithMetadata): def __init__(self, primary, reader=None, **kwargs): """ Parameters ---------- primary : tkinter.Toplevel|tkinter.Tk reader : None|str|CRSDTypeReader|CRSDTypeCanvasImageReader kwargs Keyword arguments passed through to the Frame """ self.root = primary self.variables = AppVariables() Frame.__init__(self, primary, **kwargs) WidgetWithMetadata.__init__(self, primary) self.pyplot_panel = PyplotImagePanel( self, navigation=True) # type: PyplotImagePanel self.pyplot_panel.cmap_name = 'turbo' self.pyplot_panel.set_ylabel('Freq (GHz)') self.pyplot_panel.set_xlabel('Time (\u03BCsec)') self.pyplot_panel.set_title('Pulse Visualization') self.scanner_panel = Frame(self, padding=10) # type: Frame self.scanner_panel.columnconfigure(0, weight=0) self.scanner_panel.columnconfigure(1, weight=0) self.scanner_panel.columnconfigure(2, weight=1) self.scanner_panel.columnconfigure(3, weight=0) self.scanner_panel.columnconfigure(4, weight=0) self.dir_buttons = DirectionWidget( self.scanner_panel) # type: DirectionWidget self.slider = SliderWidget(self.scanner_panel) # type: SliderWidget self.dir_buttons.button_rev.grid(row=0, column=0, sticky='w') self.dir_buttons.button_prev.grid(row=0, column=1, sticky='w') self.slider.grid(row=0, column=2, sticky='ew') self.dir_buttons.button_next.grid(row=0, column=3, sticky='e') self.dir_buttons.button_fwd.grid(row=0, column=4, sticky='e') self.pyplot_panel.pack(side="top", expand=True, fill='both') self.scanner_panel.pack(side="bottom", expand=False, fill='x') self.rowconfigure(0, weight=1) self.rowconfigure(1, weight=0) self.pack(expand=True, fill='both') self.set_title() self._refresh_vdata() # define menus self.menu_bar = tkinter.Menu() # file menu self.file_menu = tkinter.Menu(self.menu_bar, tearoff=0) self.file_menu.add_command(label="Open Image", command=self.callback_select_files) self.file_menu.add_separator() self.file_menu.add_command(label="Exit", command=self.exit) # menus for informational popups self.metadata_menu = tkinter.Menu(self.menu_bar, tearoff=0) self.metadata_menu.add_command(label="Metaicon", command=self.metaicon_popup) self.metadata_menu.add_command(label="Metaviewer", command=self.metaviewer_popup) # ensure menus cascade self.menu_bar.add_cascade(label="File", menu=self.file_menu) self.menu_bar.add_cascade(label="Metadata", menu=self.metadata_menu) self.root.config(menu=self.menu_bar) self.update_reader(reader) # self.image_panel.canvas.bind('<<RemapChanged>>', self.handle_remap_change) self.slider.cbx_channel.bind('<<ComboboxSelected>>', self.handle_image_index_changed) self.slider.scale_pulse.bind('<ButtonRelease>', self.handle_pulse_changed_slider) self.slider.entry_pulse.bind('<FocusOut>', self.handle_pulse_changed_entry) self.slider.entry_pulse.on_enter_or_return_key( self.handle_pulse_changed_entry) self.dir_buttons.button_rev.config( command=self.pulse_animate_backwards) self.dir_buttons.button_prev.config(command=self.pulse_step_prev) self.dir_buttons.button_next.config(command=self.pulse_step_next) self.dir_buttons.button_fwd.config(command=self.pulse_animate_forward) def set_title(self): """ Sets the window title. """ file_name = None if self.variables.image_reader is None \ else self.variables.image_reader.file_name if file_name is None: the_title = "Pulse Explorer" else: the_title = "Pulse Explorer for {}".format( os.path.split(file_name)[1]) self.winfo_toplevel().title(the_title) def set_pulse(self, value, override=False): """ Sets the pulse count. Parameters ---------- value : int override : bool """ if self.variables.image_reader is None: return value = int(value) if value == self.variables.image_reader.pulse and not override: return if not (0 <= value < self.variables.image_reader.pulse_count): showinfo('Invalid pulse', message='Pulse must be in the range [0, {})'.format( self.variables.image_reader.pulse_count)) return self.variables.image_reader.pulse = value if self.variables.vcount == 0: self.variables.vmin = numpy.min( self.variables.image_reader.pulse_data[:, :]) self.variables.vmax = numpy.max( self.variables.image_reader.pulse_data[:, :]) elif self.variables.vcount < 5: self.variables.vmin = min( self.variables.vmin, numpy.min(self.variables.image_reader.pulse_data[:, :])) self.variables.vmax = max( self.variables.vmax, numpy.max(self.variables.image_reader.pulse_data[:, :])) if self.variables.vmin != self.variables.vmax: self.variables.vcount += 1 self.slider.entry_pulse.set_text(str(value)) self.slider.var_pulse_number.set(value) self.display_in_pyplot_frame() def exit(self): self.root.destroy() def _refresh_vdata(self): self.variables.vmin = 0 self.variables.vmax = 0 self.variables.vcount = 0 # noinspection PyUnusedLocal def handle_remap_change(self, event): """ Handle that the remap for the image canvas has changed. Parameters ---------- event """ pass # noinspection PyUnusedLocal def handle_image_index_changed(self, event): """ Handle that the image index has changed. Parameters ---------- event """ if self.variables.image_reader is None: return self._refresh_vdata() self.variables.animating = False self.variables.image_reader.index = self.slider.cbx_channel.current() self.my_populate_metaicon() self.slider.cbx_channel.selection_clear() # Update number of pulses pulse_count = self.variables.image_reader.pulse_count self.slider.fullscale.configure(text=str(pulse_count - 1)) self.slider.scale_pulse.configure(to=pulse_count - 1) self.set_pulse(0, override=True) def handle_pulse_changed_entry(self, event): if self.variables.image_reader is None: return if self.variables.animating: self.variables.animating = False self.set_pulse(int(self.slider.entry_pulse.get())) def handle_pulse_changed_slider(self, event): if self.variables.image_reader is None: return if self.variables.animating: self.variables.animating = False self.set_pulse(int(float(self.slider.var_pulse_number.get()))) def pulse_step(self, direction): if self.variables.image_reader is None: return current_pulse = int(float(self.slider.var_pulse_number.get())) new_pulse = current_pulse + direction # todo: should we roll over by default? if new_pulse < 0: new_pulse += self.variables.image_reader.pulse_count elif new_pulse >= self.variables.image_reader.pulse_count: new_pulse -= self.variables.image_reader.pulse_count self.set_pulse(new_pulse) def pulse_step_prev(self): if self.variables.animating: self.variables.animating = False self.pulse_step(-1) def pulse_animate_backwards(self): if self.variables.animating: self.variables.animating = False return self.variables.animating = True self._pulse_animate(-1) def pulse_step_next(self): if self.variables.animating: self.variables.animating = False self.pulse_step(+1) def pulse_animate_forward(self): if self.variables.animating: self.variables.animating = False return self.variables.animating = True self._pulse_animate(1) def _pulse_animate(self, direction): if not self.variables.animating: return self.pulse_step(direction) self.after(self.variables.animation_delay, self._pulse_animate, direction) def update_reader(self, the_reader, update_browse=None): """ Update the reader. Parameters ---------- the_reader : None|str|CRSDTypeReader|STFTCanvasImageReader update_browse : None|str """ if the_reader is None: return if update_browse is not None: self.variables.browse_directory = update_browse elif isinstance(the_reader, str): self.variables.browse_directory = os.path.split(the_reader)[0] if isinstance(the_reader, str): the_reader = STFTCanvasImageReader(the_reader) if isinstance(the_reader, CRSDTypeReader): the_reader = STFTCanvasImageReader(the_reader) if not isinstance(the_reader, STFTCanvasImageReader): raise TypeError('Got unexpected input for the reader') self._refresh_vdata() self.variables.animating = False self.variables.image_reader = the_reader self.display_in_pyplot_frame() self.set_title() self.my_populate_metaicon() self.my_populate_metaviewer() identifiers = [ entry.Identifier for entry in the_reader.base_reader.crsd_meta.Channel.Parameters ] self.update_combobox(identifiers) # which one is set here... self.slider.fullscale['text'] = str( self.variables.image_reader.pulse_count - 1) self.slider.scale_pulse[ 'to'] = self.variables.image_reader.pulse_count - 1 self.slider.var_pulse_number.set(0) def callback_select_files(self): fname = askopenfilename(initialdir=self.variables.browse_directory, filetypes=[crsd_files, all_files]) if fname is None or fname in ['', ()]: return the_reader = STFTCanvasImageReader(fname) self.update_reader(the_reader, update_browse=os.path.split(fname)[0]) def display_in_pyplot_frame(self): times = 1e6 * self.variables.image_reader.times frequencies = 1e-9 * self.variables.image_reader.frequencies image_data = self.variables.image_reader.pulse_data[:, :] image_data = image_data[::-1, :] if self.variables.vcount > 0: self.pyplot_panel.update_image( image_data, aspect='auto', vmin=self.variables.vmin, vmax=self.variables.vmax, extent=[times[0], times[-1], frequencies[0], frequencies[-1]]) else: self.pyplot_panel.update_image( image_data, aspect='auto', extent=[times[0], times[-1], frequencies[0], frequencies[-1]]) def update_combobox(self, identifiers): # Update channels combobox. # Get channel identifiers from metadata. self.slider.cbx_channel['values'] = identifiers self.slider.cbx_channel.set(identifiers[0]) self.slider.cbx_channel.configure(state='readonly') def my_populate_metaicon(self): """ Populate the metaicon. """ self.populate_metaicon(self.variables.image_reader) def my_populate_metaviewer(self): """ Populate the metaviewer. """ self.populate_metaviewer(self.variables.image_reader)