'''
/** Original work Copyright 2024 damiano IU3QGD
 * https://to-be-defined
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.

 * Addition to the licence: NO company with more than 50employees can use this SW and you are NOT entitled to store this software 
 * on ANY storage that in some way attempts to monetize it (eg: github)
 * corporations and banks are too rich and too powerful, they MUST be split and ALL the money given back to the state

To add tk support in linux

apt-get install python3-tk


Hold classes and functions used to manage the gui

https://www.tcl.tk/

https://wiki.tcl-lang.org/page/custom+cursors


'''
# --------------------------------------------------------------------
# This would be a private class, but python is lacking object isolation
# Note that it is for all purposes a Tk, meaning that this is slightly redundant but may be useful in the future
# @param app_title: a string to display on the top window
# @param _icon_name: the name of a icon file to use for systray and/or app icon
# @param on_closing_fun function to call when the user tries to close
# @param enable_systray if true then enable systray 

# See
# https://docs.python.org/3/library/tkinter.html

from __future__ import annotations

import os
import re
from tkinter import ttk, BooleanVar, StringVar, IntVar, PhotoImage
import tkinter
from tkinter.filedialog import asksaveasfilename
from tkinter.ttk import Widget
from typing import  Mapping, Tuple
import typing

from glob_fun import frequency_1x_to_Mhz_str, frequency_1x_to_kHz_str, \
    frequency_mHz_dec_to_int_1x, frequency_kHz_dec_to_int_1x
import qpystat


table_row_selected_background_color='#adff00'

# =========================================================================
# the root should NOT be hidden, since it would be impossible to get it back
class JtkWinRoot(tkinter.Tk):

    # --------------------------------------------------------------------------------------------------------
    # the systray is all to be tested.... so, leave it false !    
    def __init__(self, stat : qpystat.Qpystat, app_title, icon_name, on_closing_fun, enable_systray = False ):  
        tkinter.Tk.__init__(self)

        self.title(app_title)
        
        self.stat=stat
        
        self.app_title = app_title
        self._icon_name = icon_name
        self.on_closing_fun = on_closing_fun

        # systray can be enabled only if icon name is given        
        self.enable_systray=icon_name and enable_systray
        
        self.w_resizable=True

        # I need a list of windows to hide/show when requested        
        self.hide_show_list : list[JtkWinToplevel] = []
#        self.hide_show_list.append(self)

        # does not work on linux
        #self.wm_attributes('-transparentcolor', '#010101')

        self.setRootIcon()
        self.enableSystray()
    
    # --------------------------------------------------------------------------------------------------------
    # just to make it clear what the purpose is, terminate the gui loop
    def gui_destroy(self):
        self.destroy()
    

    # ----------------------------------------------------------------------
    # attempt enable systray if it is what is requested
    # if no systray docking is requested add the required on_closing fun
    # the idea is that when the window is closing it is equivalent of writing quit
    # this is VERY important since it allows to save windows position before win destroy
    # If nothing is defined I define a self way to close the gui, so, it is in the right thread
    def enableSystray(self):
        if self.on_closing_fun:
            self.protocol("WM_DELETE_WINDOW", self.on_closing_fun)
        else:
            self.protocol("WM_DELETE_WINDOW", self.gui_destroy)
        
    # ---------------------------------------------------------------------
    # When on systray mode and a request to show is posted
    def systray_show_window(self, icon, item):
        
        # the icon must be stop and created again
        icon.stop()
        
        # this has to run on swing thread
        self.after_idle(self.after_idle_deiconify)
    
    # --------------------------------------------------------------------
    # callback on all windows to deiconify
    def after_idle_deiconify(self):
        
        for win in self.hide_show_list: 
            win.deiconify()
    
    # -----------------------------------------------------------------
    # When in systray mode and quit is selected we arrive at this point
    def systray_quit_window(self, icon, item):
        
        # the icon must be stop and created again
        icon.stop()
        
        if self.on_closing_fun:
            self.on_closing_fun()
        else:
            self.destroy()
    
    
    # ----------------------------------------------------------------------
    # This will set a nice icon to the root window and attempt to setup systray if requested
    # note that it is not possible to enable systray if there is no icon
    # To have windows to use this icon for the application you need to add this
    
    # myappid = 'prepperdock.release.v0' # arbitrary string
    # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)

    # apparently, there are quite some compatibility issues
    # the problem with this is that it needs PIL, another library to load...
    # imgicon = PhotoImage(file=self._icon_name)
    # self.call('wm', 'iconphoto', self, imgicon)  
    
    # somewhere in main
    def setRootIcon(self):
        
        if not self._icon_name:
            return 
        
        try:
            if os.name == 'nt':
                icon_fname = self._icon_name+'.ico'
                self.wm_iconbitmap(bitmap=icon_fname, default=icon_fname)
            else:
#                icon_fname = '@'+self._icon_name+'.xbm'
#                self.wm_iconbitmap(bitmap=icon_fname)
                icon_fname = self._icon_name+'.png'
                img = tkinter.PhotoImage(file=icon_fname)
                self.tk.call('wm', 'iconphoto', self, img)
                
        except Exception as _exc:
            print("setRootIcon: ",icon_fname,' exc ',_exc)
    
    # ----------------------------------------------------------------------
    # apparently, there is no way to know if a window is resizable or not 
    def setResizable(self, i_resizable : bool ):
        self.w_resizable=bool(i_resizable)
        # PYTHON how to call superclass methods
        super().resizable(self.w_resizable,self.w_resizable)

    # ----------------------------------------------------------------------
    def isResizable(self) -> bool:
        return self.w_resizable
        
    # ---------------------------------------------------------------------
    # Mostly here to remember how to run a function at later time in swing thread
    # NOTE that you name the function to call and then pass the arguments
    # eg: stat.runGuiIdle(stat.extension.setExtensionStatus,extnum,status)    
    def runIdle(self, func, *args ):
        self.after_idle(func, *args)
        
    # ---------------------------------------------------------------------
    # this is instead when you want to apply a specific delay    
    def runLater(self, ms, func, *args ):
        self.after(ms, func, *args)

    # ---------------------------------------------------------
    # attempt to set the geometry of the window, if the new geometry is bigger then current
    # the parameter is a dictionary where the geometry key should be
    # an example of what comes in is  "geometry": "1089x402+-1206+380" 
    # Also: The current geometry is available ONLY after GUI is UP, so, you have to adjust with a timer, after application is up and running
    # Also: Since this is the Root window it must NOT be possible to hide it on creation, you will not be able to get it back 
    def setGeometry(self, propd ):
        try:
            wg = WindowsGeometry(self.winfo_screenwidth(),self.winfo_screenheight(),self.geometry(),propd['window_geometry'])

            self.geometry(wg.risul_geometry)
                
        except Exception as exc:
            print("JtkWinRoot.setGeometry: ",exc)
            
    # ----------------------------------------------------
    # @return the equivalent geometry info as a dictionary
    def getGeometry(self) -> typing.Dict[str,str]:

        geodic=dict()
        
        geodic['window_geometry'] = self.geometry()
        geodic['window_state'] = self.state()
        
        return geodic
            

# =========================================================================
# the first two are the width and height
# an example of what comes in is  "geometry": "1089x402+-1206+380" 
# In Tk, a window's position and size on the screen are known as its geometry. A full geometry specification looks like this: widthxheight±x±y.
# Width and height (usually in pixels) are pretty self-explanatory. T
# he x (horizontal position) is specified with a leading plus or minus, so +25 means the left edge of the window should be 25 pixels from the left edge of the screen, 
# while -50 means the right edge of the window should be 50 pixels from the right edge of the screen. 
# Similarly, a y (vertical) position of +10 means the top edge of the window should be ten pixels below the top of the screen, 
# while -100 means the bottom edge of the window should be 100 pixels above the bottom of the screen.

# it is assumed that the desired position is the minimum that can be acceppted

class WindowsGeometry():

    # -----------------------------------------------------------
    def __init__(self, scr_width : int, scr_height : int, cur_geometry : str, want_geometry : str ):

        # assume that the result is the current geometry
        self.risul_geometry = cur_geometry

        # there MAY be a chance that current width and height is stupid, like 1x1+0+0
        # better take care of this and adjust the size, the position can remain 0+0, not an issue
        cur_width,cur_height,_cur_posx,_cur_posy=self.splitGeometry(cur_geometry)
        
        if cur_width < 100:
            cur_width=100
            
        if cur_height < 100:
            cur_height=100
        
        wa_width,wa_height,wa_posx,wa_posy=self.splitGeometry(want_geometry)
        
        if wa_width > cur_width:
            cur_width = wa_width
            
        if wa_height > cur_height:
            cur_height = wa_height
            
        #if cur_posx==0:
        # it actually set it correctly even on withdraw windows but the position goes to 0.0 on show
            
        scr_width = scr_width -20
        scr_height = scr_height -20

        # make sure I am dealing with a reasonably sized monitor, with positive values        
        if scr_width < 100:
            return
            
        # make sure I am dealing with a reasonably sized monitor, with positive values        
        if scr_height < 100:
            return

        # now, generally speaking I should check that the wanted position is kind of reasonably within the size of the screen
        # the logic is that the wanted position should fit within the geometry of the visible space 
        if abs(wa_posx) > scr_width:
            print ('illegal wa_posx '+str(wa_posx)+' scr_width '+str(scr_width))
            return

        if abs(wa_posy) > scr_height:
            print ('illegal wa_posy '+str(wa_posx)+' scr_width '+str(scr_height))
            return
    
        self.risul_geometry="%dx%d+%d+%d" % (cur_width,cur_height,wa_posx,wa_posy)
    
    # -----------------------------------------------------------
    # possibly duplicate, to make sure it is constant behaviour
    def splitGeometry(self, geom : str ):

        # the first blob is the dim_str
        first_array = geom.split('+')
    
        dim_str = first_array[0]
        g_posx = int(first_array[1])
        g_posy= int(first_array[2])
    
        dim_array = dim_str.split('x')
        
        width = int(dim_array[0])
        height = int(dim_array[1])
    
        return width,height,g_posx,g_posy

        
# ==================================================================================
# there should be an interface to declare if a class support GUI hiding and Show
# Window is a Toplevel window, since the GuiRoot should not be possible to be hidden
class GUI_hide_show_window():
    
    # -------------------------------------------------------------------
    # this normally maps to self.withdraw() of a toplevel    
    def GUI_hide_window(self):
        pass
    
    # -------------------------------------------------------------------
    # this normally maps to self.deiconify() of a toplevel    
    def GUI_show_window(self):
        pass

 
# ==================================================================================
# the point is that we always need a title and no window close, otherwise the object disappear
# So, a close will result into a window withdraw that can be reversed later, if needed
class JtkWinToplevel(tkinter.Toplevel):

    # -----------------------------------------------------------
    def __init__(self, title ):
        tkinter.Toplevel.__init__(self)
        
        # we need some sort of title
        self.title(title)

        # this is the way to disable the close button
        self.protocol("WM_DELETE_WINDOW", self.wm_delete_window_fun)

    # -----------------------------------------------------------
    # Called when someone click on the "close window"
    # You can get the object back to display using deiconify()
    def wm_delete_window_fun(self):
        self.withdraw()    

    
    # ---------------------------------------------------------
    # do NOT use the state command, it messes up the position !
    def _setWindowState(self, want_st : str ):
        if want_st == 'normal':
            self.deiconify()
        else:
            self.withdraw()
        
    # ---------------------------------------------------------
    # I need this to attempt to raise the window on top
    # this seems a reliable way....
    # this does not work self.attributes('-topmost',True)
    # this does not work self.after_idle(self.attributes,'-topmost',False)
    # cannot call withdraw since it will lose the size and position, apparently
    def deiconify(self):
        tkinter.Toplevel.deiconify(self)
        self.lift()
        self.focus_force()
            
    # ---------------------------------------------------------
    # attempt to set the geometry of the window
    # the parameter is a dictionary where the geometry key should be
    # an example of what comes in is  "geometry": "1089x402+-1206+380" 
    # Also, note that the current geometry is available ONLY after gui is UP, so, you have to 
    # adjust with a timer, after application is up and running 
    # do NOT use the state command to set window state, it just MESS UP the position !
    # this HAS to be protected in try catch sinceotherwise it will stop the parsing of more settings
    def setGeometry(self, propd ):
        try:
            wg = WindowsGeometry(self.winfo_screenwidth(),self.winfo_screenheight(),self.geometry(),propd['window_geometry'])
    
            self.geometry(wg.risul_geometry)
                
            self._setWindowState(propd['window_state'])
                
        except Exception as exc:
            print("tktoplevel.setGeometry: ",exc)
            
    # ----------------------------------------------------
    # return the equivalent geometry info as a dictionary
    def getGeometry(self) -> typing.Dict[str,str]:

        geodic=dict()
                    
        geodic['window_geometry'] = self.geometry()
        geodic['window_state'] = self.state()

        return geodic
            

        


# --------------------------------------------------------------------
# This class should behave similarly to a Swing BorderLayout
# the way it works is to create one of this passing the proper parent
# then declare how it will be "placed" in the parent with something like this
# panel.pack(fill=tk.BOTH,expand=True)
# NOTE that if you do not fill and expand then it will not resize when needed...
# then you can "add" the pieces you wish, using this logic
# ttk.Label(panel.pnorth, text="NORTH",borderwidth=1,relief="ridge").pack()
# ttk.Label(panel.pleft, text="LEFT",borderwidth=1,relief="ridge").pack()
# ttk.Label(panel.pcenter, text="CENTER",borderwidth=1,relief="ridge").pack()
# ttk.Label(panel.pright, text="RIGHT",borderwidth=1,relief="ridge").pack()
# ttk.Label(panel.psouth, text="SOUTH",borderwidth=1,relief="ridge").pack()
# Clearly, you have to tell to subparts HOW to pack, you may not wish to expand...
        
class BorderLayout(ttk.Frame):
    def __init__(self, container ):
        ttk.Frame.__init__(self, container)
        
        self.panel_list = []
        
        self.pnorth = ttk.Frame(self)
        self.pnorth.pack(fill='x',expand=False)
        self.panel_list.append(self.pnorth)

        # this should be a private variable, python is child crap
        self._fmiddle = ttk.Frame(self)
        self._fmiddle.pack(fill='both',expand=True)

        self.pleft = ttk.Frame(self._fmiddle)
        self.pleft.pack(fill='y',expand=False,side='right')
        self.panel_list.append(self.pleft)
        
        self.pcenter = ttk.Frame(self._fmiddle)
        self.pcenter.pack(fill='both',expand=True,side='right')
        self.panel_list.append(self.pcenter)
        
        self.pright = ttk.Frame(self._fmiddle)
        self.pright.pack(fill='y',expand=False,side='right')
        self.panel_list.append(self.pright)
        
        self.psouth = ttk.Frame(self)
        self.psouth.pack(fill='x',expand=False)
        self.panel_list.append(self.psouth)
        
    # --------------------------------------------------
    # Use as debugging method, show the border of the inner frames, so you can 
    # guess how the layout is happening    
    def showBorder(self):
        for panel in self.panel_list:
            panel['borderwidth'] = 1
            panel['relief'] = 'ridge'
        
# -----------------------------------------------------------
# Wrap a button class so I can add some magic                   
class JtkButtonText(ttk.Button):
    
    def __init__(self, win_parent : Widget, button_Text, **params ):
        ttk.Button.__init__(self, win_parent, text=button_Text, **params )
        
        
                                
                    
# -----------------------------------------------------------
# Wrap the image handling all in this class
# NOTE: this is a tkinter.Button since I need to easily hide border                    
class JtkButtonImage(tkinter.Button):
    def __init__(self, container, an_image : tkinter.PhotoImage, **params):

        #cursor_fname = '@'+str(getResourcesFname('hand-cursor.xbm'))

        # AHHHHH dammed snakes, it MUST be an instance variable since otherwise it get out of scope and it disappear!!!
        # subsample returns one pixel every the number given of pixels, so, 2 actually half the image
        self.btn_ico = an_image
        tkinter.Button.__init__(self, container, image=self.btn_ico, bd=1, relief='flat', cursor='hand2', **params )    
    
    # ------------------------------------------------------------
    # MUST be called from swing and with a good image
    def setImage(self, an_image : tkinter.PhotoImage ):
        # AH, DAMMED, I did forget this !
        self.btn_ico = an_image
        # because, otherwise, it gets garbage collected !
        self.config(image=self.btn_ico)
        



# =================================================================================
# Need to wrap the checkbox into something usable...
# it has to have a name and a simple boolean value that can be reliably read outside swing    
# @param on_changed_fun if given a function that has one parameter, the value of the checkbox
class JtkCheckbox(ttk.Checkbutton):    
    
    # -------------------------------------------------------------------------------
    # @parent the parent panel where to bind this checkbox
    # @param label_text the label to display to the user
    # @param on_changed_fun the fun to call when the user changes the checkbox
    def __init__(self, parent, label_text, on_changed_fun=None):
        
        # this value can be obtained outside the swing thread
        self.cvalBoolean = False
        
        self._on_changed_fun=on_changed_fun
        
        # this is needed since this is the logic used to propagate state in tkinter...
        # NOTE that I do NOT use the name since it may conflict if it is the same for different check
        self._boolelan_var = BooleanVar()
        
        # and this defines that a callback will be called when the value changes
        self._boolelan_var.trace_add("write", self._cboxCallback)
        
        # create the checkbox and bind it to the above _boolelan_var
        ttk.Checkbutton.__init__(self, parent, text=label_text, onvalue=True, offvalue=False, variable=self._boolelan_var, command=self._cbox_state_changed)     
        
    # --------------------------------------------------------------------
    # there is no need for a user changed variable since every time a user click it is a user changed
    def _cbox_state_changed(self):
        #print('state changed ',self._boolelan_var.get())
        if self._on_changed_fun:
            self._on_changed_fun(self._boolelan_var.get())
        
    # --------------------------------------------------------------------
    def _cboxCallback(self, *_args):
        self.cvalBoolean = self._boolelan_var.get()
            
    # --------------------------------------------------------------------
    def isChecked(self) -> bool:
        return self.cvalBoolean    
        
    # --------------------------------------------------------------------
    # when you need an int as return value
    def IsCheckedInt(self) -> int:
        if self.cvalBoolean:
            return 1
        else:
            return 0
        
    # --------------------------------------------------------------------
    # if you pass an empty or illegal value nothing will change
    def setChecked(self, abol ):
        try:
            self._boolelan_var.set(abol)
        except:
            pass

# ==================================================================================
# An entry bound to a textvariable
# Also the font is adjusted !!!

class TV_Entry(ttk.Entry):
    # -------------------------------------------------------------------------------
    # @parent the parent panel where to bind this fentry
    # @param the text variable to bind to
    def __init__(self, parent_panel, textvar, **kwargs ):

        ttk.Entry.__init__(self, parent_panel, textvariable=textvar, **kwargs)

        afont=ttk.Style().lookup("TEntry", "font")

        if afont:
            self['font'] = afont
        
    # -------------------------------------------------------------------------------
    # tk.DISABLED        
    def setEnabled(self, new_state : str ):
        self.config(state=new_state)

# ==================================================================================
# An entry with a Unit Measure
class TV_Entry_UM(ttk.Entry):
    # -------------------------------------------------------------------------------
    # @parent the parent panel where to bind this fentry
    # @param the text variable to bind to
    # @param the text to put as unit measure
    def __init__(self, parent, textvar, um_text : str, **kwargs ):

        ttk.Entry.__init__(self, parent, textvariable=textvar, **kwargs)

        afont=ttk.Style().lookup("TEntry", "font")

        if afont:
            self['font'] = afont





# ===============================================================================
# A frame that has PACK layout as defauult layout, with specific methods   
# this pack is always TOP, meaning that panels are stacked on top of each other  
class JtkPanelPackTop(ttk.Frame):
    # ---------------------------------------------------------------------------
    def __init__(self, parent, **params):
        ttk.Frame.__init__(self, parent, **params)

        self.p_pady = 2

    # ---------------------------------------------------------------------------
    # I probably need to mess it up in a different way to support passing override values
    # generally speaking, I wish for it to fill the whole x
    # a_panel.addItem(self._wiradio_log, fill='noth',expand=True)
    def addItem(self, jcomponent, **pad_params):

        if 'fill' in pad_params:
            jcomponent.pack(side='top', pady=self.p_pady,  **pad_params)
        else:
            jcomponent.pack(side='top' ,fill='x' ,pady=self.p_pady,  **pad_params)
            
        
# ===============================================================================
# A frame that has PACK layout as defauult layout, with specific methods     
# this always pack left
# you want to center some content ? put at the beginning and at the end of the line
# a_panel.addItem(JtkLabel(a_panel),fill='x',expand=True)
class JtkPanelPackLeft(ttk.Frame):
    
    # ---------------------------------------------------------------------------
    def __init__(self, parent, **params):
        ttk.Frame.__init__(self, parent, **params)

        self.p_padx=2

    # ---------------------------------------------------------------------------
    # I probably need to mess it up in a different way to support passing override values
    def addItem(self, jcomponent, **pad_params):

        jcomponent.pack(side='left', padx=self.p_padx, **pad_params)
        
        
        
# ===============================================================================
# a Frame that implements a layered panel, where you can raise subpanels to the top
# this is mostly to make it clear on how it is done...
class JtkPanelLayered(ttk.Frame):

    # ---------------------------------------------------------------------------
    def __init__(self, parent_panel, **params):
        ttk.Frame.__init__(self, parent_panel, **params)

        # The magic of tkraise works ONLY with the grid layout
        # the idea is that you sticky ="nsew" and put all Frames at 0,0
        # then, you can raise them to the top with tkraise
        # HOWEVER, the panel size is the MAX dimension of all subpanels....
        self.grid_rowconfigure(0, weight = 1)
        self.grid_columnconfigure(0, weight = 1)
        
    # ---------------------------------------------------------------------------
    # All items are stacked on top of each others
    # you show them by "raise()" 
    def addItem(self, jcomponent : ttk.Widget, **gridparams):
        
        jcomponent.grid(row = 0, column = 0, sticky ="nsew", **gridparams)
            

        
# ===============================================================================
# A frame that has grid layout as defauult layout, with specific methods     
# since this crap language cannot handle post increment
# idiots, reinventing the wheel as an hexagon   
# apanel = JtkPanelGrid( parent, borderwidth=1, relief='solid')
# to hide a component self.en_connection.grid_remove()

# the column with the name takes all the extra space, normally weight is zero
#a_panel.columnconfigure(1, weight=1)

class JtkPanelGrid(ttk.Frame):
    
    # ---------------------------------------------------------------------------
    def __init__(self, parent_panel, **params):
        ttk.Frame.__init__(self, parent_panel, **params)
        
        self._current_row=0;
        self._current_column=0;
        self._current_sticky='nsew'
        self.g_padx = 2
        self.g_pady = 1
        
    # ---------------------------------------------------------------------------
    # Call this when you wish to go to the next row
    def nextRow(self):
        self._current_row += 1
        self._current_column = 0
        
    # ---------------------------------------------------------------------------
    # If you ever need to go to the next column, without adding a component
    def nextColumn(self, how_many=1):
        self._current_column += how_many
                        
    # ---------------------------------------------------------------------------
    # this has the logic to allow adding / overriding the default values !
    # if you need an item to span multiple columns, use columnspan=2
    # you need to align right, use sticky='e'
    def addItem(self, jcomponent : ttk.Widget, **gridparams):

        if not 'sticky' in gridparams:
            gridparams['sticky']=self._current_sticky
            
        jcomponent.grid(row=self._current_row, column=self._current_column, **gridparams)
            
        if self.g_padx:
            jcomponent.grid(padx=self.g_padx)
            
        if self.g_pady:
            jcomponent.grid(pady=self.g_pady)

        self._current_column += 1
        
        
        
# ========================================================================
# This is logically the same as a Java interface, minus some type enforcement 
# Classes should implement this so they can be handled as a eeprom event listener

class EE_events_listener ():

    # ----------------------------------------------------------------
    def __init__(self ):
        pass
    
    # -------------------------------------------------------------------
    # This is called by all registered listeners for EEPROM read
    # The subclass must pick up the data it wishes and updated the gui
    # So, the method picks the block it wants and uses it !
    def updateGuiFromEeprom(self):
        pass
        
    # -------------------------------------------------------------------
    # MUST be overriden by children
    # Update the block to be sent to radio getting data from the GUI
    # It is duty of the method to mark the EEPROM block as dirty, if updated    
    # the method does not return anything since the result is stored in the block
    def update_EEblock_from_GUI(self):
        pass
        
        
# ====================================================================
# Mostly to wrap some idiotic behaviour        
# this shows how to pick up an optional parameter and strip it off !!!
# JtkCombo(a_panel, values=CONN_CHOICES)
class JtkCombo(ttk.Combobox):
    
    # ----------------------------------------------------------------
    def __init__(self, parent_frame : ttk.Frame, **varargs ):

        self._on_selected_fun=None

        self._string_var = StringVar(parent_frame)
        
        if 'on_selected_fun' in varargs:
            self.set_on_selected_fun(varargs['on_selected_fun'])
            # need to strip it off since otherwise combobox complains
            varargs.pop('on_selected_fun')

        ttk.Combobox.__init__(self, parent_frame, textvariable=self._string_var, **varargs)

        self.bind("<<ComboboxSelected>>", self._onComboBoxSelected)
        

        
    # ----------------------------------------------------------------
    # CAREFUL: the callback signature MUST be like the following one !
    # def _on_serial_combo_change(self, _v1, _v2, _v3):
    def setWriteCallback(self, a_fun ):
        self._string_var.trace('w', a_fun)
        
    def setStringVarValue(self, a_value : str ):
        self._string_var.set(a_value)
        
    def getStringVarValue(self):
        return self._string_var.get()
        
    # ----------------------------------------------------------------
    # this is not the best solution, but it is something
    def getSelectedIndex(self):
        c_index = self.current()
        
        if c_index < 0:
            c_index = 0
            
        return c_index

    def set_on_selected_fun(self, a_fun):
        self._on_selected_fun=a_fun
        
    # ---------------------------------------------------------------------------------
    # this handles when the combo is selected
    def _onComboBoxSelected(self, _event ):
        
        if not self._on_selected_fun:
            return
        
        self._on_selected_fun(self.getSelectedIndex())


        
# ======================================================================================
# I am mostly interested on the current index or setting the current index
# I probably need to mess with it     
# Good candidate to be a generic component   
class JtkComboIndexed(ttk.Combobox):
    
    # ---------------------------------------------------------------------------------
    def __init__(self, parent_panel : ttk.Frame, link_index : IntVar,  **varargs ):
        
        if not 'width' in varargs:
            varargs['width']=5
        
        ttk.Combobox.__init__(self, parent_panel, **varargs)
        
        self._link_idex = link_index
        
        self.bind("<<ComboboxSelected>>", self._onComboBoxSelected)
        
        self._link_idex.trace_add('write', self._onLinkIndexChange)
        
    # ---------------------------------------------------------------------------------
    # This handles the case when someone change the IntVar
    def _onLinkIndexChange(self, *args):
        try:
            self.current(self._link_idex.get())
        except:
            pass
        
    def set_on_selected_fun(self, a_fun):
        self._on_selected_fun=a_fun
        
    # ---------------------------------------------------------------------------------
    # this handles when the combo is selected
    def _onComboBoxSelected(self, _event ):
        self._link_idex.set(self.current())
        
        try:
            self._on_selected_fun()
        except Exception as _exc:
            pass
        
        
            

        
# --------------------------------------------------------------------------------------------------
# The idea here is to have a log window that has a scrollable part, up to a certain number of rows
# stat is the general holder of application vars, container is where to put this log
# NOTE: you MUST apply the proper layout of THIS class on the caller side
# Since python is shy on making things private, I just make this class a Frame
# Not only, but since you have to layout on parent you eventually end up on forwarding quite a few useless methods
# so, keep this to extend ttk.Frame, this is the only reasonable thing to do with python
# Note that since the printing is done in the swing thread, there is no need to hold a lock
class LogPanel(JtkPanelPackTop):
    
    # ------------------------------------------------------------------------
    # You can decide the max number of lines to show
    # to pad contente you can use padding="3p.3p" or you can set padx=3, pady=3
    # Note that the name will be used to save the log content into a file 
    def __init__(self, parent_panel, name='Log', maxlines=5000, wrap=True):

        JtkPanelPackTop.__init__(self, parent_panel)     

        self._maxlines=maxlines
        self.log_fname = name+".txt"
        
        # Create label inside the frame
        self.addItem(self._newNorthPanel(self, name))

        # create a text inside the frame
        self.addItem(self._newTextPanel(self),fill='both',expand=True)
        
        self.setWrap(wrap) 
        
    # -----------------------------------------------------------
    # Mostly a reminder that this method is available everywhere there is a 'frame'   
    def runOnGuiIdle(self, func, *args ):
        self.after_idle(func, *args)
        
    # ------------------------------------------------------------
    # create a new north panel
    # The layout of this panel is done at PARENT level
    def _newNorthPanel(self,parent, name):
        apanel = JtkPanelPackLeft(parent)

        # Create label inside the apanel
        apanel.addItem(JtkLabel(apanel, name, style='Log.TLabel'))

        # add a checkbox to pause the writing
        self._checkPaused=JtkCheckbox(apanel,'Pause')
        apanel.addItem(self._checkPaused)
    
        apanel.addItem(ttk.Button(apanel,text="Clear" , command=self._clickClear ))

        apanel.addItem(ttk.Button(apanel,text="Save" , command=self._clicSaveLog ))
    
        return apanel

    # --------------------------------------------------------------------------------------
    # https://docs.python.org/3/library/dialog.html
    def _clicSaveLog(self):
        try:
            
            files = [('Text Document', '.txt'), ('All Files', '*.*')]
            ffname = asksaveasfilename(parent=self, title="Save Log to File", initialfile=self.log_fname, filetypes = files, defaultextension = '.txt' ) 

            if not ffname:
                return 
            
            with open(ffname, 'wb') as fileo:
                astring=self.getTextString()
                fileo.write(astring.encode('UTF-8', 'replace'))
                
                self.println('--')
                self.println("saved file name "+str(ffname))
                
        except Exception as exc :
            self.println("_clicSaveLog "+str(exc))


    # --------------------------------------------------------------
    # When button is click, clear, it is thread safe since is in gui thread
    def _clickClear(self):
        self.text.delete('1.0', 'end')
        
    # ------------------------------------------------------------
    # this creates a new panel to hold both the text and scrollbar    
    def _newTextPanel(self,parent_panel):
        # The layout of this apanel is done at caller level
        apanel = ttk.Frame(parent_panel)
        
        # Create text widget and specify size.
        self.text = tkinter.Text(apanel, height = 10, width = 80)
        self.text.pack(fill='both',expand=True,side='left')
        
        scroll_bar = ttk.Scrollbar(apanel,orient='vertical',command=self.text.yview)
        scroll_bar.pack(fill='y', expand=False, side='left')
        
        self.text['yscrollcommand'] = scroll_bar.set
        
        self.setWrap(False)
                
        return apanel
        
        
    # --------------------------------------------------------------    
    def getTextString(self) -> str :
        astring = self.text.get("1.0",'end-1c')    
        return astring
        
    # -------------------------------------------------------------
    # true, enables wrap, false nowrap
    def setWrap (self, enabled):
        if enabled:
            self.text.config(wrap='word')    
        else:
            self.text.config(wrap='none')
        
    # --------------------------------------------------------------------
    # Use this one to actually print something
    # @param addnl if true a newline will be added after the given text
    def _println_gui (self, msg : str, addnl : bool ):

        if self._checkPaused.isChecked():
            return

        if not self.text.winfo_exists():
            # this may happen if in closing mode and messages want to be logged
            # if the widget is destroyed then print into the standard out
            # NOTE this will NOT happen if you call the window.destroy() of the main GUI from a gui idle callback
            print('logui wnot: ',msg)
            return 
        
        endpos=self.text.index('end')
        split=endpos.split('.')
        lines = int(split[0])
        
        if lines > self._maxlines :
            delindex=lines - self._maxlines 
            self.text.delete('1.0', "%d.end" % delindex)
            
        self.text.insert(tkinter.END, msg)
        
        if addnl:
            self.text.insert(tkinter.END, "\n")
        
        self.text.see(tkinter.END)

    # -------------------------------------------------------------------------
    # Always run the print on GUI idle, this solves a lot of issues on threading
    def println(self,msg):
        if self._checkPaused.isChecked():
            return

        if len(msg) > 0:
            self.runOnGuiIdle(self._println_gui,msg,True)

    # -------------------------------------------------------------------------
    # this does NOT add a newline at the end
    def write(self,msg):
        if self._checkPaused.isChecked():
            return

        if len(msg) > 0:
            self.runOnGuiIdle(self._println_gui,msg, False)

            
# ========================================================================
# https://gist.github.com/novel-yet-trivial/3eddfce704db3082e38c84664fc1fdf8/stargazers    
class VerticalScrolledFrame:
    """
    A vertically scrolled Frame that can be treated like any other Frame
    ie it needs a master and layout and it can be a master.
    :width:, :height:, :bg: are passed to the underlying Canvas
    :bg: and all other keyword arguments are passed to the inner Frame
    note that a widget layed out in this frame will have a self.master 3 layers deep,
    (outer Frame, Canvas, inner Frame) so 
    if you subclass this there is no built in way for the children to access it.
    You need to provide the controller separately.
    """
    def __init__(self, master, **kwargs):
        width = kwargs.pop('width', None)
        height = kwargs.pop('height', None)
        bg = kwargs.pop('bg', kwargs.pop('background', None))
        self.outer = ttk.Frame(master, **kwargs)

        self.vsb = tkinter.Scrollbar(self.outer, orient=tkinter.VERTICAL)
        self.vsb.pack(fill=tkinter.Y, side=tkinter.RIGHT)
        self.canvas = tkinter.Canvas(self.outer, highlightthickness=0, width=width, height=height, bg=bg)
        self.canvas.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True)
        self.canvas['yscrollcommand'] = self.vsb.set
        # mouse scroll does not seem to work with just "bind"; You have
        # to use "bind_all". Therefore to use multiple windows you have
        # to bind_all in the current widget
        self.canvas.bind("<Enter>", self._bind_mouse)
        self.canvas.bind("<Leave>", self._unbind_mouse)
        self.vsb['command'] = self.canvas.yview

        self.inner = ttk.Frame(self.canvas)
        # pack the inner Frame into the Canvas with the topleft corner 4 pixels offset
        self.canvas.create_window(4, 4, window=self.inner, anchor='nw')
        self.inner.bind("<Configure>", self._on_frame_configure)

        self.outer_attr = set(dir(tkinter.Widget))

    def __getattr__(self, item):
        if item in self.outer_attr:
            # geometry attributes etc (eg pack, destroy, tkraise) are passed on to self.outer
            return getattr(self.outer, item)
        else:
            # all other attributes (_w, children, etc) are passed to self.inner
            return getattr(self.inner, item)

    def _on_frame_configure(self, event=None):
        _x1, _y1, x2, y2 = self.canvas.bbox("all")
        height = self.canvas.winfo_height()
        self.canvas.config(scrollregion = (0,0, x2, max(y2, height)))

    def _bind_mouse(self, event=None):
        self.canvas.bind_all("<4>", self._on_mousewheel)
        self.canvas.bind_all("<5>", self._on_mousewheel)
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

    def _unbind_mouse(self, event=None):
        self.canvas.unbind_all("<4>")
        self.canvas.unbind_all("<5>")
        self.canvas.unbind_all("<MouseWheel>")

    def _on_mousewheel(self, event):
        """Linux uses event.num; Windows / Mac uses event.delta"""
        if event.num == 4 or event.delta > 0:
            self.canvas.yview_scroll(-1, "units" )
        elif event.num == 5 or event.delta < 0:
            self.canvas.yview_scroll(1, "units" )

    def __str__(self):
        return str(self.outer)
    
    
    
# ==================================================================================
# Column definition is stored into an object since I need to switch the sorting for a given column
# This is used in TreeView to display a table
class JtkTableColdef:

    # ----------------------------------------------------------------
    # Constructor
    def __init__(self, qcol_name : str , col_label : str , isNumeric, width, stretch=False ):
        self.col_name : str = qcol_name
        self.col_label : str =col_label
        self.sort_down=True                # means 4,3,2,1 ...
        self.isNumeric=isNumeric
        self.width=width
        self.stretch=stretch
        
    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return self.col_name+":"+self.col_label+":"+str(self.sort_down)
    
    
# ==================================================================================
# python table handling has a few quirks that are better wrapped up
# NOTE that a work panel is created as a child, and can be returned/used to layout
# the latest use is in wiradio_gui

class JtkTable():
    
    # ----------------------------------------------------------------------------------
    # You need a parent and a mapping for the columns    
    # Also, since 99% of times, you need a scrollbar, attach it to a panel, as it should be
    # list(qschcols_map.keys()) is needed since just qschcols_map.keys() produce a view of the map and the Treeview does not like it
    # takefocus=0 does not work to prevent the tree to steal focus from the Entry on tree filter
    # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/ttk-Treeview.html

    def __init__(self, parent_panel : ttk.Frame, col_list : Mapping[str,JtkTableColdef], **varargs ):
        
        # a frame is needed since the scrollbar takes up a place in the layout
        self._work_panel = ttk.Frame( parent_panel)

        self._work_panel.grid_rowconfigure(0, weight = 1)
        self._work_panel.grid_columnconfigure(0, weight = 1)
        
        self._col_list : Mapping[str,JtkTableColdef] = col_list
        self._tktable = ttk.Treeview(self._work_panel,columns=list(col_list.keys()), show='headings', **varargs)

        for colu in col_list.values():
            alambda=lambda _col=colu.col_name : self._table_sort_column( _col )
            self._tktable.heading(colu.col_name, text=colu.col_label, anchor='w', command=alambda )
            self._tktable.column(colu.col_name, minwidth=50, width=colu.width, stretch=colu.stretch)

        self._tktable.grid(row=0, column=0, sticky='nsew')
        
        self._scrollbar = ttk.Scrollbar(self._work_panel, orient=tkinter.VERTICAL, command=self._tktable.yview)
        self._tktable.configure(yscrollcommand=self._scrollbar.set)
        
        self._scrollbar.grid(row=0, column=1, sticky='ns')

    # ----------------------------------------------------------------------
    # When you need the inner workings
    def getComponent(self) -> ttk.Frame:
        return self._work_panel

    # ----------------------------------------------------------------------
    # When you need the inner workings
    def getTable(self) -> ttk.Treeview:
        return self._tktable

    # ----------------------------------------------------------------------
    # use parameter 0 to move to beginnit, 1 to the end
    def yview_moveto(self, fraction : float ):
        self._tktable.yview_moveto(fraction)
        
        
    # -------------------------------------------------------------------
    # Used to sort a table by a given column
    # it gets all children keys, create a list that gets sorted and then move items around        
    def _table_sort_column(self, tblcol_name ):
        a_coldef : JtkTableColdef = self._col_list[tblcol_name]
        
        # the following returns a tuple of all "keys", eg: ('I001', 'I002', 'I003', 'I004', 'I005'...... 
        # print(self._tktable.get_children())

        # self._tktable.set(key, tblcol_name)
        # returns the value associated with the given col name at the given key

        lista = [(self._tktable.set(key, tblcol_name), key) for key in self._tktable.get_children()]

        # if the column is numeric I need to convert it to int the value     
        # NOTE that I am sorting the VALUE, not the key !   
        if a_coldef.isNumeric:
            lista = [(int(val),key) for val,key in lista]
            
        lista.sort(reverse=a_coldef.sort_down)
    
        # reverse the sorting, for next time
        a_coldef.sort_down = not a_coldef.sort_down 
    
        # rearrange items in sorted positions. enumerate returns the index and the actual value
        for index, (_val, key) in enumerate(lista):
            self._tktable.move(key, '', index)

    # ----------------------------------------------------------------------
    # Clear the whole table    
    def clear(self):
        self._tktable.focus_set()

        children = self._tktable.get_children()

        for child in children:
            self._tktable.delete(child)
        
    # ----------------------------------------------------------------------
    def row_exist(self, row_id ) -> bool:
        return self._tktable.exists(str(row_id))
        
    # ----------------------------------------------------------------------
    def row_detach(self, row_id):
        self._tktable.detach(str(row_id))
                
    # ----------------------------------------------------------------------
    def row_delete(self, row_id):
        self._tktable.delete(str(row_id))

    # ----------------------------------------------------------------------
    # this will reattach/move the given iid at the beginning
    def row_reattach(self, row_id ):
        # params are the iid, the parent and where to move
        # if move is 0 then it is moved to beginning
        self._tktable.move(str(row_id),'',0)

    # ----------------------------------------------------------------------
    # this is appropriate for a table
    # I am not using the "index" since there is some problem when you want to sort ascending/descending
    def row_append(self, row_id, row_values, **varargs ):
        self._tktable.insert(parent='',index='end' ,iid=str(row_id) ,values=row_values, **varargs)
            
    # ------------------------------------------------------------------------
    # NOTE that the iid is really anything you wish !
    # @return the array of selected iid, can be multiple iid            
    def getSelected_iid(self) -> Tuple[str, ...]:
        return self._tktable.selection()
        
    def set_row(self, row_id, v_values, v_tags ):
        self._tktable.item(str(row_id), values=v_values, tags=v_tags)

    def see_row(self, row_id):
        self._tktable.see(str(row_id))
        
    def focus_row(self, row_id):
        self._tktable.focus(str(row_id))
        
        

















# ====================================================================
# mostly to have a decent name 
# a_panel.addItem(self._newBasicBeep(a_panel),"Base Beeps")

class JtkPanelTabbed(ttk.Notebook):
    
    # ----------------------------------------------------------------
    def __init__(self, parent : Widget, **varargs):
        ttk.Notebook.__init__(self, parent, **varargs)

    # ----------------------------------------------------------------
    def addItem(self, jcomponent, tab_name : str, **varargs ):
        super().add(jcomponent, text=tab_name, **varargs)



# ====================================================================
# mostly to have a decent name 
# to set background use background='red'
# to align to right, you need first to expand the label
# a_panel.addItem(JtkLabel(a_panel,"Press ENTER to send message", style='Header1.TLabel',anchor='e' ),fill='x', expand=True)
# to have a border for the label JtkLabel(a_panel,'BT OFF',borderwidth=2,relief='ridge')
class JtkLabel(ttk.Label):

    # ----------------------------------------------------------------
    def __init__(self, parent, t_label='', **varargs):
        ttk.Label.__init__(self, parent, text=t_label,  **varargs)

    # ----------------------------------------------------------------
    def setText(self, msg : str ):
        self.configure(text=msg)
        
    # ----------------------------------------------------------------
    def clear(self):
        self.configure(text='')


# ====================================================================
# mostly to have a decent name 
class ImageLabel(ttk.Label):

    # ----------------------------------------------------------------
    def __init__(self, parent, a_image : PhotoImage, **varargs):
        self._a_image = a_image
        
        ttk.Label.__init__(self, parent, image=self._a_image,  **varargs)

    # ----------------------------------------------------------------
    def updateImage (self,a_image : PhotoImage ):
        self._a_image = a_image

        self.configure(image=a_image)
        


















# ==========================================================================================
# This is the parent of the converter
class TV_EntryFrequency_generic_1x(JtkPanelPackLeft):
    
    # -------------------------------------------------------------------------------
    # NOTE that the editing variable is a string BUT the content variable is an int
    # @parent the parent panel where to bind this fentry
    def __init__(self, parent_panel : ttk.Frame, **varargs ):
        JtkPanelPackLeft.__init__(self, parent_panel)
        
        # no focus out fun, by default
        self._on_focus_out_fun=None
        
        if 'on_focus_out_fun' in varargs:
            self.set_on_focus_out_fun(varargs['on_focus_out_fun'])
            varargs.pop('on_focus_out_fun')

        self._entry_intvar=IntVar(self, **varargs)

        # needed to push from intvar set to the entry
        self._entry_intvar.trace_add('write', self._onIntvarChanged)

        # this is the actual edited value, a string, initial value from provided var
        self._entry_editing = tkinter.StringVar(self,value=self._specific_frequency_1x_to_string())
        
        # this is needed to arrive to the validator function
        self.v_command = (parent_panel.register(self._onValidate), '%P', '%V')

        
        # ok let me create the entry
        self._entry_kHz = TV_Entry(self, self._entry_editing, width=11, validate='all', validatecommand=self.v_command)
        self._entry_kHz.bind("<FocusOut>", self._entry_focus_out)

        self.addItem(self._entry_kHz)

        # apparently, it is not possible to venter vertical the label.... 
        self.addItem(JtkLabel(self,self._specific_get_umisura(),style='UMisura.TLabel'))
        
        # initially, the value is not user changed
        self._is_user_changed=False
        
        
    # ----------------------------------------------------------------------------
    def set_user_changed (self, is_changed : bool ):
        self._is_user_changed=is_changed
        
    # ----------------------------------------------------------------------------
    # If needed you can set a focus out fun here
    def set_on_focus_out_fun(self, on_focus_out_fun ):
        self._on_focus_out_fun = on_focus_out_fun

    # ----------------------------------------------------------------------------
    # On focus out I should call the appropriate fun
    def _entry_focus_out(self, event):
        print(event,' ',self._is_user_changed)
        
        if not self._is_user_changed:
            return
    
        if not self._on_focus_out_fun:
            return 
            
        self._on_focus_out_fun(self._entry_intvar.get())

    # ---------------------------------------------------------------------------
    # children must override this with proper label, like
    # kHz
    def _specific_get_umisura(self):
        return 'UM'

    # ---------------------------------------------------------------------------
    # children must override this with proper conversion
    # eg: frequency_1x_to_kHz_str(self._entry_intvar.get()
    def _specific_frequency_1x_to_string(self):
        return "0.0"

    # --------------------------------------------------------------------
    # change the raw value ONLY if it is actually changed
    def _set_value_if_changed(self, new_value : int ):
        v_now = self._entry_intvar.get()
        
        if v_now != new_value:
            self._entry_intvar.set(new_value)

    # ---------------------------------------------------------------
    # this is the system setting the value, NOT the user
    def set_value(self, f_1x : int ):
        self._entry_intvar.set(f_1x)
        self._is_user_changed=False

    def get_value(self ) -> int:
        return self._entry_intvar.get()

    # ---------------------------------------------------------------
    # this is for when the interface var changes, easy
    def _onIntvarChanged(self, *args):
        self._entry_editing.set(self._specific_frequency_1x_to_string())
        self._is_user_changed=True
        
    # --------------------------------------------------------------
    # should split the value and reassemble it
    # children MUST redefine this
    def _onFocusOutValidate(self, v_val : str ) ->bool:
        v_split=v_val.split('.')

        f_kHz = 0
        f_decs = 0

        if v_split[0]:            
            f_kHz = int(v_split[0])
        
        if len(v_split) > 1:
            f_decs=frequency_kHz_dec_to_int_1x(v_split[1])
            
        total = f_kHz*100+f_decs
        
        self._set_value_if_changed(total)
            
        return True


    # ------------------------------------------------------------------
    # children MUST redefine this
    def _onKeyTyped(self, v_val : str ) -> bool:
        
        if not v_val:
            return True

        v_split=v_val.split('.')

        if len(v_split) == 1:
            return bool(re.search("^[0-9]{1,4}$", v_val))
        elif len(v_split) == 2:
            # if ONE dot is present, accept a number of three digit , a dot, and a number up to TWO digits
            return bool(re.search("^[0-9]{0,4}\\.[0-9]{0,2}$", v_val))

        # nothing else is allowed
        return False 
    
    # ---------------------------------------------------------------
    # NOTE that this is applied to the string variable, NOT the int one !
    def _onValidate(self, n_val, operation) -> bool:
        if operation == 'focusout':
            return self._onFocusOutValidate(n_val)
        elif operation== 'key':
            return self._onKeyTyped(n_val)
        
        return True











# ==========================================================================================
# For comments, see the MHz version !
class TV_EntryFrequency_kHz_1x(TV_EntryFrequency_generic_1x):
    
    # -------------------------------------------------------------------------------
    # NOTE that the editing variable is a string BUT the content variable is an int
    # @parent the parent panel where to bind this fentry
    def __init__(self, parent_panel : ttk.Frame, **kwargs ):
        TV_EntryFrequency_generic_1x.__init__(self, parent_panel, **kwargs)

    # ---------------------------------------------------------------------------
    # children must override this with proper label, like
    # kHz
    def _specific_get_umisura(self):
        return 'kHz'

    # ---------------------------------------------------------------------------
    # children must override this with proper conversion
    # eg: frequency_1x_to_kHz_str(self._entry_intvar.get()
    def _specific_frequency_1x_to_string(self) -> str:
        return frequency_1x_to_kHz_str(self._entry_intvar.get())




# ==========================================================================================
# vedere https://tkdocs.com/tutorial/widgets.html#entry
# https://www.tcl.tk/man/tcl8.5/TkCmd/ttk_entry.htm
# %V The validation condition that triggered the callback (key, focusin, focusout, or forced).
# %P In prevalidation, the new value of the entry if the edit is accepted. In revalidation, the current value of the entry.
# %d Type of action: 1 for insert prevalidation, 0 for delete prevalidation, or -1 for revalidation.
# I want to have only the int interface and hide all the crap, so, when the content changes (focus out)
# the current value will be parsed and converted to proper int value
class TV_EntryFrequency_MHz_1x(TV_EntryFrequency_generic_1x):

    # -------------------------------------------------------------------------------
    # @parent the parent panel where to bind this fentry
    # @param the text variable to bind to
    def __init__(self, parent_panel : ttk.Frame, **kwargs ):
        TV_EntryFrequency_generic_1x.__init__(self, parent_panel, **kwargs)

    # ---------------------------------------------------------------------------
    # children must override this with proper label, like
    # kHz
    def _specific_get_umisura(self):
        return 'MHz'

    # ---------------------------------------------------------------------------
    # children must override this with proper conversion
    # eg: frequency_1x_to_kHz_str(self._entry_intvar.get()
    def _specific_frequency_1x_to_string(self) -> str:
        return frequency_1x_to_Mhz_str(self._entry_intvar.get())
        
    # --------------------------------------------------------------
    # should split the value and reassemble it
    def _onFocusOutValidate(self, v_val : str ) ->bool:
        v_split=v_val.split('.')

        f_mHz  = 144
        f_decs = 0

        if v_split[0]:            
            f_mHz = int(v_split[0])
        
        if len(v_split) > 1:
            f_decs=frequency_mHz_dec_to_int_1x(v_split[1])
            
        total = f_mHz*100000+f_decs
        
        self._set_value_if_changed(total)
            
        return True


    # ------------------------------------------------------------------
    # https://docs.python.org/3/library/re.html
    # I want some digits, one dot and some digits        
    # the {0,5} means repeat the previous "rule" zero to five times
    # the ^ means from the beginning of line
    # the $ means to the end of line
    def _onKeyTyped(self, v_val : str ) -> bool:
        
        # copy paste need to clear the value
        if not v_val:
            return True

        # handling is different if there is a dot or not
        v_split=v_val.split('.')

        if len(v_split) == 1:
            # if NO dot is present, accept a number of three digit max
            return bool(re.search("^[0-9]{1,3}$", v_val))
        elif len(v_split) == 2:
            # if ONE dot is present, accept a number of three digit , a dot, and a number up to five digits
            return bool(re.search("^[0-9]{0,3}\\.[0-9]{0,5}$", v_val))

        # nothing else is allowed
        return False 
    
# =========================================================
# I need an object since a pixel is not merely a number
class QsPPM_rgb():

    # --------------------------------------------------------
    # parent is needed to know what is current background and foreground color
    def __init__(self, parent : QsPPM_img ):
        self.p_parent = parent
        
        # initialize with parent background color, need to be a copy
        self.pix_rgb = bytearray(parent.bg_color)

    # ---------------------------------------------------------
    # need to be a copy
    def clear(self):
        self.pix_rgb = bytearray(self.p_parent.bg_color)
                                
    # --------------------------------------------------------
    # example
    def set_rgb(self,r_pix : int, g_pix : int, b_pix : int):
        self.pix_rgb[0]=r_pix
        self.pix_rgb[1]=g_pix
        self.pix_rgb[2]=b_pix

    # -----------------------------------------------------
    # if v_on is true then the foreground will be set, false will set the background
    def set_bitmap(self, v_on : bool):
        if v_on:
            self.pix_rgb = bytearray(self.p_parent.fg_color)
        else:
            self.pix_rgb = bytearray(self.p_parent.bg_color)
        
    # -----------------------------------------------------
    def to_bytearray(self) -> bytearray:
        return self.pix_rgb
    

# ==============================================================
# this wraps a row as an array of rgb objects
class QsPPM_row():

    # ----------------------------------------------------------
    def __init__(self, parent : QsPPM_img  ):
        self.p_parent = parent
        
        # a row is made of so many cols pixels
        self.r_cols : list[QsPPM_rgb] = []
        
        for _col in range(0,self.p_parent.pix_cols):
            self.r_cols.append(QsPPM_rgb(parent))    
        
    # ----------------------------------------------------------
    def set_bitmap(self, col_idx : int, v_on : bool ):        
        
        rgb : QsPPM_rgb = self.r_cols[col_idx]
        rgb.set_bitmap(v_on)
         
    # ----------------------------------------------------------
    def clear(self):
        
        for arow in self.r_cols:
            rgb : QsPPM_rgb = arow
            rgb.clear()
    
    # ----------------------------------------------------------
    # append this row of data to the given bytearray
    def append_to(self, raster : bytearray ):
        
        for arow in self.r_cols:
            rgb : QsPPM_rgb = arow

            raster.extend(rgb.to_bytearray())

# -------------------------------------------------------------
# yes, it is verbose, because KISS
# this wrap a PPM image, meaning that it can generate a PPM image
# https://netpbm.sourceforge.net/doc/ppm.html
# every pixel is a triplet of RGB
class QsPPM_img():
    
    # ----------------------------------------------------------
    # You MUST tell how big the image is
    # by default, the max value for red, green, blue is 255
    def __init__(self, pix_rows : int, pix_cols : int):
        self.pix_rows = pix_rows
        self.pix_cols = pix_cols
        
        self.bg_color = bytearray([255, 214, 13])   
        self.fg_color = bytearray([58, 35, 9])
        
        self.r_rows : list[QsPPM_row] = []
        
        for _col in range(0,self.pix_rows):
            self.r_rows.append(QsPPM_row(self))    

    # ----------------------------------------------------------
    # set a pixel on or off, meaning foreground or background
    def set_bit_pixel(self, row_idx : int, col_idx : int, v_on : bool ):        
        
        row : QsPPM_row = self.r_rows[row_idx]
        row.set_bitmap(col_idx, v_on)
    

    # ----------------------------------------------------------
    # what is returned is a bytearray
    # https://netpbm.sourceforge.net/doc/ppm.html
    def to_PPM_data(self) -> bytearray:
        
        # this is the header string as defined in the standard
        h_str='P6 {:d} {:d} 255 '.format(self.pix_cols,self.pix_rows)
        
        # the resulting content is a bytearray
        ppm_res : bytearray = bytearray(h_str,'ascii')
        
        for rawtype in self.r_rows:
            a_row : QsPPM_row = rawtype
            a_row.append_to(ppm_res)    
            
        return ppm_res
        
    # ----------------------------------------------------------
    def __str__(self) -> str:
        return "PPM {:d}x{:d}".format(self.pix_cols,self.pix_rows)


# ==================================================================
# I need a simple way to handle a circle, since the original is an oval....
# https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas-methods.html
class CanvasCircle():

    # ---------------------------------------------------------------
    # I want to define the center of the circle
    def __init__(self, canvas : tkinter.Canvas, p_x : int , p_y : int , radius_pix : int, **varargs ):
    
        self._canvas = canvas    
        self._radius = radius_pix
        self._diameter = radius_pix * 2
        
        x0 = p_x - radius_pix
        y0 = p_y - radius_pix
        
        x1 = p_x + radius_pix
        y1 = p_y + radius_pix
        
        self.obj_id = canvas.create_oval(x0,y0,x1,y1, **varargs)
    
    # ----------------------------------------------------------------
    def move_to(self, p_x : int, p_y : int ):
        
        x0 = p_x - self._radius
        y0 = p_y - self._radius
        
        x1 = p_x + self._radius
        y1 = p_y + self._radius
        
        self._canvas.coords(self.obj_id, x0,y0,x1,y1)
        
        
    # ----------------------------------------------------------------
    # see https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/create_oval.html
    # eg outline='green'
    def set_option(self, **varargs):
        
        self._canvas.itemconfigure(self.obj_id, **varargs)
    
    
    