audio-tuner-gui 0.9.1__py3-none-any.whl

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.
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # This file is part of Audio Tuner.
4
+ #
5
+ # Copyright 2025, 2026 Jessie Blue Cassell <bluesloth600@gmail.com>
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+
20
+
21
+ """File selector for the GUI."""
22
+
23
+
24
+ __author__ = 'Jessie Blue Cassell'
25
+
26
+
27
+ __all__ = [
28
+ 'FileSelector',
29
+ ]
30
+
31
+
32
+ import os
33
+ import errno
34
+ from collections import deque
35
+
36
+ from PyQt6.QtCore import (
37
+ Qt,
38
+ QDir,
39
+ pyqtSignal,
40
+ QModelIndex,
41
+ )
42
+ from PyQt6.QtWidgets import (
43
+ QWidget,
44
+ QVBoxLayout,
45
+ QHBoxLayout,
46
+ QLineEdit,
47
+ QTreeView,
48
+ QAbstractItemView,
49
+ QCompleter,
50
+ QToolButton,
51
+ )
52
+ from PyQt6.QtGui import (
53
+ QIcon,
54
+ QFileSystemModel,
55
+ QKeySequence,
56
+ )
57
+
58
+ import audio_tuner_gui.common as gcom
59
+
60
+
61
+ class _PathHistory(deque):
62
+ def __init__(self, path):
63
+ super().__init__()
64
+ self.n = 0
65
+ self.append(self._norm(path))
66
+
67
+ def _norm(self, path):
68
+ return QDir.fromNativeSeparators(path).rstrip('/') + '/'
69
+
70
+ def cd(self, path):
71
+ path = self._norm(path)
72
+ while not self.at_end():
73
+ self.pop()
74
+ if path != self[-1]:
75
+ self.append(path)
76
+ self.n += 1
77
+ return path
78
+
79
+ def at_end(self):
80
+ return len(self) == self.n + 1
81
+
82
+ def at_beginning(self):
83
+ return self.n == 0
84
+
85
+ def current(self):
86
+ return self[self.n]
87
+
88
+ def next(self, native=False):
89
+ ret = self[self.n+1]
90
+ if native:
91
+ ret = QDir.toNativeSeparators(ret)
92
+ return ret
93
+
94
+ def previous(self, native=False):
95
+ ret = self[self.n-1]
96
+ if native:
97
+ ret = QDir.toNativeSeparators(ret)
98
+ return ret
99
+
100
+ def forward(self):
101
+ if not self.at_end():
102
+ self.n += 1
103
+ else:
104
+ raise IndexError
105
+
106
+ def back(self):
107
+ if not self.at_beginning():
108
+ self.n -= 1
109
+ else:
110
+ raise IndexError
111
+
112
+
113
+ class _PathEdit(QLineEdit):
114
+ def setText(self, text):
115
+ if QDir(text).exists():
116
+ text = text.rstrip('/\\') + '/'
117
+ text = QDir.toNativeSeparators(text)
118
+ super().setText(text)
119
+
120
+
121
+ class _FileView(QTreeView):
122
+ EnterKeyPress = pyqtSignal()
123
+
124
+ def __init__(self):
125
+ super().__init__()
126
+
127
+ self.setExpandsOnDoubleClick(False)
128
+ self.setAllColumnsShowFocus(True)
129
+ self.setItemsExpandable(False)
130
+ self.setRootIsDecorated(False)
131
+ self.setSelectionMode(
132
+ QAbstractItemView.SelectionMode.ExtendedSelection)
133
+
134
+ def keyPressEvent(self, event):
135
+ if event.key() == Qt.Key.Key_Return:
136
+ self.EnterKeyPress.emit()
137
+ else:
138
+ super().keyPressEvent(event)
139
+
140
+
141
+ class FileSelector(QWidget):
142
+ """A widget for selecting files to analyze. Inherits from QWidget.
143
+
144
+ Parameters
145
+ ----------
146
+ option_panel : audio_tuner_gui.option_panel.OptionPanel
147
+ The option panel widget to get analysis options from.
148
+ """
149
+
150
+ SelectForAnalysis = pyqtSignal(str, gcom.Options)
151
+ """Signal emitted when a file is selected.
152
+
153
+ Parameters
154
+ ----------
155
+ audio_tuner_gui.common.Options
156
+ The options to use when the file is analyzed.
157
+ """
158
+
159
+ UpdateStatusbar = pyqtSignal(str)
160
+ """Signal emitted to request a string be displayed on the status bar.
161
+
162
+ Parameters
163
+ ----------
164
+ str
165
+ The string.
166
+ """
167
+
168
+
169
+ def __init__(self, option_panel):
170
+ super().__init__()
171
+
172
+ self.option_panel = option_panel
173
+
174
+ vbox = QVBoxLayout(self)
175
+ vbox.setSpacing(5)
176
+ vbox.setContentsMargins(3, 3, 3, 3)
177
+
178
+ self.path_history = _PathHistory(QDir.currentPath())
179
+
180
+ self._init_file_view()
181
+ self._init_control_panel()
182
+ self._cd(self.path_history.current())
183
+
184
+ vbox.addWidget(self.panel)
185
+ vbox.addWidget(self.view)
186
+
187
+ def _init_file_view(self):
188
+ model = QFileSystemModel(self)
189
+
190
+ self.original_filter = model.filter()
191
+ self.showhidden_filter = self.original_filter | QDir.Filter.Hidden
192
+
193
+ view = _FileView()
194
+ view.setModel(model)
195
+ view.setColumnWidth(0, 250)
196
+ view.header().moveSection(2, 1)
197
+ view.setSortingEnabled(True)
198
+ view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
199
+
200
+ view.doubleClicked.connect(self._file_selector_doubleclick)
201
+ view.EnterKeyPress.connect(self._file_selector_enter)
202
+
203
+ self.model = model
204
+ self.view = view
205
+
206
+ def _set_show_hidden(self, checked):
207
+ if checked:
208
+ self.model.setFilter(self.showhidden_filter)
209
+ else:
210
+ self.model.setFilter(self.original_filter)
211
+
212
+ def _init_control_panel(self):
213
+ panel = QWidget()
214
+ hbox = QHBoxLayout(panel)
215
+ hbox.setContentsMargins(0, 0, 0, 0)
216
+
217
+ back_act = gcom.SplitAction('&Back', self)
218
+ back_act.setIcon(QIcon.fromTheme(gcom.ICON_BACK))
219
+ back_act.setShortcut(QKeySequence('Alt+Left'))
220
+ back_act.triggered_connect(self._go_back)
221
+
222
+ back_button = QToolButton()
223
+ back_button.setDefaultAction(back_act.button())
224
+ hbox.addWidget(back_button)
225
+
226
+ forward_act = gcom.SplitAction('&Forward', self)
227
+ forward_act.setIcon(QIcon.fromTheme(gcom.ICON_FORWARD))
228
+ forward_act.setShortcut(QKeySequence('Alt+Right'))
229
+ forward_act.triggered_connect(self._go_forward)
230
+
231
+ forward_button = QToolButton()
232
+ forward_button.setDefaultAction(forward_act.button())
233
+ hbox.addWidget(forward_button)
234
+
235
+ up_act = gcom.SplitAction('&Up', self)
236
+ up_act.setIcon(QIcon.fromTheme(gcom.ICON_UP))
237
+ up_act.setShortcut(QKeySequence('Alt+Up'))
238
+ up_act.triggered_connect(self._updir)
239
+
240
+ up_button = QToolButton()
241
+ up_button.setDefaultAction(up_act.button())
242
+ hbox.addWidget(up_button)
243
+
244
+ home_act = gcom.SplitAction('&Home', self)
245
+ home_act.setIcon(QIcon.fromTheme(gcom.ICON_HOME))
246
+ home_act.setShortcut(QKeySequence('Alt+Home'))
247
+ home_act.setStatusTip('Go to home directory')
248
+ home_act.triggered_connect(self._go_home)
249
+
250
+ home_button = QToolButton()
251
+ home_button.setDefaultAction(home_act.button())
252
+ hbox.addWidget(home_button)
253
+
254
+ completer = QCompleter(self.model, self)
255
+
256
+ pathedit = _PathEdit()
257
+ pathedit.setCompleter(completer)
258
+ hbox.addWidget(pathedit)
259
+ pathedit.returnPressed.connect(self._pathedit_enter,
260
+ Qt.ConnectionType.QueuedConnection)
261
+
262
+ self.pathedit = pathedit
263
+ self.panel = panel
264
+ self.back_button = back_button
265
+ self.forward_button = forward_button
266
+ self.up_button = up_button
267
+ self.back_act = back_act
268
+ self.forward_act = forward_act
269
+ self.up_act = up_act
270
+ self.home_act = home_act
271
+
272
+ def _pathedit_enter(self):
273
+ try:
274
+ self._cd(QDir.fromNativeSeparators(self.pathedit.text()))
275
+ except NotADirectoryError:
276
+ path = self.pathedit.text()
277
+ options = self.option_panel.get_options()
278
+ if options is None:
279
+ self.option_panel.ensure_visible()
280
+ else:
281
+ self.SelectForAnalysis.emit(path, options)
282
+ self._select_filename()
283
+
284
+ def handle_command_line_arg(self, arg):
285
+ """If `arg` is a file, act like it's been selected and send it
286
+ out for analysis. If `arg` is a directory, change to that
287
+ directory.
288
+
289
+ Note that this does the same thing as the `handle_drop` method.
290
+ They're separate methods to leave open the possibility of
291
+ different behavior for command line arguments and drops in the
292
+ future.
293
+
294
+ Parameters
295
+ ----------
296
+ arg : str
297
+ File or directory path.
298
+ """
299
+
300
+ try:
301
+ self._cd(QDir.fromNativeSeparators(arg))
302
+ except NotADirectoryError:
303
+ options = self.option_panel.get_options()
304
+ if options is None:
305
+ self.option_panel.ensure_visible()
306
+ else:
307
+ self.SelectForAnalysis.emit(arg, options)
308
+
309
+ def handle_drop(self, path):
310
+ """If `path` is a file, act like it's been selected and send it
311
+ out for analysis. If `path` is a directory, change to that
312
+ directory.
313
+
314
+ Note that this does the same thing as the
315
+ `handle_command_line_arg` method. They're separate methods to
316
+ leave open the possibility of different behavior for command
317
+ line arguments and drops in the future.
318
+
319
+ Parameters
320
+ ----------
321
+ path : str
322
+ File or directory path.
323
+ """
324
+ self.handle_command_line_arg(path)
325
+
326
+ def _select_filename(self):
327
+ path = self.pathedit.text()
328
+ if not QDir(path).exists():
329
+ length = len(os.path.basename(path))
330
+ pos = len(os.path.dirname(path)) + 1
331
+ self.pathedit.setSelection(pos, length)
332
+
333
+
334
+ def _cd(self, dest=None, hist='cd'):
335
+ if hist == 'previous':
336
+ dest = self.path_history.previous()
337
+ elif hist == 'next':
338
+ dest = self.path_history.next()
339
+ if isinstance(dest, QModelIndex):
340
+ path = self.model.filePath(dest)
341
+ index = dest
342
+ elif isinstance(dest, str):
343
+ if not QDir(dest).exists():
344
+ raise NotADirectoryError(errno.ENOTDIR,
345
+ os.strerror(errno.ENOTDIR),
346
+ dest)
347
+ path = dest
348
+ index = self.model.index(dest)
349
+ self.model.setRootPath(path)
350
+ self.view.setRootIndex(index)
351
+ self.pathedit.setText(QDir(path).absolutePath())
352
+ if hist == 'previous':
353
+ self.path_history.back()
354
+ elif hist == 'next':
355
+ self.path_history.forward()
356
+ elif hist == 'cd':
357
+ self.path_history.cd(path)
358
+ if self.path_history.at_beginning():
359
+ self.back_act.setEnabled(False)
360
+ self.back_button.setAutoRaise(True)
361
+ status = 'No previous directory'
362
+ self.back_act.setStatusTip(status)
363
+ if self.back_button.underMouse():
364
+ self.UpdateStatusbar.emit(status)
365
+ else:
366
+ self.back_act.setEnabled(True)
367
+ self.back_button.setAutoRaise(False)
368
+ status = ('Go to previous directory: '
369
+ + self.path_history.previous(native=True))
370
+ self.back_act.setStatusTip(status)
371
+ if self.back_button.underMouse():
372
+ self.UpdateStatusbar.emit(status)
373
+ if self.path_history.at_end():
374
+ self.forward_act.setEnabled(False)
375
+ self.forward_button.setAutoRaise(True)
376
+ status = 'No next directory'
377
+ self.forward_act.setStatusTip(status)
378
+ if self.forward_button.underMouse():
379
+ self.UpdateStatusbar.emit(status)
380
+ else:
381
+ self.forward_act.setEnabled(True)
382
+ self.forward_button.setAutoRaise(False)
383
+ status = ('Go to next directory: '
384
+ + self.path_history.next(native=True))
385
+ self.forward_act.setStatusTip(status)
386
+ if self.forward_button.underMouse():
387
+ self.UpdateStatusbar.emit(status)
388
+ if QDir(path).isRoot():
389
+ self.up_act.setEnabled(False)
390
+ self.up_button.setAutoRaise(True)
391
+ status = 'No parent directory'
392
+ self.up_act.setStatusTip(status)
393
+ if self.up_button.underMouse():
394
+ self.UpdateStatusbar.emit(status)
395
+ else:
396
+ self.up_act.setEnabled(True)
397
+ self.up_button.setAutoRaise(False)
398
+ status = 'Go to parent directory'
399
+ self.up_act.setStatusTip(status)
400
+ if self.up_button.underMouse():
401
+ self.UpdateStatusbar.emit(status)
402
+
403
+ def _file_selector_doubleclick(self, index):
404
+ if self.model.isDir(index):
405
+ self._cd(index)
406
+ else:
407
+ self._file_selector_enter()
408
+
409
+ def _file_selector_enter(self):
410
+ selected = [x for x in self.view.selectedIndexes() if x.column() == 0]
411
+ if len(selected) == 1 and self.model.isDir(selected[0]):
412
+ self._cd(selected[0])
413
+ else:
414
+ options = self.option_panel.get_options()
415
+ if options is None:
416
+ self.option_panel.ensure_visible()
417
+ else:
418
+ for index in selected:
419
+ self.SelectForAnalysis.emit(self.model.filePath(index),
420
+ options)
421
+
422
+ def _updir(self):
423
+ directory = self.model.rootDirectory()
424
+ if directory.cdUp():
425
+ newpath = directory.absolutePath()
426
+ self._cd(newpath)
427
+
428
+ def _go_home(self):
429
+ self._cd(QDir.homePath())
430
+
431
+ def _go_back(self):
432
+ self._cd(hist='previous')
433
+
434
+ def _go_forward(self):
435
+ self._cd(hist='next')
@@ -0,0 +1,215 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ width="64"
6
+ height="64"
7
+ viewBox="0 0 16.933334 16.933334"
8
+ version="1.1"
9
+ id="svg5"
10
+ inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
11
+ sodipodi:docname="audio_tuner_icon.svg"
12
+ inkscape:export-filename="audio_tuner_icon_32.png"
13
+ inkscape:export-xdpi="48"
14
+ inkscape:export-ydpi="48"
15
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ xmlns:svg="http://www.w3.org/2000/svg"
19
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
20
+ xmlns:cc="http://creativecommons.org/ns#"
21
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
22
+ <sodipodi:namedview
23
+ id="namedview7"
24
+ pagecolor="#000000"
25
+ bordercolor="#000000"
26
+ borderopacity="0.25"
27
+ inkscape:showpageshadow="2"
28
+ inkscape:pageopacity="0"
29
+ inkscape:pagecheckerboard="0"
30
+ inkscape:deskcolor="#d1d1d1"
31
+ inkscape:document-units="mm"
32
+ showgrid="true"
33
+ inkscape:zoom="12.500449"
34
+ inkscape:cx="30.038921"
35
+ inkscape:cy="32.598829"
36
+ inkscape:window-width="1447"
37
+ inkscape:window-height="1080"
38
+ inkscape:window-x="0"
39
+ inkscape:window-y="0"
40
+ inkscape:window-maximized="0"
41
+ inkscape:current-layer="layer1">
42
+ <inkscape:grid
43
+ type="xygrid"
44
+ id="grid9"
45
+ dotted="true"
46
+ originx="0"
47
+ originy="0"
48
+ spacingx="0.52916669"
49
+ spacingy="0.52916669"
50
+ empspacing="2" />
51
+ </sodipodi:namedview>
52
+ <defs
53
+ id="defs2" />
54
+ <g
55
+ inkscape:label="Layer 1"
56
+ inkscape:groupmode="layer"
57
+ id="layer1">
58
+ <rect
59
+ style="fill:#000000;stroke-width:0.264583"
60
+ id="rect649"
61
+ width="16.933332"
62
+ height="16.933332"
63
+ x="0"
64
+ y="0"
65
+ rx="2.6458318"
66
+ ry="2.6458335" />
67
+ <path
68
+ style="fill:#00ff00;stroke-width:0.264583"
69
+ d="M 4.7625002,1.5875001 H 6.3500003 L 5.8208336,2.6458333 V 3.1750002 L 6.3500003,4.2333335 H 4.7625002 L 5.2916669,3.1750001 V 2.6458335 Z"
70
+ id="path441"
71
+ sodipodi:nodetypes="ccccccccc" />
72
+ <rect
73
+ style="fill:#666666;stroke-width:0.302621"
74
+ id="rect197"
75
+ width="14.816668"
76
+ height="0.52916676"
77
+ x="1.0583334"
78
+ y="2.6458335" />
79
+ <path
80
+ style="fill:#00ff00;stroke-width:0.264583"
81
+ d="m 11.112501,8.9958339 h 1.587501 l -0.529167,1.0583331 v 0.529167 l 0.529167,1.058334 h -1.587501 l 0.529167,-1.058334 v -0.529167 z"
82
+ id="path541"
83
+ sodipodi:nodetypes="ccccccccc" />
84
+ <path
85
+ style="fill:#00ff00;stroke-width:0.264583"
86
+ d="m 10.054167,5.291667 h 1.587501 l -0.529167,1.0583332 v 0.5291669 l 0.529167,1.0583333 H 10.054167 L 10.583334,6.879167 V 6.3500004 Z"
87
+ id="path539"
88
+ sodipodi:nodetypes="ccccccccc" />
89
+ <rect
90
+ style="fill:#666666;stroke-width:0.302621"
91
+ id="rect253"
92
+ width="14.816668"
93
+ height="0.5291667"
94
+ x="1.0583334"
95
+ y="6.3500004" />
96
+ <rect
97
+ style="fill:#666666;stroke-width:0.302621"
98
+ id="rect309"
99
+ width="14.816668"
100
+ height="0.52916718"
101
+ x="1.0583334"
102
+ y="10.054168" />
103
+ <path
104
+ style="fill:#00ff00;stroke-width:0.264583"
105
+ d="m 11.641668,12.700002 h 1.587501 l -0.529167,1.058334 v 0.529167 l 0.529167,1.058334 h -1.587501 l 0.529167,-1.058334 v -0.529167 z"
106
+ id="path543"
107
+ sodipodi:nodetypes="ccccccccc" />
108
+ <rect
109
+ style="fill:#666666;stroke-width:0.302621"
110
+ id="rect311"
111
+ width="14.816668"
112
+ height="0.5291667"
113
+ x="1.0583334"
114
+ y="13.758335" />
115
+ <rect
116
+ style="fill:#ffffff;stroke-width:0.264583"
117
+ id="rect365"
118
+ width="0.52916658"
119
+ height="1.5875001"
120
+ x="7.9375005"
121
+ y="2.1166668" />
122
+ <rect
123
+ style="fill:#ffffff;stroke-width:0.264583"
124
+ id="rect367"
125
+ width="0.52916658"
126
+ height="1.5875"
127
+ x="7.9375005"
128
+ y="5.8208337" />
129
+ <rect
130
+ style="fill:#ffffff;stroke-width:0.264583"
131
+ id="rect369"
132
+ width="0.52916658"
133
+ height="1.5875"
134
+ x="7.9375005"
135
+ y="9.5250006" />
136
+ <rect
137
+ style="fill:#ffffff;stroke-width:0.264583"
138
+ id="rect371"
139
+ width="0.52916658"
140
+ height="1.5875006"
141
+ x="7.9375005"
142
+ y="13.229167" />
143
+ <rect
144
+ style="fill:#666666;stroke-width:0.216031"
145
+ id="rect581"
146
+ width="0.52916658"
147
+ height="1.0583334"
148
+ x="14.287497"
149
+ y="2.1166668" />
150
+ <rect
151
+ style="fill:#666666;stroke-width:0.216031"
152
+ id="rect583"
153
+ width="0.52916658"
154
+ height="1.0583334"
155
+ x="14.287497"
156
+ y="5.8208337" />
157
+ <rect
158
+ style="fill:#666666;stroke-width:0.216031"
159
+ id="rect585"
160
+ width="0.52916658"
161
+ height="1.0583334"
162
+ x="14.287497"
163
+ y="9.5249996" />
164
+ <rect
165
+ style="fill:#666666;stroke-width:0.216031"
166
+ id="rect587"
167
+ width="0.52916658"
168
+ height="1.0583334"
169
+ x="14.287497"
170
+ y="13.229164" />
171
+ <rect
172
+ style="fill:#666666;stroke-width:0.216031"
173
+ id="rect589"
174
+ width="0.52916658"
175
+ height="1.0583334"
176
+ x="2.1166677"
177
+ y="13.229164" />
178
+ <rect
179
+ style="fill:#666666;stroke-width:0.216031"
180
+ id="rect591"
181
+ width="0.52916658"
182
+ height="1.0583334"
183
+ x="2.1166677"
184
+ y="9.5250006" />
185
+ <rect
186
+ style="fill:#666666;stroke-width:0.216031"
187
+ id="rect593"
188
+ width="0.52916658"
189
+ height="1.0583334"
190
+ x="2.1166677"
191
+ y="5.8208346" />
192
+ <rect
193
+ style="fill:#666666;stroke-width:0.216031"
194
+ id="rect595"
195
+ width="0.52916658"
196
+ height="1.0583334"
197
+ x="2.1166677"
198
+ y="2.1166677" />
199
+ </g>
200
+ <metadata
201
+ id="metadata11">
202
+ <rdf:RDF>
203
+ <cc:Work
204
+ rdf:about="">
205
+ <cc:license
206
+ rdf:resource="https://gnu.org/licenses/gpl.html" />
207
+ <dc:creator>
208
+ <cc:Agent>
209
+ <dc:title>Jessie Blue Cassell</dc:title>
210
+ </cc:Agent>
211
+ </dc:creator>
212
+ </cc:Work>
213
+ </rdf:RDF>
214
+ </metadata>
215
+ </svg>