Fex

Check-in [93665ca85c]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Converted config to yaml; packaging clean up; nearing beta release
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | master
Files: files | file ages | folders
SHA3-256: 93665ca85c31b3859bef5b63551f3d81cda9b1aa33ee70ae55594c0298b82262
User & Date: jmcclure 2017-08-26 23:51:00
Context
2017-08-27
00:48
Post packaging-test patch-up check-in: 401afa1988 user: jmcclure tags: trunk, master
2017-08-26
23:51
Converted config to yaml; packaging clean up; nearing beta release check-in: 93665ca85c user: jmcclure tags: trunk, master
2017-08-18
10:50
mainwin.py: handle divide-by-zero if/when fex line is zero length spectogram.py: initial eraser functionality added check-in: 5db78221ea user: jmcclure tags: trunk, master
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added LICENSE.txt.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Copyright 2017 Jesse McClure

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Changes to MANIFEST.in.

1
2

include README.md
<
|

1

include fexqt/*.yaml

Deleted README.md.

1
2
3
4
Fex
---

Coming soon ...  Development being moved here from github
<
<
<
<








Added README.rst.









>
>
>
>
1
2
3
4
Fex
===

Coming soon ...  Development being moved here from github

Deleted bin/fex.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python

import os
import sys
from PyQt5.QtWidgets import QApplication

from mainwin import MainWin

def fpath(fname):
	for p in site.getsitepackages():
		if os.path.isfile(p + '/' + fname):
			return p + '/' + fname
	return None

if __name__ == '__main__':
	app = QApplication(sys.argv)
	win = MainWin()
	sys.exit(app.exec_())
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































Changes to fexqt/__init__.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17









18
19
20

import os
import sys
from PyQt5.QtWidgets import QApplication

from mainwin import MainWin
from config import Config

def fpath(fname):
	for p in site.getsitepackages():
		if os.path.isfile(p + '/' + fname):
			return p + '/' + fname
	return None

if __name__ == '__main__':
	app = QApplication(sys.argv)
	conf = Config()









	win = MainWin(conf)
	sys.exit(app.exec_())


|
<

<
|
<

<
<
<
<
<
|
<

|
>
>
>
>
>
>
>
>
>
|
|

1
2

3

4

5





6

7
8
9
10
11
12
13
14
15
16
17
18
19
20

import os, sys, time

from PyQt5.QtWidgets import QApplication

from fexqt.mainwin import MainWin







def main():

	app = QApplication(sys.argv)
	args = sys.argv[1:]
	files = [arg for arg in args if os.path.isfile(arg)]
	flags = [arg for arg in args if arg[0] == '-']
	other = [arg for arg in args if arg not in files + flags]
	print(' INFO: Fex starting on %s' %(time.asctime(time.localtime())))
	# TODO create logger
	if flags:
		print(' WARN: ignoring unrecognized flags: ', flags)
	if other:
		print(' WARN: ignoring unrecognized arguments: ', other)
	win = MainWin(files)
	return app.exec_()

Added fexqt/actions.yaml.























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

actions:
   - name: quit
     menu: '&Quit'
     icon: *icon-quit
     shortcut: Ctrl+Q
     statustip: Quit Fex
   - name: mode_normal
     menu: '&Normal'
     icon: *icon-quit  # FIXME
     shortcut: Ctrl+X
     statustip: Set Normal Mode
   - name: mode_cursor
     menu: 'Cursor'
     icon: *icon-cursor
     shortcut: Ctrl+C
     statustip: Set Cursor Mode
   - name: mode_eraser
     menu: 'Eraser'
     icon: *icon-eraser
     shortcut: Ctrl+E
     statustip: Set Eraser Mode
   - name: prev_song
     menu: 'Previous Song'
     icon: *icon-back
     shortcut: Ctrl+H
     statustip: Go back to the previous song
   - name: next_song
     menu: 'Next Song'
     icon: *icon-forward
     shortcut: Ctrl+L
     statustip: Move on to the next song
   - name: add_songs
     menu: 'Add Songs'
     icon: *icon-fileadd
     shortcut: Ctrl+O
     statustip: Add songs to the queue
   - name: fit
     menu: 'Fit'
     icon: *icon-shape
     shortcut: Ctrl+R
     statustip: Fit to original aspect ratio

Changes to fexqt/config.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18


19


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
55
56


57
58
59
60
61
62
63
64
65
66
67
68





69
70
71
72
73

import os
import site
from configparser import SafeConfigParser
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt

class Container(object):
	pass

_files = [p + '/default.cfg' for p in site.getsitepackages()]
# TODO test for 'APPDATA' in environ on windows?
#    what's normally in the environment variables on win??

if 'XDG_CONFIG_HOME' in os.environ:
	_files.append(os.environ['XDG_CONFIG_HOME'] + '/fex/config.cfg')
elif 'HOME' in os.environ:
	_files.append(os.environ['HOME'] + '/.config/fex/config.cfg')


_files.append(os.getcwd() + '/fex.cfg')



class Config(object):
	def __init__(self):
		w = QWidget()
		self._parser = SafeConfigParser({
			'QPalette-Window': w.palette().color(QPalette.Window).name(),
			'QPalette-Background': w.palette().color(QPalette.Background).name(),
			'QPalette-WindowText': w.palette().color(QPalette.WindowText).name(),
			'QPalette-Foreground': w.palette().color(QPalette.Foreground).name(),
			'QPalette-Base': w.palette().color(QPalette.Base).name(),
			'QPalette-AlternateBase': w.palette().color(QPalette.AlternateBase).name(),
			'QPalette-ToolTipBase': w.palette().color(QPalette.ToolTipBase).name(),
			'QPalette-ToolTipText': w.palette().color(QPalette.ToolTipText).name(),
			'QPalette-Text': w.palette().color(QPalette.Text).name(),
			'QPalette-Button': w.palette().color(QPalette.Button).name(),
			'QPalette-ButtonText': w.palette().color(QPalette.ButtonText).name(),


			'QPalette-BrightText': w.palette().color(QPalette.BrightText).name(),
			'QToolButton-IconOnly': str(Qt.ToolButtonIconOnly),
			'QToolButton-TextOnly': str(Qt.ToolButtonTextOnly),
			'QToolButton-TextBesideIcon': str(Qt.ToolButtonTextBesideIcon),
			'QToolButton-TextUnderIcon': str(Qt.ToolButtonTextUnderIcon),
			'QToolButton-FollowStyle': str(Qt.ToolButtonFollowStyle)
			})
		f = self._parser.read(_files)


		self._spect = Container()
		self._spect.window = self._parser.getint('spectrogram','window')
		self._spect.overlap = self._parser.getint('spectrogram','overlap')
		self._spect.hipass = self._parser.getint('spectrogram','high-pass')
		self._spect.lopass = self._parser.getint('spectrogram','low-pass')
		self._spect.threshold = self._parser.getint('spectrogram','threshold')
		self._spect.winfunc = self._parser.get('spectrogram','window-function')
		self._spect.logfreq = self._parser.getboolean('spectrogram','log10-freq')
		self._spect.invert = self._parser.getboolean('spectrogram','invert-colors')
		self._spect.border = self._parser.get('spectrogram','border-color')
		self._spect.line = self._parser.get('spectrogram','line-color')
		self._spect.ratio = self._parser.getfloat('spectrogram','ratio')



		self._mainwin = Container()
		self._mainwin.menubar = self._parser.getboolean('main-window','menubar-visible')
		self._mainwin.macmenu = self._parser.getboolean('main-window','mac-native-menu')
		self._mainwin.toolbar = self._parser.getboolean('main-window','toolbar-visible')
		self._mainwin.toolbarposition = self._parser.get('main-window','toolbar-position')
		self._mainwin.toolbarstyle = self._parser.getint('main-window','toolbar-style')
		self._mainwin.statusbar = self._parser.getboolean('main-window','statusbar-visible')
		self._mainwin.theme = self._parser.get('main-window','theme-name')
		self._mainwin.maximized = self._parser.getboolean('main-window','start-maximized')

	def spectrogram(self):





		return self._spect

	def mainwin(self):
		return self._mainwin


|
|
<
|
<
<

|
|
|
|
|
<
>
|
|
|
|
>
>
|
>
>

<
<
<
<
|
|
|
|
|
|
|
|
|
|
|
>
>
|
|
|
|
|
|
|
|
>

<
|
|
|
|
|
|
|
|
|
|
|
>
>

<
<
|
|
|
|
|
|
|
|
|
>
>
>
>
>
|

<
<

1
2
3

4


5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21




22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74


75

import os, site, tempfile, yaml
from PyQt5.QtCore import Qt

from PyQt5.QtGui import QPalette, QColor



def configure():
	_files = []
	for fname in [ 'icons.yaml', 'actions.yaml', 'menu.yaml', 'config.yaml' ]:
		_files += [os.path.join(p, fname) for p in site.getsitepackages()]
		if 'APPDATA' in os.environ:

			_files += os.path.join(os.environ['APPDATA'], 'fex', fname)
		elif 'XDG_CONFIG_HOME' in os.environ:
			_files += [os.path.join(os.environ['XDG_CONFIG_HOME'], 'fex', fname)]
		elif 'HOME' in os.environ:
			_files += [os.path.join(os.environ['HOME'], '.config', 'fex', fname)]
		_files += [os.path.join(os.getcwd(), 'fexqt',  fname)] # FIXME DELME
		_files += [os.path.join(os.getcwd(), fname)]

	_yaml = """
constants:





   Window: &Window "%(Window)s"
   Background: &Background "%(Background)s"
   WindowText: &WindowText "%(WindowText)s"
   Foreground: &Foreground "%(Foreground)s"
   Base: &Base "%(Base)s"
   AlternateBase: &AlternateBase "%(AlternateBase)s"
   ToolTipBase: &ToolTipBase "%(ToolTipBase)s"
   ToolTipText: &ToolTipText "%(ToolTipText)s"
   Text: &Text "%(Text)s"
   Button: &Button "%(Button)s"
   ButtonText: &ButtonText "%(ButtonText)s"
   BrightText: &BrightText "%(BrightText)s"
   Highlight: &Highlight "%(Highlight)s"

   IconOnly: &IconOnly %(IconOnly)d
   TextOnly: &TextOnly %(TextOnly)d
   TextBesideIcon: &TextBesideIcon %(TextBesideIcon)d
   TextUnderIcon: &TextUnderIcon %(TextUnderIcon)d
   FollowStyle: &FollowStyle %(FollowStyle)d

   TempFile: &TempFile "%(TempFile)s"
""" % {


		'Window': QColor(QPalette.Window).name(),
		'Background': QColor(QPalette.Background).name(),
		'WindowText': QColor(QPalette.WindowText).name(),
		'Foreground': QColor(QPalette.Foreground).name(),
		'Base': QColor(QPalette.Base).name(),
		'AlternateBase': QColor(QPalette.AlternateBase).name(),
		'ToolTipBase': QColor(QPalette.ToolTipBase).name(),
		'ToolTipText': QColor(QPalette.ToolTipText).name(),
		'Text': QColor(QPalette.Text).name(),
		'Button': QColor(QPalette.Button).name(),
		'ButtonText': QColor(QPalette.ButtonText).name(),
		'BrightText': QColor(QPalette.BrightText).name(),
		'Highlight':  QColor(QPalette.Highlight).name(),



		'IconOnly': Qt.ToolButtonIconOnly,
		'TextOnly': Qt.ToolButtonTextOnly,
		'TextBesideIcon': Qt.ToolButtonTextBesideIcon,
		'TextUnderIcon': Qt.ToolButtonTextUnderIcon,
		'FollowStyle': Qt.ToolButtonFollowStyle,

		'TempFile': os.path.join(tempfile.gettempdir(), 'fex.log'),
	}

	for fname in _files:
		if (os.path.isfile(fname)):
			with open(fname, 'r') as fin:
				print(' INFO: reading config from %s' % fname)
				_yaml += fin.read()
	return yaml.load(_yaml)




Added fexqt/config.yaml.























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

spectrogram:
   fft:
      window: 256
      overlap: 192
      window-function: hamming
      # boxcar triang blackman hamming hann bartlett flattop parzen bohman
      # blackmanharris nuttall barthann
   calculations:
      high-pass: 1250
      low-pass: 10000
      threshold: 14
      log10-freq: false
   colors:
      invert: false
      border: paleblue
      line: deepskyblue
      eraser: yellow
      cursor: red
   scale-ratio: 8

main-window:
   maximized: false
   menu: *main-menu
   toolbars:
      - name: modes
        items: [ quit, bar, mode_normal, mode_cursor, mode_eraser ]
        position: top
        visible: true
        style: *TextBesideIcon
      - name: songs
        items: [ prev_song, next_song, add_songs ]
        position: top
        visible: true
        style: *TextBesideIcon
      - name: zoom
        items: [ fit ]
        position: top
        visible: true
        style: *TextBesideIcon
   statusbar:
      visible: true

Deleted fexqt/default.cfg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95


# SPECTROGRAM
# window (int number of samples)
#    window length of fft in number of samples (powers of 2 are best)
# overlap (int number of samples)
#    overlap between adjacent windows
# window-function (string)
#    available functions: boxcar triang blackman hamming hann bartlett
#    flattop parzen bohman blackmanharris nuttall barthann
# high-pass / low-pass (int Hz)
#    signal outside this pseudo-bandpass filter are dropped from
#    analysis
# threshold (in dB)
#    signals more than threshold dB below the peak of the recording are
#    dropped
# log10-freq (bool)
#    whether frequency values should be log10 transformed prior to the
#    calculation of frequency excursion (see ...)
# invert-colors (bool)
#    display white-on-black spectrogram
# border-color, line-color (color)
#    colors of spectrogram window border and excursion line overlay see
#    "COLORS" below for options
# ration (float)
#    ... (name to change soon)
[spectrogram]
window = 256
overlap = 192
window-function = hamming
high-pass = 1250
low-pass = 10000
threshold = 14
log10-freq = false
invert-colors = false
border-color = dimgrey
line-color = royalblue
ratio = 8

# MAIN-WINDOW
# menu-visible, toolbar-visible, statusbar-visible (bool)
#   whether these ui elements should be displayed
# mac-native-menu (bool)
#   osx only: use osx-style integrated menu (not implemented yet)
# toolbar-position (?)
#   not implemented yet
# toolbar-style (int)
#   ...
# theme-name (str)
#   ...
[main-window]
menubar-visible = true
mac-native-menu = false
toolbar-visible = true
toolbar-position = top
statusbar-visible = true
toolbar-style = %(QToolButton-FollowStyle)s
theme-name = oxygen
start-maximized = false

# COLORS
# All colors are specified as a string as one of the following:
# 1) hex digits:
#   #RGB (each of R, G, and B is a single hex digit)
#   #RRGGBB
#   #AARRGGBB
#   #RRRGGGBBB
#   #RRRRGGGGBBBB
# 2) An svg color name
#   (see https://www.w3.org/TR/SVG/types.html#ColorKeywords)
# 3) A Qt Color role variable (see VARIABLES below)

# VARIABLES
# This file uses pythons ConfigParser string interpolation.  Any setting
# can include the value of another setting as "%(NAME)s".  The following
# variables are set to the Qt defaults prior to this file being parsed
# and can be used as desired:
#   QPalette-Window
#   QPalette-Background
#   QPalette-WindowText
#   QPalette-Foreground
#   QPalette-Base
#   QPalette-AlternateBase
#   QPalette-ToolTipBase
#   QPalette-ToolTipText
#   QPalette-Text
#   QPalette-Button
#   QPalette-ButtonText
#   QPalette-BrightText
#   QToolButton-IconOnly
#   QToolButton-TextOnly
#   QToolButton-TextBesideIcon
#   QToolButton-TextUnderIcon
#   QToolButton-FollowStyle

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































Added fexqt/icons.yaml.

































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
icons:
   back: &icon-back !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOjIxQTY2NUIxNEJGNjExRTI5MDlCODA3QTA5NTQzODRGIiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOjIxQTY2NUIyNEJGNjExRTI5MDlCODA3QTA5NTQzODRGIj4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MjFBNjY1QUY0QkY2MTFFMjkwOUI4
      MDdBMDk1NDM4NEYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MjFBNjY1QjA0QkY2MTFFMjkw
      OUI4MDdBMDk1NDM4NEYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz4TI7b/AAABF0lEQVR42uza0QrCMAyF4VX73EqevOKFMGTO
      ZiVpmnMCu9X9HxvOrqW1tiHPbQMfAhCAAAQgAAEIgDt19ANKKVMDRp9keQtM+t5HGIH3JTRyXIxv
      Yc7fGeATDwmwj4cD+I6HAjiKhwH4FQ8BcBafHuBffGqAnvi0AL3xKQE08ekAtPHmhydAuHg3gKjx
      LgCR480BosebAqwQbwawSrwJwErxGgDNmuA95aqo8hYQ2FtgNQTrn0GBBlgBwetRWKABIiN4/x0W
      dIArCCmXxAQdQIOQellc0AF6ECBejQk6wBkC1OtxQQc4QoDcIiPoAHuEEAB1859npBWxMrrTcvWd
      onX2CcwebpUlAAEIQAACEIAABECdlwADAAxIS0j1HP7kAAAAAElFTkSuQmCC
   eraser: &icon-eraser !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOkVDNjQyQjY4NEJGNjExRTI5QTI3REVCRkIzNUMxNUY1IiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOkVDNjQyQjY5NEJGNjExRTI5QTI3REVCRkIzNUMxNUY1Ij4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RUM2NDJCNjY0QkY2MTFFMjlBMjdE
      RUJGQjM1QzE1RjUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RUM2NDJCNjc0QkY2MTFFMjlB
      MjdERUJGQjM1QzE1RjUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz6n1IgWAAADA0lEQVR42uybv0scQRTHXU+EA8EqVSCtIAgB
      0whp0wYSSCMcXKsIgVT5A2yFQFrhwDYQuCp/QNIkBFIdpBIEQ0QbQRAMwuY7l13ZXWfu5sebN+9y
      O/AtBLmb+fD9vp03s5fleb4wz2NxYc5HC2DeASyFfkCWZUkXEFrD2gjM0FwfQB+g79Be8TeNhULE
      NLahSzXdim6hIfQc6njPXziAR9CnxsJ1OocOoI3/CYCy+ZXF4pu6i8isAliDPnssPPeJiCQAHegt
      dE2weOuISAGwAf2IsPCqFNi+NADL0H5h15iL/wmtS4vAVjGxPLKOoBVJNWAFesewcK3lUwN4Bh0z
      LP6e5TG60CAVgFX15QwL11q+eLSO/m18+QG8gH6nsjxGr7qh4gRQNi95Sss3/5cLQE/TvMTSYJLl
      uQHYNi9slucE4Nu8eFu+2s6bLM8BgKp5cbJ840jMaPloACI3L0bLa+pN38V5JACYmhet5Suj67O3
      CALA2LwYLV85Mxg5fs4NtOMNgLF5IbV8IbURe+rVCzA2L1EsD32FHno1Q4zNSwzLKx2q2Dp3g8zN
      SwzLa/NuBYCxeYlleWPeXQCczqjlJ+bdJQJl4budEctb5d2nCG5G2vBQWt46776PQbXlfUPY6FBa
      3invtjXgoy5HRas7FGR557zbAsiLCb3WXSl5FklKy9/lneR22wCgetH4OLBIUlp+nHfS6/0pAMpL
      xgPd5cKUIklt+XHeyd9vsABQ6kTdtFoWSWrLj/Me5QUPBwClakWycR44JLZ8Le9SANSKpMW9gK/l
      7+VdEoBakTTMLcTy2rxLBFArkkSWN+ZdKoBakQyw/NS8SwcQIqu8xwAQ/KoswTiDXkFfpLwp+hL6
      xfT936AnqRZvtBDTeYBz3llqAMN5gHfe2QFEOA+wfr6LAUB4HuD0fBcHIPA8gCTvIgA4FknSvIsB
      YFkkyfMuDsCEIhkl7zEAZKHv+1Z+NKWK5HvoAtqF/nABCJo/IYBkG7mQsZR6AqlH+7O5FkALYL7H
      XwEGAP5qkxJidMCUAAAAAElFTkSuQmCC
   fileadd: &icon-fileadd !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOjBENDI1OERDNEJGNzExRTJCQjRCRDlGNDk0Q0Y2MERDIiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOjBENDI1OERENEJGNzExRTJCQjRCRDlGNDk0Q0Y2MERDIj4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MEQ0MjU4REE0QkY3MTFFMkJCNEJE
      OUY0OTRDRjYwREMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MEQ0MjU4REI0QkY3MTFFMkJC
      NEJEOUY0OTRDRjYwREMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz5B5WJeAAACoklEQVR42uxbwUrDQBBNrBSEQkEoKIInT0LF
      U09eC4Ve/QBBEAT/wFO/QegHCN6EIngQPAmepEVJT2KhF0EvlYAgBCpxCiPUdZPsJtns0tmBB02T
      nSQv82Z3pqkbhqFD2ZYc4mYJcKxZI20u57sJYFXzNWjNAc/Uk+CIOgFjSgQsGxABRS5F3wHrlCUw
      oJ4EhQj4QJAlYJGj4EGUgEXMA+8IsgQMZKrBMXUCRhT0H1eIrGJRtEi2zssB1mw/wHE8QJ3o/Q9n
      SbBHOAB6Lj59jygBO78fPCxLKcGbXwdQlMGfe64TjIB/id+jFv7sUrhHMfxdRgYmzwbfgFvEPW4/
      4b5dQAmwB2giSgnZf8jbYaIM3gBHgKoEWVUc8xYX/jzrGHTjX4BTQCVD1FTQx9ec307cgLpBT72R
      o3wac9GQuOzXLYNHwGbEtdUAB4ArQB9L9gl+vsJ9axFjNwDnImx1ND/5jYj+RBcQCPgI8NjUP/Dq
      kkEQEfYtfMqy/iY4NpXpkMEp5zoOAdMMPqfoQ9qKlsErJ9u3Em6etTgSmqbL4IST7PyEMaIEhOir
      ZqoMPgErzLnPBMbJEBCiTyNlcMHJ+IECAgLezBD3mtxlQWv8O2Z7H1BWcJ4y+hYmYBhVMOTdmGS2
      2wrP1ZYhoKgSecRJwGznmgcnxXHS3e8iZgM23P0ErcsaOxtIRUBRMtBmIu8Kq5YBW+erfDvlIw0B
      qmeDrYSkmHUajPMtRIBqGbCJ6UbhuVL7Vrko0roQMmE24C2FuwoI6GYNH5W1wTFzrjUTiqEiZaC6
      HG7llazINER0yGDWut6OaIn5KXNLK+9pRHWJ/BKh1VqKpmhNxTxaRG3Qj+gM/ybH2S8+13icj+jj
      d0cxbXFr1qxZs/avjUT97/M/AgwAfyghRmCejjgAAAAASUVORK5CYII=
   forward: &icon-forward !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOjIyQzI4ODNBNEJGNjExRTJBQTkwRUQyQ0ZFMjNFMEM1IiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOjIyQzI4ODNCNEJGNjExRTJBQTkwRUQyQ0ZFMjNFMEM1Ij4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MjJDMjg4Mzg0QkY2MTFFMkFBOTBF
      RDJDRkUyM0UwQzUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MjJDMjg4Mzk0QkY2MTFFMkFB
      OTBFRDJDRkUyM0UwQzUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/VnRHAAABCklEQVR42uza2w6EIAwEULvrd5vMl7PJvm32
      RqkRnZkmPGLKURQL0VpblOO2iIcBDGAAAxjAAAbQjbV6gYiYOoDqStZT4ES5bNMeoUrbM5URhHL+
      JwNIIzACpBBYAboRmAG6ENgB/iIoAPxEUAH4iqAE8BFBDeANQRHgBeEwgGSCR7RNHeCJoA5QRmAA
      KCGwAAwjMAEMIbABpBEYAVIIrEXR++4lpQvdfShPASi/BKH8GYTyQgjKS2Eo/wxBuSAC5YoQlEti
      UC6KQrksDuWNEShvjUF5cxRdnUgB0N2JEACpTmQASHciAsCQWjH/qA7i6idF19kJ+KSoAQxgAAMY
      wAAGMIAB5sRDgAEAdMaq5u7GptkAAAAASUVORK5CYII=
   quit: &icon-quit !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOjM0NUVEOEJENEJGNjExRTI5NEQ3QzQ0MTk4OUYwMURFIiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOjM0NUVEOEJFNEJGNjExRTI5NEQ3QzQ0MTk4OUYwMURFIj4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MzQ1RUQ4QkI0QkY2MTFFMjk0RDdD
      NDQxOTg5RjAxREUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MzQ1RUQ4QkM0QkY2MTFFMjk0
      RDdDNDQxOTg5RjAxREUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz57l3dUAAACLElEQVR42uybfWuDQAyH6yz7coNCwY83EAZC
      YZ9wUHA6dBxFvSS/JOfpCfm3d89DfUkuqfq+v5z5eruc/Dq9gCv6A1VVJQVAb2GNf0AzRDdE7chd
      T2s2KgalMW3gZ/yZIb6dJNTTWv20dgMxKMH3ThJC+D6QcHcVsAJvLWEJHpagDW8lYQsekmABry2B
      Ai+WYAWvJYEDL5JgCT/HQyhBAs+WYA0vlYDAhxJukAAleK4EDXiyBC94qgRNeJIET/iYBAv4qARv
      +DUJlvChhA+KgM54I68SPODnaCkC3h039HBc6y9jpT4DPCUkg4+9BY4iYRWe8h2Qu4RNeOqXYK4S
      ovCcXCA3CSR4bjZYZyKBDC+pB+xdAgteWhHaqwQ2PFIT3JsEETxaFd6LBDE8JGAnEiB4WEBiCTC8
      loAUEv6P4tD9n/50uNwC5SFYXoPlQ6h8CpdkqKTDh4HXL4hkCK9XEssYHi+KHgB+ji92WfxA8FEJ
      qeG76XgsmYQlAQ/nlLZ2XPOTejz+9Mrng3qCtYTFHoGtBomnE/xr/2/aBgnjRolYU7WFBH6LjJEE
      ake5pgR5k5SyBG47vYYEvE1OSYJ0lgCRQILnZoN3gQR0kEIigQwvqQdwJGhNkXAksOClFSGKBO0R
      GooENjxSE9ySYDU/tCVBBI9WhW8LEqyHp5YkiOE1zgVCCV6TY6EECF7rbHCU0CYYm2tR+DEqdPAw
      98HJa+oNpL7K8PTZBfwKMAAC4rSsna895QAAAABJRU5ErkJggg==
   cursor: &icon-cursor !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOkRDQkUwQ0VGNEJGNjExRTJCMkY1ODJCRDM1RkE4N0MyIiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOkRDQkUwQ0YwNEJGNjExRTJCMkY1ODJCRDM1RkE4N0MyIj4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RENCRTBDRUQ0QkY2MTFFMkIyRjU4
      MkJEMzVGQTg3QzIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RENCRTBDRUU0QkY2MTFFMkIy
      RjU4MkJEMzVGQTg3QzIiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz5gt7lOAAAAlElEQVR42uzbCwqAIBBAwd3w/lfeLhBFH0Nx
      3gEih0UxKOO6intlTNQWiwcAAAAAAFaunZzzb8/zGmSNaQIAAAAAAAAAAAAAADi8C/T6htfruWUC
      AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBhbcJ3ThPws2bZAwAAAAAAAAAAAAAAkBa8DY7Wp3+y2gQB
      AACwdLsAAwAvSAaB2BA6qAAAAABJRU5ErkJggg==
   shape: &icon-shape !!binary |
      iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
      bWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
      bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6
      eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz
      NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo
      dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw
      dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv
      IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS
      ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD
      cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlE
      PSJ4bXAuaWlkOkJDQjRCRUM5NEJGNjExRTJCODc4ODQ2MkE5NkYyRjQ3IiB4bXBNTTpEb2N1bWVu
      dElEPSJ4bXAuZGlkOkJDQjRCRUNBNEJGNjExRTJCODc4ODQ2MkE5NkYyRjQ3Ij4gPHhtcE1NOkRl
      cml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNCNEJFQzc0QkY2MTFFMkI4Nzg4
      NDYyQTk2RjJGNDciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNCNEJFQzg0QkY2MTFFMkI4
      Nzg4NDYyQTk2RjJGNDciLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l
      dGE+IDw/eHBhY2tldCBlbmQ9InIiPz7DJHK0AAABJElEQVR42uzbUQ6CMAyA4Y5T6Gl85eochMxL
      mLkQSCAIViphhb9JjT4Y10/TzaQTEXnkbHKmH7Pp33t0mNcfN7x5yFgAgGn9VX64GT78VQCAZQ1d
      7ZZvvy4AoLb8CkL/ZBxBzh2Teiu5eAAAwH777NGpOqdommA0bpVHxjPnfa0JagCS8195YBegCQKw
      CtCOXrcXqHlW73CWXjrbJ+e59N+hq1dz7j/bLkAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
      AAAAAAAAAAAA4IoAwxxgKfP/e8es3vGk9af5/7NNiEzqZU6QHgAAAN/C8+ygau3auTpv3V8198id
      IXoAAABsPfp6vTv817O+97vDqTLu897vDreWfd773eFu/W8BBgBJzJr91bypowAAAABJRU5ErkJg
      gg==

Changes to fexqt/mainwin.py.

1
2
3
4
5
6
7

8
9
10
11
12
13
14





15
16
17
18
19
20
21





22


23










24
25
26
27
28










29






























































30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

#from numpy import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

from spectrogram import Spectrogram

import time

class MainWin(QMainWindow):
	def __init__(self, config):
		super().__init__()
		self.fnames = [ '/home/jmcclure/data/warb/batch4/4/4355.wav' ]
		self.conf = config.mainwin()





		self.spect_conf = config.spectrogram()
		self.init_actions()
		self.init_menubar()
		self.init_toolbar()
		self.init_statusbar()
		self.init_body(config.spectrogram())
		self.setGeometry(0, 0, 800, 600)





		self.setWindowTitle("Fex: Frequency Excursion Calculator")


		self.setWindowIcon(QIcon('web.png'))	# TODO










		if self.conf.maximized:
			self.setWindowState(Qt.WindowMaximized)
		self.show()
		QTimer.singleShot(20,self.next_spectrogram)











	# Todo handle drag & drop of sound files































































	def update_status_fex(self, pex, tex):
		self.pex = pex
		self.tex = tex
		try:
			self.fex = pex / tex
		except ZeroDivisionError:
			self.fex = 0.0

	def update_status_text(self, x, y):
		self.status_text.setText("FEX: %.3f   %0.3fs %0.1fKHz" %(self.fex,x,y))

	def init_body(self, config):
		self.body = QWidget()
		self.body.setBackgroundRole(QPalette.AlternateBase)
		self.body.setAutoFillBackground(True)
		self.body.setContentsMargins(10,10,10,10)
		self.setCentralWidget(self.body)
		self.body.show()

	def next_spectrogram(self):
		if len(self.fnames) > 0:
			self.fname = self.fnames.pop(0)
		self.spect = Spectrogram(self.fname, self.body, self.spect_conf)
		self.spect.fexChanged.connect(self.update_status_fex)
		self.spect.mouseOver.connect(self.update_status_text)

	def init_actions(self):
		# https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
		QIcon.setThemeName(self.conf.theme)
		# TODO icons are left out until I've identified a decent set

		#self.aQuit = QAction(QIcon.fromTheme('application-exit'), '&Quit', self)
		self.aQuit = QAction('&Quit', self)
		self.aQuit.setShortcut('Ctrl+Q')
		self.aQuit.setStatusTip('Quit Fex')
		self.aQuit.triggered.connect(qApp.quit)

		self.aNormal = QAction('&Normal', self)
		self.aNormal.setShortcut('Ctrl+X')
		self.aNormal.setStatusTip('Set Normal Mode')
		self.aNormal.triggered.connect(lambda: self.spect.mode_normal())

		self.aCursor = QAction('Cursor', self)
		self.aCursor.setShortcut('Ctrl+C')
		self.aCursor.setStatusTip('Enable Cursor Tool')
		self.aCursor.triggered.connect(lambda: self.spect.mode_cursor())

		self.aErase = QAction('Erase', self)
		self.aErase.setShortcut('Ctrl+E')
		self.aErase.setStatusTip('Eraser Tool To Remove Noise')
		self.aErase.triggered.connect(lambda: self.spect.mode_erase())

		self.aZoom = QAction(QIcon.fromTheme('zoom-original'), '&Normal', self)
		self.aZoom.setShortcut('Ctrl+0')
		self.aZoom.setStatusTip('Fit Window')
		self.aZoom.triggered.connect(lambda: self.spect.mode_normal())

	def init_menubar(self):
		self.menubar = self.menuBar()
		self.menubar.setVisible(self.conf.menubar)
		fileMenu = self.menubar.addMenu('&File')
		fileMenu.addAction(self.aQuit)
		fileTools = self.menubar.addMenu('&Tools')
		fileTools.addAction(self.aNormal)
		fileTools.addAction(self.aCursor)
		fileTools.addAction(self.aErase)

	def init_toolbar(self):
		self.toolbar = self.addToolBar('ToolBar1')
		self.toolbar.setVisible(self.conf.toolbar)
		self.toolbar.setToolButtonStyle(self.conf.toolbarstyle)
		self.toolbar.addAction(self.aQuit)
		self.toolbar.addSeparator()
		self.toolbar.addAction(self.aNormal)
		self.toolbar.addAction(self.aCursor)
		self.toolbar.addAction(self.aErase)
		self.toolbar.addSeparator()
		self.toolbar.addAction(self.aZoom)

	def init_statusbar(self):
		self.statusbar = self.statusBar()
		self.status_text = QLabel()
		self.statusBar().addPermanentWidget(self.status_text)
		# TODO add song count a/b


|
|
|
|
<
|
>
|


|

|
|
>
>
>
>
>
|




|
|
>
>
>
>
>
|
>
>
|
>
>
>
>
>
>
>
>
>
>
|
|
|
|

>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>












<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135











































































import os
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPalette, QPixmap
from PyQt5.QtWidgets import qApp, QAction, QFileDialog, QLabel, QMainWindow, QWidget

from fexqt.spectrogram import Spectrogram
from fexqt.config import configure


class MainWin(QMainWindow):
	def __init__(self, files):
		super().__init__()
		self.files = files
		self.config = configure()
		self.setGeometry(0, 0, 800, 600)
		self.setWindowTitle("Fex: Frequency Excursion Calculator")
		self.setWindowIcon(QIcon('web.png'))	# FIXME
		if self.config['main-window']['maximized']:
			self.setWindowState(Qt.WindowMaximized)
		self.init_calls()
		self.init_actions()
		self.init_menubar()
		self.init_toolbar()
		self.init_statusbar()
		self.init_body()
		self.show()
		self.spect = None
		# Spectrograms are sized relative to parent window.  The timer allows
		# the window manager to adjust the parent window prior to this sizing.
		if self.files:
			QTimer.singleShot(20,self.next_spectrogram)

	# TODO handle drag & drop of sound files to add to queue
	# and update window title

	# TODO file dialog to add files to queue

	def action_unknown(self):
		print(' WARN: unknown action binding')

	def action_add_songs(self):
		fnames, kind = QFileDialog.getOpenFileNames(self, 'Select Audio Files', os.getcwd(), "Audio (*.wav)")
		if not fnames:
			return
		self.files += fnames
		if self.spect:
			self.setWindowTitle(self.basename + ' [+%d more]' % len(self.files))
		else:
			self.next_spectrogram()

	def init_calls(self):
		self.calls = {
			'add_songs': self.action_add_songs,
			'fit': lambda: self.spect.fit() if self.spect else None,
			'mode_normal': lambda: self.spect.mode_normal() if self.spect else None,
			'mode_cursor': lambda: self.spect.mode_cursor() if self.spect else None,
			'mode_eraser': lambda: self.spect.mode_eraser() if self.spect else None,
			'quit': qApp.quit,
			'unknown': self.action_unknown,
		}

	def init_body(self):
		self.body = QWidget()
		self.body.setBackgroundRole(QPalette.AlternateBase)
		self.body.setAutoFillBackground(True)
		self.body.setContentsMargins(10,10,10,10)
		self.setCentralWidget(self.body)

	def init_actions(self):
		self.actions = dict()
		for act in self.config['actions']:
			pix = QPixmap()
			pix.loadFromData(act['icon'])
			a = QAction(QIcon(pix), act['menu'], self)
			a.setShortcut(act['shortcut'])
			a.setStatusTip(act['statustip'])
			if act['name'] in self.calls:
				a.triggered.connect(self.calls[act['name']])
			else:
				a.triggered.connect(self.calls['unknown'])
			self.actions[act['name']] = a

	def init_menubar(self):
		self.menubar = self.menuBar()
		fileMenu = self.menubar.addMenu('&File')
		fileMenu.addAction(self.actions['quit'])
		fileTools = self.menubar.addMenu('&Tools')
		fileTools.addAction(self.actions['mode_normal'])
		fileTools.addAction(self.actions['mode_cursor'])
		fileTools.addAction(self.actions['mode_eraser'])

	def init_statusbar(self):
		self.statusbar = self.statusBar()
		self.status_text = QLabel()
		self.statusBar().addPermanentWidget(self.status_text)

	def init_toolbar(self):
		for bar in self.config['main-window']['toolbars']:
			toolbar = self.addToolBar(bar['name'])
			toolbar.setVisible(bar['visible'])
			toolbar.setToolButtonStyle(bar['style'])
			for item in bar['items']:
				if item == 'bar':
					toolbar.addSeparator()
				else:  # if act[''] exists
					toolbar.addAction(self.actions[item])

	def next_spectrogram(self):
		if self.spect:
			pass # TODO save data and print(' DATA: ...')
		if self.files:
			fname = self.files.pop(0)
			self.basename = os.path.basename(fname)
			title = self.basename
			if self.files:
				title += ' [+%d more]' % len(self.files)
			self.setWindowTitle(title)
			self.spect = Spectrogram(fname, self.body, self.config['spectrogram'])
			self.spect.fexChanged.connect(self.update_status_fex)
			self.spect.mouseOver.connect(self.update_status_text)
		else:
			self.fname = None
			self.spect = None

	def update_status_fex(self, pex, tex):
		self.pex = pex
		self.tex = tex
		try:
			self.fex = pex / tex
		except ZeroDivisionError:
			self.fex = 0.0

	def update_status_text(self, x, y):
		self.status_text.setText("FEX: %.3f   %0.3fs %0.1fKHz" %(self.fex,x,y))











































































Added fexqt/menu.yaml.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

menu-items: &main-menu
   - name: &file-menu "&File"
     items:
      - quit
      - bar
      - name: &file-sub-menu "Submenu"
        items:
         - quit
   - name: &tools-menu "Tools"
     items:
      - mode_normal
      - mode_cursor
      - mode_eraser


Changes to fexqt/spectrogram.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61































































62
63
64
65
66
67
68
69
70
71
72

73
74








75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

164
165
166
167
168
169
170

import wave
from numpy import *
from scipy.signal import spectrogram
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Spectrogram(QWidget):

	fexChanged = pyqtSignal(float, float)
	mouseOver = pyqtSignal(float, float)

	def __init__(self, fname, parent, config):
		super().__init__(parent)

		self.conf = config
		self.fname = fname
		self.setMouseTracking(True)
		self.setGeometry(parent.contentsRect())
		oldPos = QPoint()
		with wave.open(fname, 'rb') as song:
			channels, bs, samprate, nsamp, comp, compnam  = song.getparams()
			# TODO checks
			data = fromstring(song.readframes(nsamp), dtype=uint16)
		f, t, amp = spectrogram(data, samprate, config.winfunc, config.window,
				config.overlap, detrend=False, mode = 'psd',
				return_onesided=True)
		# pseudo-bandpass filter
		bot = argmax(f > config.hipass)
		top = argmin(f < config.lopass)
		f = f[bot:top]
		amp = amp[bot:top,:]

		thresh = amp.max() / 10**(config.threshold / 10.0)
		thresh = 255.0 - thresh * 255.0 / amp.max()
		config.threshold = thresh
		amp = 255.0 - amp * 255.0 / amp.max()
		print("Avg pts per time bin = %s" %(sum(amp < thresh) / len(t)))

		f /= 1000.0
		# create image
		# TODO only grab inside bandpass
		amp = flipud(amp)
		self.imgdata = array(amp, uint8)
		self.freq = flipud(f)
		self.time = t
		h, w = self.imgdata.shape

		self.image = QImage(self.imgdata.tobytes(), w, h, w, QImage.Format_Grayscale8)
		if self.conf.invert:
			self.image.invertPixels()
		self.fit()
		self.eraser = self.create_eraser(10, 200)
		self.eraser.maxheight = 200
		self.cursorX = self.create_cursor(1, self.height() * 4)
		self.cursorY = self.create_cursor(self.width() * 4, 1)
		self.cursorX.hide()
		self.cursorY.hide()
		self.eraser.hide()
		self.show()
































































	def create_eraser(self, w, h):
		eraser = QWidget(self)
		eraser.setAttribute(Qt.WA_TransparentForMouseEvents)
		effect = QGraphicsOpacityEffect(eraser)
		effect.setOpacity(0.4)
		eraser.setGraphicsEffect(effect)
		eraser.setGeometry(0, 0, w, h)
		eraser.setAutoFillBackground(True)
		p = eraser.palette()
		p.setColor(eraser.backgroundRole(), Qt.yellow)
		eraser.setPalette(p)

		return eraser









	def mode_normal(self):
		self.unsetCursor()
		self.eraser.hide()
		self.cursorX.hide()
		self.cursorY.hide()

	def mode_cursor(self):
		self.setCursor(Qt.BlankCursor)
		self.eraser.hide()
		self.cursorX.show()
		self.cursorY.show()

	def mode_erase(self):
		self.setCursor(Qt.BlankCursor)
		self.cursorX.hide()
		self.cursorY.hide()
		self.eraser.show()

	def create_cursor(self, w, h):
		cursor = QWidget(self)
		cursor.setAttribute(Qt.WA_TransparentForMouseEvents)
		effect = QGraphicsOpacityEffect(cursor)
		effect.setOpacity(0.4)
		cursor.setGraphicsEffect(effect)
		cursor.setGeometry(0, 0, w, h)
		cursor.setAutoFillBackground(True)
		p = cursor.palette()
		p.setColor(cursor.backgroundRole(), Qt.red)
		cursor.setPalette(p)
		return cursor

	def paintEvent(self, e):

		qp = QPainter()
		qp.begin(self)
		qp.setRenderHint(QPainter.SmoothPixmapTransform)
		qp.drawImage(self.contentsRect(), self.image, self.image.rect())
		r = QRect(0, 0, self.width() - 1, self.height() - 1)
		qp.setPen(QColor(self.conf.border))
		qp.drawRect(r)
		path = QPainterPath()
		pfreq = 0.0
		ptime = 0.0
		pex = 0.0
		tex = 0.0
		qp.setPen(QColor(self.conf.line))
		for x, y in enumerate(argmin(self.imgdata, 0)):
			if self.imgdata[y,x] > self.conf.threshold:
				continue
			t = self.time[x]
			f = self.freq[y]	# TODO log10?
			if pfreq == 0.0:
				path.moveTo(x * self.sx, y * self.sy)
			else:
				path.lineTo(x * self.sx, y * self.sy)
				pex += sqrt((f - pfreq)**2 + (t - ptime)**2)
				tex += t - ptime
			ptime = t
			pfreq = f
		qp.drawPath(path)
		qp.end()
		self.fexChanged.emit(pex, tex)

	def fit(self):
		s = self.image.size()
		asp = s.width() / (s.height() * self.conf.ratio)
		if self.size().height() * asp > self.size().width():
			self.resize(self.width(), self.width() / asp)
		else:
			self.resize(self.height() * asp, self.height())

	def resizeEvent(self, e):
		self.sx = self.width() / self.image.width()
		self.sy = self.height() / self.image.height()

	def mousePressEvent(self, e):
		if self.cursorX.visible():
			pass
		elif self.eraser.visible():
			# TODO create undo/backup of imgdata & initial erase
			pass
		elif e.button() == Qt.MiddleButton:
			self.fit()
		else:
			self.__pos = self.pos()
			self.__size = self.size()
			self.__epos = self.mapToParent(e.pos())

	def mouseMoveEvent(self, e):
		# TODO refactor this rats nest

		y = e.pos().y()
		if y < self.eraser.maxheight / 2.0:
			fixed = y * 2.0
		elif y > self.height() - self.eraser.maxheight / 2.0:
			fixed = (self.height() - y) * 2.0
		else:
			fixed = self.eraser.maxheight




|
|
|








>




|
<
|
|
<
<
<
|
<
<
<
<
<
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<

|

|
|
<
<
<


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|








|

>


>
>
>
>
>
>
>
>












|





<
<
<
<
<
<
<
<
<
<
<
<
<

>
|
<
|


|


<
<
<
|
|
|
|

|
|






<
|

<


<
<
<
<
<
<
<
<





|

|
|










>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

22
23



24





25





26












27
28
29
30
31



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136













137
138
139

140
141
142
143
144
145



146
147
148
149
150
151
152
153
154
155
156
157
158

159
160

161
162








163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

import wave
from numpy import *
from scipy.signal import spectrogram
from PyQt5.QtCore import pyqtSignal, Qt, QPoint, QRect
from PyQt5.QtGui import QColor, QImage, QPainter, QPainterPath
from PyQt5.QtWidgets import QGraphicsOpacityEffect, QWidget

class Spectrogram(QWidget):

	fexChanged = pyqtSignal(float, float)
	mouseOver = pyqtSignal(float, float)

	def __init__(self, fname, parent, config):
		super().__init__(parent)
		print(' INFO: processing %s' % fname)
		self.conf = config
		self.fname = fname
		self.setMouseTracking(True)
		self.setGeometry(parent.contentsRect())
		self.init_fft()

		if self.amp is None:
			# TODO complete / load next



			return





		self.init_calculations()





		self.init_image()












		self.fit()
		self.eraser = self.init_eraser(10, 200)
		self.eraser.maxheight = 200
		self.cursorX = self.init_cursor(1, self.height() * 4)
		self.cursorY = self.init_cursor(self.width() * 4, 1)



		self.show()

	def init_fft(self):
		fft = self.conf['fft']
		try:
			with wave.open(self.fname, 'rb') as song:
				channels, bs, samprate, nsamp, comp, compnam  = song.getparams()
				data = fromstring(song.readframes(nsamp), dtype=uint16)
		except wave.Error:
			print(' WARN: skipping \"%s\": not a wave file.' % self.fname)
			return None
		f, t, amp = spectrogram(data, samprate, fft['window-function'],
				fft['window'], fft['overlap'], detrend=False, mode='psd' )
		self.freq = f
		self.time = t
		self.amp = amp

	def init_calculations(self):
		calc = self.conf['calculations']
		bot = argmax(self.freq > calc['high-pass'])
		top = argmin(self.freq < calc['low-pass'])
		self.freq = self.freq[bot:top]
		self.amp = self.amp[bot:top,:]

		thresh = self.amp.max() / 10**(calc['threshold'] / 10.0)
		thresh = 255.0 - thresh * 255.0 / self.amp.max()
		calc['threshold'] = thresh
		self.amp = 255.0 - self.amp * 255.0 / self.amp.max()

		arr = asarray(self.amp.flat)
		per = 100 * (arr <= thresh).sum() / len(arr)
		msg = '%.2f%% of signal is above the threshold.' % per
		if per > 35.0:
			print(' WARN: %s  Consider using a lower threshold.' % msg)
		elif per < 5.0:
			print(' WARN: %s  Consider using a higher threshold.' % msg)
		else:
			print(' INFO: %s' % msg)

	def init_image(self):
		colors = self.conf['colors']
		self.amp = flipud(self.amp)
		self.amp_back = empty_like(self.amp)
		self.freq = flipud(self.freq)
		self.imgdata = array(self.amp, uint8)
		h, w = self.imgdata.shape
		self.freq /= 1000.0
		self.image = QImage(self.imgdata.tobytes(), w, h, w, QImage.Format_Grayscale8)
		if colors['invert']:
			self.image.invertPixels()

	def init_cursor(self, w, h):
		cursor = QWidget(self)
		cursor.setAttribute(Qt.WA_TransparentForMouseEvents)
		effect = QGraphicsOpacityEffect(cursor)
		effect.setOpacity(0.4)
		cursor.setGraphicsEffect(effect)
		cursor.setGeometry(0, 0, w, h)
		cursor.setAutoFillBackground(True)
		p = cursor.palette()
		p.setColor(cursor.backgroundRole(), QColor(self.conf['colors']['cursor']))
		cursor.setPalette(p)
		cursor.hide()
		return cursor

	def init_eraser(self, w, h):
		eraser = QWidget(self)
		eraser.setAttribute(Qt.WA_TransparentForMouseEvents)
		effect = QGraphicsOpacityEffect(eraser)
		effect.setOpacity(0.4)
		eraser.setGraphicsEffect(effect)
		eraser.setGeometry(0, 0, w, h)
		eraser.setAutoFillBackground(True)
		p = eraser.palette()
		p.setColor(eraser.backgroundRole(), QColor(self.conf['colors']['eraser']))
		eraser.setPalette(p)
		eraser.hide()
		return eraser

	def fit(self):
		s = self.image.size()
		asp = s.width() / (s.height() * self.conf['scale-ratio'])
		if self.size().height() * asp > self.size().width():
			self.resize(self.width(), self.width() / asp)
		else:
			self.resize(self.height() * asp, self.height())

	def mode_normal(self):
		self.unsetCursor()
		self.eraser.hide()
		self.cursorX.hide()
		self.cursorY.hide()

	def mode_cursor(self):
		self.setCursor(Qt.BlankCursor)
		self.eraser.hide()
		self.cursorX.show()
		self.cursorY.show()

	def mode_eraser(self):
		self.setCursor(Qt.BlankCursor)
		self.cursorX.hide()
		self.cursorY.hide()
		self.eraser.show()














	def paintEvent(self, e):
		# TODO refactor this rats nest
		qp = QPainter(self)

		qp.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.Antialiasing)
		qp.drawImage(self.contentsRect(), self.image, self.image.rect())
		r = QRect(0, 0, self.width() - 1, self.height() - 1)
		qp.setPen(QColor(self.conf['colors']['border']))
		qp.drawRect(r)
		path = QPainterPath()



		pfreq, ptime, pex, tex = 0.0, 0.0, 0.0, 0.0
		qp.setPen(QColor(self.conf['colors']['line']))
		for x, y in enumerate(argmin(self.amp, 0)):
			if self.amp[y,x] > self.conf['calculations']['threshold']:
				continue
			t, f = self.time[x], self.freq[y]
			# TODO log10f?
			if pfreq == 0.0:
				path.moveTo(x * self.sx, y * self.sy)
			else:
				path.lineTo(x * self.sx, y * self.sy)
				pex += sqrt((f - pfreq)**2 + (t - ptime)**2)
				tex += t - ptime

			ptime, pfreq = t, f
		qp.drawPath(path)

		self.fexChanged.emit(pex, tex)









	def resizeEvent(self, e):
		self.sx = self.width() / self.image.width()
		self.sy = self.height() / self.image.height()

	def mousePressEvent(self, e):
		if self.cursorX.isVisible():
			pass
		elif self.eraser.isVisible():
			copyto(self.amp_back, self.amp)
			pass
		elif e.button() == Qt.MiddleButton:
			self.fit()
		else:
			self.__pos = self.pos()
			self.__size = self.size()
			self.__epos = self.mapToParent(e.pos())

	def mouseMoveEvent(self, e):
		# TODO refactor this rats nest
		# TODO add eraser size changes
		y = e.pos().y()
		if y < self.eraser.maxheight / 2.0:
			fixed = y * 2.0
		elif y > self.height() - self.eraser.maxheight / 2.0:
			fixed = (self.height() - y) * 2.0
		else:
			fixed = self.eraser.maxheight
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
	def mouseEraseEvent(self, e):
		w = self.eraser.width()
		h = self.eraser.height()
		x = e.pos().x() - w / 2.0
		y = e.pos().y() - h / 2.0
		x1, y1 = self.mapToImgdata(x, y)
		x2, y2 = self.mapToImgdata(x + w, y + h)
		self.imgdata[y1:y2,x1:x2] = 255.0
		self.update(x, 0, w, self.height())

	def mouseNormalEvent(self, e):
		if e.buttons() == Qt.LeftButton:
			diff = self.mapToParent(e.pos()) - self.__epos + self.__pos
			self.move(diff)
		elif e.buttons() == Qt.RightButton:







|







221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
	def mouseEraseEvent(self, e):
		w = self.eraser.width()
		h = self.eraser.height()
		x = e.pos().x() - w / 2.0
		y = e.pos().y() - h / 2.0
		x1, y1 = self.mapToImgdata(x, y)
		x2, y2 = self.mapToImgdata(x + w, y + h)
		self.amp[y1:y2,x1:x2] = 255.0
		self.update(x, 0, w, self.height())

	def mouseNormalEvent(self, e):
		if e.buttons() == Qt.LeftButton:
			diff = self.mapToParent(e.pos()) - self.__epos + self.__pos
			self.move(diff)
		elif e.buttons() == Qt.RightButton:

Changes to setup.py.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25

26
27
28

from setuptools import setup

def readme():
	with open('README.md') as f:
		return f.read()


setup(name='fexqt',
	version='0.1a',
	description='Frequency Excursion Calculator',
	long_description=readme(),
	classifiers=[
		'Development Status :: 3 - Alpha',
		'License :: OSI Approved :: MIT License',
		'Intended Audience :: Science/Research',
		'Programming Language :: Python :: 3',
		'Topic :: Multimedia :: Sound/Audio :: Analysis',
		'Topic :: Scientific/Engineering :: Visualization'
	],
	url='http://jessemcclure.org',
	author='Jesse McClure',
	author_email='jmcclure@broadinstitute.org',
	license='MIT',
	packages=['fexqt'],

	install_requires=['PyQt5','scipy'],

	scripts=['bin/fex'],
	include_package_data=True,
	zip_safe=False)




|


>
|
|















>
|
>
|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

from setuptools import setup

def readme():
	with open('README.rst') as f:
		return f.read()

setup(
	name='fexqt',
	version='2.0.0a1',
	description='Frequency Excursion Calculator',
	long_description=readme(),
	classifiers=[
		'Development Status :: 3 - Alpha',
		'License :: OSI Approved :: MIT License',
		'Intended Audience :: Science/Research',
		'Programming Language :: Python :: 3',
		'Topic :: Multimedia :: Sound/Audio :: Analysis',
		'Topic :: Scientific/Engineering :: Visualization'
	],
	url='http://jessemcclure.org',
	author='Jesse McClure',
	author_email='jmcclure@broadinstitute.org',
	license='MIT',
	packages=['fexqt'],
	package_data={'fexqt': ['*.yaml']},
	install_requires=['pyaml', 'PyQt5', 'scipy'],
	python_requires='>=3',
	entry_points={'console_scripts': ['fex=fexqt:main']},
	include_package_data=True,
	zip_safe=False)