mytk 0.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. mytk-0.9.3/LICENSE +21 -0
  2. mytk-0.9.3/PKG-INFO +555 -0
  3. mytk-0.9.3/README.md +514 -0
  4. mytk-0.9.3/mytk/__init__.py +47 -0
  5. mytk-0.9.3/mytk/app.py +150 -0
  6. mytk-0.9.3/mytk/base.py +318 -0
  7. mytk-0.9.3/mytk/bindable.py +211 -0
  8. mytk-0.9.3/mytk/button.py +59 -0
  9. mytk-0.9.3/mytk/canvasview.py +216 -0
  10. mytk-0.9.3/mytk/checkbox.py +40 -0
  11. mytk-0.9.3/mytk/controls.py +88 -0
  12. mytk-0.9.3/mytk/dataviews.py +336 -0
  13. mytk-0.9.3/mytk/dialog.py +169 -0
  14. mytk-0.9.3/mytk/entries.py +285 -0
  15. mytk-0.9.3/mytk/example_apps/__init__.py +0 -0
  16. mytk-0.9.3/mytk/example_apps/canvas_app.py +962 -0
  17. mytk-0.9.3/mytk/example_apps/controlpanel_app.py +84 -0
  18. mytk-0.9.3/mytk/example_apps/example.py +149 -0
  19. mytk-0.9.3/mytk/example_apps/file_calculator_app.py +67 -0
  20. mytk-0.9.3/mytk/example_apps/fileviewer_app.py +46 -0
  21. mytk-0.9.3/mytk/example_apps/filters_app.py +281 -0
  22. mytk-0.9.3/mytk/example_apps/lensviewer_app.py +185 -0
  23. mytk-0.9.3/mytk/example_apps/microscope_app.py +107 -0
  24. mytk-0.9.3/mytk/example_apps/powermeter_app.py +95 -0
  25. mytk-0.9.3/mytk/example_apps/pydatagraph_app.py +307 -0
  26. mytk-0.9.3/mytk/figures.py +315 -0
  27. mytk-0.9.3/mytk/fileviewer.py +216 -0
  28. mytk-0.9.3/mytk/images.py +250 -0
  29. mytk-0.9.3/mytk/indicators.py +107 -0
  30. mytk-0.9.3/mytk/lab/tklab.py +170 -0
  31. mytk-0.9.3/mytk/labels.py +64 -0
  32. mytk-0.9.3/mytk/modulesmanager.py +63 -0
  33. mytk-0.9.3/mytk/notificationcenter.py +162 -0
  34. mytk-0.9.3/mytk/popupmenu.py +61 -0
  35. mytk-0.9.3/mytk/radiobutton.py +55 -0
  36. mytk-0.9.3/mytk/resources/error.png +0 -0
  37. mytk-0.9.3/mytk/resources/info.png +0 -0
  38. mytk-0.9.3/mytk/resources/warning.png +0 -0
  39. mytk-0.9.3/mytk/tableview.py +481 -0
  40. mytk-0.9.3/mytk/tabulardata.py +361 -0
  41. mytk-0.9.3/mytk/tests/envtest.py +38 -0
  42. mytk-0.9.3/mytk/tests/testBaseWidgets.py +182 -0
  43. mytk-0.9.3/mytk/tests/testBindings.py +247 -0
  44. mytk-0.9.3/mytk/tests/testBox.py +71 -0
  45. mytk-0.9.3/mytk/tests/testButton.py +94 -0
  46. mytk-0.9.3/mytk/tests/testCanvasView.py +19 -0
  47. mytk-0.9.3/mytk/tests/testCheckbox.py +98 -0
  48. mytk-0.9.3/mytk/tests/testDialogs.py +81 -0
  49. mytk-0.9.3/mytk/tests/testEntries.py +217 -0
  50. mytk-0.9.3/mytk/tests/testImages.py +160 -0
  51. mytk-0.9.3/mytk/tests/testLabel.py +81 -0
  52. mytk-0.9.3/mytk/tests/testModulesManager.py +53 -0
  53. mytk-0.9.3/mytk/tests/testMyApp.py +90 -0
  54. mytk-0.9.3/mytk/tests/testNotificationCenter.py +154 -0
  55. mytk-0.9.3/mytk/tests/testPopupMenu.py +72 -0
  56. mytk-0.9.3/mytk/tests/testRadioButtons.py +66 -0
  57. mytk-0.9.3/mytk/tests/testSlider.py +88 -0
  58. mytk-0.9.3/mytk/tests/testStyles.py +43 -0
  59. mytk-0.9.3/mytk/tests/testTableView.py +403 -0
  60. mytk-0.9.3/mytk/tests/testTabularData.py +323 -0
  61. mytk-0.9.3/mytk/tests/testTreeData.py +289 -0
  62. mytk-0.9.3/mytk/tests/testTreeTableView.py +218 -0
  63. mytk-0.9.3/mytk/tests/testURLLabel.py +82 -0
  64. mytk-0.9.3/mytk/tests/testView.py +48 -0
  65. mytk-0.9.3/mytk/utils.py +26 -0
  66. mytk-0.9.3/mytk/vectors.py +658 -0
  67. mytk-0.9.3/mytk/videoview.py +311 -0
  68. mytk-0.9.3/mytk/views.py +40 -0
  69. mytk-0.9.3/mytk/window.py +32 -0
  70. mytk-0.9.3/mytk.egg-info/PKG-INFO +555 -0
  71. mytk-0.9.3/mytk.egg-info/SOURCES.txt +75 -0
  72. mytk-0.9.3/mytk.egg-info/dependency_links.txt +1 -0
  73. mytk-0.9.3/mytk.egg-info/requires.txt +6 -0
  74. mytk-0.9.3/mytk.egg-info/top_level.txt +1 -0
  75. mytk-0.9.3/pyproject.toml +41 -0
  76. mytk-0.9.3/requirements.txt +6 -0
  77. mytk-0.9.3/setup.cfg +4 -0
mytk-0.9.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Daniel Côté
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mytk-0.9.3/PKG-INFO ADDED
@@ -0,0 +1,555 @@
1
+ Metadata-Version: 2.2
2
+ Name: mytk
3
+ Version: 0.9.3
4
+ Summary: A wrapper for Tkinter for busy scientists
5
+ Author-email: Daniel Côté <dccote@cervo.ulaval.ca>
6
+ Maintainer-email: Daniel Côté <dccote@cervo.ulaval.ca>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2024 Daniel Côté
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Topic :: Software Development :: Build Tools
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3.12
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: matplotlib==3.9.2
36
+ Requires-Dist: numpy
37
+ Requires-Dist: packaging
38
+ Requires-Dist: pyperclip
39
+ Requires-Dist: Requests
40
+ Requires-Dist: scipy
41
+
42
+ # myTk
43
+ by Daniel C. Côté
44
+
45
+ ## What is it?
46
+ Making a UI interface should not be complicated. **myTk** is a set of UI classes that simplifies the use of Tkinter to make simple (and not so simple!) GUIs in Python.
47
+
48
+ ## Why Tk?
49
+ Tk comes standard with Python. It is highly portable to all main platforms.
50
+
51
+ I know Qt, wxWidgets, and the many other ways to make an interface in Python, and I have programmed macOS since System 7 (Quickdraw, Powerplant) and Mac OS X (Carbon, Aqua, .nib and .xib files in Objective-C and in Swift). The issues I have found with UIs in Python is either the lack of portability or the complexity of the UI strategy:
52
+ **Qt** is very powerful but for most applications (and most scientific programmers) it is too complex, and my experience is that it is fairly fragile to transport to another device (same or different OS). On the other hand, `Tkinter` is standard on Python, but uses UI strategies that are showing their age (for example, raw function callbacks for events, inconsistent nomenclature, and some hoop jumping to get simple things done). It was easier to encapsulate `Tkinter` into something easy to use than to simplify Qt or other UI frameworks. This is therefore the objective of this micro-project: make `Tkinter` very simple to use for non-professional programmers. Many common tasks have classes ready to use.
53
+
54
+ ## Design
55
+ Having been a macOS programmer for a long time, I have lived through the many incarnations of UI frameworks. Over the years, I have come to first understand, and second appreciate, good design patterns. If interested in Design Patterns, I would recommend reading [Design Patterns](https://refactoring.guru/design-patterns). I sought to make `Tkinter` a bit more mac-like because many design patterns in Apple's libraries are particularly mature and useful. For instance, Tkinter requires the parent of the UI-element to be specified at creation, even though there is no reason for it to be required. In addition, the many callbacks get complicated to organize when they are all over the place, therefore when appropriate I implemented a simple strategy to handle many cases by default for the Table, and offer the option to extend the behaviour with delegate-functions (which are a bit cleaner than raw callbacks).
56
+
57
+ * All `Tkinter` widgets are encapsulated into a `View` that provides easy access to many behaviours, but the `widget` remains accessible for you to call functions directly.
58
+ * You can `bind` the property of a GUI-object (`View`) to the value of a control (another `View`). They will always be synchronized, via the interface or even if you change them programmatically
59
+ * You can register for changes to Tkinter.Vars
60
+ * You can register a callback for an event
61
+ * You can set a delegate to manage the details
62
+
63
+ ## Layout manager
64
+ The most important aspect to understand with Tk is how to position things on screen, and I have found it quite confusing. There are three "layout managers" in Tk: `grid`, `pack` and `place`. Grid allows you to conceptually separate a view (or widget in Tk) into a grid, and place objects on that grid. The grid may adjust its size to fit the objects (or not) depending on the options that are passed. If the window is resized, then some columns and rows may resize, depending on options (`column/row` `weight`) and the widget itself may also resize (depending on its `sticky` options ). When adding objects, they may adjust their size or force the size of the grid element (`grid_propagate`). Finally, you can place an element in a range of rows and columns by using the `rowspan` and `columnspan` keywords.
65
+
66
+ The tutorials that helped me the most are: [pythonguis.com](https://www.pythonguis.com/faq/pack-place-and-grid-in-tkinter/) and [TkDocs](https://tkdocs.com/tutorial/index.html).
67
+
68
+ The key elements to remember when using the grid layout manager:
69
+
70
+ 1.
71
+
72
+
73
+ ## Classes
74
+
75
+ Anything visible on screen is a referred to as a View, except the Window.
76
+
77
+ `App`: The main Application class, that holds the reference to the main window.
78
+
79
+ `Window`: A window that can hold other views
80
+
81
+ `Base`: A class grouping functions common to all View classes
82
+
83
+ `View`: A plain, empty view. It can be used as a container for other views in grid, so that the View is a single element of the grid even if it holds several elements itself also in a grid.
84
+
85
+ `PopupMenu`: A popup menu button to select an item in a list
86
+
87
+ `Label`: Static Onscreen text
88
+
89
+ `URLLabel`: Static URL that can be clicked and opened in your webbrowser.
90
+
91
+ `Box`: A box with an optional title at the top and possibly an outline
92
+
93
+ `Entry`: An entry box for single line text
94
+
95
+ `TableView`: A Table of items. You provide headers and items in list. You can sort columns by clicking on the header. Headers can also be used to resize the columns. If a cell is a URL, it is clickable. The table can be editable.
96
+
97
+ `Figure`: A matplotlib figure. You can let Figure create the actual matplotlib.figure or provide your own.
98
+
99
+ ## Getting started
100
+
101
+ The best way to learn is to look at the examples applicateion (`mytk.py`, `lensviewer_app.py`, `filters_app.py`, `microscope_app.py`). But here it is:
102
+
103
+ 1. Create a subclass of `App`.
104
+ 2. In you `__init__`, first call `super().__init__`, then add you interface to the window at `self.window`. See below for examples.
105
+ 1. If you add a `Tableview`, set the `delegate` to an object of your own so that the functions are called when appropriate. Everything is managed automatically. The delegate can implement any or all of the following methods.
106
+ * `selection_changed(event)`: there is no default behaviour
107
+ * `click_header(column)`: the default behaviour will sort the rows by this column
108
+ * `click_cell(item_id, column_id)`: if the text starts with `http`, will try to open the link in your default browser
109
+ * `doubleclick_header(column)`: there is no default behaviour`
110
+ * `doubleclick_cell(item_id, column_id)`: there is no default behaviour
111
+ 2. If you add a `PopupMenu`, set the `user_callback` to act upon a change.
112
+ 3. The matplotlib `Figure` can be used by providing your own plt.figure or using the one provided by the class. There is a `toolbar` that you can add to the interface.
113
+ 3. You can override the method `about(self)` or `help(self)` to provide more than the default behaviour.
114
+ 4. Instantiate your app (`app = MyApp()`), then call `app.mainloop()`
115
+
116
+ The real difficulty is to understand the Layout managers of Tkinter.
117
+
118
+ ## Examples
119
+
120
+ ### Example 1: Demo of capabilities
121
+ The myTk code includes an example:
122
+ ![image-20240315114040659](./README.assets/example-ui.png)
123
+
124
+ ### A filter database
125
+
126
+ The following filter database was created with **myTk**. As it is, it gets the data from our web server, but the code can be changed to use a local file. If you run it, it will work with our database.
127
+
128
+ ![image-20240315114209432](./README.assets/filter_database.png)
129
+
130
+ ```python
131
+ from mytk import *
132
+
133
+ import os
134
+ import re
135
+ import json
136
+ import tempfile
137
+ import shutil
138
+ import webbrowser
139
+ import urllib
140
+ import zipfile
141
+ import subprocess
142
+ from pathlib import Path
143
+
144
+ class FilterDBApp(App):
145
+ def __init__(self):
146
+ App.__init__(self, geometry="1100x650", name="Filter Database")
147
+ self.filepath_root = 'filters_data'
148
+ self.web_root = 'http://www.dccmlab.ca'
149
+ self.temp_root = os.path.join(tempfile.TemporaryDirectory().name)
150
+ self.download_files = True
151
+ self.webbrowser_download_path = None
152
+
153
+ self.window.widget.title("Filters")
154
+ self.window.row_resize_weight(0,1) # Tables
155
+ self.window.row_resize_weight(1,0) # Buttons
156
+ self.window.row_resize_weight(2,1) # Graph
157
+ self.filters = TableView(columns={"part_number":"Part number", "description":"Description","dimensions":"Dimensions","supplier":"Supplier","filename":"Filename","url":"URL", "spectral_x":"Wavelength", "spectral_y":"Transmission"})
158
+ self.filters.grid_into(self.window, row=0, column=0, padx=10, pady=10, sticky='nsew')
159
+ self.filters.widget['displaycolumn']=["part_number","description","dimensions", "supplier","filename","url"]
160
+
161
+ self.filters.widget.column(column=0, width=100)
162
+ self.filters.widget.column(column=1, width=200)
163
+ self.filters.widget.column(column=2, width=120)
164
+ self.filters.widget.column(column=3, width=70)
165
+ self.filters.delegate = self
166
+
167
+ self.filter_data = TableView(columns={"wavelength":"Wavelength", "transmission":"Transmission"})
168
+ self.filter_data.grid_into(self.window, row=0, column=1, padx=10, pady=10, sticky='nsew')
169
+ self.filter_data.widget.column(column=0, width=70)
170
+
171
+ self.controls = View(width=400, height=50)
172
+ self.controls.grid_into(self.window, row=1, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
173
+ self.controls.widget.grid_columnconfigure(0, weight=1)
174
+ self.controls.widget.grid_columnconfigure(1, weight=1)
175
+ self.controls.widget.grid_columnconfigure(2, weight=1)
176
+ self.associate_file_button = Button("Associate spectral file…", user_event_callback=self.associate_file)
177
+ self.associate_file_button.grid_into(self.controls, row=0, column=0, padx=10, pady=10, sticky='nw')
178
+ self.open_filter_data_button = Button("Show files", user_event_callback=self.show_files)
179
+ self.open_filter_data_button.grid_into(self.controls, row=0, column=1, padx=10, pady=10, sticky='nw')
180
+ self.export_filters_button = Button("Export data as Zip…", user_event_callback=self.export_filters)
181
+ self.export_filters_button.grid_into(self.controls, row=0, column=2, padx=10, pady=10, sticky='nw')
182
+ self.copy_data_button = Button("Copy data to clipboard", user_event_callback=self.copy_data)
183
+ self.copy_data_button.grid_into(self.controls, row=0, column=3, padx=10, pady=10, sticky='ne')
184
+
185
+
186
+ self.filter_plot = XYPlot(figsize=(4,4))
187
+ self.filter_plot.grid_into(self.window, row=2, column=0, columnspan=2, padx=10, pady=10, sticky='nsew')
188
+
189
+ self.filters_db = None
190
+ self.load()
191
+
192
+ def load(self):
193
+ if self.download_files:
194
+ self.filepath_root, filepath = self.get_files_from_web()
195
+ else:
196
+ filepath = os.path.join(self.filepath_root, "filters.json")
197
+
198
+ self.filters.load(filepath)
199
+
200
+ def get_files_from_web(self):
201
+ install_modules_if_absent(modules={"requests":"requests"})
202
+
203
+ import requests
204
+
205
+ url = "/".join([self.web_root, 'filters_data.zip'])
206
+ req = requests.get(url, allow_redirects=True)
207
+ open('filters_data.zip', 'wb').write(req.content)
208
+
209
+ with zipfile.ZipFile('filters_data.zip', 'r') as zip_ref:
210
+ zip_ref.extractall(self.temp_root)
211
+
212
+ return os.path.join(self.temp_root, 'filters_data'), os.path.join(self.temp_root, 'filters_data', 'filters.json')
213
+
214
+ def save(self):
215
+ filepath = os.path.join(self.filepath_root, "filters.json")
216
+ self.filters.save(filepath)
217
+
218
+ def load_filter_data(self, filepath):
219
+ data = []
220
+ with open(filepath,'r') as file:
221
+ try:
222
+ lines = file.readlines()
223
+ for line in lines:
224
+ match = re.search(r'(\d+.\d*)[\s,]+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)', line)
225
+ if match is not None:
226
+ try:
227
+ x = float(match.group(1))
228
+ y = float(match.group(2))
229
+ data.append((x,y))
230
+ except Exception as err:
231
+ # not an actual data line
232
+ pass
233
+
234
+ except Exception as err:
235
+ if len(data) == 0:
236
+ return None
237
+
238
+ return data
239
+
240
+ def load_filters_table(self, filepath):
241
+ data = []
242
+ with open(filepath,'r') as file:
243
+ try:
244
+ lines = file.readlines()
245
+ for line in lines:
246
+ records = line.split('\t')
247
+ data.append(records)
248
+ except Exception as err:
249
+ if len(data) == 0:
250
+ return None
251
+
252
+ return data
253
+
254
+ def associate_file(self, event, button):
255
+ for selected_item in self.filters.widget.selection():
256
+ item = self.filters.widget.item(selected_item)
257
+ record = item['values']
258
+
259
+ part_number_idx = list(self.filters.columns.keys()).index('part_number')
260
+ description_idx = list(self.filters.columns.keys()).index('description')
261
+ supplier_idx = list(self.filters.columns.keys()).index('supplier')
262
+
263
+ query = str(record[part_number_idx])+"+"+str(record[description_idx])
264
+ query = query+f"+{record[supplier_idx]}+filter"
265
+
266
+ webbrowser.open(f"https://www.google.com/search?q={query}")
267
+ time.sleep(0.3)
268
+ browser_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
269
+
270
+ filepath = None
271
+
272
+ if self.webbrowser_download_path is None:
273
+ filepath = filedialog.askopenfilename()
274
+ else:
275
+ pre_list = os.listdir(self.webbrowser_download_path)
276
+ frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
277
+ while frontmost_app == browser_app:
278
+ self.window.widget.update_idletasks()
279
+ self.window.widget.update()
280
+ frontmost_app = subprocess.run(["osascript","-e","return path to frontmost application as text"],capture_output=True, encoding='utf8').stdout
281
+ post_list = os.listdir(self.webbrowser_download_path)
282
+
283
+ new_filepaths = list(set(post_list) - set(pre_list))
284
+ if len(new_filepaths) == 1:
285
+ filepath = os.path.join(self.webbrowser_download_path, new_filepaths[0])
286
+ else:
287
+ filepath = ''
288
+
289
+ if filepath != '':
290
+ shutil.copy2(filepath, self.filepath_root)
291
+ filename_idx = list(self.filters.columns.keys()).index('filename')
292
+
293
+ record[filename_idx] = os.path.basename(filepath)
294
+ self.webbrowser_download_path = os.path.dirname(filepath)
295
+ self.filters.widget.item(selected_item, values=record)
296
+ self.save()
297
+
298
+ def export_filters(self, event, button):
299
+ zip_filepath = filedialog.asksaveasfilename(
300
+ parent=self.window.widget,
301
+ title="Choose a filename:",
302
+ filetypes=[('Zip files','.zip')],
303
+ )
304
+ if zip_filepath:
305
+ with zipfile.ZipFile(zip_filepath, 'w') as zip_ref:
306
+ zip_ref.mkdir(self.filepath_root)
307
+ for filepath in Path(self.filepath_root).iterdir():
308
+ zip_ref.write(filepath, arcname=os.path.join(self.filepath_root,filepath.name))
309
+
310
+ def show_files(self, event, button):
311
+ self.reveal_path(self.filepath_root)
312
+
313
+ def copy_data(self, event, button):
314
+ install_modules_if_absent(modules={"pyperclip":"pyperclip"})
315
+ try:
316
+ import pyperclip
317
+
318
+ for selected_item in self.filters.widget.selection():
319
+ item = self.filters.widget.item(selected_item)
320
+ record = item['values']
321
+
322
+ filename_idx = list(self.filters.columns.keys()).index('filename')
323
+ filename = record[filename_idx]
324
+
325
+ filepath = os.path.join(self.filepath_root, filename)
326
+ if os.path.isfile(filepath):
327
+ data = self.load_filter_data(filepath)
328
+
329
+ text = ""
330
+ for x,y in data:
331
+ text = text + "{0}\t{1}\n".format(x,y)
332
+
333
+ pyperclip.copy(text)
334
+ except Exception as err:
335
+ print(err)
336
+ showerror(
337
+ title="Unable to copy to clipboard",
338
+ message="You must have the module pyperclip installed to copy the data.",
339
+ )
340
+
341
+
342
+ def selection_changed(self, event, table):
343
+ for selected_item in table.widget.selection():
344
+ item = table.widget.item(selected_item)
345
+ record = item['values']
346
+
347
+ filename_idx = list(self.filters.columns.keys()).index('filename')
348
+ filename = record[filename_idx]
349
+ filepath = os.path.join(self.filepath_root, filename)
350
+
351
+ if os.path.exists(filepath) and not os.path.isdir(filepath):
352
+
353
+ data = self.load_filter_data(filepath)
354
+
355
+ self.filter_data.empty()
356
+ self.filter_plot.clear_plot()
357
+ for x,y in data:
358
+ self.filter_data.append((x,y))
359
+ self.filter_plot.append(x,y)
360
+ self.filter_plot.first_axis.set_ylabel("Transmission")
361
+ self.filter_plot.first_axis.set_xlabel("Wavelength [nm]")
362
+ self.filter_plot.update_plot()
363
+ self.copy_data_button.enable()
364
+ else:
365
+ self.filter_data.empty()
366
+ self.filter_plot.clear_plot()
367
+ self.filter_plot.update_plot()
368
+ self.copy_data_button.disable()
369
+
370
+
371
+ if __name__ == "__main__":
372
+ package_app_script(__file__)
373
+ install_modules_if_absent(modules={"requests":"requests","pyperclip":"pyperclip"}, ask_for_confirmation=False)
374
+ app = FilterDBApp()
375
+ app.mainloop()
376
+
377
+ ```
378
+
379
+
380
+
381
+ ### Example 2: Raytracing lens viewer
382
+
383
+ The following interface to the module ["Raytracing"](https://github.com/DCC-Lab/RayTracing) was created with **myTk**. It shows a list of lenses with their properties in a Tableview, clicking on the headers will sort the rows, clicking on a link will open the URL
384
+ in a browser. The figures underneath will reflect the properties of the selected item.
385
+ <img width="1451" alt="image" src="./README.assets/filter_database.png">
386
+
387
+ The code that generates this application is the following:
388
+ ```python
389
+ from mytk import *
390
+ import raytracing as rt
391
+ import raytracing.thorlabs as thorlabs
392
+ import raytracing.eo as eo
393
+ from raytracing.figure import GraphicOf
394
+
395
+
396
+ class OpticalComponentViewer(App):
397
+ def __init__(self):
398
+ App.__init__(self, geometry="1450x750")
399
+
400
+
401
+ self.window.widget.title("Lens viewer")
402
+ self.window.resizable = False
403
+ self.label = None
404
+ self.menu = None
405
+ self.default_figsize = (7, 5)
406
+ self.header = View(width=1450, height=200)
407
+ self.header.grid_into(
408
+ self.window, column=0, row=0, pady=5, padx=5, sticky="nsew"
409
+ )
410
+ self.graphs = View(width=1450, height=700)
411
+ self.graphs.grid_into(
412
+ self.window, column=0, row=1, pady=5, padx=5, sticky="nsew"
413
+ )
414
+ self.component = None
415
+ self.dispersion = None
416
+
417
+ self.lenses = {}
418
+ self.build_lens_dict()
419
+ self.build_table()
420
+
421
+ self.update_figure()
422
+
423
+ def build_table(self):
424
+ self.columns = {
425
+ "label": "Part number",
426
+ "backFocalLength": "Front focal length [mm]",
427
+ "frontFocalLength": "Back focal length [mm]",
428
+ "effectiveFocalLengths": "Effective focal length [mm]",
429
+ "apertureDiameter": "Diameter [mm]",
430
+ "wavelengthRef": "Design wavelength [nm]",
431
+ "materials": "Material(s)",
432
+ "url": "URL",
433
+ }
434
+ self.table = TableView(columns=self.columns)
435
+ self.table.delegate = self
436
+ self.table.grid_into(self.header, sticky="nsew", padx=5)
437
+
438
+ for column in self.columns:
439
+ self.table.widget.column(column, width=150, anchor=CENTER)
440
+ self.table.widget.column("url", width=350, anchor=W)
441
+
442
+ iids = []
443
+ for label, lens in self.lenses.items():
444
+ if lens.wavelengthRef is not None:
445
+ wavelengthRef = "{0:.1f}".format(lens.wavelengthRef * 1000)
446
+ else:
447
+ wavelengthRef = "N/A"
448
+
449
+ materials = ""
450
+ if isinstance(lens, rt.AchromatDoubletLens):
451
+ if lens.mat1 is not None and lens.mat2 is not None:
452
+ materials = "{0}/{1}".format(str(lens.mat1()), str(lens.mat2()))
453
+ elif isinstance(lens, rt.SingletLens):
454
+ if lens.mat is not None:
455
+ materials = "{0}".format(str(lens.mat()))
456
+
457
+ iid = self.table.append(
458
+ values=(
459
+ lens.label,
460
+ "{0:.1f}".format(lens.backFocalLength()),
461
+ "{0:.1f}".format(lens.frontFocalLength()),
462
+ "{0:.1f}".format(lens.effectiveFocalLengths()[0]),
463
+ "{0:.1f}".format(lens.apertureDiameter),
464
+ wavelengthRef,
465
+ materials,
466
+ lens.url,
467
+ )
468
+ )
469
+ iids.append(iid)
470
+
471
+ self.table.widget.selection_set(iids[0])
472
+
473
+ scrollbar = ttk.Scrollbar(
474
+ self.header.widget, orient=VERTICAL, command=self.table.widget.yview
475
+ )
476
+ self.table.widget.configure(yscroll=scrollbar.set)
477
+ scrollbar.grid(row=0, column=1, sticky="ns")
478
+
479
+ def update_figure(self, figure=None):
480
+ if figure is not None:
481
+ figure.set_size_inches(self.default_figsize)
482
+ self.component = Figure(figure, figsize=self.default_figsize)
483
+ self.component.grid_into(self.graphs, column=0, row=0, padx=5)
484
+ self.dispersion = Figure(figsize=self.default_figsize)
485
+ self.dispersion.grid_into(self.graphs, column=1, row=0, padx=5)
486
+
487
+ @property
488
+ def figure(self):
489
+ return self.component.figure
490
+
491
+ @figure.setter
492
+ def figure(self, value):
493
+ self.update_figure(value)
494
+
495
+ def build_lens_dict(self):
496
+ modules = [thorlabs, eo]
497
+
498
+ for i, lens in enumerate(rt.CompoundLens.all()):
499
+ for module in modules:
500
+ try:
501
+ class_ = getattr(module, lens)
502
+ lens = class_()
503
+ f1, f2 = lens.effectiveFocalLengths()
504
+ self.lenses[lens.label] = lens
505
+ except Exception as err:
506
+ pass
507
+
508
+ def selection_changed(self, event):
509
+ for selected_item in self.table.widget.selection():
510
+ item = self.table.widget.item(selected_item)
511
+ record = item["values"]
512
+ lens = self.lenses[record[0]] # label
513
+ self.update_figures(lens)
514
+
515
+ def update_figures(self, lens):
516
+ graphic = GraphicOf(lens)
517
+ self.figure = graphic.drawFigure().figure
518
+ self.figure.set_size_inches((5, 5), forward=True)
519
+
520
+ try:
521
+ wavelengths, focalShifts = lens.focalShifts()
522
+
523
+ axis = self.dispersion.figure.add_subplot()
524
+ axis.plot(wavelengths, focalShifts, "k-")
525
+ axis.set_xlabel(r"Wavelength [nm]")
526
+ axis.set_ylabel(r"Focal shift [mm]")
527
+ except Exception as err:
528
+ pass
529
+
530
+ def about(self):
531
+ showinfo(
532
+ title="About Lens Viewer",
533
+ message="A lens viewer for the Raytracing package by the DCC/M Lab.",
534
+ )
535
+
536
+ def help(self):
537
+ webbrowser.open("https://raytracing.readthedocs.io/")
538
+
539
+ def doubleclick_cell(self, item_id, column_id, item_dict):
540
+ value = item_dict["values"][column_id - 1]
541
+ pyperclip.copy(value)
542
+ return True
543
+
544
+
545
+ if __name__ == "__main__":
546
+ rt.silentMode()
547
+ app = OpticalComponentViewer()
548
+
549
+ from packaging.version import Version
550
+ if Version(rt.__version__) <= Version("1.3.10"):
551
+ showerror(title="Minimum Raytracing version", message="You need at least Raytracing 1.3.11 to run the lens viewer", icon=ERROR)
552
+ else:
553
+ app.mainloop()
554
+
555
+ ```