diff --git a/.zenodo.json b/.zenodo.json index a93ac17..04fc3fd 100755 --- a/.zenodo.json +++ b/.zenodo.json @@ -38,5 +38,5 @@ } ], "access_right": "open", - "description": "

ncvue is a minimal GUI for a quick view of netCDF files. It is aiming to be a drop-in replacement for ncview, being slightly more general than ncview, which targets maps. If ncvue is used with maps, it supports mostly structured grids, more precisely the grids supported by cartopy.

\n\n

ncvue is a Python script that can be called from within Python or as a command line tool. It is not supposed to produce publication-ready plots but rather provide a quick overview of the netCDF file.

\n\n

The complete documentation for ncvue is available from: https://mcuntz.github.io/ncvue/

\n\n

The current version features three panels, that means three different plotting styles: Scatter/Line plots, Contour Plots, and Maps, with extensive tooltips on buttons, sliders, entry boxes, spinboxes, and menus. It includes the possibility to open and change netCDF files within ncvue. The netCDF file will be changed in all panels of the primary window and any secondary window once focus changes on the panel or window. Graphical documentation exists on Github Pages.

\n\n

The current version also provides two standalone applications on macOS and on Windows that come with all necessary libraries to run ncvue, including Python. Links to the installers are given on Github and in the documentation.

\n\n

Version 3.6 added different designs on macOS, Windows and Linux.

\n\n

Version 4.0 moved to a new project structure, using pyproject.toml, automatic versioning, src layout, and Github actions for continuous integration.

\n\n

Version 4.1 ported ncvue to conda-forge.

\n\n

Version 4.2 allowed groups in netcdf files.

\n\n

Version 4.3 allowed multiple netcdf files.

\n\n

Version 4.4 added borders, rivers, and lakes.

" + "description": "

ncvue is a minimal GUI for a quick view of netCDF files. It is aiming to be a drop-in replacement for ncview, being slightly more general than ncview, which targets maps. If ncvue is used with maps, it supports mostly structured grids, more precisely the grids supported by cartopy.

\n\n

ncvue is a Python script that can be called from within Python or as a command line tool. It is not supposed to produce publication-ready plots but rather provide a quick overview of the netCDF file.

\n\n

The complete documentation for ncvue is available from: https://mcuntz.github.io/ncvue/

\n\n

The current version features three panels, that means three different plotting styles: Scatter/Line plots, Contour Plots, and Maps, with extensive tooltips on buttons, sliders, entry boxes, spinboxes, and menus. It includes the possibility to open and change netCDF files within ncvue. The netCDF file will be changed in all panels of the primary window and any secondary window once focus changes on the panel or window. Graphical documentation exists on Github Pages.

\n\n

The current version also provides two standalone applications on macOS and on Windows that come with all necessary libraries to run ncvue, including Python. Links to the installers are given on Github and in the documentation.

\n\n

Version 3.6 added different designs on macOS, Windows and Linux.

\n\n

Version 4.0 moved to a new project structure, using pyproject.toml, automatic versioning, src layout, and Github actions for continuous integration.

\n\n

Version 4.1 ported ncvue to conda-forge.

\n\n

Version 4.2 allowed groups in netcdf files.

\n\n

Version 4.3 allowed multiple netcdf files.

\n\n

Version 4.4 added borders, rivers, and lakes.

\n\n

Version 5.0 uses CustomTkinter if installed.

" } diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ddf80c9..430f95d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,8 @@ Changelog --------- -v4.5 (??? 2024) +v5.0 (Dec 2024) + * Use CustomTkinter if installed. * Add Quit button. * Correct datetime formatting in coordinate printing. * Move from token to trusted publisher on PyPI. diff --git a/MANIFEST.in b/MANIFEST.in index 22250e5..05b3b0d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ prune ** include src/ncvue/*.py include src/ncvue/images/*.png graft src/ncvue/themes/azure-2.0 +graft src/ncvue/themes/customtkinter graft tests include AUTHORS.rst CHANGELOG.rst CONTRIBUTING.rst README.rst LICENSE pyproject.toml setup.cfg diff --git a/README.rst b/README.rst index 80b701f..8a7f51a 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,7 @@ replacement for ncview_ and panoply_. :target: https://github.com/mcuntz/ncvue/actions/workflows/main.yml :alt: Build status + About ncvue ----------- @@ -52,6 +53,7 @@ The complete documentation for ``ncvue`` is available from: https://mcuntz.github.io/ncvue/ + Quick usage guide ----------------- @@ -100,6 +102,7 @@ or using `ipython --gui tk`. `Open`. You only have to do this once. It will open like any other application the next times. + General layout ^^^^^^^^^^^^^^ @@ -123,6 +126,7 @@ options. You can always choose another panel on top, and open another, identical window for the same netCDF file with the button "New Window" on the top right. + Map panel ^^^^^^^^^ @@ -159,6 +163,7 @@ longitude of your data, which is automatically detected if "central lon" is set to None. Setting "central lon" to the central longitude of the input data normally eliminates the problem. + Scatter/Line panel ^^^^^^^^^^^^^^^^^^ @@ -195,6 +200,7 @@ but more than 11.1 s when using the `datetime` variable. :align: center :alt: Example of multiple lines in the Scatter/Line panel + Contour panel ^^^^^^^^^^^^^ @@ -211,6 +217,7 @@ This produces also either pseudocolor plots ('mesh' ticked) or filled contour plots ('mesh' unticked) just as the Map panel but without any map projection. + Installation ------------ @@ -241,6 +248,7 @@ installing, for example, Miniconda_: See the installation instructions_ in the documentation_ for more information on installing `Cartopy` and ``ncvue with pip``. + License ------- @@ -249,7 +257,8 @@ for details. Copyright (c) 2020-2024 Matthias Cuntz -``ncvue`` uses the Azure_ 2.0 theme by rdbende_ on Linux and Windows. +``ncvue`` uses CustomTkinter_ if installed. Otherwise it uses the +Azure_ 2.0 theme by rdbende_ on Linux and Windows. .. Standalone applications are produced with `cx_Freeze`_, currently @@ -257,7 +266,7 @@ Copyright (c) 2020-2024 Matthias Cuntz The project structure of ``ncvue`` was very originally based on a template_ provided by `Sebastian Müller`_ but has evolved -considerably. +considerably since. Different netCDF test files were provided by `Juliane Mai`_. @@ -284,3 +293,4 @@ Different netCDF test files were provided by `Juliane Mai`_. .. _panoply: https://www.giss.nasa.gov/tools/panoply/ .. _rdbende: https://github.com/rdbende .. _template: https://github.com/MuellerSeb/template +.. _CustomTkinter: https://customtkinter.tomschimansky.com/ diff --git a/setup.cfg b/setup.cfg index 24b6ef6..7d2c202 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,6 +86,7 @@ src/ncvue = themes/azure-2.0/theme/*.tcl themes/azure-2.0/theme/light/*.png themes/azure-2.0/theme/dark/*.png + themes/customtkinter/* [options.extras_require] doc = diff --git a/src/ncvue/__init__.py b/src/ncvue/__init__.py index ac89012..50b6006 100644 --- a/src/ncvue/__init__.py +++ b/src/ncvue/__init__.py @@ -65,7 +65,7 @@ * v4.4.1 Move themes and images back to src/ncvue, Feb 2024, Matthias Cuntz * v4.4.2 Use matplotlib.colormaps[name], Jul 2024, Matthias Cuntz * v4.4.3 Use draw_idle for faster animation, Jul 2024, Matthias Cuntz - * v4.5 Add Quit button, Nov 2024, Matthias Cuntz + * v5.0 Use CustomTkinter if installed, Nov 2024, Matthias Cuntz """ # helper functions diff --git a/src/ncvue/ncvcontour.py b/src/ncvue/ncvcontour.py index 0410d29..8200024 100644 --- a/src/ncvue/ncvcontour.py +++ b/src/ncvue/ncvcontour.py @@ -19,24 +19,36 @@ ncvContour History - * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) - * Open new netcdf file, communicate via top widget, - Jan 2021, Matthias Cuntz - * Write coordinates and value on bottom of plotting canvas, - May 2021, Matthias Cuntz - * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz - * Allow groups in netcdf files, Jan 2024, Matthias Cuntz - * Allow multiple netcdf files, Jan 2024, Matthias Cuntz - * Move images/ directory from src/ncvue/ to src/ directory, - Jan 2024, Matthias Cuntz - * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz - * Add Quit button, Nov 2024, Matthias Cuntz + * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) + * Open new netcdf file, communicate via top widget, + Jan 2021, Matthias Cuntz + * Write coordinates and value on bottom of plotting canvas, + May 2021, Matthias Cuntz + * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz + * Allow groups in netcdf files, Jan 2024, Matthias Cuntz + * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Move images/ directory from src/ncvue/ to src/ directory, + Jan 2024, Matthias Cuntz + * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz + * Add Quit button, Nov 2024, Matthias Cuntz + * Use CustomTkinter if installed, Nov 2024, Matthias Cuntz """ import os import sys import tkinter as tk -import tkinter.ttk as ttk +try: + from customtkinter import CTkFrame as Frame + from customtkinter import CTkButton as Button + from customtkinter import CTkLabel as Label + from customtkinter import CTkComboBox as Combobox + ihavectk = True +except ModuleNotFoundError: + from tkinter.ttk import Frame + from tkinter.ttk import Button + from tkinter.ttk import Label + from tkinter.ttk import Combobox + ihavectk = False import netCDF4 as nc import numpy as np from .ncvutils import clone_ncvmain, format_coord_contour, selvar @@ -60,7 +72,7 @@ __all__ = ['ncvContour'] -class ncvContour(ttk.Frame): +class ncvContour(Frame): """ Panel for contour plots. @@ -103,193 +115,257 @@ def __init__(self, master, **kwargs): self.maxdim = self.top.maxdim self.cols = self.top.cols + # selections and options + columns = [''] + self.cols + + allcmaps = plt.colormaps() + self.cmaps = [ i for i in allcmaps if not i.endswith('_r') ] + self.cmaps.sort() + # self.imaps = [ tk.PhotoImage(file=os.path.dirname(__file__) + + # '/../images/' + i + '.png') + # for i in self.cmaps ] + bundle_dir = getattr(sys, '_MEIPASS', + os.path.abspath(os.path.dirname(__file__))) + self.imaps = [ tk.PhotoImage(file=bundle_dir + + '/images/' + i + '.png') + for i in self.cmaps ] + if ihavectk: + # width of combo boxes in px + combowidth = 288 + # widths of entry widgets in px + ewsmall = 20 + ewmed = 45 + ewbig = 70 + # pad between label and entry + padx = 5 + # width of animation and variables buttons + bwidth = 35 + # width of projections menu + mwidth = 70 + else: + # width of combo boxes in characters + combowidth = 33 + # widths of entry widgets in characters + ewsmall = 3 + ewmed = 4 + ewbig = 7 + # pad between label and entry (not used) + padx = 5 + # width of animation and variables buttons + bwidth = 1 + # width of projections menu + mwidth = 13 + # new window - self.rowwin = ttk.Frame(self) + self.rowwin = Frame(self) self.rowwin.pack(side=tk.TOP, fill=tk.X) - self.newfile = ttk.Button(self.rowwin, text="Open File", - command=self.newnetcdf) + self.newfile = Button(self.rowwin, text='Open File', + command=self.newnetcdf) self.newfile.pack(side=tk.LEFT) self.newfiletip = add_tooltip(self.newfile, 'Open a new netcdf file') - self.newwin = ttk.Button( - self.rowwin, text="New Window", + self.newwin = Button( + self.rowwin, text='New Window', command=partial(clone_ncvmain, self.master)) self.newwin.pack(side=tk.RIGHT) self.newwintip = add_tooltip( self.newwin, 'Open secondary ncvue window') # plotting canvas - self.figure = Figure(facecolor="white", figsize=(1, 1)) + self.figure = Figure(facecolor='white', figsize=(1, 1)) self.axes = self.figure.add_subplot(111) + self.axes2 = self.axes.twinx() + self.axes2.yaxis.set_label_position('right') + self.axes2.yaxis.tick_right() self.canvas = FigureCanvasTkAgg(self.figure, master=self) self.canvas.draw() - # pack - self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - # grid instead of pack - does not work - # self.canvas.get_tk_widget().grid(column=0, row=0, - # sticky=(tk.N, tk.S, tk.E, tk.W)) - # self.canvas.get_tk_widget().columnconfigure(0, weight=1) - # self.canvas.get_tk_widget().rowconfigure(0, weight=1) + self.tkcanvas = self.canvas.get_tk_widget() + self.tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) # matplotlib toolbar - self.toolbar = NavigationToolbar2Tk(self.canvas, self) + # toolbar uses pack internally -> put into frame + self.toolbar = NavigationToolbar2Tk(self.canvas, self, + pack_toolbar=True) self.toolbar.update() self.toolbar.pack(side=tk.TOP, fill=tk.X) - # selections and options - columns = [''] + self.cols - - allcmaps = plt.colormaps() - self.cmaps = [ i for i in allcmaps if not i.endswith('_r') ] - self.cmaps.sort() - # self.imaps = [ tk.PhotoImage(file=os.path.dirname(__file__) + - # '/../images/' + i + '.png') - # for i in self.cmaps ] - bundle_dir = getattr(sys, '_MEIPASS', - os.path.abspath(os.path.dirname(__file__))) - self.imaps = [ tk.PhotoImage(file=bundle_dir + - '/images/' + i + '.png') - for i in self.cmaps ] - # 1. row # z-axis selection - self.rowzz = ttk.Frame(self) + self.rowzz = Frame(self) self.rowzz.pack(side=tk.TOP, fill=tk.X) - self.blockz = ttk.Frame(self.rowzz) + self.blockz = Frame(self.rowzz) self.blockz.pack(side=tk.LEFT) - self.rowz = ttk.Frame(self.blockz) + self.rowz = Frame(self.blockz) self.rowz.pack(side=tk.TOP, fill=tk.X) self.zlbl = tk.StringVar() - self.zlbl.set("z") - zlab = ttk.Label(self.rowz, textvariable=self.zlbl) + self.zlbl.set('z') + lkwargs = {'textvariable': self.zlbl} + if ihavectk: + lkwargs.update({'padx': padx}) + zlab = Label(self.rowz, **lkwargs) zlab.pack(side=tk.LEFT) - self.bprev_z = ttk.Button(self.rowz, text="<", width=1, - command=self.prev_z) + self.bprev_z = Button(self.rowz, text='<', width=bwidth, + command=self.prev_z) self.bprev_z.pack(side=tk.LEFT) self.bprev_ztip = add_tooltip(self.bprev_z, 'Previous variable') - self.bnext_z = ttk.Button(self.rowz, text=">", width=1, - command=self.next_z) + self.bnext_z = Button(self.rowz, text='>', width=bwidth, + command=self.next_z) self.bnext_z.pack(side=tk.LEFT) self.bnext_ztip = add_tooltip(self.bnext_z, 'Next variable') - self.z = ttk.Combobox(self.rowz, values=columns, width=25) - self.z.bind("<>", self.selected_z) + if ihavectk: + self.z = Combobox(self.rowz, values=columns, width=combowidth, + command=self.selected_z) + else: + self.z = Combobox(self.rowz, values=columns, width=combowidth) + self.z.bind('<>', self.selected_z) self.z.pack(side=tk.LEFT) self.ztip = add_tooltip(self.z, 'Choose variable') - self.trans_zlbl, self.trans_z, self.trans_ztip = add_checkbutton( - self.rowz, label="transpose z", value=False, command=self.checked, - tooltip="Transpose matrix") - spacez = ttk.Label(self.rowz, text=" " * 1) + self.trans_zframe, self.trans_zlbl, self.trans_z, self.trans_ztip = ( + add_checkbutton(self.rowz, label='transpose z', value=False, + command=self.checked, + tooltip='Transpose matrix')) + self.trans_zframe.pack(side=tk.LEFT) + spacez_label = tk.StringVar() + spacez_label.set(' ') + spacez = Label(self.rowz, textvariable=spacez_label) spacez.pack(side=tk.LEFT) - self.zminlbl, self.zmin, self.zmintip = add_entry( - self.rowz, label="zmin", text='None', width=7, + self.zminframe, self.zminlbl, self.zmin, self.zmintip = add_entry( + self.rowz, label='zmin', text='None', width=ewbig, padx=padx, command=self.entered_z, - tooltip="Minimal display value. Free scaling if 'None'.") - self.zmaxlbl, self.zmax, self.zmaxtip = add_entry( - self.rowz, label="zmax", text='None', width=7, + tooltip='Minimal display value. Free scaling if "None".') + self.zminframe.pack(side=tk.LEFT) + self.zmaxframe, self.zmaxlbl, self.zmax, self.zmaxtip = add_entry( + self.rowz, label='zmax', text='None', width=ewbig, padx=padx, command=self.entered_z, - tooltip="Maximal display value. Free scaling if 'None'.") + tooltip='Maximal display value. Free scaling if "None".') + self.zmaxframe.pack(side=tk.LEFT) # levels z - self.rowzd = ttk.Frame(self.blockz) + self.rowzd = Frame(self.blockz) self.rowzd.pack(side=tk.TOP, fill=tk.X) + self.zdframe = [] self.zdlblval = [] self.zdlbl = [] self.zdval = [] self.zd = [] self.zdtip = [] for i in range(self.maxdim): - zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( + zdframe, zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( self.rowzd, label=str(i), values=(0,), wrap=True, - command=self.spinned_z, state=tk.DISABLED, tooltip="None") + command=self.spinned_z, state=tk.DISABLED, tooltip='None') + self.zdframe.append(zdframe) self.zdlblval.append(zdlblval) self.zdlbl.append(zdlbl) self.zdval.append(zdval) self.zd.append(zd) self.zdtip.append(zdtip) + zdframe.pack(side=tk.LEFT) # 2. row # x-axis selection - self.rowxy = ttk.Frame(self) + self.rowxy = Frame(self) self.rowxy.pack(side=tk.TOP, fill=tk.X) - self.blockx = ttk.Frame(self.rowxy) + self.blockx = Frame(self.rowxy) self.blockx.pack(side=tk.LEFT) - self.rowx = ttk.Frame(self.blockx) + self.rowx = Frame(self.blockx) self.rowx.pack(side=tk.TOP, fill=tk.X) - self.xlbl, self.x, self.xtip = add_combobox( - self.rowx, label="x", values=columns, command=self.selected_x, - tooltip="Choose variable of x-axis.\nTake index if 'None' (fast).") - self.inv_xlbl, self.inv_x, self.inv_xtip = add_checkbutton( - self.rowx, label="invert x", value=False, command=self.checked, - tooltip="Invert x-axis") - self.rowxd = ttk.Frame(self.blockx) + self.xframe, self.xlbl, self.x, self.xtip = add_combobox( + self.rowx, label='x', values=columns, command=self.selected_x, + width=combowidth, padx=padx, + tooltip=('Choose variable of x-axis.\nTakes index if "None",' + ' which is much faster.')) + self.xframe.pack(side=tk.LEFT) + self.inv_xframe, self.inv_xlbl, self.inv_x, self.inv_xtip = ( + add_checkbutton(self.rowx, label='invert x', value=False, + command=self.checked, + tooltip='Invert x-axis')) + self.inv_xframe.pack(side=tk.LEFT) + self.rowxd = Frame(self.blockx) self.rowxd.pack(side=tk.TOP, fill=tk.X) + self.xdframe = [] self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): - xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( + xdframe, xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, - command=self.spinned_x, state=tk.DISABLED, tooltip="None") + command=self.spinned_x, state=tk.DISABLED, tooltip='None') + self.xdframe.append(xdframe) self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) self.xd.append(xd) self.xdtip.append(xdtip) + xdframe.pack(side=tk.LEFT) # y-axis selection - spacex = ttk.Label(self.rowxy, text=" " * 3) + spacex_label = tk.StringVar() + spacex_label.set(' ') + spacex = Label(self.rowxy, textvariable=spacex_label) # !!! spacex.pack(side=tk.LEFT) - self.blocky = ttk.Frame(self.rowxy) + self.blocky = Frame(self.rowxy) self.blocky.pack(side=tk.LEFT) - self.rowy = ttk.Frame(self.blocky) + self.rowy = Frame(self.blocky) self.rowy.pack(side=tk.TOP, fill=tk.X) - self.ylbl, self.y, self.ytip = add_combobox( - self.rowy, label="y", values=columns, command=self.selected_y, - tooltip="Choose variable of y-axis.\nTake index if 'None'.") - self.inv_ylbl, self.inv_y, self.inv_ytip = add_checkbutton( - self.rowy, label="invert y", value=False, command=self.checked, - tooltip="Invert y-axis") - self.rowyd = ttk.Frame(self.blocky) + self.yframe, self.ylbl, self.y, self.ytip = add_combobox( + self.rowy, label='y', values=columns, command=self.selected_y, + width=combowidth, padx=padx, + tooltip='Choose variable of y-axis.\nTakes index if "None".') + self.yframe.pack(side=tk.LEFT) + self.inv_yframe, self.inv_ylbl, self.inv_y, self.inv_ytip = ( + add_checkbutton(self.rowy, label='invert y', value=False, + command=self.checked, + tooltip='Invert y-axis')) + self.inv_yframe.pack(side=tk.LEFT) + self.rowyd = Frame(self.blocky) self.rowyd.pack(side=tk.TOP, fill=tk.X) + self.ydframe = [] self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): - ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( + ydframe, ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, - command=self.spinned_y, state=tk.DISABLED, tooltip="None") + command=self.spinned_y, state=tk.DISABLED, tooltip='None') + self.ydframe.append(ydframe) self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) self.yd.append(yd) self.ydtip.append(ydtip) + ydframe.pack(side=tk.LEFT) # 3. row # options - self.rowcmap = ttk.Frame(self) + self.rowcmap = Frame(self) self.rowcmap.pack(side=tk.TOP, fill=tk.X) - self.cmaplbl, self.cmap, self.cmaptip = add_imagemenu( - self.rowcmap, label="cmap", values=self.cmaps, + self.cmapframe, self.cmaplbl, self.cmap, self.cmaptip = add_imagemenu( + self.rowcmap, label='cmap', values=self.cmaps, images=self.imaps, command=self.selected_cmap, - tooltip="Choose colormap") + tooltip='Choose colormap') + self.cmapframe.pack(side=tk.LEFT) self.cmap['text'] = 'RdYlBu' self.cmap['image'] = self.imaps[self.cmaps.index('RdYlBu')] - self.rev_cmaplbl, self.rev_cmap, self.rev_cmaptip = add_checkbutton( - self.rowcmap, label="reverse cmap", value=False, - command=self.checked, - tooltip="Reverse colormap") - self.meshlbl, self.mesh, self.meshtip = add_checkbutton( - self.rowcmap, label="mesh", value=True, - command=self.checked, - tooltip="Pseudocolor plot if checked, plot contours if unchecked") - self.gridlbl, self.grid, self.gridtip = add_checkbutton( - self.rowcmap, label="grid", value=False, - command=self.checked, - tooltip="Draw major grid lines") + self.rev_cmapframe, self.rev_cmaplbl, self.rev_cmap, self.rev_cmaptip = ( + add_checkbutton(self.rowcmap, label='reverse cmap', value=False, + command=self.checked, + tooltip='Reverse colormap')) + self.rev_cmapframe.pack(side=tk.LEFT) + self.meshframe, self.meshlbl, self.mesh, self.meshtip = ( + add_checkbutton(self.rowcmap, label='mesh', value=True, + command=self.checked, + tooltip=('Pseudocolor plot if checked,' + ' plot contours if unchecked'))) + self.meshframe.pack(side=tk.LEFT) + self.gridframe, self.gridlbl, self.grid, self.gridtip = ( + add_checkbutton(self.rowcmap, label='grid', value=False, + command=self.checked, + tooltip='Draw major grid lines')) + self.gridframe.pack(side=tk.LEFT) # Quit button - self.bquit = ttk.Button(self.rowcmap, text="Quit", - command=self.master.top.destroy) + self.bquit = Button(self.rowcmap, text='Quit', + command=self.master.top.destroy) self.bquit.pack(side=tk.RIGHT) self.bquittip = add_tooltip(self.bquit, 'Quit ncvue') @@ -302,6 +378,7 @@ def checked(self): Command called if any checkbutton was checked or unchecked. Redraws plot. + """ self.redraw() @@ -312,6 +389,7 @@ def entered_z(self, event): Triggering `event` was bound to entry. Redraws plot. + """ self.redraw() @@ -321,9 +399,13 @@ def next_z(self): Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. + """ z = self.z.get() - cols = self.z["values"] + if ihavectk: + cols = self.z.cget('values') + else: + cols = self.z['values'] idx = cols.index(z) idx += 1 if idx < len(cols): @@ -346,9 +428,13 @@ def prev_z(self): Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. + """ z = self.z.get() - cols = self.z["values"] + if ihavectk: + cols = self.z.cget('values') + else: + cols = self.z['values'] idx = cols.index(z) idx -= 1 if idx > 0: @@ -367,6 +453,7 @@ def prev_z(self): def newnetcdf(self): """ Open a new netcdf file and connect it to top. + """ # get new netcdf file name ncfile = tk.filedialog.askopenfilename( @@ -421,6 +508,7 @@ def selected_cmap(self, value): `value` is the chosen colormap. Sets text and image on the menubutton. + """ self.cmap['text'] = value self.cmap['image'] = self.imaps[self.cmaps.index(value)] @@ -433,6 +521,7 @@ def selected_x(self, event): Triggering `event` was bound to the combobox. Resets `x` options and dimensions. Redraws plot. + """ self.inv_x.set(0) set_dim_x(self) @@ -445,6 +534,7 @@ def selected_y(self, event): Triggering `event` was bound to the combobox. Resets `y` options and dimensions. Redraws plot. + """ self.inv_y.set(0) set_dim_y(self) @@ -458,6 +548,7 @@ def selected_z(self, event): Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. + """ self.x.set('') self.y.set('') @@ -477,6 +568,7 @@ def spinned_x(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -487,6 +579,7 @@ def spinned_y(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -497,6 +590,7 @@ def spinned_z(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -507,6 +601,7 @@ def spinned_z(self, event=None): def reinit(self): """ Reinitialise the panel from top. + """ # reinit from top self.fi = self.top.fi @@ -528,15 +623,17 @@ def reinit(self): ll.destroy() for ll in self.zd: ll.destroy() + self.zdframe = [] self.zdlblval = [] self.zdlbl = [] self.zdval = [] self.zd = [] self.zdtip = [] for i in range(self.maxdim): - zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( + zdframe, zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( self.rowzd, label=str(i), values=(0,), wrap=True, - command=self.spinned_z, state=tk.DISABLED, tooltip="None") + command=self.spinned_z, state=tk.DISABLED, tooltip='None') + self.zdframe.append(zdframe) self.zdlblval.append(zdlblval) self.zdlbl.append(zdlbl) self.zdval.append(zdval) @@ -546,15 +643,17 @@ def reinit(self): ll.destroy() for ll in self.xd: ll.destroy() + self.xdframe = [] self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): - xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( + xdframe, xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, - command=self.spinned_x, state=tk.DISABLED, tooltip="None") + command=self.spinned_x, state=tk.DISABLED, tooltip='None') + self.xdframe.append(xdframe) self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) @@ -564,15 +663,17 @@ def reinit(self): ll.destroy() for ll in self.yd: ll.destroy() + self.ydframe = [] self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): - ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( + ydframe, ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, - command=self.spinned_y, state=tk.DISABLED, tooltip="None") + command=self.spinned_y, state=tk.DISABLED, tooltip='None') + self.ydframe.append(ydframe) self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) @@ -580,13 +681,22 @@ def reinit(self): self.ydtip.append(ydtip) # set variables columns = [''] + self.cols - self.z['values'] = columns + if ihavectk: + self.z.configure(values=columns) + else: + self.z['values'] = columns self.z.set(columns[0]) self.zmin.set('None') self.zmax.set('None') - self.x['values'] = columns + if ihavectk: + self.x.configure(values=columns) + else: + self.x['values'] = columns self.x.set(columns[0]) - self.y['values'] = columns + if ihavectk: + self.y.configure(values=columns) + else: + self.y['values'] = columns self.y.set(columns[0]) # @@ -600,6 +710,7 @@ def redraw(self): Reads `x`, `y`, `z` variable names, the current settings of their dimension spinboxes, as well as all other plotting options. Then redraws the plot. + """ # get all states # rowz diff --git a/src/ncvue/ncvmain.py b/src/ncvue/ncvmain.py index 45eedad..c8d3d13 100644 --- a/src/ncvue/ncvmain.py +++ b/src/ncvue/ncvmain.py @@ -21,16 +21,25 @@ ncvMain History - * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) - * Added check_new_netcdf method that re-initialises all panels if netcdf - file changed, Jan 2021, Matthias Cuntz - * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz - * Allow groups in netcdf files, Jan 2024, Matthias Cuntz - * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) + * Added check_new_netcdf method that re-initialises all panels if netcdf + file changed, Jan 2021, Matthias Cuntz + * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz + * Allow groups in netcdf files, Jan 2024, Matthias Cuntz + * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Use CustomTkinter if installed, Nov 2024, Matthias Cuntz + * Have same tab order and only select Map if detected, + Dec 2024, Matthias Cuntz """ import tkinter as tk import tkinter.ttk as ttk +try: + from customtkinter import CTkTabview as Frame + ihavectk = True +except ModuleNotFoundError: + from tkinter.ttk import Frame + ihavectk = False import numpy as np from .ncvutils import vardim2var, selvar from .ncvscatter import ncvScatter @@ -44,7 +53,7 @@ # Window with plot panels # -class ncvMain(ttk.Frame): +class ncvMain(Frame): """ Main ncvue notebook window with the plotting panels. @@ -68,14 +77,6 @@ def __init__(self, master, **kwargs): self.master = master # master window, i.e. root self.top = master.top # top window - # Notebook for tabs for future plot types - self.tabs = ttk.Notebook(self) - self.tabs.pack(side=tk.TOP, fill=tk.BOTH, expand=1) - - self.tab_scatter = ncvScatter(self) - self.tab_contour = ncvContour(self) - self.tab_map = ncvMap(self) - mapfirst = False if any(self.top.latvar): idx = [ i for i, l in enumerate(self.top.latvar) if l ] @@ -92,15 +93,57 @@ def __init__(self, master, **kwargs): if np.prod(vv.shape) > 1: mapfirst = True - if mapfirst: - self.tabs.add(self.tab_map, text=self.tab_map.name) - self.tabs.add(self.tab_scatter, text=self.tab_scatter.name) - self.tabs.add(self.tab_contour, text=self.tab_contour.name) - if not mapfirst: + if ihavectk: # self is CTkTabview + stab = 'Scatter/Line' + self.add(stab) + istab = self.tab(stab) + istab.name = self.name + istab.master = self.master + istab.top = self.top + self.tab_scatter = ncvScatter(istab) + self.tab_scatter.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + ctab = 'Contour' + self.add(ctab) + ictab = self.tab(ctab) + ictab.name = self.name + ictab.master = self.master + ictab.top = self.top + self.tab_contour = ncvContour(ictab) + self.tab_contour.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + mtab = 'Map' + self.add(mtab) + imtab = self.tab(mtab) + imtab.name = self.name + imtab.master = self.master + imtab.top = self.top + self.tab_map = ncvMap(imtab) + self.tab_map.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + if mapfirst: + self.set("Map") + + # self.bind("<>", self.check_new_netcdf) + # self.bind("", self.check_new_netcdf) + else: + # Notebook for tabs for future plot types + self.tabs = ttk.Notebook(self) + self.tabs.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + self.tab_scatter = ncvScatter(self) + self.tab_contour = ncvContour(self) + self.tab_map = ncvMap(self) + + self.tabs.add(self.tab_scatter, text=self.tab_scatter.name) + self.tabs.add(self.tab_contour, text=self.tab_contour.name) self.tabs.add(self.tab_map, text=self.tab_map.name) - self.tabs.bind("<>", self.check_new_netcdf) - self.tabs.bind("", self.check_new_netcdf) + if mapfirst: + self.tabs.select(self.tab_map) + + self.tabs.bind("<>", self.check_new_netcdf) + self.tabs.bind("", self.check_new_netcdf) # # Methods diff --git a/src/ncvue/ncvmap.py b/src/ncvue/ncvmap.py index 3cfe0a7..4fd8789 100644 --- a/src/ncvue/ncvmap.py +++ b/src/ncvue/ncvmap.py @@ -20,32 +20,44 @@ ncvMap History - * Written Dec 2020-Jan 2021 by Matthias Cuntz (mc (at) macu (dot) de) - * Open new netcdf file, communicate via top widget, - Jan 2021, Matthias Cuntz - * Write coordinates and value on bottom of plotting canvas, - May 2021, Matthias Cuntz - * Larger pad for colorbar, Jun 2021, Matthias Cuntz - * Work with files without an unlimited (time) dimension (set_tstep), - Oct 2021, Matthias Cuntz - * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz - * Allow groups in netcdf files, Jan 2024, Matthias Cuntz - * Allow multiple netcdf files, Jan 2024, Matthias Cuntz - * Move images/ directory from src/ncvue/ to src/ directory, - Jan 2024, Matthias Cuntz - * Added borders, rivers, and lakes checkbuttons, Feb 2024, Matthias Cuntz - * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz - * Use matplotlib.colormaps[name] instead of - matplotlib.colormaps.get_cmap(name), Jul 2024, Matthias Cuntz - * Use draw_idle instead of draw in update method for faster animation, - Jul 2024, Matthias Cuntz - * Add Quit button, Nov 2024, Matthias Cuntz + * Written Dec 2020-Jan 2021 by Matthias Cuntz (mc (at) macu (dot) de) + * Open new netcdf file, communicate via top widget, + Jan 2021, Matthias Cuntz + * Write coordinates and value on bottom of plotting canvas, + May 2021, Matthias Cuntz + * Larger pad for colorbar, Jun 2021, Matthias Cuntz + * Work with files without an unlimited (time) dimension (set_tstep), + Oct 2021, Matthias Cuntz + * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz + * Allow groups in netcdf files, Jan 2024, Matthias Cuntz + * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Move images/ directory from src/ncvue/ to src/ directory, + Jan 2024, Matthias Cuntz + * Added borders, rivers, and lakes checkbuttons, Feb 2024, Matthias Cuntz + * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz + * Use matplotlib.colormaps[name] instead of + matplotlib.colormaps.get_cmap(name), Jul 2024, Matthias Cuntz + * Use draw_idle instead of draw in update method for faster animation, + Jul 2024, Matthias Cuntz + * Add Quit button, Nov 2024, Matthias Cuntz + * Use CustomTkinter if installed, Nov 2024, Matthias Cuntz """ import os import sys import tkinter as tk -import tkinter.ttk as ttk +try: + from customtkinter import CTkFrame as Frame + from customtkinter import CTkButton as Button + from customtkinter import CTkLabel as Label + from customtkinter import CTkComboBox as Combobox + ihavectk = True +except ModuleNotFoundError: + from tkinter.ttk import Frame + from tkinter.ttk import Button + from tkinter.ttk import Label + from tkinter.ttk import Combobox + ihavectk = False import cartopy.crs as ccrs import cartopy.feature as cfeature import netCDF4 as nc @@ -70,7 +82,7 @@ __all__ = ['ncvMap'] -class ncvMap(ttk.Frame): +class ncvMap(Frame): """ Panel for maps. @@ -118,48 +130,6 @@ def __init__(self, master, **kwargs): self.iunlim = -1 # index of dunlim in dimensions of current var self.nunlim = 0 # length of dunlim of current plot variable - # new window - self.rowwin = ttk.Frame(self) - self.rowwin.pack(side=tk.TOP, fill=tk.X) - self.newfile = ttk.Button(self.rowwin, text="Open File", - command=self.newnetcdf) - self.newfile.pack(side=tk.LEFT) - self.newfiletip = add_tooltip(self.newfile, 'Open a new netcdf file') - spacew = ttk.Label(self.rowwin, text=" " * 3) - spacew.pack(side=tk.LEFT) - time_label1 = ttk.Label(self.rowwin, text='Time: ') - time_label1.pack(side=tk.LEFT) - self.timelbl = tk.StringVar() - self.timelbl.set('') - time_label2 = ttk.Label(self.rowwin, textvariable=self.timelbl) - time_label2.pack(side=tk.LEFT) - self.newwin = ttk.Button( - self.rowwin, text="New Window", - command=partial(clone_ncvmain, self.master)) - self.newwin.pack(side=tk.RIGHT) - self.newwintip = add_tooltip( - self.newwin, 'Open secondary ncvue window') - - # plotting canvas - self.figure = Figure(facecolor="white", figsize=(1, 1)) - # self.axes = self.figure.add_subplot(111) - self.axes = self.figure.add_subplot(111, - projection=ccrs.PlateCarree()) - self.canvas = FigureCanvasTkAgg(self.figure, master=self) - self.canvas.draw() - # pack - self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - # grid instead of pack - does not work - # self.canvas.get_tk_widget().grid(column=0, row=0, - # sticky=(tk.N, tk.S, tk.E, tk.W)) - # self.canvas.get_tk_widget().columnconfigure(0, weight=1) - # self.canvas.get_tk_widget().rowconfigure(0, weight=1) - - # matplotlib toolbar - self.toolbar = NavigationToolbar2Tk(self.canvas, self) - self.toolbar.update() - self.toolbar.pack(side=tk.TOP, fill=tk.X) - # selections and options columns = [''] + self.cols @@ -194,259 +164,385 @@ def __init__(self, master, **kwargs): ccrs.Mollweide, ccrs.NorthPolarStereo, ccrs.PlateCarree, ccrs.Robinson, ccrs.Sinusoidal, ccrs.SouthPolarStereo, ccrs.Stereographic] + if ihavectk: + # width of combo boxes in px + combowidth = 297 + smallcombowidth = 81 + # widths of entry widgets in px + ewsmall = 18 + ewmed = 45 + ewbig = 72 + ewvbig = 117 + # pad between label and entry + padx = 5 + # width of animation and variables buttons + bwidth = 35 + # width of projections menu + mwidth = 70 + else: + # width of combo boxes in characters + combowidth = 32 + smallcombowidth = 5 + # widths of entry widgets in characters + ewsmall = 3 + ewmed = 4 + ewbig = 7 + ewvbig = 11 + # pad between label and entry (not used) + padx = 5 + # width of animation and variables buttons + bwidth = 1 + # width of projections menu + mwidth = 13 + + # new window + self.rowwin = Frame(self) + self.rowwin.pack(side=tk.TOP, fill=tk.X) + self.newfile = Button(self.rowwin, text='Open File', + command=self.newnetcdf) + self.newfile.pack(side=tk.LEFT) + self.newfiletip = add_tooltip(self.newfile, 'Open a new netcdf file') + spacew_label = tk.StringVar() + spacew_label.set(' ') + spacew = Label(self.rowwin, textvariable=spacew_label) + spacew.pack(side=tk.LEFT) + time_label1text = tk.StringVar() + time_label1text.set('Time: ') + time_label1 = Label(self.rowwin, textvariable=time_label1text) + time_label1.pack(side=tk.LEFT) + self.timelbl = tk.StringVar() + self.timelbl.set('') + time_label2 = Label(self.rowwin, textvariable=self.timelbl) + time_label2.pack(side=tk.LEFT) + self.newwin = Button( + self.rowwin, text='New Window', + command=partial(clone_ncvmain, self.master)) + self.newwin.pack(side=tk.RIGHT) + self.newwintip = add_tooltip( + self.newwin, 'Open secondary ncvue window') + + # plotting canvas + self.figure = Figure(facecolor='white', figsize=(1, 1)) + # self.axes = self.figure.add_subplot(111) + self.axes = self.figure.add_subplot(111, + projection=ccrs.PlateCarree()) + self.canvas = FigureCanvasTkAgg(self.figure, master=self) + self.canvas.draw() + # pack + self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) + # grid instead of pack - does not work + # self.canvas.get_tk_widget().grid(column=0, row=0, + # sticky=(tk.N, tk.S, tk.E, tk.W)) + # self.canvas.get_tk_widget().columnconfigure(0, weight=1) + # self.canvas.get_tk_widget().rowconfigure(0, weight=1) + + # matplotlib toolbar + self.toolbar = NavigationToolbar2Tk(self.canvas, self) + self.toolbar.update() + self.toolbar.pack(side=tk.TOP, fill=tk.X) # 1. row # controls - self.rowt = ttk.Frame(self) + self.rowt = Frame(self) self.rowt.pack(side=tk.TOP, fill=tk.X) ntime = 1 - self.tsteplbl, self.tstepval, self.tstep, self.tsteptip = add_scale( - self.rowt, label="step", ini=0, from_=0, to=ntime, - length=100, orient=tk.HORIZONTAL, command=self.tstep_t, - tooltip="Slide to go to time step") - spacet = ttk.Label(self.rowt, text=" " * 1) + self.tstepframe, self.tsteplbl, self.tstepval, self.tstep, self.tsteptip = ( + add_scale(self.rowt, label='step', ini=0, from_=0, to=ntime, + length=100, orient=tk.HORIZONTAL, command=self.tstep_t, + tooltip='Slide to go to time step')) + self.tstepframe.pack(side=tk.LEFT) + spacet_label = tk.StringVar() + spacet_label.set(' ') + spacet = Label(self.rowt, textvariable=spacet_label) + spacet = Label(self.rowt, text=' ' * 1) spacet.pack(side=tk.LEFT) # first t - self.first_t = ttk.Button(self.rowt, text="|<<", width=3, - command=self.first_t) + self.first_t = Button(self.rowt, text='|<<', width=bwidth, + command=self.first_t) self.first_t.pack(side=tk.LEFT) self.first_ttip = add_tooltip(self.first_t, 'First time step') # previous t - self.prev_t = ttk.Button(self.rowt, text="|<", width=2, - command=self.prev_t) + self.prev_t = Button(self.rowt, text='|<', width=bwidth, + command=self.prev_t) self.prev_t.pack(side=tk.LEFT) self.prev_ttip = add_tooltip(self.prev_t, 'Previous time step') # run t backwards - self.prun_t = ttk.Button(self.rowt, text="<", width=1, - command=self.prun_t) + self.prun_t = Button(self.rowt, text='<', width=bwidth, + command=self.prun_t) self.prun_t.pack(side=tk.LEFT) self.prun_ttip = add_tooltip(self.prun_t, 'Run backwards') # pause t - self.pause_t = ttk.Button(self.rowt, text="||", width=1, - command=self.pause_t) + self.pause_t = Button(self.rowt, text='||', width=bwidth, + command=self.pause_t) self.pause_t.pack(side=tk.LEFT) self.pause_ttip = add_tooltip(self.pause_t, 'Pause/Stop') # run t forward - self.nrun_t = ttk.Button(self.rowt, text=">", width=1, - command=self.nrun_t) + self.nrun_t = Button(self.rowt, text='>', width=bwidth, + command=self.nrun_t) self.nrun_t.pack(side=tk.LEFT) self.nrun_ttip = add_tooltip(self.nrun_t, 'Run forwards') # next t - self.next_t = ttk.Button(self.rowt, text=">|", width=2, - command=self.next_t) + self.next_t = Button(self.rowt, text='>|', width=bwidth, + command=self.next_t) self.next_t.pack(side=tk.LEFT) self.next_ttip = add_tooltip(self.next_t, 'Next time step') # last t - self.last_t = ttk.Button(self.rowt, text=">>|", width=3, - command=self.last_t) + self.last_t = Button(self.rowt, text='>>|', width=bwidth, + command=self.last_t) self.last_t.pack(side=tk.LEFT) self.last_ttip = add_tooltip(self.last_t, 'Last time step') # repeat - spacer = ttk.Label(self.rowt, text=" " * 1) + spacer = Label(self.rowt, text=' ' * 1) spacer.pack(side=tk.LEFT) reps = ['once', 'repeat', 'reflect'] - tstr = "Run time steps once, repeat from start when at end," - tstr += " or continue running backwards when at end" - self.repeatlbl, self.repeat, self.repeattip = add_combobox( - self.rowt, label="repeat", values=reps, width=5, - command=self.repeat_t, tooltip=tstr) + tstr = 'Run time steps once, repeat from start when at end,' + tstr += ' or continue running backwards when at end' + self.repeatframe, self.repeatlbl, self.repeat, self.repeattip = ( + add_combobox(self.rowt, label='repeat', values=reps, + width=smallcombowidth, padx=padx, + command=self.repeat_t, tooltip=tstr)) self.repeat.set('repeat') - self.last_t.pack(side=tk.LEFT) + self.repeatframe.pack(side=tk.LEFT) # delay - spaced = ttk.Label(self.rowt, text=" " * 1) + spaced = Label(self.rowt, text=' ' * 1) spaced.pack(side=tk.LEFT) - tstr = "Delay run between time steps from 1 to 1000 ms" - self.delaylbl, self.delayval, self.delay, self.delaytip = add_scale( - self.rowt, label="delay (ms)", ini=1, from_=1, to=1000, - length=100, orient=tk.HORIZONTAL, command=self.delay_t, - tooltip=tstr) + tstr = 'Delay run between time steps from 1 to 1000 ms' + self.delayframe, self.delaylbl, self.delayval, self.delay, self.delaytip = ( + add_scale(self.rowt, label='delay (ms)', ini=1, from_=1, to=1000, + length=100, orient=tk.HORIZONTAL, command=self.delay_t, + tooltip=tstr)) + self.delayframe.pack(side=tk.LEFT) # 2. row # variable-axis selection - self.rowvv = ttk.Frame(self) + self.rowvv = Frame(self) self.rowvv.pack(side=tk.TOP, fill=tk.X) - self.blockv = ttk.Frame(self.rowvv) + self.blockv = Frame(self.rowvv) self.blockv.pack(side=tk.LEFT) - self.rowv = ttk.Frame(self.blockv) + self.rowv = Frame(self.blockv) self.rowv.pack(side=tk.TOP, fill=tk.X) self.vlbl = tk.StringVar() - self.vlbl.set("var") - vlab = ttk.Label(self.rowv, textvariable=self.vlbl) + self.vlbl.set('var') + lkwargs = {'textvariable': self.vlbl} + if ihavectk: + lkwargs.update({'padx': padx}) + vlab = Label(self.rowv, **lkwargs) vlab.pack(side=tk.LEFT) - self.bprev_v = ttk.Button(self.rowv, text="<", width=1, - command=self.prev_v) + self.bprev_v = Button(self.rowv, text='<', width=bwidth, + command=self.prev_v) self.bprev_v.pack(side=tk.LEFT) self.bprev_vtip = add_tooltip(self.bprev_v, 'Previous variable') - self.bnext_v = ttk.Button(self.rowv, text=">", width=1, - command=self.next_v) + self.bnext_v = Button(self.rowv, text='>', width=bwidth, + command=self.next_v) self.bnext_v.pack(side=tk.LEFT) self.bnext_vtip = add_tooltip(self.bnext_v, 'Next variable') - self.v = ttk.Combobox(self.rowv, values=columns, width=25) - self.v.bind("<>", self.selected_v) + if ihavectk: + self.v = Combobox(self.rowv, values=columns, width=combowidth, + command=self.selected_v) + else: + self.v = Combobox(self.rowv, values=columns, width=combowidth) + self.v.bind('<>', self.selected_v) self.v.pack(side=tk.LEFT) self.vtip = add_tooltip(self.v, 'Choose variable') - self.trans_vlbl, self.trans_v, self.trans_vtip = add_checkbutton( - self.rowv, label="transpose var", value=False, - command=self.checked, - tooltip="Transpose array, i.e. exchanging lat and lon") - spacev = ttk.Label(self.rowv, text=" " * 1) + self.trans_vframe, self.trans_vlbl, self.trans_v, self.trans_vtip = ( + add_checkbutton(self.rowv, label='transpose var', value=False, + command=self.checked, + tooltip=('Transpose array, i.e. exchanging lat and' + ' lon'))) + self.trans_vframe.pack(side=tk.LEFT) + spacev = Label(self.rowv, text=' ' * 1) spacev.pack(side=tk.LEFT) - self.vminlbl, self.vmin, self.vmintip = add_entry( - self.rowv, label="vmin", text=0, width=11, command=self.entered_v, - tooltip="Minimal display value") - self.vmaxlbl, self.vmax, self.vmaxtip = add_entry( - self.rowv, label="vmax", text=1, width=11, command=self.entered_v, - tooltip="Maximal display value") - tstr = "If checked, determine vmin/vmax from all fields,\n" - tstr += "otherwise from 50 random fields" - self.valllbl, self.vall, self.valltip = add_checkbutton( - self.rowv, label="all", value=False, command=self.checked_all, - tooltip=tstr) + self.vminframe, self.vminlbl, self.vmin, self.vmintip = add_entry( + self.rowv, label='vmin', text=0, width=ewvbig, command=self.entered_v, + tooltip='Minimal display value', padx=padx) + self.vminframe.pack(side=tk.LEFT) + self.vmaxframe, self.vmaxlbl, self.vmax, self.vmaxtip = add_entry( + self.rowv, label='vmax', text=1, width=ewvbig, command=self.entered_v, + tooltip='Maximal display value', padx=padx) + self.vmaxframe.pack(side=tk.LEFT) + tstr = 'If checked, determine vmin/vmax from all fields,\n' + tstr += 'otherwise from 50 random fields' + self.vallframe, self.valllbl, self.vall, self.valltip = ( + add_checkbutton(self.rowv, label='all', value=False, + command=self.checked_all, tooltip=tstr)) + self.vallframe.pack(side=tk.LEFT) # levels var - self.rowvd = ttk.Frame(self.blockv) + self.rowvd = Frame(self.blockv) self.rowvd.pack(side=tk.TOP, fill=tk.X) + self.vdframe = [] self.vdlblval = [] self.vdlbl = [] self.vdval = [] self.vd = [] self.vdtip = [] for i in range(self.maxdim): - vdlblval, vdlbl, vdval, vd, vdtip = add_spinbox( + vdframe, vdlblval, vdlbl, vdval, vd, vdtip = add_spinbox( self.rowvd, label=str(i), values=(0,), wrap=True, - command=self.spinned_v, state=tk.DISABLED, tooltip="None") + command=self.spinned_v, state=tk.DISABLED, tooltip='None') + self.vdframe.append(vdframe) self.vdlblval.append(vdlblval) self.vdlbl.append(vdlbl) self.vdval.append(vdval) self.vd.append(vd) self.vdtip.append(vdtip) + vdframe.pack(side=tk.LEFT) # 3. row # lon-axis selection - self.rowll = ttk.Frame(self) + self.rowll = Frame(self) self.rowll.pack(side=tk.TOP, fill=tk.X) - self.blocklon = ttk.Frame(self.rowll) + self.blocklon = Frame(self.rowll) self.blocklon.pack(side=tk.LEFT) - self.rowlon = ttk.Frame(self.blocklon) + self.rowlon = Frame(self.blocklon) self.rowlon.pack(side=tk.TOP, fill=tk.X) - self.lonlbl, self.lon, self.lontip = add_combobox( - self.rowlon, label="lon", values=columns, - command=self.selected_lon, - tooltip="Longitude variable.\nSet 'empty' for matrix plot.") - self.inv_lonlbl, self.inv_lon, self.inv_lontip = add_checkbutton( - self.rowlon, label="invert lon", value=False, - command=self.checked, - tooltip="Invert longitudes") - self.shift_lonlbl, self.shift_lon, self.shift_lontip = add_checkbutton( - self.rowlon, label="shift lon/2", value=False, - command=self.checked, - tooltip="Roll longitudes by half its size") - self.rowlond = ttk.Frame(self.blocklon) + self.lonframe, self.lonlbl, self.lon, self.lontip = add_combobox( + self.rowlon, label='lon', values=columns, padx=padx, + command=self.selected_lon, width=combowidth, + tooltip='Longitude variable.\nSet "empty" for matrix plot.') + self.lonframe.pack(side=tk.LEFT) + self.inv_lonframe, self.inv_lonlbl, self.inv_lon, self.inv_lontip = ( + add_checkbutton(self.rowlon, label='invert lon', value=False, + command=self.checked, + tooltip='Invert longitudes')) + self.inv_lonframe.pack(side=tk.LEFT) + self.shift_lonframe, self.shift_lonlbl, self.shift_lon, self.shift_lontip = ( + add_checkbutton(self.rowlon, label='shift lon/2', value=False, + command=self.checked, + tooltip='Roll longitudes by half its size')) + self.shift_lonframe.pack(side=tk.LEFT) + self.rowlond = Frame(self.blocklon) self.rowlond.pack(side=tk.TOP, fill=tk.X) + self.londframe = [] self.londlblval = [] self.londlbl = [] self.londval = [] self.lond = [] self.londtip = [] for i in range(self.maxdim): - londlblval, londlbl, londval, lond, londtip = add_spinbox( - self.rowlond, label=str(i), values=(0,), wrap=True, - command=self.spinned_lon, state=tk.DISABLED, tooltip="None") + londframe, londlblval, londlbl, londval, lond, londtip = ( + add_spinbox(self.rowlond, label=str(i), values=(0,), wrap=True, + command=self.spinned_lon, state=tk.DISABLED, + tooltip='None')) + self.londframe.append(londframe) self.londlblval.append(londlblval) self.londlbl.append(londlbl) self.londval.append(londval) self.lond.append(lond) self.londtip.append(londtip) + londframe.pack(side=tk.LEFT) # lat-axis selection - spacex = ttk.Label(self.rowll, text=" " * 3) + spacex = Label(self.rowll, text=' ' * 3) spacex.pack(side=tk.LEFT) - self.blocklat = ttk.Frame(self.rowll) + self.blocklat = Frame(self.rowll) self.blocklat.pack(side=tk.LEFT) - self.rowlat = ttk.Frame(self.blocklat) + self.rowlat = Frame(self.blocklat) self.rowlat.pack(side=tk.TOP, fill=tk.X) - self.latlbl, self.lat, self.lattip = add_combobox( - self.rowlat, label="lat", values=columns, - command=self.selected_lat, - tooltip="Longitude variable.\nSet 'empty' for matrix plot.") - self.inv_latlbl, self.inv_lat, self.inv_lattip = add_checkbutton( - self.rowlat, label="invert lat", value=False, command=self.checked, - tooltip="Invert longitudes") - self.rowlatd = ttk.Frame(self.blocklat) + self.latframe, self.latlbl, self.lat, self.lattip = add_combobox( + self.rowlat, label='lat', values=columns, padx=padx, + command=self.selected_lat, width=combowidth, + tooltip='Longitude variable.\nSet "empty" for matrix plot.') + self.latframe.pack(side=tk.LEFT) + self.inv_latframe, self.inv_latlbl, self.inv_lat, self.inv_lattip = ( + add_checkbutton(self.rowlat, label='invert lat', value=False, + command=self.checked, + tooltip='Invert longitudes')) + self.inv_latframe.pack(side=tk.LEFT) + self.rowlatd = Frame(self.blocklat) self.rowlatd.pack(side=tk.TOP, fill=tk.X) + self.latdframe = [] self.latdlblval = [] self.latdlbl = [] self.latdval = [] self.latd = [] self.latdtip = [] for i in range(self.maxdim): - latdlblval, latdlbl, latdval, latd, latdtip = add_spinbox( - self.rowlatd, label=str(i), values=(0,), wrap=True, - command=self.spinned_lat, state=tk.DISABLED, tooltip="None") + latdframe, latdlblval, latdlbl, latdval, latd, latdtip = ( + add_spinbox(self.rowlatd, label=str(i), values=(0,), wrap=True, + command=self.spinned_lat, state=tk.DISABLED, + tooltip='None')) + self.latdframe.append(latdframe) self.latdlblval.append(latdlblval) self.latdlbl.append(latdlbl) self.latdval.append(latdval) self.latd.append(latd) self.latdtip.append(latdtip) + latdframe.pack(side=tk.LEFT) # 4. row # options - self.rowcmap = ttk.Frame(self) + self.rowcmap = Frame(self) self.rowcmap.pack(side=tk.TOP, fill=tk.X) - self.cmaplbl, self.cmap, self.cmaptip = add_imagemenu( - self.rowcmap, label="cmap", values=self.cmaps, + self.cmapframe, self.cmaplbl, self.cmap, self.cmaptip = add_imagemenu( + self.rowcmap, label='cmap', values=self.cmaps, images=self.imaps, command=self.selected_cmap, - tooltip="Choose colormap") + tooltip='Choose colormap') self.cmap['text'] = 'RdYlBu' self.cmap['image'] = self.imaps[self.cmaps.index('RdYlBu')] - self.rev_cmaplbl, self.rev_cmap, self.rev_cmaptip = add_checkbutton( - self.rowcmap, label="reverse cmap", value=False, - command=self.checked, - tooltip="Reverse colormap") - self.meshlbl, self.mesh, self.meshtip = add_checkbutton( - self.rowcmap, label="mesh", value=True, - command=self.checked, - tooltip="Pseudocolor plot if checked, plot contours if unchecked") - self.igloballbl, self.iglobal, self.iglobaltip = add_checkbutton( - self.rowcmap, label="global", value=False, - command=self.checked, - tooltip="Assume global extent") - self.coastlbl, self.coast, self.coasttip = add_checkbutton( - self.rowcmap, label="coast", value=True, - command=self.checked, - tooltip="Draw continental coast lines") - self.borderslbl, self.borders, self.borderstip = add_checkbutton( - self.rowcmap, label="borders", value=False, - command=self.checked, - tooltip="Draw country borders") - self.riverslbl, self.rivers, self.riverstip = add_checkbutton( - self.rowcmap, label="rivers", value=False, - command=self.checked, - tooltip="Draw rivers") - self.lakeslbl, self.lakes, self.lakestip = add_checkbutton( - self.rowcmap, label="lakes", value=False, - command=self.checked, - tooltip="Draw major lakes") - self.gridlbl, self.grid, self.gridtip = add_checkbutton( - self.rowcmap, label="grid", value=False, - command=self.checked, - tooltip="Draw major grid lines") + self.cmapframe.pack(side=tk.LEFT) + self.rev_cmapframe, self.rev_cmaplbl, self.rev_cmap, self.rev_cmaptip = ( + add_checkbutton(self.rowcmap, label='reverse cmap', value=False, + command=self.checked, + tooltip='Reverse colormap')) + self.rev_cmapframe.pack(side=tk.LEFT) + self.meshframe, self.meshlbl, self.mesh, self.meshtip = ( + add_checkbutton(self.rowcmap, label='mesh', value=True, + command=self.checked, + tooltip=('Pseudocolor plot if checked, plot ' + 'contours if unchecked'))) + self.meshframe.pack(side=tk.LEFT) + self.iglobalframe, self.igloballbl, self.iglobal, self.iglobaltip = ( + add_checkbutton(self.rowcmap, label='global', value=False, + command=self.checked, + tooltip='Assume global extent')) + self.iglobalframe.pack(side=tk.LEFT) + self.coastframe, self.coastlbl, self.coast, self.coasttip = ( + add_checkbutton(self.rowcmap, label='coast', value=True, + command=self.checked, + tooltip='Draw continental coast lines')) + self.coastframe.pack(side=tk.LEFT) + self.bordersframe, self.borderslbl, self.borders, self.borderstip = ( + add_checkbutton(self.rowcmap, label='borders', value=False, + command=self.checked, + tooltip='Draw country borders')) + self.bordersframe.pack(side=tk.LEFT) + self.riversframe, self.riverslbl, self.rivers, self.riverstip = ( + add_checkbutton(self.rowcmap, label='rivers', value=False, + command=self.checked, + tooltip='Draw rivers')) + self.riversframe.pack(side=tk.LEFT) + self.lakesframe, self.lakeslbl, self.lakes, self.lakestip = ( + add_checkbutton(self.rowcmap, label='lakes', value=False, + command=self.checked, + tooltip='Draw major lakes')) + self.lakesframe.pack(side=tk.LEFT) + self.gridframe, self.gridlbl, self.grid, self.gridtip = ( + add_checkbutton(self.rowcmap, label='grid', value=False, + command=self.checked, + tooltip='Draw major grid lines')) + self.gridframe.pack(side=tk.LEFT) # 7. row # projections - self.rowproj = ttk.Frame(self) + self.rowproj = Frame(self) self.rowproj.pack(side=tk.TOP, fill=tk.X) - self.projlbl, self.proj, self.projtip = add_menu( - self.rowproj, label="projection", values=self.projs, - command=self.selected_proj, width=26, - tooltip="Choose projection") - self.proj['text'] = 'PlateCarree' - tstr = "Central longitude of projection.\n" - tstr += "Determined from longitude variable if None." - self.clonlbl, self.clon, self.clontip = add_entry( - self.rowproj, label="central lon", text='None', width=4, - command=self.entered_clon, tooltip=tstr) + self.projframe, self.projlbl, self.proj, self.projtip = add_menu( + self.rowproj, label='projection', values=self.projs, + command=self.selected_proj, width=mwidth, + tooltip='Choose projection') + if ihavectk: + self.proj.set('PlateCarree') + else: + self.proj['text'] = 'PlateCarree' + self.projframe.pack(side=tk.LEFT) + tstr = 'Central longitude of projection.\n' + tstr += 'Determined from longitude variable if None.' + self.clonframe, self.clonlbl, self.clon, self.clontip = add_entry( + self.rowproj, label='central lon', text='None', width=ewmed, + command=self.entered_clon, tooltip=tstr, padx=padx) + self.clonframe.pack(side=tk.LEFT) # Quit button - self.bquit = ttk.Button(self.rowproj, text="Quit", - command=self.master.top.destroy) + self.bquit = Button(self.rowproj, text='Quit', + command=self.master.top.destroy) self.bquit.pack(side=tk.RIGHT) self.bquittip = add_tooltip(self.bquit, 'Quit ncvue') @@ -504,6 +600,7 @@ def checked(self): Command called if any checkbutton was checked or unchecked. Redraws plot. + """ self.redraw() @@ -513,6 +610,7 @@ def checked_all(self): unchecked. Resets vmin/vmax, redraws plot. + """ vmin, vmax = self.get_vminmax() self.vmin.set(vmin) @@ -524,6 +622,7 @@ def delay_t(self, delay): Command called if delay scale was changed. `delay` is the chosen value on the scale slider. + """ self.anim.event_source.interval = int(float(delay)) @@ -534,6 +633,7 @@ def entered_clon(self, event): Triggering `event` was bound to entry. Redraws plot. + """ self.redraw() @@ -544,12 +644,14 @@ def entered_v(self, event): Triggering `event` was bound to entry. Redraws plot. + """ self.redraw() def first_t(self): """ Command called if first frame button was pressed. + """ it = 0 self.set_tstep(it) @@ -558,6 +660,7 @@ def first_t(self): def last_t(self): """ Command called if last frame button was pressed. + """ it = self.nunlim - 1 self.set_tstep(it) @@ -566,6 +669,7 @@ def last_t(self): def newnetcdf(self): """ Open a new netcdf file and connect it to top. + """ # get new netcdf file name ncfile = tk.filedialog.askopenfilename( @@ -616,6 +720,7 @@ def newnetcdf(self): def nrun_t(self): """ Command called if forward run button was pressed. + """ if not self.anim_running: self.anim_inc = 1 @@ -625,6 +730,7 @@ def nrun_t(self): def next_t(self): """ Command called if next frame button was pressed. + """ try: it = int(self.vdval[self.iunlim].get()) @@ -649,15 +755,25 @@ def next_v(self): Command called if next button for the plotting variable was pressed. Resets `vmin`/`vmax` and variable-dimensions. Redraws plot. + """ v = self.v.get() - cols = self.v["values"] + if ihavectk: + cols = self.v.cget('values') + else: + cols = self.v['values'] idx = cols.index(v) idx += 1 if idx < len(cols): self.v.set(cols[idx]) self.set_unlim(cols[idx]) - self.tstep['to'] = self.nunlim - 1 + if ihavectk: + self.tstep.configure(to=self.nunlim - 1) + from_ = self.tstep.cget('from_') + to = self.tstep.cget('to') + self.tstep.configure(number_of_steps=to - from_ + 1) + else: + self.tstep['to'] = self.nunlim - 1 self.set_tstep(0) vmin, vmax = self.get_vminmax() self.vmin.set(vmin) @@ -668,6 +784,7 @@ def next_v(self): def pause_t(self): """ Command called if pause button was pressed. + """ if self.anim_running: self.anim.event_source.stop() @@ -676,6 +793,7 @@ def pause_t(self): def prev_t(self): """ Command called if previous frame button was pressed. + """ try: it = int(self.vdval[self.iunlim].get()) @@ -701,15 +819,25 @@ def prev_v(self): pressed. Resets `vmin`/`vmax` and v-dimensions. Redraws plot. + """ v = self.v.get() - cols = self.v["values"] + if ihavectk: + cols = self.v.cget('values') + else: + cols = self.v['values'] idx = cols.index(v) idx -= 1 if idx > 0: self.v.set(cols[idx]) self.set_unlim(cols[idx]) - self.tstep['to'] = self.nunlim - 1 + if ihavectk: + self.tstep.configure(to=self.nunlim - 1) + from_ = self.tstep.cget('from_') + to = self.tstep.cget('to') + self.tstep.configure(number_of_steps=to - from_ + 1) + else: + self.tstep['to'] = self.nunlim - 1 self.set_tstep(0) vmin, vmax = self.get_vminmax() self.vmin.set(vmin) @@ -720,6 +848,7 @@ def prev_v(self): def prun_t(self): """ Command called if run backwards button was pressed. + """ if not self.anim_running: self.anim_inc = -1 @@ -731,6 +860,7 @@ def repeat_t(self, event): Command called if repeat option was chosen with combobox. Triggering `event` was bound to the combobox. + """ rep = self.repeat.get() if rep == 'once': @@ -747,6 +877,7 @@ def selected_cmap(self, value): `value` is the chosen colormap. Sets text and image on the menubutton. + """ self.cmap['text'] = value self.cmap['image'] = self.imaps[self.cmaps.index(value)] @@ -759,6 +890,7 @@ def selected_lat(self, event): Triggering `event` was bound to the combobox. Resets `lat` options and dimensions. Redraws plot. + """ self.inv_lat.set(0) set_dim_lat(self) @@ -771,6 +903,7 @@ def selected_lon(self, event): Triggering `event` was bound to the combobox. Resets `x` options and dimensions. Redraws plot. + """ self.inv_lon.set(0) self.shift_lon.set(0) @@ -784,8 +917,12 @@ def selected_proj(self, value): `value` is the chosen projection. Sets text on the menubutton. + """ - self.proj['text'] = value + if ihavectk: + self.proj.set(value) + else: + self.proj['text'] = value self.redraw() def selected_v(self, event): @@ -795,10 +932,17 @@ def selected_v(self, event): Triggering `event` was bound to the combobox. Resets `vmin`/`vmax` and variable-dimensions. Redraws plot. + """ v = self.v.get() self.set_unlim(v) - self.tstep['to'] = self.nunlim - 1 + if ihavectk: + self.tstep.configure(to=self.nunlim - 1) + from_ = self.tstep.cget('from_') + to = self.tstep.cget('to') + self.tstep.configure(number_of_steps=to - from_ + 1) + else: + self.tstep['to'] = self.nunlim - 1 self.set_tstep(0) vmin, vmax = self.get_vminmax() self.vmin.set(vmin) @@ -813,6 +957,7 @@ def spinned_lon(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -823,6 +968,7 @@ def spinned_lat(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -833,6 +979,7 @@ def spinned_v(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ try: it = int(self.vdval[self.iunlim].get()) @@ -846,6 +993,7 @@ def tstep_t(self, step): Command called if tstep was changed. `step` is the chosen value on the scale slider. + """ it = int(float(step)) self.set_tstep(it) @@ -895,6 +1043,7 @@ def get_vminmax(self): def reinit(self): """ Reinitialise the panel from top. + """ # reinit from top self.fi = self.top.fi @@ -918,15 +1067,17 @@ def reinit(self): ll.destroy() for ll in self.vd: ll.destroy() + self.vdframe = [] self.vdlblval = [] self.vdlbl = [] self.vdval = [] self.vd = [] self.vdtip = [] for i in range(self.maxdim): - vdlblval, vdlbl, vdval, vd, vdtip = add_spinbox( + vdframe, vdlblval, vdlbl, vdval, vd, vdtip = add_spinbox( self.rowvd, label=str(i), values=(0,), wrap=True, - command=self.spinned_v, state=tk.DISABLED, tooltip="None") + command=self.spinned_v, state=tk.DISABLED, tooltip='None') + self.vdframe.append(vdframe) self.vdlblval.append(vdlblval) self.vdlbl.append(vdlbl) self.vdval.append(vdval) @@ -936,15 +1087,18 @@ def reinit(self): ll.destroy() for ll in self.latd: ll.destroy() + self.latdframe = [] self.latdlblval = [] self.latdlbl = [] self.latdval = [] self.latd = [] self.latdtip = [] for i in range(self.maxdim): - latdlblval, latdlbl, latdval, latd, latdtip = add_spinbox( - self.rowlatd, label=str(i), values=(0,), wrap=True, - command=self.spinned_lat, state=tk.DISABLED, tooltip="None") + latdframe, latdlblval, latdlbl, latdval, latd, latdtip = ( + add_spinbox(self.rowlatd, label=str(i), values=(0,), wrap=True, + command=self.spinned_lat, state=tk.DISABLED, + tooltip='None')) + self.latdframe.append(latdframe) self.latdlblval.append(latdlblval) self.latdlbl.append(latdlbl) self.latdval.append(latdval) @@ -954,34 +1108,52 @@ def reinit(self): ll.destroy() for ll in self.lond: ll.destroy() + self.londframe = [] self.londlblval = [] self.londlbl = [] self.londval = [] self.lond = [] self.londtip = [] for i in range(self.maxdim): - londlblval, londlbl, londval, lond, londtip = add_spinbox( - self.rowlond, label=str(i), values=(0,), wrap=True, - command=self.spinned_lon, state=tk.DISABLED, tooltip="None") + londframe, londlblval, londlbl, londval, lond, londtip = ( + add_spinbox(self.rowlond, label=str(i), values=(0,), wrap=True, + command=self.spinned_lon, state=tk.DISABLED, + tooltip='None')) + self.londframe.append(londframe) self.londlblval.append(londlblval) self.londlbl.append(londlbl) self.londval.append(londval) self.lond.append(lond) self.londtip.append(londtip) # set time step - self.tstep['to'] = 0 + if ihavectk: + self.tstep.configure(to=0) + from_ = self.tstep.cget('from_') + to = self.tstep.cget('to') + self.tstep.configure(number_of_steps=to - from_ + 1) + else: + self.tstep['to'] = 0 self.tstepval.set(0) self.repeat.set('repeat') # set variables columns = [''] + self.cols - self.v['values'] = columns + if ihavectk: + self.v.configure(values=columns) + else: + self.v['values'] = columns self.v.set(columns[0]) self.vmin.set('None') self.vmax.set('None') # set lat/lon - self.lon['values'] = columns + if ihavectk: + self.lon.configure(values=columns) + else: + self.lon['values'] = columns self.lon.set(columns[0]) - self.lat['values'] = columns + if ihavectk: + self.lat.configure(values=columns) + else: + self.lat['values'] = columns self.lat.set(columns[0]) if any(self.latvar): idx = [ i for i, l in enumerate(self.latvar) if l ] @@ -1013,6 +1185,7 @@ def set_tstep(self, it): Sets the time dimension spinbox, sets the time step scale, write the time on top. + """ v = self.v.get() gz, vz = vardim2var(v, self.groups) @@ -1043,6 +1216,7 @@ def set_unlim(self, v): Takes `self.iunlim=0` and `self.nunlim=variable.shape[0]` if self.dunlim == ''` or `self.dunlim` not in var.dimensions. + """ gz, vz = vardim2var(v, self.groups) if vz == self.tname[gz]: @@ -1074,6 +1248,7 @@ def redraw(self): Reads `lon`, `lat`, `variable` names, the current settings of their dimension spinboxes, as well as all other plotting options. Then redraws the plot. + """ # stop animation self.anim.event_source.stop() @@ -1114,7 +1289,10 @@ def redraw(self): rivers = self.rivers.get() lakes = self.lakes.get() grid = self.grid.get() - proj = self.proj['text'] + if ihavectk: + proj = self.proj.get() + else: + proj = self.proj['text'] self.iproj = self.iprojs[self.projs.index(proj)] clon = self.clon.get() # set x, y, axes labels @@ -1433,7 +1611,7 @@ def update(self, frame, isframe=False): self.ivv = vv # set data if mesh: - # update works well on "normal" pcolormesh but not on Cartopy's + # update works well on 'normal' pcolormesh but not on Cartopy's # self.cc.set_array(vv) # Both, imshow and pcolormesh need to remove the old # image.AxesImage or collections.QuadMesh first and then redraw diff --git a/src/ncvue/ncvscatter.py b/src/ncvue/ncvscatter.py index 6406b1d..c7f1f21 100644 --- a/src/ncvue/ncvscatter.py +++ b/src/ncvue/ncvscatter.py @@ -21,20 +21,32 @@ ncvScatter History - * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) - * Open new netcdf file, communicate via top widget, - Jan 2021, Matthias Cuntz - * Write left-hand side and right-hand side values on bottom of plotting - canvas, May 2021, Matthias Cuntz - * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz - * Allow groups in netcdf files, Jan 2024, Matthias Cuntz - * Allow multiple netcdf files, Jan 2024, Matthias Cuntz - * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz - * Add Quit button, Nov 2024, Matthias Cuntz + * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) + * Open new netcdf file, communicate via top widget, + Jan 2021, Matthias Cuntz + * Write left-hand side and right-hand side values on bottom of plotting + canvas, May 2021, Matthias Cuntz + * Address fi.variables[name] directly by fi[name], Jan 2024, Matthias Cuntz + * Allow groups in netcdf files, Jan 2024, Matthias Cuntz + * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz + * Add Quit button, Nov 2024, Matthias Cuntz + * Use CustomTkinter if installed, Nov 2024, Matthias Cuntz """ import tkinter as tk -import tkinter.ttk as ttk +try: + from customtkinter import CTkFrame as Frame + from customtkinter import CTkButton as Button + from customtkinter import CTkLabel as Label + from customtkinter import CTkComboBox as Combobox + ihavectk = True +except ModuleNotFoundError: + from tkinter.ttk import Frame + from tkinter.ttk import Button + from tkinter.ttk import Label + from tkinter.ttk import Combobox + ihavectk = False import numpy as np import netCDF4 as nc from .ncvutils import clone_ncvmain, format_coord_scatter, selvar @@ -58,7 +70,7 @@ __all__ = ['ncvScatter'] -class ncvScatter(ttk.Frame): +class ncvScatter(Frame): """ Panel for scatter and line plots. @@ -106,273 +118,339 @@ def __init__(self, master, **kwargs): self.maxdim = self.top.maxdim self.cols = self.top.cols + # selections and options + columns = [''] + self.cols + # colors + c = list(plt.rcParams['axes.prop_cycle']) + col1 = c[0]['color'] # blue + col2 = c[3]['color'] # red + # color tooltip + ctstr = ('- color names: red, green, blue, yellow, ...\n' + '- single characters: b (blue), g (green), r (red), c (cyan),' + ' m (magenta), y (yellow), k (black), w (white)\n' + '- hex RGB: #rrggbb such such as #ff9300 (orange)\n' + '- gray level: float between 0 and 1\n' + '- RGA (red, green, blue) or RGBA (red, green, blue, alpha)' + ' tuples between 0 and 1, e.g. (1, 0.57, 0) for orange\n' + '- name from xkcd color survey, e.g. xkcd:sky blue') + # marker tooltip + mtstr = ('. (point), "," (pixel), o (circle),\n' + 'v (triangle_down), ^ (triangle_up),\n' + '< (triangle_left), > (triangle_right),\n' + '1 (tri_down), 2 (tri_up), 3 (tri_left), 4 (tri_right),' + ' 8 (octagon),\n' + 's (square), p (pentagon), P (plus (filled)),\n' + '* (star), h (hexagon1), H (hexagon2),\n' + '+ (plus), x (x), X (x (filled)),\n' + 'D (diamond), d (thin_diamond),\n' + '| (vline), _ (hline), or None') + if ihavectk: + # width of combo boxes in px + combowidth = 288 + # widths of entry widgets in px + ewsmall = 20 + ewmed = 45 + ewbig = 70 + # pad between label and entry + padx = 5 + # width of animation and variables buttons + bwidth = 35 + # width of projections menu + mwidth = 70 + else: + # width of combo boxes in characters + combowidth = 30 + # widths of entry widgets in characters + ewsmall = 3 + ewmed = 4 + ewbig = 7 + # pad between label and entry (not used) + padx = 5 + # width of animation and variables buttons + bwidth = 1 + # width of projections menu + mwidth = 13 + # new window - self.rowwin = ttk.Frame(self) + self.rowwin = Frame(self) self.rowwin.pack(side=tk.TOP, fill=tk.X) - self.newfile = ttk.Button(self.rowwin, text="Open File", - command=self.newnetcdf) - self.newfile.pack(side=tk.LEFT) + self.newfile = Button(self.rowwin, text='Open File', + command=self.newnetcdf) self.newfiletip = add_tooltip(self.newfile, 'Open a new netcdf file') - self.newwin = ttk.Button( - self.rowwin, text="New Window", + self.newfile.pack(side=tk.LEFT) + self.newwin = Button( + self.rowwin, text='New Window', command=partial(clone_ncvmain, self.master)) - self.newwin.pack(side=tk.RIGHT) self.newwintip = add_tooltip( self.newwin, 'Open secondary ncvue window') + self.newwin.pack(side=tk.RIGHT) # plotting canvas - self.figure = Figure(facecolor="white", figsize=(1, 1)) + self.figure = Figure(facecolor='white', figsize=(1, 1)) self.axes = self.figure.add_subplot(111) self.axes2 = self.axes.twinx() - self.axes2.yaxis.set_label_position("right") + self.axes2.yaxis.set_label_position('right') self.axes2.yaxis.tick_right() self.canvas = FigureCanvasTkAgg(self.figure, master=self) self.canvas.draw() - # pack - self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - # grid instead of pack - does not work - # self.canvas.get_tk_widget().grid(column=0, row=0, - # sticky=(tk.N, tk.S, tk.E, tk.W)) - # self.canvas.get_tk_widget().columnconfigure(0, weight=1) - # self.canvas.get_tk_widget().rowconfigure(0, weight=1) - - # self.canvas.mpl_connect('pick_event', self.onpick) - # self.canvas.mpl_connect('button_press_event', self.onpick) + self.tkcanvas = self.canvas.get_tk_widget() + self.tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) # matplotlib toolbar - self.toolbar = NavigationToolbar2Tk(self.canvas, self) + # toolbar uses pack internally -> put into frame + self.toolbar = NavigationToolbar2Tk(self.canvas, self, + pack_toolbar=True) self.toolbar.update() self.toolbar.pack(side=tk.TOP, fill=tk.X) - # selections and options - columns = [''] + self.cols - # colors - c = list(plt.rcParams['axes.prop_cycle']) - col1 = c[0]['color'] # blue - col2 = c[3]['color'] # red - # color tooltip - ctstr = "- color names: red, green, blue, yellow, ...\n" - ctstr += "- single characters: b (blue), g (green), r (red), c (cyan)," - ctstr += " m (magenta), y (yellow), k (black), w (white)\n" - ctstr += "- hex RGB: #rrggbb such such as #ff9300 (orange)\n" - ctstr += "- gray level: float between 0 and 1\n" - ctstr += "- RGA (red, green, blue) or RGBA (red, green, blue, alpha)" - ctstr += " tuples between 0 and 1, e.g. (1, 0.57, 0) for orange\n" - ctstr += "- name from xkcd color survey, e.g. xkcd:sky blue" - # marker tooltip - mtstr = ". (point), ',' (pixel), o (circle),\n" - mtstr += "v (triangle_down), ^ (triangle_up),\n" - mtstr += "< (triangle_left), > (triangle_right),\n" - mtstr += "1 (tri_down), 2 (tri_up), 3 (tri_left), 4 (tri_right)," - mtstr += " 8 (octagon),\n" - mtstr += "s (square), p (pentagon), P (plus (filled)),\n" - mtstr += "* (star), h (hexagon1), H (hexagon2),\n" - mtstr += "+ (plus), x (x), X (x (filled)),\n" - mtstr += "D (diamond), d (thin_diamond),\n" - mtstr += "| (vline), _ (hline), or None" - - # 1. row - # x- and lhs y-axis selection - self.rowxy = ttk.Frame(self) + # 1. row : x- and lhs y-axis selection + self.rowxy = Frame(self) self.rowxy.pack(side=tk.TOP, fill=tk.X) - # block x with dimensions - self.blockx = ttk.Frame(self.rowxy) + # block with x and its dimensions + self.blockx = Frame(self.rowxy) self.blockx.pack(side=tk.LEFT) - self.rowx = ttk.Frame(self.blockx) + # x + self.rowx = Frame(self.blockx) self.rowx.pack(side=tk.TOP, fill=tk.X) - self.xlbl, self.x, self.xtip = add_combobox( - self.rowx, label="x", values=columns, command=self.selected_x, - tooltip="Choose variable of x-axis.\nTake index if 'None' (fast).") - self.x0 = '' - self.line_x = [] - self.inv_xlbl, self.inv_x, self.inv_xtip = add_checkbutton( - self.rowx, label="invert x", value=False, - command=self.checked_x, - tooltip="Invert x-axis") - self.rowxd = ttk.Frame(self.blockx) + self.xframe, self.xlbl, self.x, self.xtip = add_combobox( + self.rowx, label='x', values=columns, command=self.selected_x, + width=combowidth, padx=padx, + tooltip=('Choose variable of x-axis.\nTakes index if "None",' + ' which is much faster.')) + self.xframe.pack(side=tk.LEFT) + # invert x + self.inv_xframe, self.inv_xlbl, self.inv_x, self.inv_xtip = ( + add_checkbutton(self.rowx, label='invert x', value=False, + command=self.checked_x, tooltip='Invert x-axis')) + self.inv_xframe.pack(side=tk.LEFT) + # x dimensions + self.rowxd = Frame(self.blockx) self.rowxd.pack(side=tk.TOP, fill=tk.X) + self.xdframe = [] self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): - xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( + xdframe, xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, - command=self.spinned_x, state=tk.DISABLED, tooltip="None") + command=self.spinned_x, state=tk.DISABLED, tooltip='None') + self.xdframe.append(xdframe) self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) self.xd.append(xd) self.xdtip.append(xdtip) - # block y with dimensions - spacex = ttk.Label(self.rowxy, text=" " * 3) + xdframe.pack(side=tk.LEFT) + # space between x and y blocks + spacex_label = tk.StringVar() + spacex_label.set(' ') + spacex = Label(self.rowxy, textvariable=spacex_label) spacex.pack(side=tk.LEFT) - self.blocky = ttk.Frame(self.rowxy) + # block with y and its dimensions + self.blocky = Frame(self.rowxy) self.blocky.pack(side=tk.LEFT) - self.rowy = ttk.Frame(self.blocky) + # y + self.rowy = Frame(self.blocky) self.rowy.pack(side=tk.TOP, fill=tk.X) self.ylbl = tk.StringVar() - self.ylbl.set("y") - ylab = ttk.Label(self.rowy, textvariable=self.ylbl) + self.ylbl.set('y') + lkwargs = {'textvariable': self.ylbl} + if ihavectk: + lkwargs.update({'padx': padx}) + ylab = Label(self.rowy, **lkwargs) ylab.pack(side=tk.LEFT) - self.bprev_y = ttk.Button(self.rowy, text="<", width=1, - command=self.prev_y) + self.bprev_y = Button(self.rowy, text='<', width=bwidth, + command=self.prev_y) self.bprev_y.pack(side=tk.LEFT) self.bprev_ytip = add_tooltip(self.bprev_y, 'Previous variable') - self.bnext_y = ttk.Button(self.rowy, text=">", width=1, - command=self.next_y) + self.bnext_y = Button(self.rowy, text='>', width=bwidth, + command=self.next_y) self.bnext_y.pack(side=tk.LEFT) self.bnext_ytip = add_tooltip(self.bnext_y, 'Next variable') - self.y = ttk.Combobox(self.rowy, values=columns, width=25) - # long = len(max(columns, key=len)) - # self.y.configure(width=(max(20, long//2))) - self.y.bind("<>", self.selected_y) + if ihavectk: + self.y = Combobox(self.rowy, values=columns, width=combowidth, + command=self.selected_y) + else: + self.y = Combobox(self.rowy, values=columns, width=combowidth) + # long = len(max(columns, key=len)) + # self.y.configure(width=(max(20, long//2))) + self.y.bind('<>', self.selected_y) self.y.pack(side=tk.LEFT) self.ytip = add_tooltip(self.y, 'Choose variable of y-axis') - self.y0 = '' self.line_y = [] - self.inv_ylbl, self.inv_y, self.inv_ytip = add_checkbutton( - self.rowy, label="invert y", value=False, - command=self.checked_y, - tooltip="Inert y-axis") - self.rowyd = ttk.Frame(self.blocky) + self.inv_yframe, self.inv_ylbl, self.inv_y, self.inv_ytip = ( + add_checkbutton(self.rowy, label='invert y', value=False, + command=self.checked_y, tooltip='Inert y-axis')) + self.inv_yframe.pack(side=tk.LEFT) + # y dimensions + self.rowyd = Frame(self.blocky) self.rowyd.pack(side=tk.TOP, fill=tk.X) + self.ydframe = [] self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): - ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( + ydframe, ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, - command=self.spinned_y, state=tk.DISABLED, tooltip="None") + command=self.spinned_y, state=tk.DISABLED, tooltip='None') + self.ydframe.append(ydframe) self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) self.yd.append(yd) self.ydtip.append(ydtip) + ydframe.pack(side=tk.LEFT) + # redraw button - self.bredraw = ttk.Button(self.rowxy, text="Redraw", - command=self.redraw) + self.bredraw = Button(self.rowxy, text='Redraw', + command=self.redraw) self.bredraw.pack(side=tk.RIGHT) self.bredrawtip = add_tooltip(self.bredraw, 'Redraw, resetting zoom') # 2. row # options for lhs y-axis - self.rowxyopt = ttk.Frame(self) + self.rowxyopt = Frame(self) self.rowxyopt.pack(side=tk.TOP, fill=tk.X) - self.lslbl, self.ls, self.lstip = add_entry( - self.rowxyopt, label="ls", text='-', width=4, - command=self.entered_y, - tooltip="Line style: -, --, -., :, or None") - self.lwlbl, self.lw, self.lwtip = add_entry( - self.rowxyopt, label="lw", text='1', width=3, - command=self.entered_y, tooltip="Line width") - self.lclbl, self.lc, self.lctip = add_entry( - self.rowxyopt, label="c", text=col1, width=7, - command=self.entered_y, - tooltip="Line color:\n" + ctstr) - self.markerlbl, self.marker, self.markertip = add_entry( - self.rowxyopt, label="marker", text='None', width=4, - command=self.entered_y, - tooltip="Marker symbol:\n" + mtstr) - self.mslbl, self.ms, self.mstip = add_entry( - self.rowxyopt, label="ms", text='1', width=3, - command=self.entered_y, tooltip="Marker size") - self.mfclbl, self.mfc, self.mfctip = add_entry( - self.rowxyopt, label="mfc", text=col1, width=7, - command=self.entered_y, - tooltip="Marker fill color:\n" + ctstr) - self.meclbl, self.mec, self.mectip = add_entry( - self.rowxyopt, label="mec", text=col1, width=7, - command=self.entered_y, - tooltip="Marker edge color:\n" + ctstr) - self.mewlbl, self.mew, self.mewtip = add_entry( - self.rowxyopt, label="mew", text='1', width=3, - command=self.entered_y, tooltip="Marker edge width") + self.lsframe, self.lslbl, self.ls, self.lstip = add_entry( + self.rowxyopt, label='ls', text='-', width=ewmed, + command=self.entered_y, padx=padx, + tooltip='Line style: -, --, -., :, or None') + self.lsframe.pack(side=tk.LEFT) + self.lwframe, self.lwlbl, self.lw, self.lwtip = add_entry( + self.rowxyopt, label='lw', text='1', width=ewsmall, + command=self.entered_y, tooltip='Line width', padx=padx) + self.lwframe.pack(side=tk.LEFT) + self.lcframe, self.lclbl, self.lc, self.lctip = add_entry( + self.rowxyopt, label='c', text=col1, width=ewbig, + command=self.entered_y, padx=padx, + tooltip='Line color:\n' + ctstr) + self.lcframe.pack(side=tk.LEFT) + self.markerframe, self.markerlbl, self.marker, self.markertip = ( + add_entry(self.rowxyopt, label='marker', text='None', width=ewmed, + command=self.entered_y, padx=padx, + tooltip='Marker symbol:\n' + mtstr)) + self.markerframe.pack(side=tk.LEFT) + self.msframe, self.mslbl, self.ms, self.mstip = add_entry( + self.rowxyopt, label='ms', text='1', width=ewsmall, + command=self.entered_y, tooltip='Marker size', padx=padx) + self.msframe.pack(side=tk.LEFT) + self.mfcframe, self.mfclbl, self.mfc, self.mfctip = add_entry( + self.rowxyopt, label='mfc', text=col1, width=ewbig, + command=self.entered_y, padx=padx, + tooltip='Marker fill color:\n' + ctstr) + self.mfcframe.pack(side=tk.LEFT) + self.mecframe, self.meclbl, self.mec, self.mectip = add_entry( + self.rowxyopt, label='mec', text=col1, width=ewbig, + command=self.entered_y, padx=padx, + tooltip='Marker edge color:\n' + ctstr) + self.mecframe.pack(side=tk.LEFT) + self.mewframe, self.mewlbl, self.mew, self.mewtip = add_entry( + self.rowxyopt, label='mew', text='1', width=ewsmall, padx=padx, + command=self.entered_y, tooltip='Marker edge width') + self.mewframe.pack(side=tk.LEFT) # space - self.rowspace = ttk.Frame(self) + self.rowspace = Frame(self) self.rowspace.pack(side=tk.TOP, fill=tk.X) - rowspace = ttk.Label(self.rowspace, text=" ") + rowspace_label = tk.StringVar() + rowspace_label.set(' ') + rowspace = Label(self.rowspace, textvariable=spacex_label) rowspace.pack(side=tk.LEFT) # 3. row # rhs y-axis 2 selection - self.rowyy2 = ttk.Frame(self) + self.rowyy2 = Frame(self) self.rowyy2.pack(side=tk.TOP, fill=tk.X) - self.blocky2 = ttk.Frame(self.rowyy2) + self.blocky2 = Frame(self.rowyy2) self.blocky2.pack(side=tk.LEFT) - self.rowy2 = ttk.Frame(self.blocky2) + self.rowy2 = Frame(self.blocky2) self.rowy2.pack(side=tk.TOP, fill=tk.X) - self.y2lbl, self.y2, self.y2tip = add_combobox( - self.rowy2, label="y2", values=columns, - command=self.selected_y2, - tooltip="Choose variable for right-hand-side y-axis") - self.y20 = '' + self.y2frame, self.y2lbl, self.y2, self.y2tip = add_combobox( + self.rowy2, label='y2', values=columns, + command=self.selected_y2, width=combowidth, padx=padx, + tooltip='Choose variable for right-hand-side y-axis') + self.y2frame.pack(side=tk.LEFT) self.line_y2 = [] - self.inv_y2lbl, self.inv_y2, self.inv_y2tip = add_checkbutton( - self.rowy2, label="invert y2", value=False, - command=self.checked_y2, - tooltip="Invert right-hand-side y-axis") - spacey2 = ttk.Label(self.rowy2, text=" " * 1) + self.inv_y2frame, self.inv_y2lbl, self.inv_y2, self.inv_y2tip = ( + add_checkbutton(self.rowy2, label='invert y2', value=False, + command=self.checked_y2, + tooltip='Invert right-hand-side y-axis')) + self.inv_y2frame.pack(side=tk.LEFT) + spacey2_label = tk.StringVar() + spacey2_label.set(' ') + spacey2 = Label(self.rowy2, textvariable=spacey2_label) spacey2.pack(side=tk.LEFT) - tstr = "Same limits for left-hand-side and right-hand-side y-axes" - self.same_ylbl, self.same_y, self.same_ytip = add_checkbutton( - self.rowy2, label="same y-axes", value=False, - command=self.checked_yy2, tooltip=tstr) - self.rowy2d = ttk.Frame(self.blocky2) + tstr = 'Same limits for left-hand-side and right-hand-side y-axes' + self.same_yframe, self.same_ylbl, self.same_y, self.same_ytip = ( + add_checkbutton(self.rowy2, label='same y-axes', value=False, + command=self.checked_yy2, tooltip=tstr)) + self.same_yframe.pack(side=tk.LEFT) + self.rowy2d = Frame(self.blocky2) self.rowy2d.pack(side=tk.TOP, fill=tk.X) + self.y2dframe = [] self.y2dlblval = [] self.y2dlbl = [] self.y2dval = [] self.y2d = [] self.y2dtip = [] for i in range(self.maxdim): - y2dlblval, y2dlbl, y2dval, y2d, y2dtip = add_spinbox( + y2dframe, y2dlblval, y2dlbl, y2dval, y2d, y2dtip = add_spinbox( self.rowy2d, label=str(i), values=(0,), wrap=True, - command=self.spinned_y2, state=tk.DISABLED, tooltip="None") + command=self.spinned_y2, state=tk.DISABLED, tooltip='None') + self.y2dframe.append(y2dframe) self.y2dlblval.append(y2dlblval) self.y2dlbl.append(y2dlbl) self.y2dval.append(y2dval) self.y2d.append(y2d) self.y2dtip.append(y2dtip) + y2dframe.pack(side=tk.LEFT) # 4. row # options for rhs y-axis 2 - self.rowy2opt = ttk.Frame(self) + self.rowy2opt = Frame(self) self.rowy2opt.pack(side=tk.TOP, fill=tk.X) - self.ls2lbl, self.ls2, self.ls2tip = add_entry( - self.rowy2opt, label="ls", text='-', width=4, - command=self.entered_y2, - tooltip="Line style: -, --, -., :, or None") - self.lw2lbl, self.lw2, self.lw2tip = add_entry( - self.rowy2opt, label="lw", text='1', width=3, - command=self.entered_y2, tooltip="Line width") - self.lc2lbl, self.lc2, self.lc2tip = add_entry( - self.rowy2opt, label="c", text=col2, width=7, - command=self.entered_y2, - tooltip="Line color:\n" + ctstr) - self.marker2lbl, self.marker2, self.marker2tip = add_entry( - self.rowy2opt, label="marker", text='None', width=4, - command=self.entered_y2, - tooltip="Marker symbol:\n" + mtstr) - self.ms2lbl, self.ms2, self.ms2tip = add_entry( - self.rowy2opt, label="ms", text='1', width=3, - command=self.entered_y2, tooltip="Marker size") - self.mfc2lbl, self.mfc2, self.mfc2tip = add_entry( - self.rowy2opt, label="mfc", text=col2, width=7, - command=self.entered_y2, tooltip="Marker fill color:\n" + ctstr) - self.mec2lbl, self.mec2, self.mec2tip = add_entry( - self.rowy2opt, label="mec", text=col2, width=7, - command=self.entered_y2, tooltip="Marker edge color:\n" + ctstr) - self.mew2lbl, self.mew2, self.mew2tip = add_entry( - self.rowy2opt, label="mew", text='1', width=3, - command=self.entered_y2, tooltip="Marker edge width") + self.ls2frame, self.ls2lbl, self.ls2, self.ls2tip = add_entry( + self.rowy2opt, label='ls', text='-', width=ewmed, + command=self.entered_y2, padx=padx, + tooltip='Line style: -, --, -., :, or None') + self.ls2frame.pack(side=tk.LEFT) + self.lw2frame, self.lw2lbl, self.lw2, self.lw2tip = add_entry( + self.rowy2opt, label='lw', text='1', width=ewsmall, padx=padx, + command=self.entered_y2, tooltip='Line width') + self.lw2frame.pack(side=tk.LEFT) + self.lc2frame, self.lc2lbl, self.lc2, self.lc2tip = add_entry( + self.rowy2opt, label='c', text=col2, width=ewbig, + command=self.entered_y2, padx=padx, + tooltip='Line color:\n' + ctstr) + self.lc2frame.pack(side=tk.LEFT) + self.marker2frame, self.marker2lbl, self.marker2, self.marker2tip = ( + add_entry(self.rowy2opt, label='marker', text='None', width=ewmed, + command=self.entered_y2, padx=padx, + tooltip='Marker symbol:\n' + mtstr)) + self.markerframe.pack(side=tk.LEFT) + self.ms2frame, self.ms2lbl, self.ms2, self.ms2tip = add_entry( + self.rowy2opt, label='ms', text='1', width=ewsmall, padx=padx, + command=self.entered_y2, tooltip='Marker size') + self.ms2frame.pack(side=tk.LEFT) + self.mfc2frame, self.mfc2lbl, self.mfc2, self.mfc2tip = add_entry( + self.rowy2opt, label='mfc', text=col2, width=ewbig, padx=padx, + command=self.entered_y2, tooltip='Marker fill color:\n' + ctstr) + self.mfc2frame.pack(side=tk.LEFT) + self.mec2frame, self.mec2lbl, self.mec2, self.mec2tip = add_entry( + self.rowy2opt, label='mec', text=col2, width=ewbig, padx=padx, + command=self.entered_y2, tooltip='Marker edge color:\n' + ctstr) + self.mec2frame.pack(side=tk.LEFT) + self.mew2frame, self.mew2lbl, self.mew2, self.mew2tip = add_entry( + self.rowy2opt, label='mew', text='1', width=ewsmall, padx=padx, + command=self.entered_y2, tooltip='Marker edge width') + self.mew2frame.pack(side=tk.LEFT) # Quit button - self.bquit = ttk.Button(self.rowy2opt, text="Quit", - command=self.master.top.destroy) + self.bquit = Button(self.rowy2opt, text='Quit', + command=self.master.top.destroy) self.bquit.pack(side=tk.RIGHT) self.bquittip = add_tooltip(self.bquit, 'Quit ncvue') @@ -385,6 +463,7 @@ def checked_x(self): Command called if any checkbutton for x-axis was checked or unchecked. Redraws left-hand-side and right-hand-side y-axes. + """ self.redraw_y() self.redraw_y2() @@ -395,6 +474,7 @@ def checked_y(self): or unchecked. Redraws left-hand-side y-axis. + """ self.redraw_y() @@ -404,6 +484,7 @@ def checked_y2(self): checked or unchecked. Redraws right-hand-side y-axis. + """ self.redraw_y2() @@ -424,6 +505,7 @@ def entered_y(self, event): Command called if option was entered for left-hand-side y-axis. Redraws left-hand-side y-axis. + """ self.redraw_y() @@ -432,6 +514,7 @@ def entered_y2(self, event): Command called if option was entered for right-hand-side y-axis. Redraws right-hand-side y-axis. + """ self.redraw_y2() @@ -442,9 +525,13 @@ def next_y(self): Resets dimensions of left-hand-side y-variable. Redraws plot. + """ y = self.y.get() - cols = self.y["values"] + if ihavectk: + cols = self.y.cget('values') + else: + cols = self.y['values'] idx = cols.index(y) idx += 1 if idx < len(cols): @@ -469,9 +556,13 @@ def prev_y(self): Resets dimensions of left-hand-side y-variable. Redraws plot. + """ y = self.y.get() - cols = self.y["values"] + if ihavectk: + cols = self.y.cget('values') + else: + cols = self.y['values'] idx = cols.index(y) idx -= 1 if idx > 0: @@ -482,6 +573,7 @@ def prev_y(self): def newnetcdf(self): """ Open a new netcdf file and connect it to top. + """ # get new netcdf file name ncfile = tk.filedialog.askopenfilename( @@ -536,6 +628,7 @@ def selected_x(self, event): Triggering `event` was bound to the combobox. Resets `x` dimensions. Redraws plot. + """ set_dim_x(self) self.redraw() @@ -548,6 +641,7 @@ def selected_y(self, event): Triggering `event` was bound to the combobox. Resets left-hand-side `y` dimensions. Redraws plot. + """ set_dim_y(self) self.redraw() @@ -560,6 +654,7 @@ def selected_y2(self, event): Triggering `event` was bound to the combobox. Resets right-hand-side `y` dimensions. Redraws plot. + """ set_dim_y2(self) self.redraw() @@ -571,6 +666,7 @@ def spinned_x(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -582,6 +678,7 @@ def spinned_y(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -593,6 +690,7 @@ def spinned_y2(self, event=None): Triggering `event` was bound to the spinbox. Redraws plot. + """ self.redraw() @@ -606,6 +704,7 @@ def minmax_ylim(self, ylim, ylim2): maximum of second element of the two lists. Returns minimum, maximum. + """ if (ylim[0] is not None) and (ylim2[0] is not None): ymin = min(ylim[0], ylim2[0]) @@ -626,6 +725,7 @@ def minmax_ylim(self, ylim, ylim2): def reinit(self): """ Reinitialise the panel from top. + """ # reinit from top self.fi = self.top.fi @@ -647,15 +747,17 @@ def reinit(self): ll.destroy() for ll in self.xd: ll.destroy() + self.xdframe = [] self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): - xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( + xdframe, xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, - command=self.spinned_x, state=tk.DISABLED, tooltip="None") + command=self.spinned_x, state=tk.DISABLED, tooltip='None') + self.xdframe.append(xdframe) self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) @@ -665,15 +767,17 @@ def reinit(self): ll.destroy() for ll in self.yd: ll.destroy() + self.ydframe = [] self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): - ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( + ydframe, ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, - command=self.spinned_y, state=tk.DISABLED, tooltip="None") + command=self.spinned_y, state=tk.DISABLED, tooltip='None') + self.ydframe.append(ydframe) self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) @@ -683,15 +787,17 @@ def reinit(self): ll.destroy() for ll in self.y2d: ll.destroy() + self.y2dframe = [] self.y2dlblval = [] self.y2dlbl = [] self.y2dval = [] self.y2d = [] self.y2dtip = [] for i in range(self.maxdim): - y2dlblval, y2dlbl, y2dval, y2d, y2dtip = add_spinbox( + y2dframe, y2dlblval, y2dlbl, y2dval, y2d, y2dtip = add_spinbox( self.rowy2d, label=str(i), values=(0,), wrap=True, - command=self.spinned_y2, state=tk.DISABLED, tooltip="None") + command=self.spinned_y2, state=tk.DISABLED, tooltip='None') + self.y2dframe.append(y2dframe) self.y2dlblval.append(y2dlblval) self.y2dlbl.append(y2dlbl) self.y2dval.append(y2dval) @@ -699,11 +805,20 @@ def reinit(self): self.y2dtip.append(y2dtip) # set variables columns = [''] + self.cols - self.x['values'] = columns + if ihavectk: + self.x.configure(values=columns) + else: + self.x['values'] = columns self.x.set(columns[0]) - self.y['values'] = columns + if ihavectk: + self.y.configure(values=columns) + else: + self.y['values'] = columns self.y.set(columns[0]) - self.y2['values'] = columns + if ihavectk: + self.y2.configure(values=columns) + else: + self.y2['values'] = columns self.y2.set(columns[0]) # @@ -717,6 +832,7 @@ def redraw_y(self): Reads left-hand-side `y` variable name, the current settings of its dimension spinboxes, as well as all other plotting options. Then redraws the left-hand-side y-axis. + """ # get all states # rowxy @@ -821,6 +937,7 @@ def redraw_y2(self): Reads right-hand-side `y` variable name, the current settings of its dimension spinboxes, as well as all other plotting options. Then redraws the right-hand-side y-axis. + """ # get all states # rowy2 @@ -926,6 +1043,7 @@ def redraw(self, event=None): Reads the two `y` variable names, the current settings of their dimension spinboxes, as well as all other plotting options. Then redraws the both y-axes. + """ # get all states # rowxy @@ -936,11 +1054,9 @@ def redraw(self, event=None): # Clear both axes first, otherwise x-axis only shows # if line2 is chosen. - # if (x != self.x0) or (y != self.y0): self.axes.clear() - # if (x != self.x0) or (y2 != self.y20): self.axes2.clear() - self.axes2.yaxis.set_label_position("right") + self.axes2.yaxis.set_label_position('right') self.axes2.yaxis.tick_right() # ylim = [None, None] # ylim2 = [None, None] @@ -1025,8 +1141,5 @@ def redraw(self, event=None): self.redraw_y() self.redraw_y2() # redraw - self.x0 = x - self.y0 = y - self.y20 = y2 self.canvas.draw() self.toolbar.update() diff --git a/src/ncvue/ncvue.py b/src/ncvue/ncvue.py index d237062..5c7af6b 100644 --- a/src/ncvue/ncvue.py +++ b/src/ncvue/ncvue.py @@ -20,20 +20,21 @@ ncvue History - * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) - * Separate Tk() and Toplevel() widgets to communicate via Tk() between - windows, Jan 2021, Matthias Cuntz - * Set titlebar and taskbar icon only if "standalone" not in ipython or - jupyter, May 2021, Matthias Cuntz - * Different themes for different OS, May 2021, Matthias Cuntz - * Font size 13 on Windows for plots, Jun 2021, Matthias Cuntz - * Allow groups in netcdf files, Jan 2024, Matthias Cuntz - * Allow multiple netcdf files, Jan 2024, Matthias Cuntz - * Move themes/ and images/ directories from src/ncvue/ to src/ directory, - Jan 2024, Matthias Cuntz - * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz - * Change formatting of file string for multiple files, - Jul 2024, Matthias Cuntz + * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) + * Separate Tk() and Toplevel() widgets to communicate via Tk() between + windows, Jan 2021, Matthias Cuntz + * Set titlebar and taskbar icon only if "standalone" not in ipython or + jupyter, May 2021, Matthias Cuntz + * Different themes for different OS, May 2021, Matthias Cuntz + * Font size 13 on Windows for plots, Jun 2021, Matthias Cuntz + * Allow groups in netcdf files, Jan 2024, Matthias Cuntz + * Allow multiple netcdf files, Jan 2024, Matthias Cuntz + * Move themes/ and images/ directories from src/ncvue/ to src/ directory, + Jan 2024, Matthias Cuntz + * Move themes/ and images/ back to src/ncvue/, Feb 2024, Matthias Cuntz + * Change formatting of file string for multiple files, + Jul 2024, Matthias Cuntz + * Use CustomTkinter if installed, Nov 2024, Matthias Cuntz """ import os @@ -41,6 +42,15 @@ import sys import tkinter as tk import tkinter.ttk as ttk +try: + import customtkinter + from customtkinter import CTk as Tk + from customtkinter import CTkToplevel as Toplevel + ihavectk = True +except ModuleNotFoundError: + from tkinter import Tk + from tkinter import Toplevel + ihavectk = False from matplotlib import pyplot as plt import netCDF4 as nc import numpy as np @@ -77,7 +87,7 @@ def ncvue(ncfile=[], miss=np.nan): bundle_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) - top = tk.Tk() + top = Tk() top.withdraw() # top.option_add("*Font", "Helvetica 10") @@ -144,6 +154,15 @@ def ncvue(ncfile=[], miss=np.nan): theme = 'light' # light, dark top.tk.call("set_theme", theme) + if ihavectk: + # customtkinter.set_default_color_theme("blue") + # customtkinter.set_default_color_theme("dark-blue") + # customtkinter.set_default_color_theme("green") + # customtkinter.set_default_color_theme( + # f'{bundle_dir}/themes/customtkinter/dark-blue.json') + customtkinter.set_default_color_theme( + f'{bundle_dir}/themes/customtkinter/ncvue-blue.json') + # set titlebar and taskbar icon only if "standalone", # i.e. not ipython or jupyter try: @@ -156,7 +175,7 @@ def ncvue(ncfile=[], miss=np.nan): else: icon = None - root = tk.Toplevel() + root = Toplevel() root.name = 'ncvOne' if len(ncfile) == 1: root.title("ncvue " + ncfile[0]) @@ -218,5 +237,6 @@ def on_closing(): # 1st plotting window main_frame = ncvMain(root) + main_frame.pack(fill=tk.BOTH, expand=1) top.mainloop() diff --git a/src/ncvue/ncvutils.py b/src/ncvue/ncvutils.py index abd92a9..7213c2e 100644 --- a/src/ncvue/ncvutils.py +++ b/src/ncvue/ncvutils.py @@ -55,12 +55,18 @@ * Add selvar to allow multiple netcdf files, Jan 2024, Matthias Cuntz * Remove [ms] from check for datetime in format_coord on axes, Oct 2024, Matthias Cuntz + * Use CustomTkinter in clone_ncvmain if installed, Nov 2024, Matthias Cuntz """ import tkinter as tk +try: + from customtkinter import CTkToplevel as Toplevel +except ModuleNotFoundError: + from tkinter import Toplevel import numpy as np import matplotlib.dates as mpld import cartopy.crs as ccrs +import ncvue __all__ = ['DIMMETHODS', @@ -429,7 +435,7 @@ def clone_ncvmain(widget): import sys sys.exit() - root = tk.Toplevel() + root = Toplevel() root.name = 'ncvClone' root.title("Secondary ncvue window") root.geometry('1000x800+150+100') @@ -439,9 +445,15 @@ def clone_ncvmain(widget): # https://stackoverflow.com/questions/46505982/is-there-a-way-to-clone-a-tkinter-widget cls = widget.__class__ clone = cls(root) - for key in widget.configure(): - if key != 'class': - clone.configure({key: widget.cget(key)}) + try: + for key in widget.configure(): + if key != 'class': + clone.configure({key: widget.cget(key)}) + except TypeError: + # in case of CustomTkinter + cls = ncvue.ncvMain + clone = cls(root) + clone.pack(fill=tk.BOTH, expand=1) return clone diff --git a/src/ncvue/ncvwidgets.py b/src/ncvue/ncvwidgets.py index 66171a2..e19cec2 100644 --- a/src/ncvue/ncvwidgets.py +++ b/src/ncvue/ncvwidgets.py @@ -8,7 +8,7 @@ Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy, France. -:copyright: Copyright 2020-2023 Matthias Cuntz - mc (at) macu (dot) de +:copyright: Copyright 2020- Matthias Cuntz - mc (at) macu (dot) de :license: MIT License, see LICENSE for details. .. moduleauthor:: Matthias Cuntz @@ -29,26 +29,55 @@ Treeview History - * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) - * Added tooltips to all widgets with class Tooltip, - Jan 2021, Matthias Cuntz - * Added add_tooltip widget, Jan 2021, Matthias Cuntz - * add_spinbox returns also label widget, Jan 2021, Matthias Cuntz - * padlabel for add_entry to add space to previous widget, - Jul 2023, Matthias Cuntz - * labelwidth for add_entry to align columns with pack, - Jul 2023, Matthias Cuntz - * Replace tk constants with strings such as tk.LEFT with 'left', - Jul 2023, Matthias Cuntz - * Use Hovertip from local copy of tooltip.py, Jul 2023, Matthias Cuntz - * Added Treeview class with optional horizontal and vertical scroolbars, - Jul 2023, Matthias Cuntz - * Added callurl function, Dec 2023, Matthias Cuntz + * Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de) + * Added tooltips to all widgets with class Tooltip, + Jan 2021, Matthias Cuntz + * Added add_tooltip widget, Jan 2021, Matthias Cuntz + * add_spinbox returns also label widget, Jan 2021, Matthias Cuntz + * padlabel for add_entry to add space to previous widget, + Jul 2023, Matthias Cuntz + * labelwidth for add_entry to align columns with pack, + Jul 2023, Matthias Cuntz + * Replace tk constants with strings such as tk.LEFT with 'left', + Jul 2023, Matthias Cuntz + * Use Hovertip from local copy of tooltip.py, Jul 2023, Matthias Cuntz + * Added Treeview class with optional horizontal and vertical scroolbars, + Jul 2023, Matthias Cuntz + * Added callurl function, Dec 2023, Matthias Cuntz + * Use CustomTkinter, Jun 2024, Matthias Cuntz + * Use CustomTkinter only if installed, Jun 2024, Matthias Cuntz + * Small bugfix in Combobox if no CustomTkinter, Nov 2024, Matthias Cuntz + * Pass width to Checkbutton if CustomTkinter, Dec 2024, Matthias Cuntz + * Pass padx for space between label and combobox, Dec 2024, Matthias Cuntz + * Use CustomTkinter also in add_menu and add_scale, + Dec 2024, Matthias Cuntz + * Bugfix: did not make new frame in add_spinbox, Dec 2024, Matthias Cuntz """ import tkinter as tk import tkinter.ttk as ttk +try: + from customtkinter import CTkFrame as Frame + from customtkinter import CTkLabel as Label + from customtkinter import CTkCheckBox as Checkbutton + from customtkinter import CTkComboBox as Combobox + from customtkinter import CTkEntry as Entry + from customtkinter import CTkOptionMenu as Menubutton + from customtkinter import CTkSlider as Scale + from customtkinter import CTkScrollbar as Scrollbar + ihavectk = True +except ModuleNotFoundError: + from tkinter.ttk import Frame + from tkinter.ttk import Label + from tkinter.ttk import Checkbutton + from tkinter.ttk import Combobox + from tkinter.ttk import Entry + from tkinter.ttk import Menubutton + from tkinter.ttk import Scale + from tkinter.ttk import Scrollbar + ihavectk = False import webbrowser + from .tooltip import Hovertip @@ -75,13 +104,13 @@ def callurl(url): Examples -------- - >>> opthead = ttk.Frame(self) + >>> opthead = Frame(self) >>> opthead.pack(side='top', fill='x') - >>> optheadlabel1 = ttk.Label(opthead, text='Options for') + >>> optheadlabel1 = Label(opthead, text='Options for') >>> optheadlabel1.pack(side='left') >>> ttk.Style().configure('blue.TLabel', foreground='blue') - >>> optheadlabel2 = ttk.Label(opthead, text='pandas.read_csv', - ... style='blue.TLabel') + >>> optheadlabel2 = Label(opthead, text='pandas.read_csv', + ... style='blue.TLabel') >>> optheadlabel2.pack(side='left') >>> font = tkfont.Font(optheadlabel2, optheadlabel2.cget("font")) >>> font.configure(underline=True) @@ -120,21 +149,27 @@ def __init__(self, anchor_widget, text, hover_delay=1000): def showcontents(self): # light yellow = #ffffe0 - label = tk.Label(self.tipwindow, textvariable=self.text, - background="#ffffe0", foreground="#000000", - justify='left', relief='flat', borderwidth=0, - padx=1, pady=1) + if ihavectk: + label = Label(self.tipwindow, textvariable=self.text, + fg_color="#ffffe0", text_color="#000000", + justify='left', padx=1, pady=1) + else: + label = tk.Label(self.tipwindow, textvariable=self.text, + background="#ffffe0", foreground="#000000", + justify='left', relief='flat', borderwidth=0, + padx=1, pady=1) label.pack() + # label.grid() def add_checkbutton(frame, label="", value=False, command=None, tooltip="", **kwargs): """ - Add a left-aligned ttk.Checkbutton. + Add a left-aligned Checkbutton. Parameters ---------- - frame : tk widget + frame : CustomTkinter widget Parent widget label : str, optional Text that appears on the checkbutton (default: "") @@ -147,7 +182,7 @@ def add_checkbutton(frame, label="", value=False, command=None, tooltip="", Tooltip appearing after one second when hovering over the checkbutton (default: "" = no tooltip) **kwargs : option=value pairs, optional - All other options will be passed to ttk.Checkbutton + All other options will be passed to Checkbutton Returns ------- @@ -159,35 +194,39 @@ def add_checkbutton(frame, label="", value=False, command=None, tooltip="", Examples -------- - >>> self.rowzxy = ttk.Frame(self) + >>> self.rowzxy = Frame(self) >>> self.rowzxy.pack(side='top', fill='x') >>> self.inv_xlbl, self.inv_x = add_checkbutton( ... self.rowzxy, label="invert x", value=False, command=self.checked) """ + iframe = Frame(frame) check_label = tk.StringVar() check_label.set(label) bvar = tk.BooleanVar(value=value) - cb = ttk.Checkbutton(frame, variable=bvar, textvariable=check_label, - command=command, **kwargs) + if ihavectk and ('width' not in kwargs): + width = len(label) * 9 + kwargs.update({'width': width}) + cb = Checkbutton(iframe, variable=bvar, textvariable=check_label, + command=command, **kwargs) cb.pack(side='left', padx=3) if tooltip: ttip = tk.StringVar() ttip.set(tooltip) cbtip = Tooltip(cb, ttip) - return check_label, bvar, ttip + return iframe, check_label, bvar, ttip else: - return check_label, bvar + return iframe, check_label, bvar def add_combobox(frame, label="", values=[], command=None, tooltip="", **kwargs): """ - Add a left-aligned ttk.Combobox with a ttk.Label before. + Add a left-aligned Combobox with a Label before. Parameters ---------- - frame : tk widget + frame : CustomTkinter widget Parent widget label : str, optional Text that appears in front of the combobox (default: "") @@ -199,52 +238,64 @@ def add_combobox(frame, label="", values=[], command=None, tooltip="", tooltip : str, optional Tooltip appearing after one second when hovering over the combobox (default: "" = no tooltip) + padx : int, optional + Extra space in px added left and right of the label text (default: 1) **kwargs : option=value pairs, optional - All other options will be passed to ttk.Combobox + All other options will be passed to Combobox Returns ------- - tk.StringVar, ttk.Combobox + tk.StringVar, Combobox variable for the text before the combobox, combobox widget tk.StringVar variable for the text of the tooltip, if given. Examples -------- - >>> self.rowzxy = ttk.Frame(self) + >>> self.rowzxy = Frame(self) >>> self.rowzxy.pack(side='top', fill='x') >>> self.xlbl, self.x = add_combobox( ... self.rowzxy, label="x", values=columns, command=self.selected) """ + iframe = Frame(frame) width = kwargs.pop('width', 25) cb_label = tk.StringVar() cb_label.set(label) - label = ttk.Label(frame, textvariable=cb_label) + lkwargs = {'textvariable': cb_label} + if ihavectk: + lkwargs.update({'padx': kwargs.pop('padx', 1)}) + else: + _ = kwargs.pop('padx', 1) + label = Label(iframe, **lkwargs) label.pack(side='left') - cb = ttk.Combobox(frame, values=values, width=width, **kwargs) - # long = len(max(values, key=len)) - # cb.configure(width=(max(20, long//2))) - if command is not None: - cb.bind("<>", command) + if ihavectk: + cb = Combobox(iframe, values=values, width=width, command=command, + **kwargs) + else: + cb = ttk.Combobox(iframe, values=values, width=width, **kwargs) + # long = len(max(values, key=len)) + # cb.configure(width=(max(20, long//2))) + if command is not None: + cb.bind("<>", command) cb.pack(side='left') if tooltip: ttip = tk.StringVar() ttip.set(tooltip) cbtip = Tooltip(cb, ttip) - return cb_label, cb, ttip + return iframe, cb_label, cb, ttip else: - return cb_label, cb + return iframe, cb_label, cb def add_entry(frame, label="", text="", command=None, tooltip="", padlabel=0, labelwidth=None, **kwargs): """ - Add a left-aligned ttk.Entry with a ttk.Label before. + Add a left-aligned Entry with a Label before. Parameters ---------- - frame : tk widget + frame : CustomTkinter widget Parent widget label : str, optional Text that appears in front of the entry (default: "") @@ -259,11 +310,13 @@ def add_entry(frame, label="", text="", command=None, tooltip="", Tooltip appearing after one second when hovering over the entry (default: "" = no tooltip) padlabel : int, optional - Prepend number of spaces to create distance other widgets (default: 0) + Prepend number of spaces to create distance to other widgets (default: 0) + padx : int, optional + Extra space in px added left and right of the label text (default: 1) labelwidth : int, optional If given, set width of Label **kwargs : option=value pairs, optional - All other options will be passed to ttk.Entry + All other options will be passed to Entry Returns ------- @@ -275,13 +328,15 @@ def add_entry(frame, label="", text="", command=None, tooltip="", Examples -------- - >>> self.rowxyopt = ttk.Frame(self) + >>> self.rowxyopt = Frame(self) >>> self.rowxyopt.pack(side='top', fill='x') >>> self.lslbl, self.ls = add_entry( ... self.rowxyopt, label="ls", text='-', ... width=4, command=self.selected_y) """ + # label + iframe = Frame(frame) entry_label = tk.StringVar() nlab = len(label) + padlabel lab = f'{label:>{nlab}s}' @@ -289,9 +344,19 @@ def add_entry(frame, label="", text="", command=None, tooltip="", lkwargs = {'textvariable': entry_label} if labelwidth is not None: lkwargs.update({'width': labelwidth}) - label = ttk.Label(frame, **lkwargs) + # if labelwidth is None: + # labelwidth = len(lab) + # if ihavectk: + # labelwidth *= 9 + # lkwargs.update({'width': labelwidth}) + if ihavectk: + lkwargs.update({'padx': kwargs.pop('padx', 1)}) + else: + _ = kwargs.pop('padx', 1) + label = Label(iframe, **lkwargs) # print(label.configure()) label.pack(side='left') + # entry entry_text = tk.StringVar() if text is None: tt = 'None' @@ -303,7 +368,7 @@ def add_entry(frame, label="", text="", command=None, tooltip="", else: tt = str(text) entry_text.set(tt) - entry = ttk.Entry(frame, textvariable=entry_text, **kwargs) + entry = Entry(iframe, textvariable=entry_text, **kwargs) if command is not None: if isinstance(command, (list, tuple)): com0 = command[0] @@ -319,20 +384,21 @@ def add_entry(frame, label="", text="", command=None, tooltip="", entry.bind('', com1) # return entry.bind('', com1) # return of numeric keypad entry.pack(side='left') + # tooltip if tooltip: ttip = tk.StringVar() ttip.set(tooltip) etip = Tooltip(entry, ttip) - return entry_label, entry_text, ttip + return iframe, entry_label, entry_text, ttip else: - return entry_label, entry_text + return iframe, entry_label, entry_text def add_imagemenu(frame, label="", values=[], images=[], command=None, tooltip="", **kwargs): """ Add a left-aligned menu with menubuttons having text and images - with a ttk.Label before. + with a Label before. Parameters ---------- @@ -362,7 +428,7 @@ def add_imagemenu(frame, label="", values=[], images=[], command=None, Examples -------- - >>> self.rowcmap = ttk.Frame(self) + >>> self.rowcmap = Frame(self) >>> self.rowcmap.pack(side='top', fill='x') >>> self.cmaplbl, self.cmap = add_imagemenu( ... self.rowcmap, label="cmap", values=self.cmaps, @@ -372,15 +438,16 @@ def add_imagemenu(frame, label="", values=[], images=[], command=None, """ from functools import partial + iframe = Frame(frame) estr = 'Same number of values and images needed for add_imagemenu.' estr += ' values (' + str(len(values)) + '): ' + str(values) estr += ', images (' + str(len(images)) + '): ' + str(images) assert len(values) == len(images), estr mb_label = tk.StringVar() mb_label.set(label) - label = ttk.Label(frame, textvariable=mb_label) + label = Label(iframe, textvariable=mb_label) label.pack(side='left') - mb = ttk.Menubutton(frame, image=images[0], text=values[0], + mb = ttk.Menubutton(iframe, image=images[0], text=values[0], compound='left') sb = tk.Menu(mb, tearoff=False) mb.config(menu=sb) @@ -392,18 +459,18 @@ def add_imagemenu(frame, label="", values=[], images=[], command=None, ttip = tk.StringVar() ttip.set(tooltip) mbtip = Tooltip(mb, ttip) - return mb_label, mb, ttip + return iframe, mb_label, mb, ttip else: - return mb_label, mb + return iframe, mb_label, mb def add_menu(frame, label="", values=[], command=None, tooltip="", **kwargs): """ - Add a left-aligned menu with menubuttons with a ttk.Label before. + Add a left-aligned menu with menubuttons with a Label before. Parameters ---------- - frame : tk widget + frame : CustomTkinter widget Parent widget label : str, optional Text that appears in front of the menu (default: "") @@ -415,51 +482,55 @@ def add_menu(frame, label="", values=[], command=None, tooltip="", **kwargs): Tooltip appearing after one second when hovering over the menu (default: "" = no tooltip) **kwargs : option=value pairs, optional - All other options will be passed to the main ttk.Menubutton + All other options will be passed to the main Menubutton tk.StringVar variable for the text of the tooltip, if given. Returns ------- - tk.StringVar, ttk.Menubutton + tk.StringVar, Menubutton variable for the text before the menu, main tt.Menubutton widget Examples -------- - >>> self.rowzxy = ttk.Frame(self) + >>> self.rowzxy = Frame(self) >>> self.rowzxy.pack(side='top', fill='x') >>> self.xlbl, self.x = add_combobox( ... self.rowzxy, label="x", values=columns, command=self.selected) """ from functools import partial + iframe = Frame(frame) mb_label = tk.StringVar() mb_label.set(label) - label = ttk.Label(frame, textvariable=mb_label) + label = Label(iframe, textvariable=mb_label) label.pack(side='left') - mb = ttk.Menubutton(frame, text=values[0], compound='left') - sb = tk.Menu(mb, tearoff=False) - mb.config(menu=sb) - for i, v in enumerate(values): - sb.add_command(label=v, compound='left', - command=partial(command, v)) + if ihavectk: + mb = Menubutton(iframe, values=values, command=command, **kwargs) + else: + mb = Menubutton(iframe, text=values[0], compound='left', **kwargs) + sb = tk.Menu(mb, tearoff=False) + mb.config(menu=sb) + for i, v in enumerate(values): + sb.add_command(label=v, compound='left', + command=partial(command, v)) mb.pack(side='left') if tooltip: ttip = tk.StringVar() ttip.set(tooltip) mbtip = Tooltip(mb, ttip) - return mb_label, mb, ttip + return iframe, mb_label, mb, ttip else: - return mb_label, mb + return iframe, mb_label, mb def add_scale(frame, label="", ini=0, tooltip="", **kwargs): """ - Add a left-aligned ttk.Scale with a ttk.Label before. + Add a left-aligned Scale with a Label before. Parameters ---------- - frame : tk widget + frame : CustomTkinter widget Parent widget label : str, optional Text that appears in front of the scale (default: "") @@ -469,11 +540,11 @@ def add_scale(frame, label="", ini=0, tooltip="", **kwargs): Tooltip appearing after one second when hovering over the scale (default: "" = no tooltip) **kwargs : option=value pairs, optional - All other options will be passed to ttk.Scale + All other options will be passed to Scale Returns ------- - tk.StringVar, tk.DoubleVar, ttk.Scale + tk.StringVar, tk.DoubleVar, Scale variable for the text before the scale, value of scale, scale widget @@ -482,27 +553,42 @@ def add_scale(frame, label="", ini=0, tooltip="", **kwargs): Examples -------- - >>> self.rowzxy = ttk.Frame(self) + >>> self.rowzxy = Frame(self) >>> self.rowzxy.pack(side='top', fill='x') >>> self.xlbl, self.x = add_scale( ... self.rowzxy, label="x", values=columns, command=self.selected) """ + iframe = Frame(frame) s_label = tk.StringVar() s_label.set(label) - label = ttk.Label(frame, textvariable=s_label) + label = Label(iframe, textvariable=s_label) label.pack(side='left') s_val = tk.DoubleVar() s_val.set(ini) - s = ttk.Scale(frame, variable=s_val, **kwargs) + if 'from_' not in kwargs: + kwargs.update({'from_': 0}) + if 'to' not in kwargs: + kwargs.update({'to': 100}) + if ihavectk: + kwargs.update({'number_of_steps': kwargs['to'] - kwargs['from_'] + 1}) + length = kwargs.pop('length', -1) + orient = kwargs.pop('orient', tk.HORIZONTAL) + if length < 0: + length = 100 + if orient == tk.HORIZONTAL: + kwargs.update({'width': length}) + if orient == tk.VERTICAL: + kwargs.update({'height': length}) + s = Scale(iframe, variable=s_val, **kwargs) s.pack(side='left') if tooltip: ttip = tk.StringVar() ttip.set(tooltip) stip = Tooltip(s, ttip) - return s_label, s_val, s, ttip + return iframe, s_label, s_val, s, ttip else: - return s_label, s_val, s + return iframe, s_label, s_val, s def add_spinbox(frame, label="", values=[], command=None, tooltip="", @@ -546,15 +632,16 @@ def add_spinbox(frame, label="", values=[], command=None, tooltip="", ... command=self.spinned) """ + iframe = Frame(frame) width = kwargs.pop('width', 1) sbl_val = tk.StringVar() sbl_val.set(label) - sbl = ttk.Label(frame, textvariable=sbl_val) + sbl = ttk.Label(iframe, textvariable=sbl_val) sbl.pack(side='left') sb_val = tk.StringVar() if len(values) > 0: sb_val.set(str(values[0])) - sb = tk.Spinbox(frame, values=values, command=command, width=width, + sb = tk.Spinbox(iframe, values=values, command=command, width=width, textvariable=sb_val, **kwargs) if command is not None: sb.bind('', command) # return @@ -566,9 +653,9 @@ def add_spinbox(frame, label="", values=[], command=None, tooltip="", ttip = tk.StringVar() ttip.set(tooltip) sbtip = Tooltip(sb, ttip) - return sbl_val, sbl, sb_val, sb, ttip + return iframe, sbl_val, sbl, sb_val, sb, ttip else: - return sbl_val, sbl, sb_val, sb + return iframe, sbl_val, sbl, sb_val, sb def add_tooltip(frame, tooltip="", **kwargs): @@ -607,7 +694,7 @@ def add_tooltip(frame, tooltip="", **kwargs): # https://pythonassets.com/posts/scrollbar-in-tk-tkinter/ -class Treeview(ttk.Frame): +class Treeview(Frame): """ Treeview class with optional horizontal and vertical scrollbars @@ -636,19 +723,25 @@ def __init__(self, *args, xscroll=False, yscroll=False, **kwargs): super().__init__(*args, **kwargs) # scrollbars if xscroll: - self.hscrollbar = ttk.Scrollbar(self, orient='horizontal') + if ihavectk: + self.hscrollbar = Scrollbar(self, orientation='horizontal') + else: + self.hscrollbar = Scrollbar(self, orient='horizontal') if yscroll: - self.vscrollbar = ttk.Scrollbar(self, orient='vertical') + if ihavectk: + self.vscrollbar = Scrollbar(self, orientation='vertical') + else: + self.vscrollbar = Scrollbar(self, orient='vertical') # treeview self.tv = ttk.Treeview(self) # pack scrollbars and treeview together if xscroll: self.tv.config(xscrollcommand=self.hscrollbar.set) - self.hscrollbar.config(command=self.tv.xview) + self.hscrollbar.configure(command=self.tv.xview) self.hscrollbar.pack(side='bottom', fill='x') if yscroll: self.tv.config(yscrollcommand=self.vscrollbar.set) - self.vscrollbar.config(command=self.tv.yview) + self.vscrollbar.configure(command=self.tv.yview) self.vscrollbar.pack(side='right', fill='y') self.tv.pack() # convenience functions