From 68b09fd3e9d58965b5904cf931d1e81f6cead420 Mon Sep 17 00:00:00 2001 From: Clark Teeple Date: Mon, 21 Feb 2022 17:52:30 -0500 Subject: [PATCH] moved gui modules to the src directory --- armstron/scripts/gui.py | 654 +------------------ armstron/src/armstron/gui/__init__.py | 0 armstron/src/armstron/gui/profile_editor.py | 417 ++++++++++++ armstron/src/armstron/gui/profile_handler.py | 229 +++++++ armstron/src/armstron/gui/utils.py | 44 ++ 5 files changed, 697 insertions(+), 647 deletions(-) create mode 100644 armstron/src/armstron/gui/__init__.py create mode 100644 armstron/src/armstron/gui/profile_editor.py create mode 100644 armstron/src/armstron/gui/profile_handler.py create mode 100644 armstron/src/armstron/gui/utils.py diff --git a/armstron/scripts/gui.py b/armstron/scripts/gui.py index fa42b58..6ec7145 100755 --- a/armstron/scripts/gui.py +++ b/armstron/scripts/gui.py @@ -5,7 +5,6 @@ import copy import time import threading -from itertools import cycle from turtle import width if sys.version_info[0] == 3: import tkinter as tk @@ -28,8 +27,12 @@ import armstron.msg as msg from armstron.srv import Balance, Estop from armstron.test_interface import TestRunner + from armstron.gui.profile_editor import ProfileEditor + from armstron.gui.profile_handler import ProfileHandler + from armstron.gui.utils import Spinbox, OptionSwitcher filepath_config = os.path.join(rospkg.RosPack().get_path('armstron'), 'config') except: + raise # Don't load ros stuff (for development on a machine without ROS) filepath_config = '../config' sys.path.append("../src/armstron") @@ -37,37 +40,6 @@ -def _from_rgb(rgb): - """translates an rgb tuple of int to a tkinter friendly color code - """ - rgb_int = [0]*3 - for i, color in enumerate(rgb): - rgb_int[i] = int(color*255) - return "#%02x%02x%02x" % tuple(rgb_int) - - - -class Spinbox(ttk.Entry): - - def __init__(self, master=None, **kw): - - ttk.Entry.__init__(self, master, "ttk::spinbox", **kw) - def set(self, value): - self.tk.call(self._w, "set", value) - - -class OptionSwitcher(ttk.OptionMenu): - def __init__(self, container, variable, default=None, *values, **kwargs): - fill_option = "< Choose >" - values_cp = copy.deepcopy(*values) - values_cp.insert(0, fill_option) - if default is None: - default = values_cp[0] - - ttk.OptionMenu.__init__(self, container,variable, default, *values_cp, **kwargs) - - - class ArmstronControlGui: def __init__(self): @@ -92,7 +64,9 @@ def load_settings(self, filename="default.yaml"): self.color_scheme = self.settings['color_scheme'] self.curr_save_file = {'basename':'test.csv', 'dirname': os.path.expanduser(self.settings['data_save_loc'])} - os.makedirs(self.curr_save_file['dirname']) + + if not os.path.exists(self.curr_save_file['dirname']): + os.makedirs(self.curr_save_file['dirname']) def update_save_file(self): @@ -280,620 +254,6 @@ def shutdown(self): -class ProfileHandler: - def __init__(self, parent, file_types, curr_config_file, incldue_btns=['open', 'save','saveas'], name="", side='left'): - file_types_tup=[] - for ft in file_types: - file_types_tup.append(tuple(ft)) - self.file_types = file_types_tup - self.curr_config_file = curr_config_file - self.config=None - self._init_buttons(parent, incldue_btns, name, side=side) - self.callbacks={'open_before':self._empty, 'open_after':self._empty, - 'save_before':self._empty, 'save_after':self._empty, - 'saveas_before':self._empty, 'saveas_after':self._empty, - 'folder_before':self._empty, 'folder_after':self._empty} - - def _empty(self): - pass - - - def set_callback(self, btn_name, cb): - """ - Set a button callback by name - - Parameters - ---------- - btn_name : str - The button name to attach the callback to - cb : function - The callback function to attach - """ - self.callbacks[btn_name]=cb - - - def _init_buttons(self, parent, incldue_btns, name, side='left'): - default_btns = ['open', 'save','saveas'] - self.buttons={} - for btn in default_btns: - self.buttons[btn] = None - - self.fr_buttons = tk.LabelFrame(parent, bd=2, text=name) - - if 'open' in incldue_btns: - open_btn = ttk.Button(self.fr_buttons, - text="Open", - command = self.open_file - ) - open_btn.grid(row=1, column=0, sticky='ns', padx=5, pady=5) - self.buttons["open"] = open_btn - - if 'folder' in incldue_btns: - folder_btn = ttk.Button(self.fr_buttons, - text="Open Folder", - command = self.open_folder - ) - folder_btn.grid(row=1, column=0, sticky='ns', padx=5, pady=5) - self.buttons["folder"] = folder_btn - - if 'save' in incldue_btns: - save_btn = ttk.Button(self.fr_buttons, - text="Save", - command = self.save_file, - state = 'disabled', - ) - save_btn.grid(row=1, column=1, sticky='ns', padx=5, pady=5) - self.buttons["save"] = save_btn - - if 'saveas' in incldue_btns: - save_as_btn = ttk.Button(self.fr_buttons, - text="Save As", - command = self.save_file_as, - state = 'disabled', - ) - save_as_btn.grid(row=1, column=2, sticky='ns', padx=5, pady=5) - self.buttons["saveas"] = save_as_btn - - - self.fr_buttons.pack(expand=False, fill="y", side=side) - - def _check_enable_buttons(self): - if self.config is not None: - if self.buttons['save'] is not None: - self.buttons['save'].configure(state='normal') - if self.buttons['saveas'] is not None: - self.buttons['saveas'].configure(state='normal') - else: - if self.buttons['save'] is not None: - self.buttons['save'].configure(state='disabled') - if self.buttons['saveas'] is not None: - self.buttons['saveas'].configure(state='disabled') - - - def open_file(self): - - self.callbacks['open_before']() - - filepath = fdialog.askopenfilename( - filetypes=self.file_types, - initialdir=self.curr_config_file['dirname'], - initialfile=self.curr_config_file['basename'] - ) - if not filepath: - return None - self.curr_config_file['basename'] = os.path.basename(filepath) - self.curr_config_file['dirname'] = os.path.dirname(filepath) - self.load_file() - self._check_enable_buttons() - - self.callbacks['open_after']() - - return self.curr_config_file - - - def save_file_as(self): - - self.callbacks['saveas_before']() - - filepath = fdialog.asksaveasfilename( - defaultextension="txt", - filetypes=self.file_types, - initialdir=self.curr_config_file['dirname'], - initialfile=self.curr_config_file['basename'] - ) - - if not filepath: - return None - - self.curr_config_file['basename'] = os.path.basename(filepath) - self.curr_config_file['dirname'] = os.path.dirname(filepath) - - self.save_file() - - self.callbacks['saveas_after']() - - return self.curr_config_file - - - def save_file(self): - self.callbacks['save_before']() - - if self.config is not None: - """Save the current file as a new file.""" - filepath = os.path.join( - self.curr_config_file['dirname'], - self.curr_config_file['basename'] - ) - - if not os.path.exists(self.curr_config_file['dirname']): - os.makedirs(self.curr_config_file['dirname']) - - with open(filepath, "w") as output_file: - utils.save_yaml(self.config, filepath) - - self.callbacks['save_after']() - - - def open_folder(self): - self.callbacks['folder_before']() - - from sys import platform - if platform == "linux" or platform == "linux2": - cmd='xdg-open' - elif platform == "darwin": - cmd='open' - - elif 'win' in platform: - cmd='start' - - os.system(r'%s %s'%(cmd,self.curr_config_file['dirname'])) - self.callbacks['folder_after']() - - - def load_file(self): - """Open a file for editing.""" - if self.curr_config_file['dirname'] is None or self.curr_config_file['basename'] is None: - return - - filepath = os.path.join( - self.curr_config_file['dirname'], - self.curr_config_file['basename'] - ) - - new_config = False - if filepath.endswith(".yaml"): - new_config = self.load_config(filepath) - if new_config: - self.config = new_config - - - def get_config(self): - return self.config - - - def set_config(self, config): - self.config = config - - - def load_config(self, filename): - try: - config = utils.load_yaml(filename) - if isinstance(config, dict): - return config - - else: - print('Incorrect config format') - return False - except: - print('New config was not loaded') - return False - - def __del__(self): - self.fr_buttons.destroy() - - - -class ProfileEditor: - def __init__(self, - parent, - profile, - default_values, - root, - colors=None): - self.variable_tree, self.profile = self._generate_variable_tree(profile) - self.stop_values = default_values['stop_conditions'] - self.balance_values = default_values['balance_options'] - - self.parent = parent - self.root = root - - if colors is None: - self.colors = {} - for key in self.stop_values: - self.colors[key] = ['#ffffff', '#ffffff'] - else: - self.colors = colors - - self._init_inputs(parent, self.profile, self.variable_tree) - - def _empty(self): - pass - - - def _split_condition_str(self, string): - """ - Split a stop condition string key into a dictionary - - Parameters - ---------- - string : str - String key for the condition - - Returns - ------- - out_dict : dict - Condition disctionary with ``condition`` and ``signal`` values. - """ - out = {'condition':None, 'signal': None} - - string_list = string.split('_',1) - if len(string_list)==2: - out['condition'] = string_list[0] - out['signal'] = string_list[1] - else: - out['signal'] = string_list[0] - - return out - - - def _combine_condition(self,condition): - """ - Combine a stop condition dictionary to recover the string key - - Parameters - ---------- - condition : dict - Condition to combine - - Returns - ------- - out_str : str - String key for the condition - """ - out_str="" - if condition['condition'] is not None: - out_str+=condition['condition'] - out_str+="_" - if condition['signal'] is not None: - out_str+=condition['signal'] - return out_str - - - def _generate_variable_tree(self, profile): - """ - Generate a tree of variables for a given profile, first - expanding the stop conditions to enable editing of conditions. - - Parameters - ---------- - profile : dict - A test profile to convert - - Returns - ------- - var_tree : dict - A tree of Tk variables with the same structure as the profile - profile_expanded : dict - A copy of the test profile with the stop conditions expanded. - """ - profile_expanded = copy.deepcopy(profile) - - params = profile_expanded['params'] - - # Make each group into a list of steps - for key in params: - curr_group = params[key] - if isinstance(curr_group, dict): - params[key] = [curr_group] - - # Expand stop conditions - for key in params: - curr_group = params[key] - for curr_step in curr_group: - stop_conditions = curr_step.get('stop_conditions',None) - if stop_conditions is not None: - condition_list = [] - for key in stop_conditions: - cond = self._split_condition_str(key) - condition_list.append({'condition':cond['condition'],'signal':cond['signal'], 'value':stop_conditions[key]}) - - curr_step['stop_conditions'] = condition_list - - # Generate Tk variables - var_tree = self._generate_tk_variables(profile_expanded) - return var_tree, profile_expanded - - - def _generate_tk_variables(self,input_obj): - """ - Generate a tree of Tk variables that matches the input structure - - Parameters - ---------- - input_obj : Any() - Input object to convert - - Returns - ------- - output_obj: Any() - A tree of Tk variables with the same structure as the input - """ - var = None - if isinstance(input_obj, str): - var = tk.StringVar() - elif isinstance(input_obj, int): - var = tk.DoubleVar() - elif isinstance(input_obj, float): - var = tk.DoubleVar() - elif isinstance(input_obj, bool): - var = tk.BooleanVar() - - if var is not None: - var.set(input_obj) - #cb = lambda name, index, val: self.update_inputs() - #var.trace('w', cb) - return var - - if isinstance(input_obj, list): - var_list = [] - for var in input_obj: - var_list.append(self._generate_tk_variables(var)) - return var_list - - elif isinstance(input_obj, dict): - var_tree={} - - for key in input_obj: - curr_value = input_obj[key] - var_tree[key] = self._generate_tk_variables(curr_value) - - return var_tree - else: - return None - - - def _get_tk_values(self, input_obj): - """ - Get values of tk variables recursively - - Parameters - ---------- - input_obj : Any() - Input object to convert - - Returns - ------- - output_obj: Any() - A tree of numbers with the same structure as the input - """ - if isinstance(input_obj, tk.StringVar) or \ - isinstance(input_obj, tk.DoubleVar) or \ - isinstance(input_obj, tk.BooleanVar): - return input_obj.get() - - elif isinstance(input_obj, list): - var_list = [] - for var in input_obj: - var_list.append(self._get_tk_values(var)) - return var_list - - elif isinstance(input_obj, dict): - var_tree={} - - for key in input_obj: - curr_value = input_obj[key] - var_tree[key] = self._get_tk_values(curr_value) - - return var_tree - else: - return None - - - def get_values(self): - """ - Get the values set by the gui - - Returns - ------- - profile_vals : dict - A dictionary of new profile values set by the gui - """ - profile = self._get_tk_values(self.variable_tree) - - params = profile['params'] - - # Condense stop conditions back into a list - for key in params: - curr_group = params[key] - for curr_step in curr_group: - stop_conditions = curr_step.get('stop_conditions',None) - if stop_conditions is not None: - condition_dict = {} - for values in stop_conditions: - cond_str = self._combine_condition(values) - condition_dict[cond_str] = values['value'] - - curr_step['stop_conditions'] = condition_dict - - return copy.deepcopy(profile) - - - def _make_input_group(self, parent, config, vars, index): - - fr_group = tk.LabelFrame(parent, text="Step %d"%(index), - font=('Arial', 10, 'bold'), bd=2, - fg = self.colors['default'][0]) - - balance = config.get('balance', False) - if balance: - label = tk.Label(fr_group, text="Balance: ", bg=self.colors['default'][1]) - label.grid(row=0,column=0, sticky="ew") - - cond = OptionSwitcher(fr_group, vars['balance'], balance, self.balance_values) - cond.grid(row=0, column=1, sticky='ew') - return fr_group - - - motion = config['motion'] - fr_motion = tk.Frame(fr_group, bd=2) - - - label = tk.Label(fr_motion, text="Linear: ") - label.grid(row=0,column=0, sticky="ew") - idx=1 - for curr, var in zip(motion['linear'], vars['motion']['linear']): - box = Spinbox(fr_motion, width=7, textvariable=var) - box.set(curr) - box.grid(row=0, column=idx, sticky='ew') - idx+=1 - - label = tk.Label(fr_motion,text="Angular: ") - label.grid(row=1,column=0, sticky="ew") - idx=1 - for curr, var in zip(motion['angular'], vars['motion']['angular']): - box = Spinbox(fr_motion, width=7, textvariable=var) - box.set(curr) - box.grid(row=1, column=idx,sticky='ew') - idx+=1 - - fr_motion.pack(expand=True, fill="x") - - stop_conditions = config['stop_conditions'] - fr_stop = tk.Frame(fr_group, bd=2) - - for condition, var in zip(stop_conditions,vars['stop_conditions']): - fr_stop_inner = tk.Frame(fr_stop, bg=self.colors[condition['signal']][1], bd=2) - - cond = OptionSwitcher(fr_stop_inner, var['condition'], - condition['condition'], - self.stop_values[condition['signal']]) - - signal = OptionSwitcher(fr_stop_inner, - var['signal'], - condition['signal'], - sorted(self.stop_values.keys())) - - box = Spinbox(fr_stop_inner, textvariable=var['value']) - box.set(condition['value']) - cond.pack(expand=False, fill="y", side='left') - signal.pack(expand=True, fill="y", side='left') - box.pack(expand=False, fill="y", side='left') - fr_stop_inner.pack(expand=False,fill='y') - - fr_stop.pack(expand=True, fill="x") - - return fr_group - - - def _move_step(self, key, idx, dir): - steps = self.profile['params'][key] - if dir =='up': - if idx == 0: - return - else: - idx_mv = idx-1 - else: #down - if idx == len(steps)-1: - return - else: - idx_mv = idx+1 - - v = self.variable_tree['params'][key] - v.insert(idx_mv, v.pop(idx)) - p = self.profile['params'][key] - p.insert(idx_mv, p.pop(idx)) - - # Regenerate - self.update_inputs() - - - - def _make_controls(self, parent, key, idx): - fr_group = tk.Frame(parent, bd=2) - - up_btn = tk.Button(fr_group, - text=u'\u25B2', - command = lambda key=key, idx=idx : self._move_step(key, idx, 'up'), - state = 'normal',) - dn_btn = tk.Button(fr_group, - text=u'\u25BC', - command = lambda key=key, idx=idx : self._move_step(key, idx, 'down'), - state = 'normal',) - - up_btn.pack(expand=False, fill="x", padx=2, pady=2, side='top') - dn_btn.pack(expand=False, fill="x", padx=2, pady=2, side='top') - - return fr_group - - - def clear(self): - profile = self.get_values() - self._del_inputs() - self.variable_tree, self.profile = self._generate_variable_tree(profile) - - - def _del_inputs(self): - try: - self.fr_buttons.destroy() - except: - pass - - - def update_inputs(self): - self.clear() - self._init_inputs(self.parent, self.profile, self.variable_tree) - - - def _init_inputs(self, parent, profile, var_tree): - self.fr_buttons = tk.Frame(parent, bd=2) - - preload = profile['params'].get('preload') - test = profile['params'].get('test') - preload_vars = var_tree['params'].get('preload') - test_vars = var_tree['params'].get('test') - - fr_preload = tk.LabelFrame(self.fr_buttons, text="Preload", font=('Arial', 12, 'bold'), bd=2) - for idx,seg in enumerate(preload): - fr_step=tk.Frame(fr_preload) - fr_step.pack(expand=False, fill='both', side='top') - - fr_ctrl = self._make_controls(fr_step,'preload',idx) - fr = self._make_input_group(fr_step,seg,preload_vars[idx], idx) - fr_ctrl.pack(expand=False, fill="both", padx=5, pady=5, side='left') - fr.pack(expand=False, fill="both", padx=5, pady=5, side='left') - - fr_preload.pack(expand=False, fill="both", padx=5, pady=5, side='left') - - fr_test = tk.LabelFrame(self.fr_buttons ,text="Main Test", font=('Arial', 12, 'bold'), bd=2) - for idx,seg in enumerate(test): - fr_step=tk.Frame(fr_test) - fr_step.pack(expand=False, fill='both', side='top') - - fr_ctrl = self._make_controls(fr_step, 'test',idx) - fr = self._make_input_group(fr_step,seg, test_vars[idx], idx) - fr_ctrl.pack(expand=False, fill="both", padx=5, pady=5, side='left') - fr.pack(expand=False, fill="both", padx=5, pady=5, side='left') - - fr_test.pack(expand=False, fill="both", padx=5, pady=5, side='left') - - self.fr_buttons.pack(expand=False, fill="x", side='top') - - - def __del__(self): - self.fr_buttons.destroy() - - - if __name__ == "__main__": rospy.init_node('v_inst_test_gui', disable_signals=True) diff --git a/armstron/src/armstron/gui/__init__.py b/armstron/src/armstron/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/armstron/src/armstron/gui/profile_editor.py b/armstron/src/armstron/gui/profile_editor.py new file mode 100644 index 0000000..98c90bd --- /dev/null +++ b/armstron/src/armstron/gui/profile_editor.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python +# tkinter Stuff +import os +import sys +import copy +if sys.version_info[0] == 3: + import tkinter as tk + import tkinter.ttk as ttk + import tkinter.filedialog as fdialog +else: + import Tkinter as tk + import ttk + import tkFileDialog as fdialog + +from ttkthemes import ThemedTk + +from armstron.gui.utils import Spinbox, OptionSwitcher + + +class ProfileEditor: + def __init__(self, + parent, + profile, + default_values, + root, + colors=None): + self.variable_tree, self.profile = self._generate_variable_tree(profile) + self.stop_values = default_values['stop_conditions'] + self.balance_values = default_values['balance_options'] + + self.parent = parent + self.root = root + + if colors is None: + self.colors = {} + for key in self.stop_values: + self.colors[key] = ['#ffffff', '#ffffff'] + else: + self.colors = colors + + self._init_inputs(parent, self.profile, self.variable_tree) + + def _empty(self): + pass + + + def _split_condition_str(self, string): + """ + Split a stop condition string key into a dictionary + + Parameters + ---------- + string : str + String key for the condition + + Returns + ------- + out_dict : dict + Condition disctionary with ``condition`` and ``signal`` values. + """ + out = {'condition':None, 'signal': None} + + string_list = string.split('_',1) + if len(string_list)==2: + out['condition'] = string_list[0] + out['signal'] = string_list[1] + else: + out['signal'] = string_list[0] + + return out + + + def _combine_condition(self,condition): + """ + Combine a stop condition dictionary to recover the string key + + Parameters + ---------- + condition : dict + Condition to combine + + Returns + ------- + out_str : str + String key for the condition + """ + out_str="" + if condition['condition'] is not None: + out_str+=condition['condition'] + out_str+="_" + if condition['signal'] is not None: + out_str+=condition['signal'] + return out_str + + + def _generate_variable_tree(self, profile): + """ + Generate a tree of variables for a given profile, first + expanding the stop conditions to enable editing of conditions. + + Parameters + ---------- + profile : dict + A test profile to convert + + Returns + ------- + var_tree : dict + A tree of Tk variables with the same structure as the profile + profile_expanded : dict + A copy of the test profile with the stop conditions expanded. + """ + profile_expanded = copy.deepcopy(profile) + + params = profile_expanded['params'] + + # Make each group into a list of steps + for key in params: + curr_group = params[key] + if isinstance(curr_group, dict): + params[key] = [curr_group] + + # Expand stop conditions + for key in params: + curr_group = params[key] + for curr_step in curr_group: + stop_conditions = curr_step.get('stop_conditions',None) + if stop_conditions is not None: + condition_list = [] + for key in stop_conditions: + cond = self._split_condition_str(key) + condition_list.append({'condition':cond['condition'],'signal':cond['signal'], 'value':stop_conditions[key]}) + + curr_step['stop_conditions'] = condition_list + + # Generate Tk variables + var_tree = self._generate_tk_variables(profile_expanded) + return var_tree, profile_expanded + + + def _generate_tk_variables(self,input_obj): + """ + Generate a tree of Tk variables that matches the input structure + + Parameters + ---------- + input_obj : Any() + Input object to convert + + Returns + ------- + output_obj: Any() + A tree of Tk variables with the same structure as the input + """ + var = None + if isinstance(input_obj, str): + var = tk.StringVar() + elif isinstance(input_obj, int): + var = tk.DoubleVar() + elif isinstance(input_obj, float): + var = tk.DoubleVar() + elif isinstance(input_obj, bool): + var = tk.BooleanVar() + + if var is not None: + var.set(input_obj) + #cb = lambda name, index, val: self.update_inputs() + #var.trace('w', cb) + return var + + if isinstance(input_obj, list): + var_list = [] + for var in input_obj: + var_list.append(self._generate_tk_variables(var)) + return var_list + + elif isinstance(input_obj, dict): + var_tree={} + + for key in input_obj: + curr_value = input_obj[key] + var_tree[key] = self._generate_tk_variables(curr_value) + + return var_tree + else: + return None + + + def _get_tk_values(self, input_obj): + """ + Get values of tk variables recursively + + Parameters + ---------- + input_obj : Any() + Input object to convert + + Returns + ------- + output_obj: Any() + A tree of numbers with the same structure as the input + """ + if isinstance(input_obj, tk.StringVar) or \ + isinstance(input_obj, tk.DoubleVar) or \ + isinstance(input_obj, tk.BooleanVar): + return input_obj.get() + + elif isinstance(input_obj, list): + var_list = [] + for var in input_obj: + var_list.append(self._get_tk_values(var)) + return var_list + + elif isinstance(input_obj, dict): + var_tree={} + + for key in input_obj: + curr_value = input_obj[key] + var_tree[key] = self._get_tk_values(curr_value) + + return var_tree + else: + return None + + + def get_values(self): + """ + Get the values set by the gui + + Returns + ------- + profile_vals : dict + A dictionary of new profile values set by the gui + """ + profile = self._get_tk_values(self.variable_tree) + + params = profile['params'] + + # Condense stop conditions back into a list + for key in params: + curr_group = params[key] + for curr_step in curr_group: + stop_conditions = curr_step.get('stop_conditions',None) + if stop_conditions is not None: + condition_dict = {} + for values in stop_conditions: + cond_str = self._combine_condition(values) + condition_dict[cond_str] = values['value'] + + curr_step['stop_conditions'] = condition_dict + + return copy.deepcopy(profile) + + + def _make_input_group(self, parent, config, vars, index): + + fr_group = tk.LabelFrame(parent, text="Step %d"%(index), + font=('Arial', 10, 'bold'), bd=2, + fg = self.colors['default'][0]) + + balance = config.get('balance', False) + if balance: + label = tk.Label(fr_group, text="Balance: ", bg=self.colors['default'][1]) + label.grid(row=0,column=0, sticky="ew") + + cond = OptionSwitcher(fr_group, vars['balance'], balance, self.balance_values) + cond.grid(row=0, column=1, sticky='ew') + return fr_group + + + motion = config['motion'] + fr_motion = tk.Frame(fr_group, bd=2) + + + label = tk.Label(fr_motion, text="Linear: ") + label.grid(row=0,column=0, sticky="ew") + idx=1 + for curr, var in zip(motion['linear'], vars['motion']['linear']): + box = Spinbox(fr_motion, width=7, textvariable=var) + box.set(curr) + box.grid(row=0, column=idx, sticky='ew') + idx+=1 + + label = tk.Label(fr_motion,text="Angular: ") + label.grid(row=1,column=0, sticky="ew") + idx=1 + for curr, var in zip(motion['angular'], vars['motion']['angular']): + box = Spinbox(fr_motion, width=7, textvariable=var) + box.set(curr) + box.grid(row=1, column=idx,sticky='ew') + idx+=1 + + fr_motion.pack(expand=True, fill="x") + + stop_conditions = config['stop_conditions'] + fr_stop = tk.Frame(fr_group, bd=2) + + for condition, var in zip(stop_conditions,vars['stop_conditions']): + fr_stop_inner = tk.Frame(fr_stop, bg=self.colors[condition['signal']][1], bd=2) + + cond = OptionSwitcher(fr_stop_inner, var['condition'], + condition['condition'], + self.stop_values[condition['signal']]) + + signal = OptionSwitcher(fr_stop_inner, + var['signal'], + condition['signal'], + sorted(self.stop_values.keys())) + + box = Spinbox(fr_stop_inner, textvariable=var['value']) + box.set(condition['value']) + cond.pack(expand=False, fill="y", side='left') + signal.pack(expand=True, fill="y", side='left') + box.pack(expand=False, fill="y", side='left') + fr_stop_inner.pack(expand=False,fill='y') + + fr_stop.pack(expand=True, fill="x") + + return fr_group + + + def _move_step(self, key, idx, dir): + steps = self.profile['params'][key] + if dir =='up': + if idx == 0: + return + else: + idx_mv = idx-1 + else: #down + if idx == len(steps)-1: + return + else: + idx_mv = idx+1 + + v = self.variable_tree['params'][key] + v.insert(idx_mv, v.pop(idx)) + p = self.profile['params'][key] + p.insert(idx_mv, p.pop(idx)) + + # Regenerate + self.update_inputs() + + + + def _make_controls(self, parent, key, idx): + fr_group = tk.Frame(parent, bd=2) + + up_btn = tk.Button(fr_group, + text=u'\u25B2', + command = lambda key=key, idx=idx : self._move_step(key, idx, 'up'), + state = 'normal',) + dn_btn = tk.Button(fr_group, + text=u'\u25BC', + command = lambda key=key, idx=idx : self._move_step(key, idx, 'down'), + state = 'normal',) + + up_btn.pack(expand=False, fill="x", padx=2, pady=2, side='top') + dn_btn.pack(expand=False, fill="x", padx=2, pady=2, side='top') + + return fr_group + + + def clear(self): + profile = self.get_values() + self._del_inputs() + self.variable_tree, self.profile = self._generate_variable_tree(profile) + + + def _del_inputs(self): + try: + self.fr_buttons.destroy() + except: + pass + + + def update_inputs(self): + self.clear() + self._init_inputs(self.parent, self.profile, self.variable_tree) + + + def _init_inputs(self, parent, profile, var_tree): + self.fr_buttons = tk.Frame(parent, bd=2) + + preload = profile['params'].get('preload') + test = profile['params'].get('test') + preload_vars = var_tree['params'].get('preload') + test_vars = var_tree['params'].get('test') + + fr_preload = tk.LabelFrame(self.fr_buttons, text="Preload", font=('Arial', 12, 'bold'), bd=2) + for idx,seg in enumerate(preload): + fr_step=tk.Frame(fr_preload) + fr_step.pack(expand=False, fill='both', side='top') + + fr_ctrl = self._make_controls(fr_step,'preload',idx) + fr = self._make_input_group(fr_step,seg,preload_vars[idx], idx) + fr_ctrl.pack(expand=False, fill="both", padx=5, pady=5, side='left') + fr.pack(expand=False, fill="both", padx=5, pady=5, side='left') + + fr_preload.pack(expand=False, fill="both", padx=5, pady=5, side='left') + + fr_test = tk.LabelFrame(self.fr_buttons ,text="Main Test", font=('Arial', 12, 'bold'), bd=2) + for idx,seg in enumerate(test): + fr_step=tk.Frame(fr_test) + fr_step.pack(expand=False, fill='both', side='top') + + fr_ctrl = self._make_controls(fr_step, 'test',idx) + fr = self._make_input_group(fr_step,seg, test_vars[idx], idx) + fr_ctrl.pack(expand=False, fill="both", padx=5, pady=5, side='left') + fr.pack(expand=False, fill="both", padx=5, pady=5, side='left') + + fr_test.pack(expand=False, fill="both", padx=5, pady=5, side='left') + + self.fr_buttons.pack(expand=False, fill="x", side='top') + + + def __del__(self): + self.fr_buttons.destroy() \ No newline at end of file diff --git a/armstron/src/armstron/gui/profile_handler.py b/armstron/src/armstron/gui/profile_handler.py new file mode 100644 index 0000000..4a9aaca --- /dev/null +++ b/armstron/src/armstron/gui/profile_handler.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# tkinter Stuff +import os +import sys +import copy +if sys.version_info[0] == 3: + import tkinter as tk + import tkinter.ttk as ttk + import tkinter.filedialog as fdialog +else: + import Tkinter as tk + import ttk + import tkFileDialog as fdialog + +from ttkthemes import ThemedTk + +from armstron.gui.utils import Spinbox, OptionSwitcher +import armstron.utils as utils + +class ProfileHandler: + def __init__(self, parent, file_types, curr_config_file, incldue_btns=['open', 'save','saveas'], name="", side='left'): + file_types_tup=[] + for ft in file_types: + file_types_tup.append(tuple(ft)) + self.file_types = file_types_tup + self.curr_config_file = curr_config_file + self.config=None + self._init_buttons(parent, incldue_btns, name, side=side) + self.callbacks={'open_before':self._empty, 'open_after':self._empty, + 'save_before':self._empty, 'save_after':self._empty, + 'saveas_before':self._empty, 'saveas_after':self._empty, + 'folder_before':self._empty, 'folder_after':self._empty} + + def _empty(self): + pass + + + def set_callback(self, btn_name, cb): + """ + Set a button callback by name + + Parameters + ---------- + btn_name : str + The button name to attach the callback to + cb : function + The callback function to attach + """ + self.callbacks[btn_name]=cb + + + def _init_buttons(self, parent, incldue_btns, name, side='left'): + default_btns = ['open', 'save','saveas'] + self.buttons={} + for btn in default_btns: + self.buttons[btn] = None + + self.fr_buttons = tk.LabelFrame(parent, bd=2, text=name) + + if 'open' in incldue_btns: + open_btn = ttk.Button(self.fr_buttons, + text="Open", + command = self.open_file + ) + open_btn.grid(row=1, column=0, sticky='ns', padx=5, pady=5) + self.buttons["open"] = open_btn + + if 'folder' in incldue_btns: + folder_btn = ttk.Button(self.fr_buttons, + text="Open Folder", + command = self.open_folder + ) + folder_btn.grid(row=1, column=0, sticky='ns', padx=5, pady=5) + self.buttons["folder"] = folder_btn + + if 'save' in incldue_btns: + save_btn = ttk.Button(self.fr_buttons, + text="Save", + command = self.save_file, + state = 'disabled', + ) + save_btn.grid(row=1, column=1, sticky='ns', padx=5, pady=5) + self.buttons["save"] = save_btn + + if 'saveas' in incldue_btns: + save_as_btn = ttk.Button(self.fr_buttons, + text="Save As", + command = self.save_file_as, + state = 'disabled', + ) + save_as_btn.grid(row=1, column=2, sticky='ns', padx=5, pady=5) + self.buttons["saveas"] = save_as_btn + + + self.fr_buttons.pack(expand=False, fill="y", side=side) + + def _check_enable_buttons(self): + if self.config is not None: + if self.buttons['save'] is not None: + self.buttons['save'].configure(state='normal') + if self.buttons['saveas'] is not None: + self.buttons['saveas'].configure(state='normal') + else: + if self.buttons['save'] is not None: + self.buttons['save'].configure(state='disabled') + if self.buttons['saveas'] is not None: + self.buttons['saveas'].configure(state='disabled') + + + def open_file(self): + + self.callbacks['open_before']() + + filepath = fdialog.askopenfilename( + filetypes=self.file_types, + initialdir=self.curr_config_file['dirname'], + initialfile=self.curr_config_file['basename'] + ) + if not filepath: + return None + self.curr_config_file['basename'] = os.path.basename(filepath) + self.curr_config_file['dirname'] = os.path.dirname(filepath) + self.load_file() + self._check_enable_buttons() + + self.callbacks['open_after']() + + return self.curr_config_file + + + def save_file_as(self): + + self.callbacks['saveas_before']() + + filepath = fdialog.asksaveasfilename( + defaultextension="txt", + filetypes=self.file_types, + initialdir=self.curr_config_file['dirname'], + initialfile=self.curr_config_file['basename'] + ) + + if not filepath: + return None + + self.curr_config_file['basename'] = os.path.basename(filepath) + self.curr_config_file['dirname'] = os.path.dirname(filepath) + + self.save_file() + + self.callbacks['saveas_after']() + + return self.curr_config_file + + + def save_file(self): + self.callbacks['save_before']() + + if self.config is not None: + """Save the current file as a new file.""" + filepath = os.path.join( + self.curr_config_file['dirname'], + self.curr_config_file['basename'] + ) + + if not os.path.exists(self.curr_config_file['dirname']): + os.makedirs(self.curr_config_file['dirname']) + + with open(filepath, "w") as output_file: + utils.save_yaml(self.config, filepath) + + self.callbacks['save_after']() + + + def open_folder(self): + self.callbacks['folder_before']() + + from sys import platform + if platform == "linux" or platform == "linux2": + cmd='xdg-open' + elif platform == "darwin": + cmd='open' + + elif 'win' in platform: + cmd='start' + + os.system(r'%s %s'%(cmd,self.curr_config_file['dirname'])) + self.callbacks['folder_after']() + + + def load_file(self): + """Open a file for editing.""" + if self.curr_config_file['dirname'] is None or self.curr_config_file['basename'] is None: + return + + filepath = os.path.join( + self.curr_config_file['dirname'], + self.curr_config_file['basename'] + ) + + new_config = False + if filepath.endswith(".yaml"): + new_config = self.load_config(filepath) + if new_config: + self.config = new_config + + + def get_config(self): + return self.config + + + def set_config(self, config): + self.config = config + + + def load_config(self, filename): + try: + config = utils.load_yaml(filename) + if isinstance(config, dict): + return config + + else: + print('Incorrect config format') + return False + except: + print('New config was not loaded') + return False + + def __del__(self): + self.fr_buttons.destroy() \ No newline at end of file diff --git a/armstron/src/armstron/gui/utils.py b/armstron/src/armstron/gui/utils.py new file mode 100644 index 0000000..8354d07 --- /dev/null +++ b/armstron/src/armstron/gui/utils.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# tkinter Stuff +import os +import sys +import copy +if sys.version_info[0] == 3: + import tkinter as tk + import tkinter.ttk as ttk + import tkinter.filedialog as fdialog +else: + import Tkinter as tk + import ttk + import tkFileDialog as fdialog + +from ttkthemes import ThemedTk + +def _from_rgb(rgb): + """translates an rgb tuple of int to a tkinter friendly color code + """ + rgb_int = [0]*3 + for i, color in enumerate(rgb): + rgb_int[i] = int(color*255) + return "#%02x%02x%02x" % tuple(rgb_int) + + + +class Spinbox(ttk.Entry): + + def __init__(self, master=None, **kw): + + ttk.Entry.__init__(self, master, "ttk::spinbox", **kw) + def set(self, value): + self.tk.call(self._w, "set", value) + + +class OptionSwitcher(ttk.OptionMenu): + def __init__(self, container, variable, default=None, *values, **kwargs): + fill_option = "< Choose >" + values_cp = copy.deepcopy(*values) + values_cp.insert(0, fill_option) + if default is None: + default = values_cp[0] + + ttk.OptionMenu.__init__(self, container,variable, default, *values_cp, **kwargs) \ No newline at end of file