1
|
# Copyright 2008 Dan Smith <dsmith@danplanet.com>
|
2
|
# Copyright 2012 Tom Hayward <tom@tomh.us>
|
3
|
#
|
4
|
# This program is free software: you can redistribute it and/or modify
|
5
|
# it under the terms of the GNU General Public License as published by
|
6
|
# the Free Software Foundation, either version 3 of the License, or
|
7
|
# (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
|
17
|
import os
|
18
|
import tempfile
|
19
|
import urllib
|
20
|
import webbrowser
|
21
|
from glob import glob
|
22
|
import shutil
|
23
|
import time
|
24
|
import logging
|
25
|
import gtk
|
26
|
import gobject
|
27
|
import sys
|
28
|
|
29
|
from chirp.ui import inputdialog, common
|
30
|
from chirp import platform, directory, util
|
31
|
from chirp.drivers import generic_xml, generic_csv
|
32
|
from chirp.drivers import ic9x, kenwood_live, idrp, vx7, vx5, vx6
|
33
|
from chirp.drivers import icf, ic9x_icf
|
34
|
from chirp import CHIRP_VERSION, chirp_common, detect, errors
|
35
|
from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips
|
36
|
from chirp.ui import bandplans
|
37
|
|
38
|
gobject.threads_init()
|
39
|
|
40
|
LOG = logging.getLogger(__name__)
|
41
|
|
42
|
if __name__ == "__main__":
|
43
|
sys.path.insert(0, "..")
|
44
|
|
45
|
try:
|
46
|
import serial
|
47
|
except ImportError, e:
|
48
|
common.log_exception()
|
49
|
common.show_error("\nThe Pyserial module is not installed!")
|
50
|
|
51
|
|
52
|
CONF = config.get()
|
53
|
|
54
|
KEEP_RECENT = 8
|
55
|
|
56
|
RB_BANDS = {
|
57
|
"--All--": 0,
|
58
|
"10 meters (29MHz)": 29,
|
59
|
"6 meters (54MHz)": 5,
|
60
|
"2 meters (144MHz)": 14,
|
61
|
"1.25 meters (220MHz)": 22,
|
62
|
"70 centimeters (440MHz)": 4,
|
63
|
"33 centimeters (900MHz)": 9,
|
64
|
"23 centimeters (1.2GHz)": 12,
|
65
|
}
|
66
|
|
67
|
|
68
|
def key_bands(band):
|
69
|
if band.startswith("-"):
|
70
|
return -1
|
71
|
|
72
|
amount, units, mhz = band.split(" ")
|
73
|
scale = units == "meters" and 100 or 1
|
74
|
|
75
|
return 100000 - (float(amount) * scale)
|
76
|
|
77
|
|
78
|
class ModifiedError(Exception):
|
79
|
pass
|
80
|
|
81
|
|
82
|
class ChirpMain(gtk.Window):
|
83
|
def get_current_editorset(self):
|
84
|
page = self.tabs.get_current_page()
|
85
|
if page is not None:
|
86
|
return self.tabs.get_nth_page(page)
|
87
|
else:
|
88
|
return None
|
89
|
|
90
|
def ev_tab_switched(self, pagenum=None):
|
91
|
def set_action_sensitive(action, sensitive):
|
92
|
self.menu_ag.get_action(action).set_sensitive(sensitive)
|
93
|
|
94
|
if pagenum is not None:
|
95
|
eset = self.tabs.get_nth_page(pagenum)
|
96
|
else:
|
97
|
eset = self.get_current_editorset()
|
98
|
|
99
|
upload_sens = bool(eset and
|
100
|
isinstance(eset.radio, chirp_common.CloneModeRadio))
|
101
|
|
102
|
if not eset or isinstance(eset.radio, chirp_common.LiveRadio):
|
103
|
save_sens = False
|
104
|
elif isinstance(eset.radio, chirp_common.NetworkSourceRadio):
|
105
|
save_sens = False
|
106
|
else:
|
107
|
save_sens = True
|
108
|
|
109
|
for i in ["import", "importsrc", "stock"]:
|
110
|
set_action_sensitive(i,
|
111
|
eset is not None and not eset.get_read_only())
|
112
|
|
113
|
for i in ["save", "saveas"]:
|
114
|
set_action_sensitive(i, save_sens)
|
115
|
|
116
|
for i in ["upload"]:
|
117
|
set_action_sensitive(i, upload_sens)
|
118
|
|
119
|
for i in ["cancelq"]:
|
120
|
set_action_sensitive(i, eset is not None and not save_sens)
|
121
|
|
122
|
for i in ["export", "close", "columns", "irbook", "irfinder",
|
123
|
"move_up", "move_dn", "exchange", "iradioreference",
|
124
|
"cut", "copy", "paste", "delete", "viewdeveloper",
|
125
|
"all", "properties"]:
|
126
|
set_action_sensitive(i, eset is not None)
|
127
|
|
128
|
def ev_status(self, editorset, msg):
|
129
|
self.sb_radio.pop(0)
|
130
|
self.sb_radio.push(0, msg)
|
131
|
|
132
|
def ev_usermsg(self, editorset, msg):
|
133
|
self.sb_general.pop(0)
|
134
|
self.sb_general.push(0, msg)
|
135
|
|
136
|
def ev_editor_selected(self, editorset, editortype):
|
137
|
mappings = {
|
138
|
"memedit": ["view", "edit"],
|
139
|
}
|
140
|
|
141
|
for _editortype, actions in mappings.items():
|
142
|
for _action in actions:
|
143
|
action = self.menu_ag.get_action(_action)
|
144
|
action.set_sensitive(editortype.startswith(_editortype))
|
145
|
|
146
|
def _connect_editorset(self, eset):
|
147
|
eset.connect("want-close", self.do_close)
|
148
|
eset.connect("status", self.ev_status)
|
149
|
eset.connect("usermsg", self.ev_usermsg)
|
150
|
eset.connect("editor-selected", self.ev_editor_selected)
|
151
|
|
152
|
def do_diff_radio(self):
|
153
|
if self.tabs.get_n_pages() < 2:
|
154
|
common.show_error("Diff tabs requires at least two open tabs!")
|
155
|
return
|
156
|
|
157
|
esets = []
|
158
|
for i in range(0, self.tabs.get_n_pages()):
|
159
|
esets.append(self.tabs.get_nth_page(i))
|
160
|
|
161
|
d = gtk.Dialog(title="Diff Radios",
|
162
|
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
|
163
|
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
|
164
|
parent=self)
|
165
|
|
166
|
label = gtk.Label("")
|
167
|
label.set_markup("<b>-1</b> for either Mem # does a full-file hex " +
|
168
|
"dump with diffs highlighted.\n" +
|
169
|
"<b>-2</b> for first Mem # shows " +
|
170
|
"<b>only</b> the diffs.")
|
171
|
d.vbox.pack_start(label, True, True, 0)
|
172
|
label.show()
|
173
|
|
174
|
choices = []
|
175
|
for eset in esets:
|
176
|
choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR,
|
177
|
eset.rthread.radio.MODEL,
|
178
|
eset.filename))
|
179
|
choice_a = miscwidgets.make_choice(choices, False, choices[0])
|
180
|
choice_a.show()
|
181
|
chan_a = gtk.SpinButton()
|
182
|
chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0)
|
183
|
chan_a.show()
|
184
|
hbox = gtk.HBox(False, 3)
|
185
|
hbox.pack_start(choice_a, 1, 1, 1)
|
186
|
hbox.pack_start(chan_a, 0, 0, 0)
|
187
|
hbox.show()
|
188
|
d.vbox.pack_start(hbox, 0, 0, 0)
|
189
|
|
190
|
choice_b = miscwidgets.make_choice(choices, False, choices[1])
|
191
|
choice_b.show()
|
192
|
chan_b = gtk.SpinButton()
|
193
|
chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0)
|
194
|
chan_b.show()
|
195
|
hbox = gtk.HBox(False, 3)
|
196
|
hbox.pack_start(choice_b, 1, 1, 1)
|
197
|
hbox.pack_start(chan_b, 0, 0, 0)
|
198
|
hbox.show()
|
199
|
d.vbox.pack_start(hbox, 0, 0, 0)
|
200
|
|
201
|
r = d.run()
|
202
|
sel_a = choice_a.get_active_text()
|
203
|
sel_chan_a = chan_a.get_value()
|
204
|
sel_b = choice_b.get_active_text()
|
205
|
sel_chan_b = chan_b.get_value()
|
206
|
d.destroy()
|
207
|
if r == gtk.RESPONSE_CANCEL:
|
208
|
return
|
209
|
|
210
|
if sel_a == sel_b:
|
211
|
common.show_error("Can't diff the same tab!")
|
212
|
return
|
213
|
|
214
|
LOG.debug("Selected %s@%i and %s@%i" %
|
215
|
(sel_a, sel_chan_a, sel_b, sel_chan_b))
|
216
|
name_a = os.path.basename(sel_a)
|
217
|
name_a = name_a[:name_a.rindex(")")]
|
218
|
name_b = os.path.basename(sel_b)
|
219
|
name_b = name_b[:name_b.rindex(")")]
|
220
|
diffwintitle = "%s@%i diff %s@%i" % (
|
221
|
name_a, sel_chan_a, name_b, sel_chan_b)
|
222
|
|
223
|
eset_a = esets[choices.index(sel_a)]
|
224
|
eset_b = esets[choices.index(sel_b)]
|
225
|
|
226
|
def _show_diff(mem_b, mem_a):
|
227
|
# Step 3: Show the diff
|
228
|
diff = common.simple_diff(mem_a, mem_b)
|
229
|
common.show_diff_blob(diffwintitle, diff)
|
230
|
|
231
|
def _get_mem_b(mem_a):
|
232
|
# Step 2: Get memory b
|
233
|
job = common.RadioJob(_show_diff, "get_raw_memory",
|
234
|
int(sel_chan_b))
|
235
|
job.set_cb_args(mem_a)
|
236
|
eset_b.rthread.submit(job)
|
237
|
|
238
|
if sel_chan_a >= 0 and sel_chan_b >= 0:
|
239
|
# Diff numbered memory
|
240
|
# Step 1: Get memory a
|
241
|
job = common.RadioJob(_get_mem_b, "get_raw_memory",
|
242
|
int(sel_chan_a))
|
243
|
eset_a.rthread.submit(job)
|
244
|
elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\
|
245
|
isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio):
|
246
|
# Diff whole (can do this without a job, since both are clone-mode)
|
247
|
try:
|
248
|
addrfmt = CONF.get('hexdump_addrfmt', section='developer',
|
249
|
raw=True)
|
250
|
except:
|
251
|
pass
|
252
|
a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(),
|
253
|
addrfmt=addrfmt)
|
254
|
b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(),
|
255
|
addrfmt=addrfmt)
|
256
|
if sel_chan_a == -2:
|
257
|
diffsonly = True
|
258
|
else:
|
259
|
diffsonly = False
|
260
|
common.show_diff_blob(diffwintitle,
|
261
|
common.simple_diff(a, b, diffsonly))
|
262
|
else:
|
263
|
common.show_error("Cannot diff whole live-mode radios!")
|
264
|
|
265
|
def do_new(self):
|
266
|
eset = editorset.EditorSet(_("Untitled") + ".csv", self)
|
267
|
self._connect_editorset(eset)
|
268
|
eset.prime()
|
269
|
eset.show()
|
270
|
|
271
|
tab = self.tabs.append_page(eset, eset.get_tab_label())
|
272
|
self.tabs.set_current_page(tab)
|
273
|
|
274
|
def _do_manual_select(self, filename):
|
275
|
radiolist = {}
|
276
|
for drv, radio in directory.DRV_TO_RADIO.items():
|
277
|
if not issubclass(radio, chirp_common.CloneModeRadio):
|
278
|
continue
|
279
|
radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv
|
280
|
|
281
|
lab = gtk.Label("""<b><big>Unable to detect model!</big></b>
|
282
|
|
283
|
If you think that it is valid, you can select a radio model below to
|
284
|
force an open attempt. If selecting the model manually works, please
|
285
|
file a bug on the website and attach your image. If selecting the model
|
286
|
does not work, it is likely that you are trying to open some other type
|
287
|
of file.
|
288
|
""")
|
289
|
|
290
|
lab.set_justify(gtk.JUSTIFY_FILL)
|
291
|
lab.set_line_wrap(True)
|
292
|
lab.set_use_markup(True)
|
293
|
lab.show()
|
294
|
choice = miscwidgets.make_choice(sorted(radiolist.keys()), False,
|
295
|
sorted(radiolist.keys())[0])
|
296
|
d = gtk.Dialog(title="Detection Failed",
|
297
|
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
|
298
|
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
|
299
|
d.vbox.pack_start(lab, 0, 0, 0)
|
300
|
d.vbox.pack_start(choice, 0, 0, 0)
|
301
|
d.vbox.set_spacing(5)
|
302
|
choice.show()
|
303
|
d.set_default_size(400, 200)
|
304
|
# d.set_resizable(False)
|
305
|
r = d.run()
|
306
|
d.destroy()
|
307
|
if r != gtk.RESPONSE_OK:
|
308
|
return
|
309
|
try:
|
310
|
rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]]
|
311
|
return rc(filename)
|
312
|
except:
|
313
|
return
|
314
|
|
315
|
def do_open(self, fname=None, tempname=None):
|
316
|
if not fname:
|
317
|
types = [(_("CHIRP Radio Images") + " (*.img)", "*.img"),
|
318
|
(_("CHIRP Files") + " (*.chirp)", "*.chirp"),
|
319
|
(_("CSV Files") + " (*.csv)", "*.csv"),
|
320
|
(_("DAT Files") + " (*.dat)", "*.dat"),
|
321
|
(_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
|
322
|
(_("ICF Files") + " (*.icf)", "*.icf"),
|
323
|
(_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
|
324
|
(_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
|
325
|
(_("VX7 Commander Files") + " (*.vx7)", "*.vx7"),
|
326
|
]
|
327
|
fname = platform.get_platform().gui_open_file(types=types)
|
328
|
if not fname:
|
329
|
return
|
330
|
|
331
|
self.record_recent_file(fname)
|
332
|
|
333
|
if icf.is_icf_file(fname):
|
334
|
a = common.ask_yesno_question(
|
335
|
_("ICF files cannot be edited, only displayed or imported "
|
336
|
"into another file. Open in read-only mode?"),
|
337
|
self)
|
338
|
if not a:
|
339
|
return
|
340
|
read_only = True
|
341
|
else:
|
342
|
read_only = False
|
343
|
|
344
|
if icf.is_9x_icf(fname):
|
345
|
# We have to actually instantiate the IC9xICFRadio to get its
|
346
|
# sub-devices
|
347
|
radio = ic9x_icf.IC9xICFRadio(fname)
|
348
|
else:
|
349
|
try:
|
350
|
radio = directory.get_radio_by_image(fname)
|
351
|
except errors.ImageDetectFailed:
|
352
|
radio = self._do_manual_select(fname)
|
353
|
if not radio:
|
354
|
return
|
355
|
LOG.debug("Manually selected %s" % radio)
|
356
|
except Exception, e:
|
357
|
common.log_exception()
|
358
|
common.show_error(os.path.basename(fname) + ": " + str(e))
|
359
|
return
|
360
|
|
361
|
first_tab = False
|
362
|
try:
|
363
|
eset = editorset.EditorSet(radio, self,
|
364
|
filename=fname,
|
365
|
tempname=tempname)
|
366
|
except Exception, e:
|
367
|
common.log_exception()
|
368
|
common.show_error(
|
369
|
_("There was an error opening {fname}: {error}").format(
|
370
|
fname=fname,
|
371
|
error=e))
|
372
|
return
|
373
|
|
374
|
eset.set_read_only(read_only)
|
375
|
self._connect_editorset(eset)
|
376
|
eset.show()
|
377
|
self.tabs.append_page(eset, eset.get_tab_label())
|
378
|
|
379
|
if hasattr(eset.rthread.radio, "errors") and \
|
380
|
eset.rthread.radio.errors:
|
381
|
msg = _("{num} errors during open:").format(
|
382
|
num=len(eset.rthread.radio.errors))
|
383
|
common.show_error_text(msg,
|
384
|
"\r\n".join(eset.rthread.radio.errors))
|
385
|
|
386
|
def do_live_warning(self, radio):
|
387
|
d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
|
388
|
d.set_markup("<big><b>" + _("Note:") + "</b></big>")
|
389
|
msg = _("The {vendor} {model} operates in <b>live mode</b>. "
|
390
|
"This means that any changes you make are immediately sent "
|
391
|
"to the radio. Because of this, you cannot perform the "
|
392
|
"<u>Save</u> or <u>Upload</u> operations. If you wish to "
|
393
|
"edit the contents offline, please <u>Export</u> to a CSV "
|
394
|
"file, using the <b>File menu</b>.")
|
395
|
msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL)
|
396
|
d.format_secondary_markup(msg)
|
397
|
|
398
|
again = gtk.CheckButton(_("Don't show this again"))
|
399
|
again.show()
|
400
|
d.vbox.pack_start(again, 0, 0, 0)
|
401
|
d.run()
|
402
|
CONF.set_bool("live_mode", again.get_active(), "noconfirm")
|
403
|
d.destroy()
|
404
|
|
405
|
def do_open_live(self, radio, tempname=None, read_only=False):
|
406
|
eset = editorset.EditorSet(radio, self, tempname=tempname)
|
407
|
eset.connect("want-close", self.do_close)
|
408
|
eset.connect("status", self.ev_status)
|
409
|
eset.set_read_only(read_only)
|
410
|
eset.show()
|
411
|
self.tabs.append_page(eset, eset.get_tab_label())
|
412
|
|
413
|
if isinstance(radio, chirp_common.LiveRadio):
|
414
|
reporting.report_model_usage(radio, "live", True)
|
415
|
if not CONF.get_bool("live_mode", "noconfirm"):
|
416
|
self.do_live_warning(radio)
|
417
|
|
418
|
def do_save(self, eset=None):
|
419
|
if not eset:
|
420
|
eset = self.get_current_editorset()
|
421
|
|
422
|
# For usability, allow Ctrl-S to short-circuit to Save-As if
|
423
|
# we are working on a yet-to-be-saved image
|
424
|
if not os.path.exists(eset.filename):
|
425
|
return self.do_saveas()
|
426
|
|
427
|
eset.save()
|
428
|
|
429
|
def do_saveas(self):
|
430
|
eset = self.get_current_editorset()
|
431
|
|
432
|
label = _("{vendor} {model} image file").format(
|
433
|
vendor=eset.radio.VENDOR,
|
434
|
model=eset.radio.MODEL)
|
435
|
|
436
|
types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION,
|
437
|
eset.radio.FILE_EXTENSION)]
|
438
|
|
439
|
if isinstance(eset.radio, vx7.VX7Radio):
|
440
|
types += [(_("VX7 Commander") + " (*.vx7)", "vx7")]
|
441
|
elif isinstance(eset.radio, vx6.VX6Radio):
|
442
|
types += [(_("VX6 Commander") + " (*.vx6)", "vx6")]
|
443
|
elif isinstance(eset.radio, vx5.VX5Radio):
|
444
|
types += [(_("EVE") + " (*.eve)", "eve")]
|
445
|
types += [(_("VX5 Commander") + " (*.vx5)", "vx5")]
|
446
|
|
447
|
while True:
|
448
|
fname = platform.get_platform().gui_save_file(types=types)
|
449
|
if not fname:
|
450
|
return
|
451
|
|
452
|
if os.path.exists(fname):
|
453
|
dlg = inputdialog.OverwriteDialog(fname)
|
454
|
owrite = dlg.run()
|
455
|
dlg.destroy()
|
456
|
if owrite == gtk.RESPONSE_OK:
|
457
|
break
|
458
|
else:
|
459
|
break
|
460
|
|
461
|
try:
|
462
|
eset.save(fname)
|
463
|
except Exception, e:
|
464
|
d = inputdialog.ExceptionDialog(e)
|
465
|
d.run()
|
466
|
d.destroy()
|
467
|
|
468
|
def cb_clonein(self, radio, emsg=None):
|
469
|
radio.pipe.close()
|
470
|
reporting.report_model_usage(radio, "download", bool(emsg))
|
471
|
if not emsg:
|
472
|
self.do_open_live(radio, tempname="(" + _("Untitled") + ")")
|
473
|
else:
|
474
|
d = inputdialog.ExceptionDialog(emsg)
|
475
|
d.run()
|
476
|
d.destroy()
|
477
|
|
478
|
def cb_cloneout(self, radio, emsg=None):
|
479
|
radio.pipe.close()
|
480
|
reporting.report_model_usage(radio, "upload", True)
|
481
|
if emsg:
|
482
|
d = inputdialog.ExceptionDialog(emsg)
|
483
|
d.run()
|
484
|
d.destroy()
|
485
|
|
486
|
def _get_recent_list(self):
|
487
|
recent = []
|
488
|
for i in range(0, KEEP_RECENT):
|
489
|
fn = CONF.get("recent%i" % i, "state")
|
490
|
if fn:
|
491
|
recent.append(fn)
|
492
|
return recent
|
493
|
|
494
|
def _set_recent_list(self, recent):
|
495
|
for fn in recent:
|
496
|
CONF.set("recent%i" % recent.index(fn), fn, "state")
|
497
|
|
498
|
def update_recent_files(self):
|
499
|
i = 0
|
500
|
for fname in self._get_recent_list():
|
501
|
action_name = "recent%i" % i
|
502
|
path = "/MenuBar/file/recent"
|
503
|
|
504
|
old_action = self.menu_ag.get_action(action_name)
|
505
|
if old_action:
|
506
|
self.menu_ag.remove_action(old_action)
|
507
|
|
508
|
file_basename = os.path.basename(fname).replace("_", "__")
|
509
|
action = gtk.Action(
|
510
|
action_name, "_%i. %s" % (i+1, file_basename),
|
511
|
_("Open recent file {name}").format(name=fname), "")
|
512
|
action.connect("activate", lambda a, f: self.do_open(f), fname)
|
513
|
mid = self.menu_uim.new_merge_id()
|
514
|
self.menu_uim.add_ui(mid, path,
|
515
|
action_name, action_name,
|
516
|
gtk.UI_MANAGER_MENUITEM, False)
|
517
|
self.menu_ag.add_action(action)
|
518
|
i += 1
|
519
|
|
520
|
def record_recent_file(self, filename):
|
521
|
|
522
|
recent_files = self._get_recent_list()
|
523
|
if filename not in recent_files:
|
524
|
if len(recent_files) == KEEP_RECENT:
|
525
|
del recent_files[-1]
|
526
|
recent_files.insert(0, filename)
|
527
|
self._set_recent_list(recent_files)
|
528
|
|
529
|
self.update_recent_files()
|
530
|
|
531
|
def import_stock_config(self, action, config):
|
532
|
eset = self.get_current_editorset()
|
533
|
count = eset.do_import(config)
|
534
|
|
535
|
def copy_shipped_stock_configs(self, stock_dir):
|
536
|
execpath = platform.get_platform().executable_path()
|
537
|
basepath = os.path.abspath(os.path.join(execpath, "stock_configs"))
|
538
|
if not os.path.exists(basepath):
|
539
|
basepath = "/usr/share/chirp/stock_configs"
|
540
|
|
541
|
files = glob(os.path.join(basepath, "*.csv"))
|
542
|
for fn in files:
|
543
|
if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))):
|
544
|
LOG.info("Skipping existing stock config")
|
545
|
continue
|
546
|
try:
|
547
|
shutil.copy(fn, stock_dir)
|
548
|
LOG.debug("Copying %s -> %s" % (fn, stock_dir))
|
549
|
except Exception, e:
|
550
|
LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e))
|
551
|
return False
|
552
|
return True
|
553
|
|
554
|
def update_stock_configs(self):
|
555
|
stock_dir = platform.get_platform().config_file("stock_configs")
|
556
|
if not os.path.isdir(stock_dir):
|
557
|
try:
|
558
|
os.mkdir(stock_dir)
|
559
|
except Exception, e:
|
560
|
LOG.error("Unable to create directory: %s" % stock_dir)
|
561
|
return
|
562
|
if not self.copy_shipped_stock_configs(stock_dir):
|
563
|
return
|
564
|
|
565
|
def _do_import_action(config):
|
566
|
name = os.path.splitext(os.path.basename(config))[0]
|
567
|
action_name = "stock-%i" % configs.index(config)
|
568
|
path = "/MenuBar/radio/stock"
|
569
|
action = gtk.Action(action_name,
|
570
|
name,
|
571
|
_("Import stock "
|
572
|
"configuration {name}").format(name=name),
|
573
|
"")
|
574
|
action.connect("activate", self.import_stock_config, config)
|
575
|
mid = self.menu_uim.new_merge_id()
|
576
|
mid = self.menu_uim.add_ui(mid, path,
|
577
|
action_name, action_name,
|
578
|
gtk.UI_MANAGER_MENUITEM, False)
|
579
|
self.menu_ag.add_action(action)
|
580
|
|
581
|
def _do_open_action(config):
|
582
|
name = os.path.splitext(os.path.basename(config))[0]
|
583
|
action_name = "openstock-%i" % configs.index(config)
|
584
|
path = "/MenuBar/file/openstock"
|
585
|
action = gtk.Action(action_name,
|
586
|
name,
|
587
|
_("Open stock "
|
588
|
"configuration {name}").format(name=name),
|
589
|
"")
|
590
|
action.connect("activate", lambda a, c: self.do_open(c), config)
|
591
|
mid = self.menu_uim.new_merge_id()
|
592
|
mid = self.menu_uim.add_ui(mid, path,
|
593
|
action_name, action_name,
|
594
|
gtk.UI_MANAGER_MENUITEM, False)
|
595
|
self.menu_ag.add_action(action)
|
596
|
|
597
|
configs = glob(os.path.join(stock_dir, "*.csv"))
|
598
|
for config in configs:
|
599
|
_do_import_action(config)
|
600
|
_do_open_action(config)
|
601
|
|
602
|
def _confirm_experimental(self, rclass):
|
603
|
sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass)
|
604
|
if CONF.is_defined(sql_key, "state") and \
|
605
|
not CONF.get_bool(sql_key, "state"):
|
606
|
return True
|
607
|
|
608
|
title = _("Proceed with experimental driver?")
|
609
|
text = rclass.get_prompts().experimental
|
610
|
msg = _("This radio's driver is experimental. "
|
611
|
"Do you want to proceed?")
|
612
|
resp, squelch = common.show_warning(msg, text,
|
613
|
title=title,
|
614
|
buttons=gtk.BUTTONS_YES_NO,
|
615
|
can_squelch=True)
|
616
|
if resp == gtk.RESPONSE_YES:
|
617
|
CONF.set_bool(sql_key, not squelch, "state")
|
618
|
return resp == gtk.RESPONSE_YES
|
619
|
|
620
|
def _show_instructions(self, radio, message):
|
621
|
if message is None:
|
622
|
return
|
623
|
|
624
|
if CONF.get_bool("clone_instructions", "noconfirm"):
|
625
|
return
|
626
|
|
627
|
d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
|
628
|
d.set_markup("<big><b>" + _("{name} Instructions").format(
|
629
|
name=radio.get_name()) + "</b></big>")
|
630
|
msg = _("{instructions}").format(instructions=message)
|
631
|
d.format_secondary_markup(msg)
|
632
|
|
633
|
again = gtk.CheckButton(
|
634
|
_("Don't show instructions for any radio again"))
|
635
|
again.show()
|
636
|
again.connect("toggled", lambda action:
|
637
|
self.clonemenu.set_active(not action.get_active()))
|
638
|
d.vbox.pack_start(again, 0, 0, 0)
|
639
|
h_button_box = d.vbox.get_children()[2]
|
640
|
try:
|
641
|
ok_button = h_button_box.get_children()[0]
|
642
|
ok_button.grab_default()
|
643
|
ok_button.grab_focus()
|
644
|
except AttributeError:
|
645
|
# don't grab focus on GTK+ 2.0
|
646
|
pass
|
647
|
d.run()
|
648
|
d.destroy()
|
649
|
|
650
|
|
651
|
def do_download(self, port=None, rtype=None):
|
652
|
d = clone.CloneSettingsDialog(parent=self)
|
653
|
settings = d.run()
|
654
|
d.destroy()
|
655
|
if not settings:
|
656
|
return
|
657
|
|
658
|
rclass = settings.radio_class
|
659
|
if issubclass(rclass, chirp_common.ExperimentalRadio) and \
|
660
|
not self._confirm_experimental(rclass):
|
661
|
# User does not want to proceed with experimental driver
|
662
|
return
|
663
|
|
664
|
self._show_instructions(rclass, rclass.get_prompts().pre_download)
|
665
|
|
666
|
LOG.debug("User selected %s %s on port %s" %
|
667
|
(rclass.VENDOR, rclass.MODEL, settings.port))
|
668
|
|
669
|
try:
|
670
|
ser = serial.Serial(port=settings.port,
|
671
|
baudrate=rclass.BAUD_RATE,
|
672
|
rtscts=rclass.HARDWARE_FLOW,
|
673
|
timeout=0.25)
|
674
|
ser.flushInput()
|
675
|
except serial.SerialException, e:
|
676
|
d = inputdialog.ExceptionDialog(e)
|
677
|
d.run()
|
678
|
d.destroy()
|
679
|
return
|
680
|
|
681
|
radio = settings.radio_class(ser)
|
682
|
|
683
|
fn = tempfile.mktemp()
|
684
|
if isinstance(radio, chirp_common.CloneModeRadio):
|
685
|
ct = clone.CloneThread(radio, "in", cb=self.cb_clonein,
|
686
|
parent=self)
|
687
|
ct.start()
|
688
|
else:
|
689
|
self.do_open_live(radio)
|
690
|
|
691
|
def do_upload(self, port=None, rtype=None):
|
692
|
eset = self.get_current_editorset()
|
693
|
radio = eset.radio
|
694
|
|
695
|
settings = clone.CloneSettings()
|
696
|
settings.radio_class = radio.__class__
|
697
|
|
698
|
d = clone.CloneSettingsDialog(settings, parent=self)
|
699
|
settings = d.run()
|
700
|
d.destroy()
|
701
|
if not settings:
|
702
|
return
|
703
|
prompts = radio.get_prompts()
|
704
|
|
705
|
if prompts.display_pre_upload_prompt_before_opening_port is True:
|
706
|
LOG.debug("Opening port after pre_upload prompt.")
|
707
|
self._show_instructions(radio, prompts.pre_upload)
|
708
|
|
709
|
if isinstance(radio, chirp_common.ExperimentalRadio) and \
|
710
|
not self._confirm_experimental(radio.__class__):
|
711
|
# User does not want to proceed with experimental driver
|
712
|
return
|
713
|
|
714
|
try:
|
715
|
ser = serial.Serial(port=settings.port,
|
716
|
baudrate=radio.BAUD_RATE,
|
717
|
rtscts=radio.HARDWARE_FLOW,
|
718
|
timeout=0.25)
|
719
|
ser.flushInput()
|
720
|
except serial.SerialException, e:
|
721
|
d = inputdialog.ExceptionDialog(e)
|
722
|
d.run()
|
723
|
d.destroy()
|
724
|
return
|
725
|
|
726
|
if prompts.display_pre_upload_prompt_before_opening_port is False:
|
727
|
LOG.debug("Opening port before pre_upload prompt.")
|
728
|
self._show_instructions(radio, prompts.pre_upload)
|
729
|
|
730
|
radio.set_pipe(ser)
|
731
|
|
732
|
ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self)
|
733
|
ct.start()
|
734
|
|
735
|
def do_close(self, tab_child=None):
|
736
|
if tab_child:
|
737
|
eset = tab_child
|
738
|
else:
|
739
|
eset = self.get_current_editorset()
|
740
|
|
741
|
if not eset:
|
742
|
return False
|
743
|
|
744
|
if eset.is_modified():
|
745
|
dlg = miscwidgets.YesNoDialog(
|
746
|
title=_("Save Changes?"), parent=self,
|
747
|
buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES,
|
748
|
gtk.STOCK_NO, gtk.RESPONSE_NO,
|
749
|
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
|
750
|
dlg.set_text(_("File is modified, save changes before closing?"))
|
751
|
res = dlg.run()
|
752
|
dlg.destroy()
|
753
|
|
754
|
if res == gtk.RESPONSE_YES:
|
755
|
self.do_save(eset)
|
756
|
elif res != gtk.RESPONSE_NO:
|
757
|
raise ModifiedError()
|
758
|
|
759
|
eset.rthread.stop()
|
760
|
eset.rthread.join()
|
761
|
|
762
|
eset.prepare_close()
|
763
|
|
764
|
if eset.radio.pipe:
|
765
|
eset.radio.pipe.close()
|
766
|
|
767
|
if isinstance(eset.radio, chirp_common.LiveRadio):
|
768
|
action = self.menu_ag.get_action("openlive")
|
769
|
if action:
|
770
|
action.set_sensitive(True)
|
771
|
|
772
|
page = self.tabs.page_num(eset)
|
773
|
if page is not None:
|
774
|
self.tabs.remove_page(page)
|
775
|
|
776
|
return True
|
777
|
|
778
|
def do_import(self):
|
779
|
types = [(_("CHIRP Files") + " (*.chirp)", "*.chirp"),
|
780
|
(_("CHIRP Radio Images") + " (*.img)", "*.img"),
|
781
|
(_("CSV Files") + " (*.csv)", "*.csv"),
|
782
|
(_("DAT Files") + " (*.dat)", "*.dat"),
|
783
|
(_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
|
784
|
(_("ICF Files") + " (*.icf)", "*.icf"),
|
785
|
(_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
|
786
|
(_("Kenwood ITM Files") + " (*.itm)", "*.itm"),
|
787
|
(_("Travel Plus Files") + " (*.tpe)", "*.tpe"),
|
788
|
(_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
|
789
|
(_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
|
790
|
(_("VX7 Commander Files") + " (*.vx7)", "*.vx7")]
|
791
|
filen = platform.get_platform().gui_open_file(types=types)
|
792
|
if not filen:
|
793
|
return
|
794
|
|
795
|
eset = self.get_current_editorset()
|
796
|
count = eset.do_import(filen)
|
797
|
reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
|
798
|
|
799
|
def do_repeaterbook_prompt(self):
|
800
|
if not CONF.get_bool("has_seen_credit", "repeaterbook"):
|
801
|
d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
|
802
|
d.set_markup("<big><big><b>RepeaterBook</b></big>\r\n" +
|
803
|
"<i>North American Repeater Directory</i></big>")
|
804
|
d.format_secondary_markup("For more information about this " +
|
805
|
"free service, please go to\r\n" +
|
806
|
"http://www.repeaterbook.com")
|
807
|
d.run()
|
808
|
d.destroy()
|
809
|
CONF.set_bool("has_seen_credit", True, "repeaterbook")
|
810
|
|
811
|
default_state = "Oregon"
|
812
|
default_county = "--All--"
|
813
|
default_band = "--All--"
|
814
|
try:
|
815
|
try:
|
816
|
code = int(CONF.get("state", "repeaterbook"))
|
817
|
except:
|
818
|
code = CONF.get("state", "repeaterbook")
|
819
|
for k, v in fips.FIPS_STATES.items():
|
820
|
if code == v:
|
821
|
default_state = k
|
822
|
break
|
823
|
|
824
|
code = CONF.get("county", "repeaterbook")
|
825
|
items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items()
|
826
|
for k, v in items:
|
827
|
if code == v:
|
828
|
default_county = k
|
829
|
break
|
830
|
|
831
|
code = int(CONF.get("band", "repeaterbook"))
|
832
|
for k, v in RB_BANDS.items():
|
833
|
if code == v:
|
834
|
default_band = k
|
835
|
break
|
836
|
except:
|
837
|
pass
|
838
|
|
839
|
state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()),
|
840
|
False, default_state)
|
841
|
county = miscwidgets.make_choice(
|
842
|
sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()),
|
843
|
False, default_county)
|
844
|
band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands),
|
845
|
False, default_band)
|
846
|
|
847
|
def _changed(box, county):
|
848
|
state = fips.FIPS_STATES[box.get_active_text()]
|
849
|
county.get_model().clear()
|
850
|
for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()):
|
851
|
county.append_text(fips_county)
|
852
|
county.set_active(0)
|
853
|
|
854
|
state.connect("changed", _changed, county)
|
855
|
|
856
|
d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self)
|
857
|
d.add_field("State", state)
|
858
|
d.add_field("County", county)
|
859
|
d.add_field("Band", band)
|
860
|
|
861
|
r = d.run()
|
862
|
d.destroy()
|
863
|
if r != gtk.RESPONSE_OK:
|
864
|
return False
|
865
|
|
866
|
code = fips.FIPS_STATES[state.get_active_text()]
|
867
|
county_id = fips.FIPS_COUNTIES[code][county.get_active_text()]
|
868
|
freq = RB_BANDS[band.get_active_text()]
|
869
|
CONF.set("state", str(code), "repeaterbook")
|
870
|
CONF.set("county", str(county_id), "repeaterbook")
|
871
|
CONF.set("band", str(freq), "repeaterbook")
|
872
|
|
873
|
return True
|
874
|
|
875
|
def do_repeaterbook(self, do_import):
|
876
|
self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
|
877
|
if not self.do_repeaterbook_prompt():
|
878
|
self.window.set_cursor(None)
|
879
|
return
|
880
|
|
881
|
try:
|
882
|
code = "%02i" % int(CONF.get("state", "repeaterbook"))
|
883
|
except:
|
884
|
try:
|
885
|
code = CONF.get("state", "repeaterbook")
|
886
|
except:
|
887
|
code = '41' # Oregon default
|
888
|
|
889
|
try:
|
890
|
county = CONF.get("county", "repeaterbook")
|
891
|
except:
|
892
|
county = '%' # --All-- default
|
893
|
|
894
|
try:
|
895
|
band = int(CONF.get("band", "repeaterbook"))
|
896
|
except:
|
897
|
band = 14 # 2m default
|
898
|
|
899
|
query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \
|
900
|
"?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \
|
901
|
"&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%"
|
902
|
query = query % (code,
|
903
|
band and band or "%%",
|
904
|
county and county or "%%")
|
905
|
|
906
|
# Do this in case the import process is going to take a while
|
907
|
# to make sure we process events leading up to this
|
908
|
gtk.gdk.window_process_all_updates()
|
909
|
while gtk.events_pending():
|
910
|
gtk.main_iteration(False)
|
911
|
|
912
|
fn = tempfile.mktemp(".csv")
|
913
|
filename, headers = urllib.urlretrieve(query, fn)
|
914
|
if not os.path.exists(filename):
|
915
|
LOG.error("Failed, headers were: %s", headers)
|
916
|
common.show_error(_("RepeaterBook query failed"))
|
917
|
self.window.set_cursor(None)
|
918
|
return
|
919
|
|
920
|
class RBRadio(generic_csv.CSVRadio,
|
921
|
chirp_common.NetworkSourceRadio):
|
922
|
VENDOR = "RepeaterBook"
|
923
|
MODEL = ""
|
924
|
|
925
|
try:
|
926
|
# Validate CSV
|
927
|
radio = RBRadio(filename)
|
928
|
if radio.errors:
|
929
|
reporting.report_misc_error("repeaterbook",
|
930
|
("query=%s\n" % query) +
|
931
|
("\n") +
|
932
|
("\n".join(radio.errors)))
|
933
|
except errors.InvalidDataError, e:
|
934
|
common.show_error(str(e))
|
935
|
self.window.set_cursor(None)
|
936
|
return
|
937
|
except Exception, e:
|
938
|
common.log_exception()
|
939
|
|
940
|
reporting.report_model_usage(radio, "import", True)
|
941
|
|
942
|
self.window.set_cursor(None)
|
943
|
if do_import:
|
944
|
eset = self.get_current_editorset()
|
945
|
count = eset.do_import(filename)
|
946
|
else:
|
947
|
self.do_open_live(radio, read_only=True)
|
948
|
|
949
|
def do_przemienniki_prompt(self):
|
950
|
d = inputdialog.FieldDialog(title='przemienniki.net query',
|
951
|
parent=self)
|
952
|
fields = {
|
953
|
"Country":
|
954
|
(miscwidgets.make_choice(
|
955
|
['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi',
|
956
|
'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se',
|
957
|
'sk', 'ua', 'uk'], False),
|
958
|
lambda x: str(x.get_active_text())),
|
959
|
"Band":
|
960
|
(miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm',
|
961
|
'23cm', '13cm', '3cm'], False, '2m'),
|
962
|
lambda x: str(x.get_active_text())),
|
963
|
"Mode":
|
964
|
(miscwidgets.make_choice(['fm', 'dv'], False),
|
965
|
lambda x: str(x.get_active_text())),
|
966
|
"Only Working":
|
967
|
(miscwidgets.make_choice(['', 'yes'], False),
|
968
|
lambda x: str(x.get_active_text())),
|
969
|
"Latitude": (gtk.Entry(), lambda x: float(x.get_text())),
|
970
|
"Longitude": (gtk.Entry(), lambda x: float(x.get_text())),
|
971
|
"Range": (gtk.Entry(), lambda x: int(x.get_text())),
|
972
|
}
|
973
|
for name in sorted(fields.keys()):
|
974
|
value, fn = fields[name]
|
975
|
d.add_field(name, value)
|
976
|
while d.run() == gtk.RESPONSE_OK:
|
977
|
query = "http://przemienniki.net/export/chirp.csv?"
|
978
|
args = []
|
979
|
for name, (value, fn) in fields.items():
|
980
|
if isinstance(value, gtk.Entry):
|
981
|
contents = value.get_text()
|
982
|
else:
|
983
|
contents = value.get_active_text()
|
984
|
if contents:
|
985
|
try:
|
986
|
_value = fn(value)
|
987
|
except ValueError:
|
988
|
common.show_error(_("Invalid value for %s") % name)
|
989
|
query = None
|
990
|
continue
|
991
|
|
992
|
args.append("=".join((name.replace(" ", "").lower(),
|
993
|
contents)))
|
994
|
query += "&".join(args)
|
995
|
LOG.debug(query)
|
996
|
d.destroy()
|
997
|
return query
|
998
|
|
999
|
d.destroy()
|
1000
|
return query
|
1001
|
|
1002
|
def do_przemienniki(self, do_import):
|
1003
|
url = self.do_przemienniki_prompt()
|
1004
|
if not url:
|
1005
|
return
|
1006
|
|
1007
|
fn = tempfile.mktemp(".csv")
|
1008
|
filename, headers = urllib.urlretrieve(url, fn)
|
1009
|
if not os.path.exists(filename):
|
1010
|
LOG.error("Failed, headers were: %s", str(headers))
|
1011
|
common.show_error(_("Query failed"))
|
1012
|
return
|
1013
|
|
1014
|
class PRRadio(generic_csv.CSVRadio,
|
1015
|
chirp_common.NetworkSourceRadio):
|
1016
|
VENDOR = "przemienniki.net"
|
1017
|
MODEL = ""
|
1018
|
|
1019
|
try:
|
1020
|
radio = PRRadio(filename)
|
1021
|
except Exception, e:
|
1022
|
common.show_error(str(e))
|
1023
|
return
|
1024
|
|
1025
|
if do_import:
|
1026
|
eset = self.get_current_editorset()
|
1027
|
count = eset.do_import(filename)
|
1028
|
else:
|
1029
|
self.do_open_live(radio, read_only=True)
|
1030
|
|
1031
|
def do_rfinder_prompt(self):
|
1032
|
fields = {"1Email": (gtk.Entry(), lambda x: "@" in x),
|
1033
|
"2Password": (gtk.Entry(), lambda x: x),
|
1034
|
"3Latitude": (gtk.Entry(),
|
1035
|
lambda x: float(x) < 90 and float(x) > -90),
|
1036
|
"4Longitude": (gtk.Entry(),
|
1037
|
lambda x: float(x) < 180 and float(x) > -180),
|
1038
|
"5Range_in_Miles": (gtk.Entry(),
|
1039
|
lambda x: int(x) > 0 and int(x) < 5000),
|
1040
|
}
|
1041
|
|
1042
|
d = inputdialog.FieldDialog(title="RFinder Login", parent=self)
|
1043
|
for k in sorted(fields.keys()):
|
1044
|
d.add_field(k[1:].replace("_", " "), fields[k][0])
|
1045
|
fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "")
|
1046
|
fields[k][0].set_visibility(k != "2Password")
|
1047
|
|
1048
|
while d.run() == gtk.RESPONSE_OK:
|
1049
|
valid = True
|
1050
|
for k in sorted(fields.keys()):
|
1051
|
widget, validator = fields[k]
|
1052
|
try:
|
1053
|
if validator(widget.get_text()):
|
1054
|
CONF.set(k[1:], widget.get_text(), "rfinder")
|
1055
|
continue
|
1056
|
except Exception:
|
1057
|
pass
|
1058
|
common.show_error("Invalid value for %s" % k[1:])
|
1059
|
valid = False
|
1060
|
break
|
1061
|
|
1062
|
if valid:
|
1063
|
d.destroy()
|
1064
|
return True
|
1065
|
|
1066
|
d.destroy()
|
1067
|
return False
|
1068
|
|
1069
|
def do_rfinder(self, do_import):
|
1070
|
self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
|
1071
|
if not self.do_rfinder_prompt():
|
1072
|
self.window.set_cursor(None)
|
1073
|
return
|
1074
|
|
1075
|
lat = CONF.get_float("Latitude", "rfinder")
|
1076
|
lon = CONF.get_float("Longitude", "rfinder")
|
1077
|
passwd = CONF.get("Password", "rfinder")
|
1078
|
email = CONF.get("Email", "rfinder")
|
1079
|
miles = CONF.get_int("Range_in_Miles", "rfinder")
|
1080
|
|
1081
|
# Do this in case the import process is going to take a while
|
1082
|
# to make sure we process events leading up to this
|
1083
|
gtk.gdk.window_process_all_updates()
|
1084
|
while gtk.events_pending():
|
1085
|
gtk.main_iteration(False)
|
1086
|
|
1087
|
if do_import:
|
1088
|
eset = self.get_current_editorset()
|
1089
|
rfstr = "rfinder://%s/%s/%f/%f/%i" % \
|
1090
|
(email, passwd, lat, lon, miles)
|
1091
|
count = eset.do_import(rfstr)
|
1092
|
else:
|
1093
|
from chirp.drivers import rfinder
|
1094
|
radio = rfinder.RFinderRadio(None)
|
1095
|
radio.set_params((lat, lon), miles, email, passwd)
|
1096
|
self.do_open_live(radio, read_only=True)
|
1097
|
|
1098
|
self.window.set_cursor(None)
|
1099
|
|
1100
|
def do_radioreference_prompt(self):
|
1101
|
fields = {"1Username": (gtk.Entry(), lambda x: x),
|
1102
|
"2Password": (gtk.Entry(), lambda x: x),
|
1103
|
"3Zipcode": (gtk.Entry(), lambda x: x),
|
1104
|
}
|
1105
|
|
1106
|
d = inputdialog.FieldDialog(title=_("RadioReference.com Query"),
|
1107
|
parent=self)
|
1108
|
for k in sorted(fields.keys()):
|
1109
|
d.add_field(k[1:], fields[k][0])
|
1110
|
fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "")
|
1111
|
fields[k][0].set_visibility(k != "2Password")
|
1112
|
|
1113
|
while d.run() == gtk.RESPONSE_OK:
|
1114
|
valid = True
|
1115
|
for k in sorted(fields.keys()):
|
1116
|
widget, validator = fields[k]
|
1117
|
try:
|
1118
|
if validator(widget.get_text()):
|
1119
|
CONF.set(k[1:], widget.get_text(), "radioreference")
|
1120
|
continue
|
1121
|
except Exception:
|
1122
|
pass
|
1123
|
common.show_error("Invalid value for %s" % k[1:])
|
1124
|
valid = False
|
1125
|
break
|
1126
|
|
1127
|
if valid:
|
1128
|
d.destroy()
|
1129
|
return True
|
1130
|
|
1131
|
d.destroy()
|
1132
|
return False
|
1133
|
|
1134
|
def do_radioreference(self, do_import):
|
1135
|
self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
|
1136
|
if not self.do_radioreference_prompt():
|
1137
|
self.window.set_cursor(None)
|
1138
|
return
|
1139
|
|
1140
|
username = CONF.get("Username", "radioreference")
|
1141
|
passwd = CONF.get("Password", "radioreference")
|
1142
|
zipcode = CONF.get("Zipcode", "radioreference")
|
1143
|
|
1144
|
# Do this in case the import process is going to take a while
|
1145
|
# to make sure we process events leading up to this
|
1146
|
gtk.gdk.window_process_all_updates()
|
1147
|
while gtk.events_pending():
|
1148
|
gtk.main_iteration(False)
|
1149
|
|
1150
|
if do_import:
|
1151
|
eset = self.get_current_editorset()
|
1152
|
rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd)
|
1153
|
count = eset.do_import(rrstr)
|
1154
|
else:
|
1155
|
try:
|
1156
|
from chirp import radioreference
|
1157
|
radio = radioreference.RadioReferenceRadio(None)
|
1158
|
radio.set_params(zipcode, username, passwd)
|
1159
|
self.do_open_live(radio, read_only=True)
|
1160
|
except errors.RadioError, e:
|
1161
|
common.show_error(e)
|
1162
|
|
1163
|
self.window.set_cursor(None)
|
1164
|
|
1165
|
def do_export(self):
|
1166
|
types = [(_("CSV Files") + " (*.csv)", "csv"),
|
1167
|
(_("CHIRP Files") + " (*.chirp)", "chirp"),
|
1168
|
]
|
1169
|
|
1170
|
eset = self.get_current_editorset()
|
1171
|
|
1172
|
if os.path.exists(eset.filename):
|
1173
|
base = os.path.basename(eset.filename)
|
1174
|
if "." in base:
|
1175
|
base = base[:base.rindex(".")]
|
1176
|
defname = base
|
1177
|
else:
|
1178
|
defname = "radio"
|
1179
|
|
1180
|
filen = platform.get_platform().gui_save_file(default_name=defname,
|
1181
|
types=types)
|
1182
|
if not filen:
|
1183
|
return
|
1184
|
|
1185
|
if os.path.exists(filen):
|
1186
|
dlg = inputdialog.OverwriteDialog(filen)
|
1187
|
owrite = dlg.run()
|
1188
|
dlg.destroy()
|
1189
|
if owrite != gtk.RESPONSE_OK:
|
1190
|
return
|
1191
|
os.remove(filen)
|
1192
|
|
1193
|
count = eset.do_export(filen)
|
1194
|
reporting.report_model_usage(eset.rthread.radio, "export", count > 0)
|
1195
|
|
1196
|
def do_about(self):
|
1197
|
d = gtk.AboutDialog()
|
1198
|
d.set_transient_for(self)
|
1199
|
import sys
|
1200
|
verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % (
|
1201
|
".".join([str(x) for x in gtk.gtk_version]),
|
1202
|
".".join([str(x) for x in gtk.pygtk_version]),
|
1203
|
sys.version.split()[0])
|
1204
|
|
1205
|
# Set url hook to handle user activating a URL link in the about dialog
|
1206
|
gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url))
|
1207
|
|
1208
|
d.set_name("CHIRP")
|
1209
|
d.set_version(CHIRP_VERSION)
|
1210
|
d.set_copyright("Copyright 2015 Dan Smith (KK7DS)")
|
1211
|
d.set_website("http://chirp.danplanet.com")
|
1212
|
d.set_authors(("Dan Smith KK7DS <dsmith@danplanet.com>",
|
1213
|
_("With significant contributions from:"),
|
1214
|
"Tom KD7LXL",
|
1215
|
"Marco IZ3GME",
|
1216
|
"Jim KC9HI"
|
1217
|
))
|
1218
|
d.set_translator_credits("Polish: Grzegorz SQ2RBY" +
|
1219
|
os.linesep +
|
1220
|
"Italian: Fabio IZ2QDH" +
|
1221
|
os.linesep +
|
1222
|
"Dutch: Michael PD4MT" +
|
1223
|
os.linesep +
|
1224
|
"German: Benjamin HB9EUK" +
|
1225
|
os.linesep +
|
1226
|
"Hungarian: Attila HA5JA" +
|
1227
|
os.linesep +
|
1228
|
"Russian: Dmitry Slukin" +
|
1229
|
os.linesep +
|
1230
|
"Portuguese (BR): Crezivando PP7CJ")
|
1231
|
d.set_comments(verinfo)
|
1232
|
|
1233
|
d.run()
|
1234
|
d.destroy()
|
1235
|
|
1236
|
def do_gethelp(self):
|
1237
|
webbrowser.open("http://chirp.danplanet.com")
|
1238
|
|
1239
|
def do_columns(self):
|
1240
|
eset = self.get_current_editorset()
|
1241
|
driver = directory.get_driver(eset.rthread.radio.__class__)
|
1242
|
radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR,
|
1243
|
eset.rthread.radio.MODEL,
|
1244
|
eset.rthread.radio.VARIANT)
|
1245
|
d = gtk.Dialog(title=_("Select Columns"),
|
1246
|
parent=self,
|
1247
|
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
|
1248
|
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
|
1249
|
|
1250
|
vbox = gtk.VBox()
|
1251
|
vbox.show()
|
1252
|
sw = gtk.ScrolledWindow()
|
1253
|
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
1254
|
sw.add_with_viewport(vbox)
|
1255
|
sw.show()
|
1256
|
d.vbox.pack_start(sw, 1, 1, 1)
|
1257
|
d.set_size_request(-1, 300)
|
1258
|
d.set_resizable(False)
|
1259
|
|
1260
|
labelstr = _("Visible columns for {radio}").format(radio=radio_name)
|
1261
|
label = gtk.Label(labelstr)
|
1262
|
label.show()
|
1263
|
vbox.pack_start(label)
|
1264
|
|
1265
|
fields = []
|
1266
|
memedit = eset.get_current_editor() # .editors["memedit"]
|
1267
|
unsupported = memedit.get_unsupported_columns()
|
1268
|
for colspec in memedit.cols:
|
1269
|
if colspec[0].startswith("_"):
|
1270
|
continue
|
1271
|
elif colspec[0] in unsupported:
|
1272
|
continue
|
1273
|
label = colspec[0]
|
1274
|
visible = memedit.get_column_visible(memedit.col(label))
|
1275
|
widget = gtk.CheckButton(label)
|
1276
|
widget.set_active(visible)
|
1277
|
fields.append(widget)
|
1278
|
vbox.pack_start(widget, 1, 1, 1)
|
1279
|
widget.show()
|
1280
|
|
1281
|
res = d.run()
|
1282
|
selected_columns = []
|
1283
|
if res == gtk.RESPONSE_OK:
|
1284
|
for widget in fields:
|
1285
|
colnum = memedit.col(widget.get_label())
|
1286
|
memedit.set_column_visible(colnum, widget.get_active())
|
1287
|
if widget.get_active():
|
1288
|
selected_columns.append(widget.get_label())
|
1289
|
|
1290
|
d.destroy()
|
1291
|
|
1292
|
CONF.set(driver, ",".join(selected_columns), "memedit_columns")
|
1293
|
|
1294
|
def do_hide_unused(self, action):
|
1295
|
eset = self.get_current_editorset()
|
1296
|
if eset is None:
|
1297
|
conf = config.get("memedit")
|
1298
|
conf.set_bool("hide_unused", action.get_active())
|
1299
|
else:
|
1300
|
for editortype, editor in eset.editors.iteritems():
|
1301
|
if "memedit" in editortype:
|
1302
|
editor.set_hide_unused(action.get_active())
|
1303
|
|
1304
|
def do_clearq(self):
|
1305
|
eset = self.get_current_editorset()
|
1306
|
eset.rthread.flush()
|
1307
|
|
1308
|
def do_copy(self, cut):
|
1309
|
eset = self.get_current_editorset()
|
1310
|
eset.get_current_editor().copy_selection(cut)
|
1311
|
|
1312
|
def do_paste(self):
|
1313
|
eset = self.get_current_editorset()
|
1314
|
eset.get_current_editor().paste_selection()
|
1315
|
|
1316
|
def do_delete(self):
|
1317
|
eset = self.get_current_editorset()
|
1318
|
eset.get_current_editor().copy_selection(True)
|
1319
|
|
1320
|
def do_toggle_report(self, action):
|
1321
|
if not action.get_active():
|
1322
|
d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self)
|
1323
|
markup = "<b><big>" + _("Reporting is disabled") + "</big></b>"
|
1324
|
d.set_markup(markup)
|
1325
|
msg = _("The reporting feature of CHIRP is designed to help "
|
1326
|
"<u>improve quality</u> by allowing the authors to focus "
|
1327
|
"on the radio drivers used most often and errors "
|
1328
|
"experienced by the users. The reports contain no "
|
1329
|
"identifying information and are used only for "
|
1330
|
"statistical purposes by the authors. Your privacy is "
|
1331
|
"extremely important, but <u>please consider leaving "
|
1332
|
"this feature enabled to help make CHIRP better!</u>\n\n"
|
1333
|
"<b>Are you sure you want to disable this feature?</b>")
|
1334
|
d.format_secondary_markup(msg.replace("\n", "\r\n"))
|
1335
|
r = d.run()
|
1336
|
d.destroy()
|
1337
|
if r == gtk.RESPONSE_NO:
|
1338
|
action.set_active(not action.get_active())
|
1339
|
|
1340
|
conf = config.get()
|
1341
|
conf.set_bool("no_report", not action.get_active())
|
1342
|
|
1343
|
def do_toggle_no_smart_tmode(self, action):
|
1344
|
CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit")
|
1345
|
|
1346
|
def do_toggle_developer(self, action):
|
1347
|
conf = config.get()
|
1348
|
conf.set_bool("developer", action.get_active(), "state")
|
1349
|
|
1350
|
for name in ["viewdeveloper", "loadmod"]:
|
1351
|
devaction = self.menu_ag.get_action(name)
|
1352
|
devaction.set_visible(action.get_active())
|
1353
|
|
1354
|
def do_toggle_clone_instructions(self, action):
|
1355
|
CONF.set_bool("clone_instructions", not action.get_active(), "noconfirm")
|
1356
|
|
1357
|
def do_change_language(self):
|
1358
|
langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German",
|
1359
|
"Hungarian", "Russian", "Portuguese (BR)", "French", "Spanish"]
|
1360
|
d = inputdialog.ChoiceDialog(langs, parent=self,
|
1361
|
title="Choose Language")
|
1362
|
d.label.set_text(_("Choose a language or Auto to use the "
|
1363
|
"operating system default. You will need to "
|
1364
|
"restart the application before the change "
|
1365
|
"will take effect"))
|
1366
|
d.label.set_line_wrap(True)
|
1367
|
r = d.run()
|
1368
|
if r == gtk.RESPONSE_OK:
|
1369
|
LOG.debug("Chose language %s" % d.choice.get_active_text())
|
1370
|
conf = config.get()
|
1371
|
conf.set("language", d.choice.get_active_text(), "state")
|
1372
|
d.destroy()
|
1373
|
|
1374
|
def load_module(self):
|
1375
|
types = [(_("Python Modules") + "*.py", "*.py")]
|
1376
|
filen = platform.get_platform().gui_open_file(types=types)
|
1377
|
if not filen:
|
1378
|
return
|
1379
|
|
1380
|
# We're in development mode, so we need to tell the directory to
|
1381
|
# allow a loaded module to override an existing driver, against
|
1382
|
# its normal better judgement
|
1383
|
directory.enable_reregistrations()
|
1384
|
|
1385
|
try:
|
1386
|
module = file(filen)
|
1387
|
code = module.read()
|
1388
|
module.close()
|
1389
|
pyc = compile(code, filen, 'exec')
|
1390
|
# See this for why:
|
1391
|
# http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec
|
1392
|
exec(pyc, globals(), globals())
|
1393
|
except Exception, e:
|
1394
|
common.log_exception()
|
1395
|
common.show_error("Unable to load module: %s" % e)
|
1396
|
|
1397
|
def mh(self, _action, *args):
|
1398
|
action = _action.get_name()
|
1399
|
|
1400
|
if action == "quit":
|
1401
|
gtk.main_quit()
|
1402
|
elif action == "new":
|
1403
|
self.do_new()
|
1404
|
elif action == "open":
|
1405
|
self.do_open()
|
1406
|
elif action == "save":
|
1407
|
self.do_save()
|
1408
|
elif action == "saveas":
|
1409
|
self.do_saveas()
|
1410
|
elif action.startswith("download"):
|
1411
|
self.do_download(*args)
|
1412
|
elif action.startswith("upload"):
|
1413
|
self.do_upload(*args)
|
1414
|
elif action == "close":
|
1415
|
self.do_close()
|
1416
|
elif action == "import":
|
1417
|
self.do_import()
|
1418
|
elif action in ["qrfinder", "irfinder"]:
|
1419
|
self.do_rfinder(action[0] == "i")
|
1420
|
elif action in ["qradioreference", "iradioreference"]:
|
1421
|
self.do_radioreference(action[0] == "i")
|
1422
|
elif action == "export":
|
1423
|
self.do_export()
|
1424
|
elif action in ["qrbook", "irbook"]:
|
1425
|
self.do_repeaterbook(action[0] == "i")
|
1426
|
elif action in ["qpr", "ipr"]:
|
1427
|
self.do_przemienniki(action[0] == "i")
|
1428
|
elif action == "about":
|
1429
|
self.do_about()
|
1430
|
elif action == "gethelp":
|
1431
|
self.do_gethelp()
|
1432
|
elif action == "columns":
|
1433
|
self.do_columns()
|
1434
|
elif action == "hide_unused":
|
1435
|
self.do_hide_unused(_action)
|
1436
|
elif action == "cancelq":
|
1437
|
self.do_clearq()
|
1438
|
elif action == "report":
|
1439
|
self.do_toggle_report(_action)
|
1440
|
elif action == "channel_defaults":
|
1441
|
# The memedit thread also has an instance of bandplans.
|
1442
|
bp = bandplans.BandPlans(CONF)
|
1443
|
bp.select_bandplan(self)
|
1444
|
elif action == "no_smart_tmode":
|
1445
|
self.do_toggle_no_smart_tmode(_action)
|
1446
|
elif action == "developer":
|
1447
|
self.do_toggle_developer(_action)
|
1448
|
elif action == "clone_instructions":
|
1449
|
self.do_toggle_clone_instructions(_action)
|
1450
|
elif action in ["cut", "copy", "paste", "delete",
|
1451
|
"move_up", "move_dn", "exchange", "all",
|
1452
|
"devshowraw", "devdiffraw", "properties"]:
|
1453
|
self.get_current_editorset().get_current_editor().hotkey(_action)
|
1454
|
elif action == "devdifftab":
|
1455
|
self.do_diff_radio()
|
1456
|
elif action == "language":
|
1457
|
self.do_change_language()
|
1458
|
elif action == "loadmod":
|
1459
|
self.load_module()
|
1460
|
else:
|
1461
|
return
|
1462
|
|
1463
|
self.ev_tab_switched()
|
1464
|
|
1465
|
def make_menubar(self):
|
1466
|
menu_xml = """
|
1467
|
<ui>
|
1468
|
<menubar name="MenuBar">
|
1469
|
<menu action="file">
|
1470
|
<menuitem action="new"/>
|
1471
|
<menuitem action="open"/>
|
1472
|
<menu action="openstock" name="openstock"/>
|
1473
|
<menu action="recent" name="recent"/>
|
1474
|
<menuitem action="save"/>
|
1475
|
<menuitem action="saveas"/>
|
1476
|
<menuitem action="loadmod"/>
|
1477
|
<separator/>
|
1478
|
<menuitem action="import"/>
|
1479
|
<menuitem action="export"/>
|
1480
|
<separator/>
|
1481
|
<menuitem action="close"/>
|
1482
|
<menuitem action="quit"/>
|
1483
|
</menu>
|
1484
|
<menu action="edit">
|
1485
|
<menuitem action="cut"/>
|
1486
|
<menuitem action="copy"/>
|
1487
|
<menuitem action="paste"/>
|
1488
|
<menuitem action="delete"/>
|
1489
|
<separator/>
|
1490
|
<menuitem action="all"/>
|
1491
|
<separator/>
|
1492
|
<menuitem action="move_up"/>
|
1493
|
<menuitem action="move_dn"/>
|
1494
|
<menuitem action="exchange"/>
|
1495
|
<separator/>
|
1496
|
<menuitem action="properties"/>
|
1497
|
</menu>
|
1498
|
<menu action="view">
|
1499
|
<menuitem action="columns"/>
|
1500
|
<menuitem action="hide_unused"/>
|
1501
|
<menuitem action="no_smart_tmode"/>
|
1502
|
<menu action="viewdeveloper">
|
1503
|
<menuitem action="devshowraw"/>
|
1504
|
<menuitem action="devdiffraw"/>
|
1505
|
<menuitem action="devdifftab"/>
|
1506
|
</menu>
|
1507
|
<menuitem action="language"/>
|
1508
|
</menu>
|
1509
|
<menu action="radio" name="radio">
|
1510
|
<menuitem action="download"/>
|
1511
|
<menuitem action="upload"/>
|
1512
|
<menu action="importsrc" name="importsrc">
|
1513
|
<menuitem action="iradioreference"/>
|
1514
|
<menuitem action="irbook"/>
|
1515
|
<menuitem action="ipr"/>
|
1516
|
<menuitem action="irfinder"/>
|
1517
|
</menu>
|
1518
|
<menu action="querysrc" name="querysrc">
|
1519
|
<menuitem action="qradioreference"/>
|
1520
|
<menuitem action="qrbook"/>
|
1521
|
<menuitem action="qpr"/>
|
1522
|
<menuitem action="qrfinder"/>
|
1523
|
</menu>
|
1524
|
<menu action="stock" name="stock"/>
|
1525
|
<separator/>
|
1526
|
<menuitem action="channel_defaults"/>
|
1527
|
<separator/>
|
1528
|
<menuitem action="cancelq"/>
|
1529
|
</menu>
|
1530
|
<menu action="help">
|
1531
|
<menuitem action="gethelp"/>
|
1532
|
<separator/>
|
1533
|
<menuitem action="report"/>
|
1534
|
<menuitem action="clone_instructions"/>
|
1535
|
<menuitem action="developer"/>
|
1536
|
<separator/>
|
1537
|
<menuitem action="about"/>
|
1538
|
</menu>
|
1539
|
</menubar>
|
1540
|
</ui>
|
1541
|
"""
|
1542
|
ALT_KEY = "<Alt>"
|
1543
|
CTRL_KEY = "<Ctrl>"
|
1544
|
if sys.platform == 'darwin':
|
1545
|
ALT_KEY = "<Meta>"
|
1546
|
CTRL_KEY = "<Meta>"
|
1547
|
actions = [
|
1548
|
('file', None, _("_File"), None, None, self.mh),
|
1549
|
('new', gtk.STOCK_NEW, None, None, None, self.mh),
|
1550
|
('open', gtk.STOCK_OPEN, None, None, None, self.mh),
|
1551
|
('openstock', None, _("Open stock config"), None, None, self.mh),
|
1552
|
('recent', None, _("_Recent"), None, None, self.mh),
|
1553
|
('save', gtk.STOCK_SAVE, None, None, None, self.mh),
|
1554
|
('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh),
|
1555
|
('loadmod', None, _("Load Module"), None, None, self.mh),
|
1556
|
('close', gtk.STOCK_CLOSE, None, None, None, self.mh),
|
1557
|
('quit', gtk.STOCK_QUIT, None, None, None, self.mh),
|
1558
|
('edit', None, _("_Edit"), None, None, self.mh),
|
1559
|
('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh),
|
1560
|
('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh),
|
1561
|
('paste', None, _("_Paste"),
|
1562
|
"%sv" % CTRL_KEY, None, self.mh),
|
1563
|
('delete', None, _("_Delete"), "Delete", None, self.mh),
|
1564
|
('all', None, _("Select _All"), None, None, self.mh),
|
1565
|
('move_up', None, _("Move _Up"),
|
1566
|
"%sUp" % CTRL_KEY, None, self.mh),
|
1567
|
('move_dn', None, _("Move Dow_n"),
|
1568
|
"%sDown" % CTRL_KEY, None, self.mh),
|
1569
|
('exchange', None, _("E_xchange"),
|
1570
|
"%s<Shift>x" % CTRL_KEY, None, self.mh),
|
1571
|
('properties', None, _("P_roperties"), None, None, self.mh),
|
1572
|
('view', None, _("_View"), None, None, self.mh),
|
1573
|
('columns', None, _("Columns"), None, None, self.mh),
|
1574
|
('viewdeveloper', None, _("Developer"), None, None, self.mh),
|
1575
|
('devshowraw', None, _('Show raw memory'),
|
1576
|
"%s<Shift>r" % CTRL_KEY, None, self.mh),
|
1577
|
('devdiffraw', None, _("Diff raw memories"),
|
1578
|
"%s<Shift>d" % CTRL_KEY, None, self.mh),
|
1579
|
('devdifftab', None, _("Diff tabs"),
|
1580
|
"%s<Shift>t" % CTRL_KEY, None, self.mh),
|
1581
|
('language', None, _("Change language"), None, None, self.mh),
|
1582
|
('radio', None, _("_Radio"), None, None, self.mh),
|
1583
|
('download', None, _("Download From Radio"),
|
1584
|
"%sd" % ALT_KEY, None, self.mh),
|
1585
|
('upload', None, _("Upload To Radio"),
|
1586
|
"%su" % ALT_KEY, None, self.mh),
|
1587
|
('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh),
|
1588
|
('export', None, _("Export"), "%sx" % ALT_KEY, None, self.mh),
|
1589
|
('importsrc', None, _("Import from data source"),
|
1590
|
None, None, self.mh),
|
1591
|
('iradioreference', None, _("RadioReference.com"),
|
1592
|
None, None, self.mh),
|
1593
|
('irfinder', None, _("RFinder"), None, None, self.mh),
|
1594
|
('irbook', None, _("RepeaterBook"), None, None, self.mh),
|
1595
|
('ipr', None, _("przemienniki.net"), None, None, self.mh),
|
1596
|
('querysrc', None, _("Query data source"), None, None, self.mh),
|
1597
|
('qradioreference', None, _("RadioReference.com"),
|
1598
|
None, None, self.mh),
|
1599
|
('qrfinder', None, _("RFinder"), None, None, self.mh),
|
1600
|
('qpr', None, _("przemienniki.net"), None, None, self.mh),
|
1601
|
('qrbook', None, _("RepeaterBook"), None, None, self.mh),
|
1602
|
('export_chirp', None, _("CHIRP Native File"),
|
1603
|
None, None, self.mh),
|
1604
|
('export_csv', None, _("CSV File"), None, None, self.mh),
|
1605
|
('stock', None, _("Import from stock config"),
|
1606
|
None, None, self.mh),
|
1607
|
('channel_defaults', None, _("Channel defaults"),
|
1608
|
None, None, self.mh),
|
1609
|
('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh),
|
1610
|
('help', None, _('Help'), None, None, self.mh),
|
1611
|
('about', gtk.STOCK_ABOUT, None, None, None, self.mh),
|
1612
|
('gethelp', None, _("Get Help Online..."), None, None, self.mh),
|
1613
|
]
|
1614
|
|
1615
|
conf = config.get()
|
1616
|
re = not conf.get_bool("no_report")
|
1617
|
hu = conf.get_bool("hide_unused", "memedit", default=True)
|
1618
|
dv = conf.get_bool("developer", "state")
|
1619
|
ci = not conf.get_bool("clone_instructions", "noconfirm")
|
1620
|
st = not conf.get_bool("no_smart_tmode", "memedit")
|
1621
|
|
1622
|
toggles = [('report', None, _("Report Statistics"),
|
1623
|
None, None, self.mh, re),
|
1624
|
('hide_unused', None, _("Hide Unused Fields"),
|
1625
|
None, None, self.mh, hu),
|
1626
|
('no_smart_tmode', None, _("Smart Tone Modes"),
|
1627
|
None, None, self.mh, st),
|
1628
|
('clone_instructions', None, _("Show Instructions"),
|
1629
|
None, None, self.mh, ci),
|
1630
|
('developer', None, _("Enable Developer Functions"),
|
1631
|
None, None, self.mh, dv),
|
1632
|
]
|
1633
|
|
1634
|
self.menu_uim = gtk.UIManager()
|
1635
|
self.menu_ag = gtk.ActionGroup("MenuBar")
|
1636
|
self.menu_ag.add_actions(actions)
|
1637
|
self.menu_ag.add_toggle_actions(toggles)
|
1638
|
|
1639
|
self.menu_uim.insert_action_group(self.menu_ag, 0)
|
1640
|
self.menu_uim.add_ui_from_string(menu_xml)
|
1641
|
|
1642
|
self.add_accel_group(self.menu_uim.get_accel_group())
|
1643
|
|
1644
|
self.clonemenu = self.menu_uim.get_widget("/MenuBar/help/clone_instructions")
|
1645
|
|
1646
|
# Initialize
|
1647
|
self.do_toggle_developer(self.menu_ag.get_action("developer"))
|
1648
|
|
1649
|
return self.menu_uim.get_widget("/MenuBar")
|
1650
|
|
1651
|
def make_tabs(self):
|
1652
|
self.tabs = gtk.Notebook()
|
1653
|
self.tabs.set_scrollable(True)
|
1654
|
|
1655
|
return self.tabs
|
1656
|
|
1657
|
def close_out(self):
|
1658
|
num = self.tabs.get_n_pages()
|
1659
|
while num > 0:
|
1660
|
num -= 1
|
1661
|
LOG.debug("Closing %i" % num)
|
1662
|
try:
|
1663
|
self.do_close(self.tabs.get_nth_page(num))
|
1664
|
except ModifiedError:
|
1665
|
return False
|
1666
|
|
1667
|
gtk.main_quit()
|
1668
|
|
1669
|
return True
|
1670
|
|
1671
|
def make_status_bar(self):
|
1672
|
box = gtk.HBox(False, 2)
|
1673
|
|
1674
|
self.sb_general = gtk.Statusbar()
|
1675
|
self.sb_general.set_has_resize_grip(False)
|
1676
|
self.sb_general.show()
|
1677
|
box.pack_start(self.sb_general, 1, 1, 1)
|
1678
|
|
1679
|
self.sb_radio = gtk.Statusbar()
|
1680
|
self.sb_radio.set_has_resize_grip(True)
|
1681
|
self.sb_radio.show()
|
1682
|
box.pack_start(self.sb_radio, 1, 1, 1)
|
1683
|
|
1684
|
box.show()
|
1685
|
return box
|
1686
|
|
1687
|
def ev_delete(self, window, event):
|
1688
|
if not self.close_out():
|
1689
|
return True # Don't exit
|
1690
|
|
1691
|
def ev_destroy(self, window):
|
1692
|
if not self.close_out():
|
1693
|
return True # Don't exit
|
1694
|
|
1695
|
def setup_extra_hotkeys(self):
|
1696
|
accelg = self.menu_uim.get_accel_group()
|
1697
|
|
1698
|
def memedit(a):
|
1699
|
self.get_current_editorset().editors["memedit"].hotkey(a)
|
1700
|
|
1701
|
actions = [
|
1702
|
# ("action_name", "key", function)
|
1703
|
]
|
1704
|
|
1705
|
for name, key, fn in actions:
|
1706
|
a = gtk.Action(name, name, name, "")
|
1707
|
a.connect("activate", fn)
|
1708
|
self.menu_ag.add_action_with_accel(a, key)
|
1709
|
a.set_accel_group(accelg)
|
1710
|
a.connect_accelerator()
|
1711
|
|
1712
|
def _set_icon(self):
|
1713
|
execpath = platform.get_platform().executable_path()
|
1714
|
path = os.path.abspath(os.path.join(execpath, "share", "chirp.png"))
|
1715
|
if not os.path.exists(path):
|
1716
|
path = "/usr/share/pixmaps/chirp.png"
|
1717
|
|
1718
|
if os.path.exists(path):
|
1719
|
self.set_icon_from_file(path)
|
1720
|
else:
|
1721
|
LOG.warn("Icon %s not found" % path)
|
1722
|
|
1723
|
def _updates(self, version):
|
1724
|
if not version:
|
1725
|
return
|
1726
|
|
1727
|
if version == CHIRP_VERSION:
|
1728
|
return
|
1729
|
|
1730
|
LOG.info("Server reports version %s is available" % version)
|
1731
|
|
1732
|
# Report new updates every seven days
|
1733
|
intv = 3600 * 24 * 7
|
1734
|
|
1735
|
if CONF.is_defined("last_update_check", "state") and \
|
1736
|
(time.time() - CONF.get_int("last_update_check", "state")) < intv:
|
1737
|
return
|
1738
|
|
1739
|
CONF.set_int("last_update_check", int(time.time()), "state")
|
1740
|
d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self,
|
1741
|
type=gtk.MESSAGE_INFO)
|
1742
|
d.set_property("text",
|
1743
|
_("A new version of CHIRP is available: " +
|
1744
|
"{ver}. ".format(ver=version) +
|
1745
|
"It is recommended that you upgrade, so " +
|
1746
|
"go to http://chirp.danplanet.com soon!"))
|
1747
|
d.run()
|
1748
|
d.destroy()
|
1749
|
|
1750
|
def _init_macos(self, menu_bar):
|
1751
|
try:
|
1752
|
import gtk_osxapplication
|
1753
|
macapp = gtk_osxapplication.OSXApplication()
|
1754
|
except ImportError, e:
|
1755
|
LOG.error("No MacOS support: %s" % e)
|
1756
|
return
|
1757
|
|
1758
|
menu_bar.hide()
|
1759
|
macapp.set_menu_bar(menu_bar)
|
1760
|
|
1761
|
quititem = self.menu_uim.get_widget("/MenuBar/file/quit")
|
1762
|
quititem.hide()
|
1763
|
|
1764
|
aboutitem = self.menu_uim.get_widget("/MenuBar/help/about")
|
1765
|
macapp.insert_app_menu_item(aboutitem, 0)
|
1766
|
|
1767
|
documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp")
|
1768
|
macapp.insert_app_menu_item(documentationitem, 0)
|
1769
|
|
1770
|
macapp.set_use_quartz_accelerators(False)
|
1771
|
macapp.ready()
|
1772
|
|
1773
|
LOG.debug("Initialized MacOS support")
|
1774
|
|
1775
|
def __init__(self, *args, **kwargs):
|
1776
|
gtk.Window.__init__(self, *args, **kwargs)
|
1777
|
|
1778
|
def expose(window, event):
|
1779
|
allocation = window.get_allocation()
|
1780
|
CONF.set_int("window_w", allocation.width, "state")
|
1781
|
CONF.set_int("window_h", allocation.height, "state")
|
1782
|
self.connect("expose_event", expose)
|
1783
|
|
1784
|
def state_change(window, event):
|
1785
|
CONF.set_bool(
|
1786
|
"window_maximized",
|
1787
|
event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED,
|
1788
|
"state")
|
1789
|
self.connect("window-state-event", state_change)
|
1790
|
|
1791
|
d = CONF.get("last_dir", "state")
|
1792
|
if d and os.path.isdir(d):
|
1793
|
platform.get_platform().set_last_dir(d)
|
1794
|
|
1795
|
vbox = gtk.VBox(False, 2)
|
1796
|
|
1797
|
self._recent = []
|
1798
|
|
1799
|
self.menu_ag = None
|
1800
|
mbar = self.make_menubar()
|
1801
|
|
1802
|
if os.name != "nt":
|
1803
|
self._set_icon() # Windows gets the icon from the exe
|
1804
|
if os.uname()[0] == "Darwin":
|
1805
|
self._init_macos(mbar)
|
1806
|
|
1807
|
vbox.pack_start(mbar, 0, 0, 0)
|
1808
|
|
1809
|
self.tabs = None
|
1810
|
tabs = self.make_tabs()
|
1811
|
tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p))
|
1812
|
tabs.connect("page-removed", lambda *a: self.ev_tab_switched())
|
1813
|
tabs.show()
|
1814
|
self.ev_tab_switched()
|
1815
|
vbox.pack_start(tabs, 1, 1, 1)
|
1816
|
|
1817
|
vbox.pack_start(self.make_status_bar(), 0, 0, 0)
|
1818
|
|
1819
|
vbox.show()
|
1820
|
|
1821
|
self.add(vbox)
|
1822
|
|
1823
|
try:
|
1824
|
width = CONF.get_int("window_w", "state")
|
1825
|
height = CONF.get_int("window_h", "state")
|
1826
|
except Exception:
|
1827
|
width = 800
|
1828
|
height = 600
|
1829
|
|
1830
|
self.set_default_size(width, height)
|
1831
|
if CONF.get_bool("window_maximized", "state"):
|
1832
|
self.maximize()
|
1833
|
self.set_title("CHIRP")
|
1834
|
|
1835
|
self.connect("delete_event", self.ev_delete)
|
1836
|
self.connect("destroy", self.ev_destroy)
|
1837
|
|
1838
|
if not CONF.get_bool("warned_about_reporting") and \
|
1839
|
not CONF.get_bool("no_report"):
|
1840
|
d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self)
|
1841
|
d.set_markup("<b><big>" +
|
1842
|
_("Error reporting is enabled") +
|
1843
|
"</big></b>")
|
1844
|
d.format_secondary_markup(
|
1845
|
_("If you wish to disable this feature you may do so in "
|
1846
|
"the <u>Help</u> menu"))
|
1847
|
d.run()
|
1848
|
d.destroy()
|
1849
|
CONF.set_bool("warned_about_reporting", True)
|
1850
|
|
1851
|
self.update_recent_files()
|
1852
|
self.update_stock_configs()
|
1853
|
self.setup_extra_hotkeys()
|
1854
|
|
1855
|
def updates_callback(ver):
|
1856
|
gobject.idle_add(self._updates, ver)
|
1857
|
|
1858
|
if not CONF.get_bool("skip_update_check", "state"):
|
1859
|
reporting.check_for_updates(updates_callback)
|