diff --git a/ReadMe.md b/ReadMe.md index 45b68b6..05ca2d1 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,37 +1,106 @@ -![PyCalc](media/PyCalc-ListsAndPlot.jpg) - ## Hello and welcome to PyCalc, the RPN ~~calculator~~ ~~IDE~~ program that loves Python -This project is an experiment to see what happens when you take a Python interpreter, and put an RPN calculator style -UI wrapper around it. -I have spent years using Python Terminal and Jupyter Notebooks and while each one has its strengths, neither one -offers the simplicity or immediacy of a good old fashion calculator when working math problems. But a calculator -lacks the history and exposition of a Jupyter notebook. -Why not incorporate the strengths of both a calculator -and a Jupyter Notebook into one App. +![PyCalc](media/PyCalc-ListsAndPlot.jpg) +_______________________ +![PyCalc](media/PyCalc_UserFunctions_DarkMode.jpg) + +This is the best calculator you will ever use because it packs the ecosystem of Python +into a fast and familiar User Interface. +This project is an experiment to see what happens when you take a Python interpreter, and put an RPN calculator +User Interface wrapper around it. The result is a novel and fast way to interact with Python with the familiarity of a +calculator UI. +I spent years using Python Terminal, Spyder and now Jupyter Notebooks and while each one has its strengths, +neither one +offers the simplicity or immediacy of a good old fashion calculator when working math problems. But a traditional +calculator +lacks the history and exposition of a Jupyter notebook and the power and massive catalog of tools available in Python. +Why not incorporate strengths of all into one App. So this project was born and has become a go-to tool for daily engineering math and quick Python scripting. ### Highlights #### General -- [x] Feature filled RPN style calculator with stack view, history, variables and plotting -- [x] Novel RPN style interface for interacting with a live Pyton interpreter +- [x] Feature filled RPN calculator with user functions, variables, plotting, and history. +- [x] Novel RPN style interface for interacting with a live Python interpreter - [x] Built in support for Math, NumPy and Matplotlib libraries - [x] Import any Python module and its functions are available for use in the calculator -- [x] Python Interpreter backend for full access to Python language features directly on the stack #### User Interface -- [x] Customizable Tkinter.ttk UI with Stack View, Variable View, and History View +- [x] Customizable Tkinter.ttk User Interface with Stack View, Variable View, and User Function View Window - [x] Support for copy and pasting between excel and the calculator - [x] Session history with undo and redo -- [x] Store variables of any type including functions and classes -- [x] Save / Restore sessions and state +- [x] Store variables of any type including numbers, arrays, functions, class instances, and more +- [x] Save / Restore sessions and state, by default launches with the last starte +- [x] Dark mode and light mode themes (tracks the system theme) + +### Intended Users +- People that want a really powerful calculator +- Engineers that need to do calculations using saved variables, functions, and libraries +- Python developers + +### How to install and launch the calculator +As of now the calculator is a Python script that can be run from the command line or your IDE of choice. +You must have Python installed on your system to run the calculator. +#### Using the UI +1) download the repository +2) from a terminal on mac and linux or power shell on Windows, navigate to the repository directory that contains the main.py file +3) from a terminal on mac and linux or power shell on Windows, call: `python main.py` +4) This will launch the calculator UI and you can start using it. +#### Using the API / CLI +1) If you don't want the UI you can use the calc.py module and instantiate an instance of the Calculator class +and use it like so: + + ```python + import calc + c = calc.Calculator() + ue = c.user_entry + ue('sin(43.7) + 17.3') + ue('enter') + >>> Evaluated: sin(43.7) + 17.3 to 17.0214373937804 + c.return_stack_for_display() + >>> [17.0214373937804] + ``` + At this point you can see that the calculator program is intended to have a UI wrapper around it but still works on +the command line. Of note is that the calculator can handle user_entry single chars +at a time so you can also enter 'sin(3)' as: + ```python + ... + ue('s') + ue('i') + ue('n') + ue('(') + ue('3') + ue(')') + c.enter_press() + >>> Evaluated: sin(43.7) + 17.3 to 17.0214373937804 + ``` +This is useful if you want to send user keyboard events directly to the calculator from some other User Interface. For +example if you already have a snazzy calculator UI and you want to use this calculator backend. I'm looking at you +flutter and js devs. ### How to use the calculator At the most basic level, the UI is an RPN calculator so if you know how to use an RPN calculator -it acts as expected. If you don't know how to use an RPN calculator then please use the internet to -update your knowledge base, also you can type out expressions in this calculator and it will operate -like a 'normal' calculator. +it acts as expected. If RPN is not your thing, you can enter equations as infix notation (like a regular +calculator) by using parentheses, for example: '(sin(40.2 + pi) + 1.42/24)'. After entering the desired equation +pushing 'enter' will evaluate the expression and display the result in the stack view and in the message +window. + +#### Quick notes on usage + +1) You don't need buttons to use the math functions. +For example to calculate nCr(5, 3) you can type: 5 'return' 3 'return' 'ncr' 'return' and the result will be +displayed on the stack. If you are a speedy typer this method is faster than going back and forth +between the keyboard and the mouse to enter functions. You can also type out the entire expression, then +press enter, for example: 'ncr(5, 3)' then 'return' to evaluate the same expression. All functions in the Python +math library are available for use in this way, a reference to the Python Math Library is here: +https://docs.python.org/3/library/math.html +2) Calculators typically use the notation ln = log base e and log = log base 10. Since this is a +calculator, it matches the use of ln=log base e and log=log base 10. This means that there is a +wrapper on the math.log function so calling log(3, 10) throws an error. If you want to call like +this you must explicitly call: 'math.log(3, 10)' as the builtin log method (for this calculator) is +always base 10. + + ### Examples (using the UI) @@ -58,32 +127,15 @@ like a 'normal' calculator. 4) press: 'enter' 5) The value '2' is stored in the variable 'my_var' and is now displayed in the variable view -#### Using list and array functions with NumPy and Lists - -1) press: '2' -2) press: 'enter' -3) press: '3' -4) press: 'enter' -5) press: '4' -6) press: 'stack to list' -- this will convert the stack to a list [2, 3, 4] and display it in the stack view -7) enter: 'np.array' -- this will convert the list to a NumPy array and display it in the stack view -8) enter: 'my_array=' -9) press: 'enter' -- this will store the array in the variable 'my_array' and display it in the variable view -10) enter: 'sin' -11) press: 'enter' -- this will apply the sin function to each element of the array and display the result in the stack view - -#### Plotting - -1) Continuing from the "Using list and array functions with NumPy and Lists" example -2) enter: 'my_array' -3) press: 'plot' -- this will plot the array using matplotlib and display the plot in a new window - #### Non RPN style calculations 1) enter: '(1+1)' -- the parentheses let the calculator know that this is a math expression 2) press: 'enter' -- this will evaluate the expression 1+1 and display the result '2' in the stack view -#### Making lists +#### Making lists and arrays + +Note that you can copy values in Excel or Google Sheets and paste them directly into the calculator. The calculator +will convert the pasted section to a list and display it on the stack. This example exposes the underlying power of the calculator to interact directly with the Python interpreter and how imported Python module's functions can be called directly from the stack to operate on the stack. @@ -94,9 +146,73 @@ imported Python module's functions can be called directly from the stack to oper 4) press: 'enter' -- this will convert the list to a NumPy array and display it in the stack view 5) enter: 'tan' 6) press: 'enter' -- this is the equivalent of calling np.tan() the calculator loads in all +7) enter: 'a=' +8) press: 'enter' -- this will store the array in the variable 'a' +9) enter: 'np.mean(a)' +10) press: 'enter' -- this will calculate the mean of the array and display the result in the stack view functions from the NumPy library so you can call them directly from the stack by typing the function name and then pressing enter. +#### Plotting + +The Calculator uses the awesome Matplotlib library to plot data. + +1) enter: '[10, 7, 4, 2, 7, 4, 9, 0, 1, -3]' +2) press: 'enter' -- this will evaluate the value on the stack to a list type object +3) press: 'plot' -- this will plot the array using matplotlib and display the plot in a new window + +Note that you can copy values in Excel or Google Sheets and paste them directly into the calculator. + +![PyCalc](media/PyCalc-PlotExampleA.jpg) + +#### advanced plotting + +You can interact directly with the plot library like so: +1) enter: 'np.linspace(0, 10, 100)' +2) press: 'enter' -- this will create a NumPy array of 100 points from 0 to 10 +3) enter: 'np.sin(a/7)' +4) press: 'enter' -- this will create a NumPy array of sin(x/7) for the range x = 0 to x = 10 +5) enter: 'a=' +6) press: 'enter' -- this will store the array in the variable 'a' +7) enter: 'np.cos(a/5)' +8) press: 'enter' -- this will create a NumPy array of cos(x/5) for the range x = 0 to x = 10 +9) enter: 'b=' +10) press: 'enter' -- this will store the array in the variable 'b' +11) enter: 'plt.plot(a, b, "r-")' +12) press: 'enter' -- this will plot the arrays a and b with a red line and display the plot in a new window +13) enter: 'plt.show()' -- or press the 'plot' button to directly display the plot in a new window +14) press: 'enter' -- this will display the plot in a new window + + +#### Using Python Libraries + +On launch the calculator imports: +```python +import math as math +import numpy as np +import matplotlib.pyplot as plt +``` +This means that you can call any function from these libraries directly from the stack. For example: +1) enter: 'np.sin([1, 2, 3, 4, 5])' +2) press: 'enter' -- this will evaluate the sin function from the NumPy library and display the result in the stack view + +For Python libraries that are not imported on launch you can import them directly from the stack. For example: +1) enter: 'import random' +2) press: 'enter' -- this will import the random library and make all functions from the random library available for use in the calculator +3) enter: 'random.randint(1, 100)' +4) press: 'enter' -- this will call the randint function from the random library and display the result in the stack view + +For methods that require exactly one argument you can call them directly from the stack. For example: +1) enter: '[1, 2, 3, 4, 5]' +2) press: 'enter' +3) enter: 'random.shuffle' +4) press: 'enter' -- this will shuffle the list and display the result in the stack view +For methods that do not require exactly one argument you can call them directly from the stack by typing the method +name with arguments and then pressing enter. For example: +1) enter: 'random.randint(1, 100)' +2) press: 'enter' -- this will call the randint function from the random library with the arguments 1 and 100 +and display the result in the stack view + #### Saving and Restoring Sessions The calculator has the ability to save and restore sessions. By default the calculator will save it's state on exit @@ -112,7 +228,6 @@ Restoring 3) With the calculator open press: 'ctrl+o' or navigate to the file menu and select 'open' 4) In the file dialog select the session file and press 'open' - #### User Options You can set some options in the calculator by navigating to the 'options' menu. The options are: @@ -121,20 +236,28 @@ You can set some options in the calculator by navigating to the 'options' menu. - 'Edit int format string' -- This will open a dialog where you can set the format string for integers, see: https://fstring.help/cheat/ - 'Edit plot format string' -- This will open a dialog where you can set the format string for plots, for valid strings, see Parameters: **fmt** here: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html +### Working with User Functions +The calculator allows the user to define functions that can be used in the calculator and stored with +the calculator session. +#### To add a user function: +1) Navigate to 'Options' -> 'Add FUnction' +2) In the dialog that appears, enter the function using the standard Python formatting. For example: + ```python + def sqr_x(x): + return x**2 + ``` +3) Press 'OK' and the function will be added to the calculator and will be available for use in the calculator. +#### To see all user functions: +1) navigate to 'View' -> 'Show User Functions' + +#### To remove a user function: +1) Navigate to 'Options' -> 'Remove Function' +2) This will open a popup window with a list of all user functions. Select the function you want to remove +and press 'Remove' -#### Notes on Usage -1) Due to years of using RPN calculators I have adopted the terminology of X=stack[0], Y=stack[1], Z=stack[2] and -T=stack[3] (thanks HP). In the code I often refer to X and Y as in 'pow(X, Y)' which is equivalent to -pow(stack[0], stack[1]). -2) You don't need buttons to use the math functions. Just type the argument first then the function. -3) Calculators typically use the notation ln = log base e and log = log base 10. Since this is a -calculator, it matches the use of ln=log base e and log=log base 10. This means that there is a -wrapper on the math.log function so calling log(3, 10) throws an error. If you want to call like -this you must explicitly call: math.log(3, 10) as the builtin log method (for this calculator) is -always base 10. ### Future Features - Installer for the UI @@ -145,3 +268,6 @@ always base 10. One of the additional goals I had for this project was to explore the use of Tkinter. I have used various other software packages to create UIs and I wanted to give Tkinter a turn. So far tkinter has been fast and easy to work with. + +It has taken me about twice as long to write this readme as it took to write the code itself, but it is a good +process because I found a few bugs in doing so. diff --git a/calc.py b/calc.py index f50fe73..bb97314 100644 --- a/calc.py +++ b/calc.py @@ -51,6 +51,7 @@ def __init__(self): self._button_functions = dict() # a dict of all the pre-defined calculator 'button' functions, like sqrt, sin, . self._imported_libs = set() # a set of all imported libraries self._imported_functions = set() # a set of all imported functions + self._user_functions = dict() # a dict of all user defined functions like {'name': ''} self._all_functions = set() # a set of all possible functions that can be called including buttons and imports # use the awesome math lib to grab some pre-defined math methods .... mathods? @@ -151,7 +152,7 @@ def __init__(self): } self._button_functions = built_in_functions | one_args | two_args | iterable_args - self._all_functions = set(self._button_functions.keys()) + self._all_functions = set(self._button_functions.keys()) | self._user_functions.keys() # add the math functions to the exec_globals so they can be used in eval and exec self._exec_globals.update(math.__dict__) @@ -304,11 +305,17 @@ def show_plot(self, plot_args=None): if len(self._stack) > 0: x = self._stack[0] # dont pop X, leave it on the stack for error and success try: - iter(x) - if plot_args is not None: - plt.plot(x, plot_args) + + # check if a plot type object is already loaded up, if so plot it + if isinstance(x, list): + if isinstance(x[0], plt.Artist): + pass # plt.show() is called below else: - plt.plot(x) + iter(x) + if plot_args is not None: + plt.plot(x, plot_args) + else: + plt.plot(x) except Exception as ex: self._message = f"Error: cant plot: '{x}' with error: '{ex}'" log(self._message) @@ -937,6 +944,22 @@ def clear_stack(self,): log(self._message) self._stack = [] + def clear_user_functions(self, function_name=None): + """ removes the user functions from the namespace and the user functions set """ + if function_name is None: + to_remove = copy(list(self._user_functions.keys())) + else: + to_remove = {function_name} + for func in to_remove: + try: + self._user_functions.pop(func) + self._exec_globals.pop(func, None) + del func + except Exception as ex: + self._message = f"Error: cant remove function: '{func}' with error: '{ex}'" + log(self._message) + raise Exception(self._message) # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + def load_locals(self, new_locals: dict, clear_first=False): """ loads a new locals dictionary into the calculator object @param new_locals: the new locals dictionary to load @@ -999,11 +1022,19 @@ def return_stack_for_display(self, index=None): else: return None + def return_user_functions_for_display(self): + """ returns a set of all the user defined functions """ + return copy(self._user_functions) + def return_buttons_for_display(self): """ returns a list all the buttons (functions, methods, operations, constants) known to the calculator """ keys = list(self._button_functions.keys()) return keys + def return_user_functions(self): + """ returns a set of all the user defined functions """ + return self._user_functions + def return_message(self): """ returns the message string """ return self._message @@ -1035,7 +1066,8 @@ def _print_stack(self): else: log(f'{len_stack-idx - 1}: {item}') - def _convert_to_best_numeric(self, x) -> any: + @staticmethod + def _convert_to_best_numeric(x) -> any: """ trys to convert to an int, if that fails, convert to float, if that fails raises a ValueError @param x: the value to convert to a number @return: the number or raises a ValueError """ @@ -1046,4 +1078,22 @@ def _convert_to_best_numeric(self, x) -> any: else: return val except Exception as ex: - raise ValueError(f"Cannot convert '{x}' to number with error: '{ex}'") \ No newline at end of file + raise ValueError(f"Cannot convert '{x}' to number with error: '{ex}'") + + def add_user_function(self, function_string: str): + """ adds a user defined function to the calculator object + @param function_string: the function string to add to the calculator object """ + self._message = None + try: + exec(function_string, self._exec_globals) + # get the name of the function + function_name = function_string.split(' ')[1].split('(')[0] + self._user_functions.update({function_name: function_string}) + self._all_functions.add(function_name) + self._message = f"Added user function: {function_string}" + except Exception as ex: + self._message = f"Error: adding user function: '{function_string}' with error: '{ex}'" + log(self._message) + raise Exception(self._message) + + diff --git a/media/PyCalc-PlotExampleA.jpg b/media/PyCalc-PlotExampleA.jpg new file mode 100644 index 0000000..f3ef916 Binary files /dev/null and b/media/PyCalc-PlotExampleA.jpg differ diff --git a/media/PyCalc_UserFunctions_DarkMode.jpg b/media/PyCalc_UserFunctions_DarkMode.jpg new file mode 100644 index 0000000..9ab8fb1 Binary files /dev/null and b/media/PyCalc_UserFunctions_DarkMode.jpg differ diff --git a/ui.py b/ui.py index d48b37c..21c3ea0 100644 --- a/ui.py +++ b/ui.py @@ -22,6 +22,7 @@ def __init__(self): self.float_format_string = '0.6f' self.integer_format_string = ',' self.plot_options_string = '-o' + self.last_user_function_edit_name = None # window size and appearance self.stack_rows = 7 @@ -33,7 +34,6 @@ def __init__(self): self.stack_index_width = 5 self.stack_value_width = 200 self.stack_type_width = 50 - self.message_width = 57 self.background_color = 'default' # set to 'default' or , default matches the system theme @@ -49,6 +49,7 @@ def __init__(self): self.stack = [] self.locals = dict() self.settings = CalculatorUiSettings() + self.functions = dict() class MainWindow: @@ -243,6 +244,8 @@ def __init__(self, settings: CalculatorUiSettings = None): self._menu_bar.add_cascade(label='File', menu=self._file_menu) self._edit_menu = tk.Menu(self._menu_bar) self._menu_bar.add_cascade(label='Edit', menu=self._edit_menu) + self._view_menu = tk.Menu(self._menu_bar) + self._menu_bar.add_cascade(label='View', menu=self._view_menu) self._options_menu = tk.Menu(self._menu_bar) self._menu_bar.add_cascade(label='Options', menu=self._options_menu) @@ -265,11 +268,19 @@ def __init__(self, settings: CalculatorUiSettings = None): # add a 'undo' option to the edit menu self._edit_menu.add_command(label='Undo (ctrl+z)', command=self.undo_last_action) + # VIEW MENU ........................ + + # add a 'show user functions' option to the view menu that opens a popup window + self._view_menu.add_command(label='Show user functions', command=self.popup_show_user_functions) + # OPTIONS MENU ........................ # add a check option to the menu for 'save state on exit' self._options_menu.add_checkbutton(label='Save state on exit', onvalue=True, offvalue=False) + # add a separator + self._options_menu.add_separator() + # add an option to "edit the float format string" that calls the method edit_float_format_string self._options_menu.add_command(label='Edit float format string', command=self.popup_edit_float_format_string) @@ -279,6 +290,18 @@ def __init__(self, settings: CalculatorUiSettings = None): # add an option to "edit the plot options string" that calls the method edit_plot_options_string self._options_menu.add_command(label='Edit plot options string', command=self.popup_edit_plot_options_string) + # add a line separator + self._options_menu.add_separator() + + # add an option to open the add function popup window that calls the method popup_add_function + self._options_menu.add_command(label='Add function', command=self.popup_add_function) + + # add an option to 'remove user function' that calls the method remove_user_function + self._options_menu.add_command(label='Remove function', command=self.popup_remove_user_function) + + # add an option to 'clear all user functions' that calls the method clear_all_user_functions + self._options_menu.add_command(label='Clear all functions', command=self.popup_confirm_clear_all_user_functions) + # MENU BINDINGS ........................ # @@ -335,6 +358,106 @@ def __init__(self, settings: CalculatorUiSettings = None): """ ------------------------------------- END __init__() ------------------------------------------------- """ + def popup_confirm_clear_all_user_functions(self): + """ opens a popup window to confirm the user wants to clear all user functions """ + # create a new window + window = tk.Toplevel(self._root) + window.title('Confirm Clear All Functions') + + # create a label to ask the user if they are sure + label = ttk.Label(window, text='Are you sure you want to clear all user functions?') + label.pack() + + def clear_all_user_functions(): + self._c.clear_user_functions() + window.destroy() + + # create a button to confirm the clear all user functions + ttk.Button(window, text='OK', command=clear_all_user_functions).pack() + + # create a button to cancel the clear all user functions + ttk.Button(window, text='Cancel', command=window.destroy).pack() + + def popup_remove_user_function(self): + """ popup that has a list of user functions and a button to remove the selected function """ + # create a new window + window = tk.Toplevel(self._root) + window.title('Remove User Function') + + # create a list box to show the user functions + list_box = tk.Listbox(window, height=10, width=50) + for key in self._c.return_user_functions().keys(): + list_box.insert('end', key) + list_box.pack() + + def remove_user_function(): + selected = list_box.curselection() + if len(selected) == 0: + return + key = list_box.get(selected) + self._c.clear_user_functions(key) + window.destroy() + + # create a button to remove the selected function + ttk.Button(window, text='Remove', command=remove_user_function).pack() + + # create a button to cancel the remove function + ttk.Button(window, text='Cancel', command=window.destroy).pack() + + def popup_add_function(self, function_string=None, parent_object=None): + """ opens a popup window to add a function to the calculator """ + # create a new window + if parent_object is None: + parent = self._root + else: + parent = parent_object + window = tk.Toplevel(parent) + window.title('Add Function') + + # create a text entry field + entry = tk.Text(window, height=5, width=50) + if function_string is None: + default_text = 'def sqr_x(x):\n return x**2' + txt = self._c.return_user_functions().get(self._settings.last_user_function_edit_name, default_text) + entry.insert('1.0', txt) + else: + entry.insert('1.0', function_string) + entry.focus() + entry.pack() + + def apply_function(): + function_string = entry.get('1.0', 'end') + try: + self._c.add_user_function(function_string) + except Exception as ex: + message = f"Error adding function: {ex}" + self._update_message_display(message) + else: + self._settings.last_user_function_edit_name = function_string.split('(')[0].split(' ')[1] + window.destroy() + + # create a button to save the changes + ttk.Button(window, text='OK', command=apply_function).pack() + + # create a button to cancel the changes + ttk.Button(window, text='Cancel', command=window.destroy).pack() + + def popup_show_user_functions(self): + """ opens a popup window to show the user defined functions """ + # create a new window + window = tk.Toplevel(self._root) + window.title('User Functions') + + # create a text entry field + entry = tk.Text(window, height=42, width=50) + func_dict = self._c.return_user_functions_for_display() + for key, value in func_dict.items(): + entry.insert('end', f"Name: '{key}':\n{value}____________________________________________\n") + entry.pack() + + # create a button to cancel the changes + ttk.Button(window, text='Cancel', command=window.destroy).pack() + def _load_settings_on_launch(self): """ looks for the settings file 'last_state_autosave' in the local directory and loads it if the user has selected to save state on exit """ @@ -509,6 +632,7 @@ def menu_save_state(self, save_path=None): calc_state.stack = self._c.return_stack_for_display() calc_state.locals = self._c.return_locals() calc_state.settings = copy(self._settings) + calc_state.functions = self._c.return_user_functions() pkl_dump = pickle.dumps(calc_state) file.write(pkl_dump) file.close() @@ -540,11 +664,24 @@ def _load_calc_state(self, calc_state: CalculatorUiState): self._c.user_entry(item) log(f"loaded stack: {calc_state.stack}") if calc_state.settings is not None: - self._settings = calc_state.settings + # need to handle new items were added to the settings class + incoming = calc_state.settings + latest = self._settings + dif = set(vars(incoming)) ^ set(vars(latest)) + for key in dif: + setattr(incoming, key, getattr(latest, key)) + + self._settings = incoming self._apply_ui_settings(self._settings) log(f"loaded settings: {calc_state.settings}") - # note: you cant update the UI here because this method is called before all UI objects are created + try: + if calc_state.functions is not None: + for key, value in calc_state.functions.items(): + self._c.add_user_function(value) + except Exception as ex: + pass # older versions of the calc state class did not have functions + # note: you cant update the UI here because this method is called before all UI objects are created def menu_clear_all_variables(self): self._c.clear_all_variables() @@ -677,6 +814,3 @@ def launch_ui(self): - - -