diff --git a/README.md b/README.md index 1d04730d..d042a691 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION. 27. [RINEX Conversion](#rinex) facility which supports conversion of previously-saved binary datalogs to RINEX observation and navigation format. To display the RINEX Conversion dialog, go to Menu..Options..RINEX Conversion. 28. [Import Custom Map](#custommap) facility which allows the user to import geo-referenced images for use as background maps. To display the Import Custom Map dialog, go to Menu..Options..Import Custom Map. 29. [Configuration Command Recorder](#recorder) facility which allows the user to record, save, load, import (*as a preset*) and replay UBX, NMEA or TTY configuration commands sent to a receiver. To display the Command Record Facility dialog, go to Menu..Options..Configuration Command Recorder. -30. App Configuration facility which allows the user to amend certain internal configuration parameters, such as the GUI refresh interval, Toplevel window behaviour and maximum log file size. Updates must be saved and the application restarted for any changes to take effect. **NB:** Exercise caution when updating these values and any ensure settings are commensurate with your platform's performance and capacity constraints. +30. App Configuration facility which allows the user to amend certain internal configuration parameters, such as the GUI refresh interval, Toplevel window behaviour and maximum log file size. Updates must be saved and the application restarted for any changes to take effect. **NB:** Exercise caution when updating these values and ensure settings are commensurate with your platform's performance and capacity constraints. #### GUI refresh rate setting diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7e84fa03..53e87ad5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,15 @@ # PyGPSClient Release Notes +### RELEASE 1.7.5 + +FIXES: + +1. Fix chart plotter updates, and make chart widget a full-width frame. + +ENHANCEMENTS: + +1. Add password hide/show button to panels with passwords. + ### RELEASE 1.7.4 FIXES: diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py index 0fd72f84..f3d54cd4 100644 --- a/src/pygpsclient/_version.py +++ b/src/pygpsclient/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.7.4" +__version__ = "1.7.5" diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py index e5489a00..1d6fc727 100644 --- a/src/pygpsclient/app.py +++ b/src/pygpsclient/app.py @@ -185,11 +185,12 @@ def __init__(self, **kwargs): # initialise queues and protocol handlers self._server_status = -1 # socket server status -1 = inactive - self.gnss_outqueue = Queue() # messages to GNSS receiver - self.ntrip_inqueue = Queue() # messages from NTRIP source - self.socket_inqueue = Queue() # message from socket - self.socket_outqueue = Queue() # message to socket - self.console_outqueue = Queue() # message to console + self.gnss_outqueue = Queue() # data to GNSS receiver + self.ntrip_inqueue = Queue() # data from NTRIP source + self.socket_inqueue = Queue() # data from socket + self.socket_outqueue = Queue() # data to socket + self.console_outqueue = Queue() # data to console + self.chart_outqueue = Queue() # data to chart plotter self.gnssstatus_lock = Lock() # thread lock for GNSS status data self.datalog_lock = Lock() # thread lock for datalog file self.gpx_lock = Lock() # thread lock for gpx file diff --git a/src/pygpsclient/app_config_dialog.py b/src/pygpsclient/app_config_dialog.py index 9a5d84e2..8d72ea14 100644 --- a/src/pygpsclient/app_config_dialog.py +++ b/src/pygpsclient/app_config_dialog.py @@ -42,7 +42,10 @@ READONLY, TRACEMODE_WRITE, ) -from pygpsclient.helpers import trace_update # pylint: disable=unused-import +from pygpsclient.helpers import ( # pylint: disable=unused-import + PasswordButton, + trace_update, +) from pygpsclient.strings import DLGGUIOPTIONS from pygpsclient.toplevel_dialog import ToplevelDialog @@ -125,12 +128,15 @@ def _body(self): wrap=True, textvariable=self._maprefresh, ) - self._lbl_mapkey = Label(self._frm_body, text="MapQuest API Key", anchor=W) + self._frm_mapkey = Frame(self._frm_body) + self._lbl_mapkey = Label(self._frm_mapkey, text="MapQuest API Key", anchor=W) self._ent_mapkey = Entry( self._frm_body, width=35, textvariable=self._mapkey, + show="*", ) + self._btn_mapkey = PasswordButton(self._frm_mapkey, self._ent_mapkey) self._lbl_logsize = Label( self._frm_body, text="Datalog Max File Size", anchor=W ) @@ -181,7 +187,9 @@ def _do_layout(self): self._lbl_logsize.grid(column=0, row=2, sticky=W) self._spn_logsize.grid(column=1, row=2, sticky=W) self._lbl_logsizeu.grid(column=2, row=2, padx=2, sticky=W) - self._lbl_mapkey.grid(column=0, row=3, sticky=W) + self._frm_mapkey.grid(column=0, row=3, sticky=EW) + self._lbl_mapkey.grid(column=0, row=0, sticky=W) + self._btn_mapkey.grid(column=1, row=0, sticky=E) self._ent_mapkey.grid(column=1, row=3, columnspan=2, sticky=EW) self._lbl_resizedialog.grid(column=0, row=4, sticky=W) self._chk_resizedialog.grid(column=1, row=4, columnspan=2, sticky=W) diff --git a/src/pygpsclient/attitude_frame.py b/src/pygpsclient/attitude_frame.py index 484ac215..101b286d 100644 --- a/src/pygpsclient/attitude_frame.py +++ b/src/pygpsclient/attitude_frame.py @@ -13,6 +13,7 @@ :license: BSD 3-Clause """ +import logging from tkinter import ( EW, NSEW, @@ -84,6 +85,7 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): :param kwargs: Optional kwargs to pass to Frame parent class """ self.__app = app + self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -355,6 +357,13 @@ def update_frame(self): except (KeyError, ValueError): self._canvas.delete(DATA) + # MEMORY LEAK DEBUG + # tot = len(self._canvas.find_all()) + # tags = {} + # for tag in (TAG_DATA, TAG_WAIT): + # tags[tag] = len(self._canvas.find_withtag(tag)) + # self.logger.debug((tot, tags)) + def _flag_range(self, over: bool = False): """ Flag range spinbox if data is overrange. diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py index 5755f68f..0bc5969d 100644 --- a/src/pygpsclient/chart_frame.py +++ b/src/pygpsclient/chart_frame.py @@ -14,6 +14,7 @@ :license: BSD 3-Clause """ +from queue import Empty from random import choice from time import time from tkinter import ( @@ -498,6 +499,14 @@ def update_frame(self): Plot selected chart data. """ + while True: + try: + parsed_data = self.__app.chart_outqueue.get(False) + self.update_data(parsed_data) + self.__app.chart_outqueue.task_done() + except Empty: + break + self._update_plot(self._chart_data) self.update_idletasks() diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py index 891c46c0..98c58399 100644 --- a/src/pygpsclient/globals.py +++ b/src/pygpsclient/globals.py @@ -157,6 +157,8 @@ ICON_END = path.join(DIRNAME, "resources/marker_end.png") ICON_EXIT = path.join(DIRNAME, "resources/iconmonstr-door-6-24.png") ICON_EXPAND = path.join(DIRNAME, "resources/iconmonstr-arrow-80-16.png") +ICON_EYEON = path.join(DIRNAME, "resources/iconmonstr-eye-lined-16.png") +ICON_EYEOFF = path.join(DIRNAME, "resources/iconmonstr-eye-off-lined-16.png") ICON_GITHUB = path.join(DIRNAME, "resources/github-256.png") ICON_IMPORT = path.join(DIRNAME, "resources/iconmonstr-import-24.png") ICON_LEFT = path.join(DIRNAME, "resources/iconmonstr-caret-left-filled-32.png") diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py index cf82421d..057a92c5 100644 --- a/src/pygpsclient/helpers.py +++ b/src/pygpsclient/helpers.py @@ -30,8 +30,10 @@ from time import strftime from tkinter import ( BooleanVar, + Button, DoubleVar, Entry, + Frame, IntVar, Spinbox, StringVar, @@ -41,6 +43,7 @@ from types import FunctionType, MethodType, NoneType from typing import Any, Literal +from PIL import Image, ImageTk from pygnssutils import version as PGVERSION from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine from pynmeagps import version as NMEAVERSION @@ -66,9 +69,12 @@ from pygpsclient._version import __version__ as VERSION from pygpsclient.globals import ( BSR, + CLICK_CURSOR, ERRCOL, FIXLOOKUP, GPSEPOCH0, + ICON_EYEOFF, + ICON_EYEON, M2FT, M2KM, M2MIL, @@ -120,6 +126,10 @@ "pyunigps": UNIVERSION, } +# **************************************************************** +# Start of Custom Tkinter Class Extensions +# **************************************************************** + def validate( self: Entry, @@ -226,8 +236,51 @@ def trace_update( for var in (BooleanVar, DoubleVar, IntVar, StringVar): var.trace_update = trace_update + +class PasswordButton(Button): + """ + Custom password show/hide button. + """ + + def __init__(self, parent: Frame, password: Entry, **kwargs): + """ + Constructor. + + :param Frame parent: parent Frame + :param Entry password: associated password Entry field + """ + + self._ent_password = password + self._show_password = False + self._img_eyeon = ImageTk.PhotoImage(Image.open(ICON_EYEON)) + self._img_eyeoff = ImageTk.PhotoImage(Image.open(ICON_EYEOFF)) + + super().__init__( + parent, + width=20, + height=20, + command=self.toggle_password, + cursor=CLICK_CURSOR, + image=self._img_eyeon, + **kwargs, + ) + + def toggle_password(self): + """ + Toggle password visibility. + """ + + self._show_password = not self._show_password + if self._show_password: + self._ent_password["show"] = "" + self["image"] = self._img_eyeoff + else: + self._ent_password["show"] = "*" + self["image"] = self._img_eyeon + + # **************************************************************** -# End of Custom Class Extensions +# End of Custom Tkinter Class Extensions # **************************************************************** diff --git a/src/pygpsclient/nmea_handler.py b/src/pygpsclient/nmea_handler.py index 4e1dab05..329fc0c7 100644 --- a/src/pygpsclient/nmea_handler.py +++ b/src/pygpsclient/nmea_handler.py @@ -185,7 +185,7 @@ def _process_GLL(self, data: NMEAMessage): self.__app.gnss_status.utc = data.time # datetime.time self.__app.gnss_status.lat = data.lat self.__app.gnss_status.lon = data.lon - self.__app.gnss_status.fix = fix2desc("GLL", data.posMode) + # self.__app.gnss_status.fix = fix2desc("GLL", data.posMode) # only works for NMEA 4.10 and later... # self.__app.gnss_status.diff_corr = 1 if data.posMode == "D" else 0 diff --git a/src/pygpsclient/ntrip_client_dialog.py b/src/pygpsclient/ntrip_client_dialog.py index ce105fb2..3fb0961c 100644 --- a/src/pygpsclient/ntrip_client_dialog.py +++ b/src/pygpsclient/ntrip_client_dialog.py @@ -70,7 +70,7 @@ UIK, VALFLOAT, ) -from pygpsclient.helpers import MAXALT, get_mp_info +from pygpsclient.helpers import MAXALT, PasswordButton, get_mp_info from pygpsclient.socketconfig_ntrip_frame import SocketConfigNtripFrame from pygpsclient.strings import ( DLGTNTRIP, @@ -138,6 +138,7 @@ def __init__(self, app: Tk, *args, **kwargs): # pylint: disable=unused-argument self._ntrip_gga_lon = StringVar() self._ntrip_gga_alt = StringVar() self._ntrip_gga_sep = StringVar() + self._show_password = False self._settings = {} self._connected = False self._sourcetable = None @@ -213,7 +214,7 @@ def _body(self): textvariable=self._ntrip_user, state=NORMAL, relief="sunken", - width=40, + width=30, ) self._lbl_password = Label(self._frm_body, text=LBLNTRIPPWD) self._ent_password = Entry( @@ -221,9 +222,10 @@ def _body(self): textvariable=self._ntrip_password, state=NORMAL, relief="sunken", - width=40, + width=30, show="*", ) + self._btn_password = PasswordButton(self._frm_body, self._ent_password) self._lbl_ntripggaint = Label(self._frm_body, text=LBLNTRIPGGAINT) self._spn_ntripggaint = Spinbox( self._frm_body, @@ -322,11 +324,12 @@ def _do_layout(self): self._lbl_datatype.grid(column=2, row=10, padx=3, pady=3, sticky=W) self._spn_datatype.grid(column=3, row=10, padx=3, pady=3, sticky=W) self._lbl_user.grid(column=0, row=11, padx=3, pady=3, sticky=W) - self._ent_user.grid(column=1, row=11, columnspan=3, padx=3, pady=3, sticky=W) + self._ent_user.grid(column=1, row=11, columnspan=2, padx=3, pady=3, sticky=W) self._lbl_password.grid(column=0, row=12, padx=3, pady=3, sticky=W) self._ent_password.grid( - column=1, row=12, columnspan=3, padx=3, pady=3, sticky=W + column=1, row=12, columnspan=2, padx=3, pady=3, sticky=W ) + self._btn_password.grid(column=3, row=12, padx=3, pady=3, sticky=W) ttk.Separator(self._frm_body).grid( column=0, row=13, columnspan=5, padx=3, pady=3, sticky=EW ) @@ -379,6 +382,19 @@ def _reset(self): self._get_settings() self.set_controls(self._connected) + # def _on_hide_password(self): + # """ + # Toggle NTRIP password visibility. + # """ + + # self._show_password = not self._show_password + # if self._show_password: + # self._ent_password["show"] = "" + # self._btn_password["image"] = self._img_eyeoff + # else: + # self._ent_password["show"] = "*" + # self._btn_password["image"] = self._img_eyeon + def _on_update_config(self, var, index, mode): # pylint: disable=unused-argument """ Update in-memory configuration if setting is changed. diff --git a/src/pygpsclient/resources/iconmonstr-eye-lined-16.png b/src/pygpsclient/resources/iconmonstr-eye-lined-16.png new file mode 100644 index 00000000..8c97cb10 Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-eye-lined-16.png differ diff --git a/src/pygpsclient/resources/iconmonstr-eye-off-lined-16.png b/src/pygpsclient/resources/iconmonstr-eye-off-lined-16.png new file mode 100644 index 00000000..8cdaf32d Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-eye-off-lined-16.png differ diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py index 4bbb9f20..aba5763b 100644 --- a/src/pygpsclient/rover_frame.py +++ b/src/pygpsclient/rover_frame.py @@ -15,7 +15,6 @@ # pylint: disable=invalid-name, no-member -# import logging from random import randrange from tkinter import EW, NSEW, NW, SW, Frame, Label, Spinbox, StringVar, Tk, W @@ -63,7 +62,6 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): """ self.__app = app - # self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -274,13 +272,6 @@ def update_frame(self): self._canvas.create_circle(x, y, 3, fill=PNTCOL, outline=PNTCOL, tags=TAG_DATA) self.update_idletasks() - # MEMORY LEAK DEBUG - # tot = len(self._canvas.find_all()) - # tags = {} - # for tag in (TAG_DATA, TAG_GRID, TAG_WAIT, TAG_XLABEL): - # tags[tag] = len(self._canvas.find_withtag(tag)) - # self.logger.debug((tot, tags)) - def _set_range(self, distance: float): """ Set range and scale. diff --git a/src/pygpsclient/scatter_frame.py b/src/pygpsclient/scatter_frame.py index 4370ec7c..5aaff3d1 100644 --- a/src/pygpsclient/scatter_frame.py +++ b/src/pygpsclient/scatter_frame.py @@ -22,7 +22,6 @@ # pylint: disable=no-member -# import logging from tkinter import ( EW, HORIZONTAL, @@ -105,7 +104,6 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): """ self.__app = app - # self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -557,13 +555,6 @@ def update_frame(self): self.init_frame() self._update_plot() - # MEMORY LEAK DEBUG - # tot = len(self._canvas.find_all()) - # tags = {} - # for tag in (TAG_DATA, TAG_GRID, TAG_WAIT, TAG_XLABEL): - # tags[tag] = len(self._canvas.find_withtag(tag)) - # self.logger.debug((tot, tags)) - def _limit_points(self): """ Limit number of points in in-memory array. diff --git a/src/pygpsclient/serverconfig_dialog.py b/src/pygpsclient/serverconfig_dialog.py index 353191b9..6aba1dd5 100644 --- a/src/pygpsclient/serverconfig_dialog.py +++ b/src/pygpsclient/serverconfig_dialog.py @@ -72,6 +72,7 @@ ) from pygpsclient.helpers import ( MAXPORT, + PasswordButton, lanip, publicip, ) @@ -196,6 +197,7 @@ def __init__(self, app: Tk, *args, **kwargs): self._img_expand = ImageTk.PhotoImage(Image.open(ICON_EXPAND)) self._img_contract = ImageTk.PhotoImage(Image.open(ICON_CONTRACT)) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) + self._show_password = False self._fixed_lat_temp = 0 self._fixed_lon_temp = 0 self._fixed_hae_temp = 0 @@ -293,7 +295,7 @@ def _body(self): self._frm_advanced, textvariable=self.user, relief="sunken", - width=15, + width=25, ) self._lbl_password = Label( self._frm_advanced, @@ -303,8 +305,10 @@ def _body(self): self._frm_advanced, textvariable=self.password, relief="sunken", - width=15, + width=25, + show="*", ) + self._btn_password = PasswordButton(self._frm_advanced, self._ent_password) self._lbl_configure_base = Label( self._frm_advanced, text=LBLCONFIGBASE, @@ -956,6 +960,7 @@ def _on_update_basemode_svin(self): self._ent_user.grid(column=1, row=7, columnspan=2, padx=2, pady=1, sticky=W) self._lbl_password.grid(column=0, row=8, padx=2, pady=1, sticky=E) self._ent_password.grid(column=1, row=8, columnspan=2, padx=2, pady=1, sticky=W) + self._btn_password.grid(column=3, row=8, padx=2, pady=1, sticky=W) for wid in self._ent_fixedlat, self._ent_fixedlon, self._ent_fixedhae: wid["state"] = DISABLED @@ -978,6 +983,7 @@ def _on_update_basemode_fixed(self): self._ent_user.grid(column=1, row=6, columnspan=2, padx=2, pady=1, sticky=W) self._lbl_password.grid(column=0, row=7, padx=2, pady=1, sticky=E) self._ent_password.grid(column=1, row=7, columnspan=2, padx=2, pady=1, sticky=W) + self._btn_password.grid(column=3, row=7, padx=2, pady=1, sticky=W) self._lbl_duration.grid_forget() self._spn_duration.grid_forget() self._pgb_elapsed.grid_forget() @@ -1009,6 +1015,7 @@ def _on_update_basemode_disabled(self): self._ent_user.grid_forget() self._lbl_password.grid_forget() self._ent_password.grid_forget() + self._btn_password.grid_forget() def _on_update_posmode(self, var, index, mode): """ diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py index 6ee77298..4ee39b3f 100644 --- a/src/pygpsclient/spectrum_frame.py +++ b/src/pygpsclient/spectrum_frame.py @@ -15,7 +15,6 @@ # pylint: disable=no-member, unused-argument -# import logging from tkinter import ALL, CENTER, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, Tk, W from types import NoneType @@ -107,7 +106,6 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): """ self.__app = app # Reference to main application class - # self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -257,13 +255,6 @@ def update_frame(self): if self._spectrum_snapshot != []: self._update_plot(self._spectrum_snapshot, MODESNAP, RF_LIST_SNAPSHOT) - # MEMORY LEAK DEBUG - # tot = len(self._canvas.find_all()) - # tags = {} - # for tag in (TAG_DATA, TAG_GRID, TAG_WAIT, TAG_XLABEL, TAG_YLABEL): - # tags[tag] = len(self._canvas.find_withtag(tag)) - # self.logger.debug((tot, tags)) - def init_frame(self): """ Initialise spectrum chart. diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py index 4e75a8d9..5257a42a 100644 --- a/src/pygpsclient/stream_handler.py +++ b/src/pygpsclient/stream_handler.py @@ -89,7 +89,7 @@ UBXSIMULATOR, ) from pygpsclient.strings import WARNING -from pygpsclient.widget_state import VISIBLE, WDGCONSOLE +from pygpsclient.widget_state import FRAME, VISIBLE, WDGCHART, WDGCONSOLE class StreamHandler: @@ -472,6 +472,7 @@ def _process_message( protfilter = self.__app.protocol_mask tty = self.__app.configuration.get("ttyprot_b") console = self.__app.widget_state.state[WDGCONSOLE][VISIBLE] + chart = self.__app.widget_state.state[WDGCHART][VISIBLE] with self.__app.gnssstatus_lock: if isinstance(parsed_data, NMEAMessage) and protfilter & NMEA_PROTOCOL: @@ -511,6 +512,12 @@ def _process_message( if console and msgprot: self.__app.console_outqueue.put((raw_data, parsed_data, marker)) + # if chart is visible and protocol not filtered, place parsed data + # on chart input queue + if chart and msgprot: + if parsed_data is not None: + self.__app.chart_outqueue.put(parsed_data) + # if socket server is running and has clients, place raw data on socket # output queue if self.__app.server_status > 0: diff --git a/src/pygpsclient/sysmon_frame.py b/src/pygpsclient/sysmon_frame.py index 8b185a76..55ac6d71 100644 --- a/src/pygpsclient/sysmon_frame.py +++ b/src/pygpsclient/sysmon_frame.py @@ -14,7 +14,6 @@ :license: BSD 3-Clause """ -# import logging from tkinter import EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, Tk, W from pyubx2 import BOOTTYPE, UBXMessage @@ -72,7 +71,6 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): """ self.__app = app # Reference to main application class - # self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -263,13 +261,6 @@ def update_frame(self): except KeyError: # invalid sysmon-data or comms-data self.init_chart() - # MEMORY LEAK DEBUG - # tot = len(self._canvas.find_all()) - # tags = {} - # for tag in (TAG_DATA, TAG_WAIT): - # tags[tag] = len(self._canvas.find_withtag(tag)) - # self.logger.debug((tot, tags)) - def _chart_parm( self, xoffset: int, diff --git a/src/pygpsclient/widget_state.py b/src/pygpsclient/widget_state.py index 62b8f966..97802d42 100644 --- a/src/pygpsclient/widget_state.py +++ b/src/pygpsclient/widget_state.py @@ -119,12 +119,6 @@ def __init__(self): FRAME: "frm_roverview", VISIBLE: False, }, - WDGCHART: { - CLASS: ChartviewFrame, - FRAME: "frm_chartview", - VISIBLE: False, - COLSPAN: 2, - }, WDGSYSMON: { CLASS: SysmonFrame, FRAME: "frm_sysmon", @@ -135,5 +129,11 @@ def __init__(self): FRAME: "frm_attitudemon", VISIBLE: False, }, + WDGCHART: { + CLASS: ChartviewFrame, + FRAME: "frm_chartview", + VISIBLE: False, + COLSPAN: MAXSPAN, + }, # add any new widgets here }