mainapp.py

Filippi Marco, 12/29/2014 02:31 am

Download (67 kB)

 
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
from glob import glob
21
import shutil
22
import time
23

    
24
import gtk
25
import gobject
26
gobject.threads_init()
27

    
28
if __name__ == "__main__":
29
    import sys
30
    sys.path.insert(0, "..")
31

    
32
from chirpui import inputdialog, common
33
try:
34
    import serial
35
except ImportError,e:
36
    common.log_exception()
37
    common.show_error("\nThe Pyserial module is not installed!")
38
from chirp import platform, generic_xml, generic_csv, directory, util
39
from chirp import ic9x, kenwood_live, idrp, vx7, vx5, vx6
40
from chirp import CHIRP_VERSION, chirp_common, detect, errors
41
from chirp import icf, ic9x_icf
42
from chirpui import editorset, clone, miscwidgets, config, reporting, fips
43
from chirpui import bandplans
44

    
45
CONF = config.get()
46

    
47
KEEP_RECENT = 8
48

    
49
RB_BANDS = {
50
    "--All--"                 : 0,
51
    "10 meters (29MHz)"       : 29,
52
    "6 meters (54MHz)"        : 5,
53
    "2 meters (144MHz)"       : 14,
54
    "1.25 meters (220MHz)"    : 22,
55
    "70 centimeters (440MHz)" : 4,
56
    "33 centimeters (900MHz)" : 9,
57
    "23 centimeters (1.2GHz)" : 12,
58
}
59

    
60
def key_bands(band):
61
    if band.startswith("-"):
62
        return -1
63

    
64
    amount, units, mhz = band.split(" ")
65
    scale = units == "meters" and 100 or 1
66

    
67
    return 100000 - (float(amount) * scale)
68

    
69
class ModifiedError(Exception):
70
    pass
71

    
72
class ChirpMain(gtk.Window):
73
    def get_current_editorset(self):
74
        page = self.tabs.get_current_page()
75
        if page is not None:
76
            return self.tabs.get_nth_page(page)
77
        else:
78
            return None
79

    
80
    def ev_tab_switched(self, pagenum=None):
81
        def set_action_sensitive(action, sensitive):
82
            self.menu_ag.get_action(action).set_sensitive(sensitive)
83

    
84
        if pagenum is not None:
85
            eset = self.tabs.get_nth_page(pagenum)
86
        else:
87
            eset = self.get_current_editorset()
88

    
89
        upload_sens = bool(eset and
90
                           isinstance(eset.radio, chirp_common.CloneModeRadio))
91

    
92
        if not eset or isinstance(eset.radio, chirp_common.LiveRadio):
93
            save_sens = False
94
        elif isinstance(eset.radio, chirp_common.NetworkSourceRadio):
95
            save_sens = False
96
        else:
97
            save_sens = True
98

    
99
        for i in ["import", "importsrc", "stock"]:
100
            set_action_sensitive(i,
101
                                 eset is not None and not eset.get_read_only())
102

    
103
        for i in ["save", "saveas"]:
104
            set_action_sensitive(i, save_sens)
105

    
106
        for i in ["upload"]:
107
            set_action_sensitive(i, upload_sens)
108

    
109
        for i in ["cancelq"]:
110
            set_action_sensitive(i, eset is not None and not save_sens)
111
        
112
        for i in ["export", "close", "columns", "irbook", "irfinder",
113
                  "move_up", "move_dn", "exchange", "iradioreference",
114
                  "cut", "copy", "paste", "delete", "viewdeveloper"]:
115
            set_action_sensitive(i, eset is not None)
116

    
117
    def ev_status(self, editorset, msg):
118
        self.sb_radio.pop(0)
119
        self.sb_radio.push(0, msg)
120

    
121
    def ev_usermsg(self, editorset, msg):
122
        self.sb_general.pop(0)
123
        self.sb_general.push(0, msg)
124

    
125
    def ev_editor_selected(self, editorset, editortype):
126
        mappings = {
127
            "memedit" : ["view", "edit"],
128
            }
129

    
130
        for _editortype, actions in mappings.items():
131
            for _action in actions:
132
                action = self.menu_ag.get_action(_action)
133
                action.set_sensitive(editortype.startswith(_editortype))
134

    
135
    def _connect_editorset(self, eset):
136
        eset.connect("want-close", self.do_close)
137
        eset.connect("status", self.ev_status)
138
        eset.connect("usermsg", self.ev_usermsg)
139
        eset.connect("editor-selected", self.ev_editor_selected)
140

    
141
    def do_diff_radio(self):
142
        if self.tabs.get_n_pages() < 2:
143
            common.show_error("Diff tabs requires at least two open tabs!")
144
            return
145

    
146
        esets = []
147
        for i in range(0, self.tabs.get_n_pages()):
148
            esets.append(self.tabs.get_nth_page(i))
149

    
150
        d = gtk.Dialog(title="Diff Radios",
151
                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
152
                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
153
                       parent=self)
154

    
155
        label = gtk.Label("")
156
        label.set_markup("<b>-1</b> for either Mem # does a full-file hex " +
157
                "dump with diffs highlighted.\n" +
158
                "<b>-2</b> for first Mem # shows <b>only</b> the diffs.")
159
        d.vbox.pack_start(label, True, True, 0)
160
        label.show()
161

    
162
        choices = []
163
        for eset in esets:
164
            choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR,
165
                                           eset.rthread.radio.MODEL,
166
                                           eset.filename))
167
        choice_a = miscwidgets.make_choice(choices, False, choices[0])
168
        choice_a.show()
169
        chan_a = gtk.SpinButton()
170
        chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0)
171
        chan_a.show()
172
        hbox = gtk.HBox(False, 3)
173
        hbox.pack_start(choice_a, 1, 1, 1)
174
        hbox.pack_start(chan_a, 0, 0, 0)
175
        hbox.show()
176
        d.vbox.pack_start(hbox, 0, 0, 0)
177

    
178
        choice_b = miscwidgets.make_choice(choices, False, choices[1])
179
        choice_b.show()
180
        chan_b = gtk.SpinButton()
181
        chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0)
182
        chan_b.show()
183
        hbox = gtk.HBox(False, 3)
184
        hbox.pack_start(choice_b, 1, 1, 1)
185
        hbox.pack_start(chan_b, 0, 0, 0)
186
        hbox.show()
187
        d.vbox.pack_start(hbox, 0, 0, 0)
188

    
189
        r = d.run()
190
        sel_a = choice_a.get_active_text()
191
        sel_chan_a = chan_a.get_value()
192
        sel_b = choice_b.get_active_text()
193
        sel_chan_b = chan_b.get_value()
194
        d.destroy()
195
        if r == gtk.RESPONSE_CANCEL:
196
            return
197

    
198
        if sel_a == sel_b:
199
            common.show_error("Can't diff the same tab!")
200
            return
201

    
202
        print "Selected %s@%i and %s@%i" % (sel_a, sel_chan_a,
203
                                            sel_b, sel_chan_b)
204
        name_a = os.path.basename (sel_a)
205
        name_a = name_a[:name_a.rindex(")")]
206
        name_b = os.path.basename (sel_b)
207
        name_b = name_b[:name_b.rindex(")")]
208
        diffwintitle = "%s@%i  diff  %s@%i" % (
209
            name_a, sel_chan_a, name_b, sel_chan_b)
210

    
211
        eset_a = esets[choices.index(sel_a)]
212
        eset_b = esets[choices.index(sel_b)]
213

    
214
        def _show_diff(mem_b, mem_a):
215
            # Step 3: Show the diff
216
            diff = common.simple_diff(mem_a, mem_b)
217
            common.show_diff_blob(diffwintitle, diff)
218

    
219
        def _get_mem_b(mem_a):
220
            # Step 2: Get memory b
221
            job = common.RadioJob(_show_diff, "get_raw_memory", int(sel_chan_b))
222
            job.set_cb_args(mem_a)
223
            eset_b.rthread.submit(job)
224
            
225
        if sel_chan_a >= 0 and sel_chan_b >= 0:
226
            # Diff numbered memory
227
            # Step 1: Get memory a
228
            job = common.RadioJob(_get_mem_b, "get_raw_memory", int(sel_chan_a))
229
            eset_a.rthread.submit(job)
230
        elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\
231
                isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio):
232
            # Diff whole (can do this without a job, since both are clone-mode)
233
            try:
234
                addrfmt = CONF.get('hexdump_addrfmt', section='developer',
235
                                   raw=True)
236
            except:
237
                pass
238
            a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(),
239
                              addrfmt=addrfmt)
240
            b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(),
241
                              addrfmt=addrfmt)
242
            if sel_chan_a == -2:
243
                diffsonly = True
244
            else:
245
                diffsonly = False
246
            common.show_diff_blob(diffwintitle,
247
                    common.simple_diff(a, b, diffsonly))
248
        else:
249
            common.show_error("Cannot diff whole live-mode radios!")
250

    
251
    def do_new(self):
252
        eset = editorset.EditorSet(_("Untitled") + ".csv", self)
253
        self._connect_editorset(eset)
254
        eset.prime()
255
        eset.show()
256

    
257
        tab = self.tabs.append_page(eset, eset.get_tab_label())
258
        self.tabs.set_current_page(tab)
259

    
260
    def _do_manual_select(self, filename):
261
        radiolist = {}
262
        for drv, radio in directory.DRV_TO_RADIO.items():
263
            if not issubclass(radio, chirp_common.CloneModeRadio):
264
                continue
265
            radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv
266

    
267
        lab = gtk.Label("""<b><big>Unable to detect model!</big></b>
268

    
269
If you think that it is valid, you can select a radio model below to force an open attempt. If selecting the model manually works, please file a bug on the website and attach your image. If selecting the model does not work, it is likely that you are trying to open some other type of file.
270
""")
271

    
272
        lab.set_justify(gtk.JUSTIFY_FILL)
273
        lab.set_line_wrap(True)
274
        lab.set_use_markup(True)
275
        lab.show()
276
        choice = miscwidgets.make_choice(sorted(radiolist.keys()), False,
277
                                         sorted(radiolist.keys())[0])
278
        d = gtk.Dialog(title="Detection Failed",
279
                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
280
                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
281
        d.vbox.pack_start(lab, 0, 0, 0)
282
        d.vbox.pack_start(choice, 0, 0, 0)
283
        d.vbox.set_spacing(5)
284
        choice.show()
285
        d.set_default_size(400, 200)
286
        #d.set_resizable(False)
287
        r = d.run()
288
        d.destroy()
289
        if r != gtk.RESPONSE_OK:
290
            return
291
        try:
292
            rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]]
293
            return rc(filename)
294
        except:
295
            return
296

    
297
    def do_open(self, fname=None, tempname=None):
298
        if not fname:
299
            types = [(_("CHIRP Radio Images") + " (*.img)", "*.img"),
300
                     (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
301
                     (_("CSV Files") + " (*.csv)", "*.csv"),
302
                     (_("DAT Files") + " (*.dat)", "*.dat"),
303
                     (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
304
                     (_("ICF Files") + " (*.icf)", "*.icf"),
305
                     (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
306
                     (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
307
                     (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"),
308
                     ]
309
            fname = platform.get_platform().gui_open_file(types=types)
310
            if not fname:
311
                return
312

    
313
        self.record_recent_file(fname)
314

    
315
        if icf.is_icf_file(fname):
316
            a = common.ask_yesno_question(\
317
                _("ICF files cannot be edited, only displayed or imported "
318
                  "into another file. Open in read-only mode?"),
319
                self)
320
            if not a:
321
                return
322
            read_only = True
323
        else:
324
            read_only = False
325

    
326
        if icf.is_9x_icf(fname):
327
            # We have to actually instantiate the IC9xICFRadio to get its
328
            # sub-devices
329
            radio = ic9x_icf.IC9xICFRadio(fname)
330
        else:
331
            try:
332
                radio = directory.get_radio_by_image(fname)
333
            except errors.ImageDetectFailed:
334
                radio = self._do_manual_select(fname)
335
                if not radio:
336
                    return
337
                print "Manually selected %s" % radio
338
            except Exception, e:
339
                common.log_exception()
340
                common.show_error(os.path.basename(fname) + ": " + str(e))
341
                return
342

    
343
        first_tab = False
344
        try:
345
            eset = editorset.EditorSet(radio, self,
346
                                       filename=fname,
347
                                       tempname=tempname)
348
        except Exception, e:
349
            common.log_exception()
350
            common.show_error(
351
                _("There was an error opening {fname}: {error}").format(
352
                    fname=fname,
353
                    error=e))
354
            return
355

    
356
        eset.set_read_only(read_only)
357
        self._connect_editorset(eset)
358
        eset.show()
359
        self.tabs.append_page(eset, eset.get_tab_label())
360

    
361
        if hasattr(eset.rthread.radio, "errors") and \
362
                eset.rthread.radio.errors:
363
            msg = _("{num} errors during open:").format(
364
                num=len(eset.rthread.radio.errors))
365
            common.show_error_text(msg,
366
                                   "\r\n".join(eset.rthread.radio.errors))
367

    
368
    def do_live_warning(self, radio):
369
        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
370
        d.set_markup("<big><b>" + _("Note:") + "</b></big>")
371
        msg = _("The {vendor} {model} operates in <b>live mode</b>. "
372
                "This means that any changes you make are immediately sent "
373
                "to the radio. Because of this, you cannot perform the "
374
                "<u>Save</u> or <u>Upload</u> operations. If you wish to "
375
                "edit the contents offline, please <u>Export</u> to a CSV "
376
                "file, using the <b>File menu</b>.").format(vendor=radio.VENDOR,
377
                                                            model=radio.MODEL)
378
        d.format_secondary_markup(msg)
379

    
380
        again = gtk.CheckButton(_("Don't show this again"))
381
        again.show()
382
        d.vbox.pack_start(again, 0, 0, 0)
383
        d.run()
384
        CONF.set_bool("live_mode", again.get_active(), "noconfirm")
385
        d.destroy()
386

    
387
    def do_open_live(self, radio, tempname=None, read_only=False):
388
        eset = editorset.EditorSet(radio, self, tempname=tempname)
389
        eset.connect("want-close", self.do_close)
390
        eset.connect("status", self.ev_status)
391
        eset.set_read_only(read_only)
392
        eset.show()
393
        self.tabs.append_page(eset, eset.get_tab_label())
394

    
395
        if isinstance(radio, chirp_common.LiveRadio):
396
            reporting.report_model_usage(radio, "live", True)
397
            if not CONF.get_bool("live_mode", "noconfirm"):
398
                self.do_live_warning(radio)
399

    
400
    def do_save(self, eset=None):
401
        if not eset:
402
            eset = self.get_current_editorset()
403

    
404
        # For usability, allow Ctrl-S to short-circuit to Save-As if
405
        # we are working on a yet-to-be-saved image
406
        if not os.path.exists(eset.filename):
407
            return self.do_saveas()
408

    
409
        eset.save()
410

    
411
    def do_saveas(self):
412
        eset = self.get_current_editorset()
413

    
414
        label = _("{vendor} {model} image file").format(\
415
            vendor=eset.radio.VENDOR,
416
            model=eset.radio.MODEL)
417
                                                     
418
        types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION,
419
                 eset.radio.FILE_EXTENSION)]
420

    
421
        if isinstance(eset.radio, vx7.VX7Radio):
422
            types += [(_("VX7 Commander") + " (*.vx7)", "vx7")]
423
        elif isinstance(eset.radio, vx6.VX6Radio):
424
            types += [(_("VX6 Commander") + " (*.vx6)", "vx6")]
425
        elif isinstance(eset.radio, vx5.VX5Radio):
426
            types += [(_("EVE") + " (*.eve)", "eve")]
427
            types += [(_("VX5 Commander") + " (*.vx5)", "vx5")]
428

    
429
        while True:
430
            fname = platform.get_platform().gui_save_file(types=types)
431
            if not fname:
432
                return
433

    
434
            if os.path.exists(fname):
435
                dlg = inputdialog.OverwriteDialog(fname)
436
                owrite = dlg.run()
437
                dlg.destroy()
438
                if owrite == gtk.RESPONSE_OK:
439
                    break
440
            else:
441
                break
442

    
443
        try:
444
            eset.save(fname)
445
        except Exception,e:
446
            d = inputdialog.ExceptionDialog(e)
447
            d.run()
448
            d.destroy()
449

    
450
    def cb_clonein(self, radio, emsg=None):
451
        radio.pipe.close()
452
        reporting.report_model_usage(radio, "download", bool(emsg))
453
        if not emsg:
454
            self.do_open_live(radio, tempname="(" + _("Untitled") + ")")
455
        else:
456
            d = inputdialog.ExceptionDialog(emsg)
457
            d.run()
458
            d.destroy()
459

    
460
    def cb_cloneout(self, radio, emsg= None):
461
        radio.pipe.close()
462
        reporting.report_model_usage(radio, "upload", True)
463
        if emsg:
464
            d = inputdialog.ExceptionDialog(emsg)
465
            d.run()
466
            d.destroy()
467

    
468
    def _get_recent_list(self):
469
        recent = []
470
        for i in range(0, KEEP_RECENT):
471
            fn = CONF.get("recent%i" % i, "state")
472
            if fn:
473
                recent.append(fn)
474
        return recent
475
                    
476
    def _set_recent_list(self, recent):
477
        for fn in recent:
478
            CONF.set("recent%i" % recent.index(fn), fn, "state")
479

    
480
    def update_recent_files(self):
481
        i = 0
482
        for fname in self._get_recent_list():
483
            action_name = "recent%i" % i
484
            path = "/MenuBar/file/recent"
485

    
486
            old_action = self.menu_ag.get_action(action_name)
487
            if old_action:
488
                self.menu_ag.remove_action(old_action)
489

    
490
            file_basename = os.path.basename(fname).replace("_", "__")
491
            action = gtk.Action(action_name,
492
                                "_%i. %s" % (i+1, file_basename),
493
                                _("Open recent file {name}").format(name=fname),
494
                                "")
495
            action.connect("activate", lambda a,f: self.do_open(f), fname)
496
            mid = self.menu_uim.new_merge_id()
497
            self.menu_uim.add_ui(mid, path, 
498
                                 action_name, action_name,
499
                                 gtk.UI_MANAGER_MENUITEM, False)
500
            self.menu_ag.add_action(action)
501
            i += 1
502

    
503
    def record_recent_file(self, filename):
504

    
505
        recent_files = self._get_recent_list()
506
        if filename not in recent_files:
507
            if len(recent_files) == KEEP_RECENT:
508
                del recent_files[-1]
509
            recent_files.insert(0, filename)
510
            self._set_recent_list(recent_files)
511

    
512
        self.update_recent_files()
513

    
514
    def import_stock_config(self, action, config):
515
        eset = self.get_current_editorset()
516
        count = eset.do_import(config)
517

    
518
    def copy_shipped_stock_configs(self, stock_dir):
519
        execpath = platform.get_platform().executable_path()
520
        basepath = os.path.abspath(os.path.join(execpath, "stock_configs"))
521
        if not os.path.exists(basepath):
522
            basepath = "/usr/share/chirp/stock_configs"
523

    
524
        files = glob(os.path.join(basepath, "*.csv"))
525
        for fn in files:
526
            if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))):
527
                print "Skipping existing stock config"
528
                continue
529
            try:
530
                shutil.copy(fn, stock_dir)
531
                print "Copying %s -> %s" % (fn, stock_dir)
532
            except Exception, e:
533
                print "ERROR: Unable to copy %s to %s: %s" % (fn, stock_dir, e)
534
                return False
535
        return True
536

    
537
    def update_stock_configs(self):
538
        stock_dir = platform.get_platform().config_file("stock_configs")
539
        if not os.path.isdir(stock_dir):
540
            try:
541
                os.mkdir(stock_dir)
542
            except Exception, e:
543
                print "ERROR: Unable to create directory: %s" % stock_dir
544
                return
545
        if not self.copy_shipped_stock_configs(stock_dir):
546
            return
547

    
548
        def _do_import_action(config):
549
            name = os.path.splitext(os.path.basename(config))[0]
550
            action_name = "stock-%i" % configs.index(config)
551
            path = "/MenuBar/radio/stock"
552
            action = gtk.Action(action_name,
553
                                name,
554
                                _("Import stock "
555
                                  "configuration {name}").format(name=name),
556
                                "")
557
            action.connect("activate", self.import_stock_config, config)
558
            mid = self.menu_uim.new_merge_id()
559
            mid = self.menu_uim.add_ui(mid, path,
560
                                       action_name, action_name,
561
                                       gtk.UI_MANAGER_MENUITEM, False)
562
            self.menu_ag.add_action(action)
563

    
564
        def _do_open_action(config):
565
            name = os.path.splitext(os.path.basename(config))[0]
566
            action_name = "openstock-%i" % configs.index(config)
567
            path = "/MenuBar/file/openstock"
568
            action = gtk.Action(action_name,
569
                                name,
570
                                _("Open stock "
571
                                  "configuration {name}").format(name=name),
572
                                "")
573
            action.connect("activate", lambda a,c: self.do_open(c), config)
574
            mid = self.menu_uim.new_merge_id()
575
            mid = self.menu_uim.add_ui(mid, path,
576
                                       action_name, action_name,
577
                                       gtk.UI_MANAGER_MENUITEM, False)
578
            self.menu_ag.add_action(action)
579
            
580

    
581
        configs = glob(os.path.join(stock_dir, "*.csv"))
582
        for config in configs:
583
            _do_import_action(config)
584
            _do_open_action(config)
585

    
586
    def _confirm_experimental(self, rclass):
587
        sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass)
588
        if CONF.is_defined(sql_key, "state") and \
589
                not CONF.get_bool(sql_key, "state"):
590
            return True
591

    
592
        title = _("Proceed with experimental driver?")
593
        text = rclass.get_prompts().experimental
594
        msg = _("This radio's driver is experimental. "
595
                "Do you want to proceed?")
596
        resp, squelch = common.show_warning(msg, text,
597
                                            title=title,
598
                                            buttons=gtk.BUTTONS_YES_NO,
599
                                            can_squelch=True)
600
        if resp == gtk.RESPONSE_YES:
601
            CONF.set_bool(sql_key, not squelch, "state")
602
        return resp == gtk.RESPONSE_YES
603

    
604
    def _show_instructions(self, radio, message):
605
        if message is None:
606
            return
607

    
608
        if CONF.get_bool("clone_instructions", "noconfirm"):
609
            return
610

    
611
        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
612
        d.set_markup("<big><b>" + _("{name} Instructions").format(
613
                     name=radio.get_name()) + "</b></big>")
614
        msg = _("{instructions}").format(instructions=message)
615
        d.format_secondary_markup(msg)
616

    
617
        again = gtk.CheckButton(_("Don't show instructions for any radio again"))
618
        again.show()
619
        d.vbox.pack_start(again, 0, 0, 0)
620
        h_button_box = d.vbox.get_children()[2]
621
        try:
622
            ok_button = h_button_box.get_children()[0]
623
            ok_button.grab_default()
624
            ok_button.grab_focus()
625
        except AttributeError:
626
            # don't grab focus on GTK+ 2.0
627
            pass
628
        d.run()
629
        d.destroy()
630
        CONF.set_bool("clone_instructions", again.get_active(), "noconfirm")
631

    
632
    def do_download(self, port=None, rtype=None):
633
        d = clone.CloneSettingsDialog(parent=self)
634
        settings = d.run()
635
        d.destroy()
636
        if not settings:
637
            return
638

    
639
        rclass = settings.radio_class
640
        if issubclass(rclass, chirp_common.ExperimentalRadio) and \
641
                not self._confirm_experimental(rclass):
642
            # User does not want to proceed with experimental driver
643
            return
644

    
645
        self._show_instructions(rclass, rclass.get_prompts().pre_download)
646

    
647
        print "User selected %s %s on port %s" % (rclass.VENDOR,
648
                                                  rclass.MODEL,
649
                                                  settings.port)
650

    
651
        try:
652
            ser = serial.Serial(port=settings.port,
653
                                baudrate=rclass.BAUD_RATE,
654
                                rtscts=rclass.HARDWARE_FLOW,
655
                                timeout=0.25)
656
            ser.flushInput()
657
        except serial.SerialException, e:
658
            d = inputdialog.ExceptionDialog(e)
659
            d.run()
660
            d.destroy()
661
            return
662

    
663
        radio = settings.radio_class(ser)
664

    
665
        fn = tempfile.mktemp()
666
        if isinstance(radio, chirp_common.CloneModeRadio):
667
            ct = clone.CloneThread(radio, "in", cb=self.cb_clonein, parent=self)
668
            ct.start()
669
        else:
670
            self.do_open_live(radio)
671

    
672
    def do_upload(self, port=None, rtype=None):
673
        eset = self.get_current_editorset()
674
        radio = eset.radio
675

    
676
        settings = clone.CloneSettings()
677
        settings.radio_class = radio.__class__
678

    
679
        d = clone.CloneSettingsDialog(settings, parent=self)
680
        settings = d.run()
681
        d.destroy()
682
        if not settings:
683
            return
684
        prompts = radio.get_prompts()
685

    
686
        prompt_before = prompts.display_pre_upload_prompt_before_opening_port \
687
                and not CONF.get_bool("force_after", "upload_prompt")
688

    
689
        if prompt_before == True:
690
            print "Opening port after pre_upload prompt."
691
            self._show_instructions(radio, prompts.pre_upload)
692

    
693
        if isinstance(radio, chirp_common.ExperimentalRadio) and \
694
                not self._confirm_experimental(radio.__class__):
695
            # User does not want to proceed with experimental driver
696
            return
697

    
698
        try:
699
            ser = serial.Serial(port=settings.port,
700
                                baudrate=radio.BAUD_RATE,
701
                                rtscts=radio.HARDWARE_FLOW,
702
                                timeout=0.25)
703
            ser.flushInput()
704
        except serial.SerialException, e:
705
            d = inputdialog.ExceptionDialog(e)
706
            d.run()
707
            d.destroy()
708
            return
709

    
710
        if prompt_before == False:
711
            print "Opening port before pre_upload prompt."
712
            self._show_instructions(radio, prompts.pre_upload)
713

    
714
        radio.set_pipe(ser)
715

    
716
        ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self)
717
        ct.start()
718

    
719
    def do_close(self, tab_child=None):
720
        if tab_child:
721
            eset = tab_child
722
        else:
723
            eset = self.get_current_editorset()
724

    
725
        if not eset:
726
            return False
727

    
728
        if eset.is_modified():
729
            dlg = miscwidgets.YesNoDialog(title=_("Save Changes?"),
730
                                          parent=self,
731
                                          buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES,
732
                                                   gtk.STOCK_NO, gtk.RESPONSE_NO,
733
                                                   gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
734
            dlg.set_text(_("File is modified, save changes before closing?"))
735
            res = dlg.run()
736
            dlg.destroy()
737
            if res == gtk.RESPONSE_YES:
738
                self.do_save(eset)
739
            elif res == gtk.RESPONSE_CANCEL:
740
                raise ModifiedError()
741

    
742
        eset.rthread.stop()
743
        eset.rthread.join()
744
    
745
        eset.prepare_close()
746

    
747
        if eset.radio.pipe:
748
            eset.radio.pipe.close()
749

    
750
        if isinstance(eset.radio, chirp_common.LiveRadio):
751
            action = self.menu_ag.get_action("openlive")
752
            if action:
753
                action.set_sensitive(True)
754

    
755
        page = self.tabs.page_num(eset)
756
        if page is not None:
757
            self.tabs.remove_page(page)
758

    
759
        return True
760

    
761
    def do_import(self):
762
        types = [(_("CHIRP Files") + " (*.chirp)", "*.chirp"),
763
                 (_("CHIRP Radio Images") + " (*.img)", "*.img"),
764
                 (_("CSV Files") + " (*.csv)", "*.csv"),
765
                 (_("DAT Files") + " (*.dat)", "*.dat"),
766
                 (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
767
                 (_("ICF Files") + " (*.icf)", "*.icf"),
768
                 (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
769
                 (_("Kenwood ITM Files") + " (*.itm)", "*.itm"),
770
                 (_("Travel Plus Files") + " (*.tpe)", "*.tpe"),
771
                 (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
772
                 (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
773
                 (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")]
774
        filen = platform.get_platform().gui_open_file(types=types)
775
        if not filen:
776
            return
777

    
778
        eset = self.get_current_editorset()
779
        count = eset.do_import(filen)
780
        reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
781

    
782
    def do_repeaterbook_prompt(self):
783
        if not CONF.get_bool("has_seen_credit", "repeaterbook"):
784
            d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
785
            d.set_markup("<big><big><b>RepeaterBook</b></big>\r\n" + \
786
                             "<i>North American Repeater Directory</i></big>")
787
            d.format_secondary_markup("For more information about this " +\
788
                                          "free service, please go to\r\n" +\
789
                                          "http://www.repeaterbook.com")
790
            d.run()
791
            d.destroy()
792
            CONF.set_bool("has_seen_credit", True, "repeaterbook")
793

    
794
        default_state = "Oregon"
795
        default_county = "--All--"
796
        default_band = "--All--"
797
        try:
798
            try:
799
                code = int(CONF.get("state", "repeaterbook"))
800
            except:
801
                code = CONF.get("state", "repeaterbook")
802
            for k,v in fips.FIPS_STATES.items():
803
                if code == v:
804
                    default_state = k
805
                    break
806

    
807
            code = CONF.get("county", "repeaterbook")
808
            for k,v in fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items():
809
                if code == v:
810
                    default_county = k
811
                    break
812

    
813
            code = int(CONF.get("band", "repeaterbook"))
814
            for k,v in RB_BANDS.items():
815
                if code == v:
816
                    default_band = k
817
                    break
818
        except:
819
            pass
820

    
821
        state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()),
822
                                        False, default_state)
823
        county = miscwidgets.make_choice(sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()),
824
                                        False, default_county)
825
        band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands),
826
                                       False, default_band)
827
        def _changed(box, county):
828
            state = fips.FIPS_STATES[box.get_active_text()]
829
            county.get_model().clear()
830
            for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()):
831
                county.append_text(fips_county)
832
            county.set_active(0)
833
        state.connect("changed", _changed, county)
834
        
835
        d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self)
836
        d.add_field("State", state)
837
        d.add_field("County", county)
838
        d.add_field("Band", band)
839

    
840
        r = d.run()
841
        d.destroy()
842
        if r != gtk.RESPONSE_OK:
843
            return False
844

    
845
        code = fips.FIPS_STATES[state.get_active_text()]
846
        county_id = fips.FIPS_COUNTIES[code][county.get_active_text()]
847
        freq = RB_BANDS[band.get_active_text()]
848
        CONF.set("state", str(code), "repeaterbook")
849
        CONF.set("county", str(county_id), "repeaterbook")
850
        CONF.set("band", str(freq), "repeaterbook")
851

    
852
        return True
853

    
854
    def do_repeaterbook(self, do_import):
855
        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
856
        if not self.do_repeaterbook_prompt():
857
            self.window.set_cursor(None)
858
            return
859

    
860
        try:
861
            code = "%02i" % int(CONF.get("state", "repeaterbook"))
862
        except:
863
            try:
864
                code = CONF.get("state", "repeaterbook")
865
            except:
866
                code = '41' # Oregon default
867

    
868
        try:
869
            county = CONF.get("county", "repeaterbook")
870
        except:
871
            county = '%' # --All-- default
872

    
873
        try:
874
            band = int(CONF.get("band", "repeaterbook"))
875
        except:
876
            band = 14 # 2m default
877

    
878
        query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php?" + \
879
            "func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \
880
            "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%"
881
        query = query % (code, band and band or "%%", county and county or "%%")
882

    
883
        # Do this in case the import process is going to take a while
884
        # to make sure we process events leading up to this
885
        gtk.gdk.window_process_all_updates()
886
        while gtk.events_pending():
887
            gtk.main_iteration(False)
888

    
889
        fn = tempfile.mktemp(".csv")
890
        filename, headers = urllib.urlretrieve(query, fn)
891
        if not os.path.exists(filename):
892
            print "Failed, headers were:"
893
            print str(headers)
894
            common.show_error(_("RepeaterBook query failed"))
895
            self.window.set_cursor(None)
896
            return
897

    
898
        class RBRadio(generic_csv.CSVRadio,
899
                      chirp_common.NetworkSourceRadio):
900
            VENDOR = "RepeaterBook"
901
            MODEL = ""
902

    
903
        try:
904
            # Validate CSV
905
            radio = RBRadio(filename)
906
            if radio.errors:
907
                reporting.report_misc_error("repeaterbook",
908
                                            ("query=%s\n" % query) +
909
                                            ("\n") +
910
                                            ("\n".join(radio.errors)))
911
        except errors.InvalidDataError, e:
912
            common.show_error(str(e))
913
            self.window.set_cursor(None)
914
            return
915
        except Exception, e:
916
            common.log_exception()
917

    
918
        reporting.report_model_usage(radio, "import", True)
919

    
920
        self.window.set_cursor(None)
921
        if do_import:
922
            eset = self.get_current_editorset()
923
            count = eset.do_import(filename)
924
        else:
925
            self.do_open_live(radio, read_only=True)
926

    
927
    def do_przemienniki_prompt(self):
928
        d = inputdialog.FieldDialog(title='przemienniki.net query',
929
                                    parent=self)
930
        fields = {
931
            "Country":
932
                (miscwidgets.make_choice(['by', 'cz', 'de', 'lt', 'pl',
933
                                          'sk', 'uk'], False),
934
                 lambda x: str(x.get_active_text())),
935
            "Band":
936
                (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm',
937
                                          '23cm', '13cm', '3cm'], False, '2m'),
938
                 lambda x: str(x.get_active_text())),
939
            "Mode":
940
                (miscwidgets.make_choice(['fm', 'dv'], False),
941
                 lambda x: str(x.get_active_text())),
942
            "Only Working":
943
                (miscwidgets.make_choice(['', 'yes'], False),
944
                 lambda x: str(x.get_active_text())),
945
            "Latitude": (gtk.Entry(), lambda x: float(x.get_text())),
946
            "Longitude": (gtk.Entry(), lambda x: float(x.get_text())),
947
            "Range": (gtk.Entry(), lambda x: int(x.get_text())),
948
            }
949
        for name in sorted(fields.keys()):
950
            value, fn = fields[name]
951
            d.add_field(name, value)
952
        while d.run() == gtk.RESPONSE_OK:
953
            query = "http://przemienniki.net/export/chirp.csv?"
954
            args = []
955
            for name, (value, fn) in fields.items():
956
                if isinstance(value, gtk.Entry):
957
                    contents = value.get_text()
958
                else:
959
                    contents = value.get_active_text()
960
                if contents:
961
                    try:
962
                        _value = fn(value)
963
                    except ValueError:
964
                        common.show_error(_("Invalid value for %s") % name)
965
                        query = None
966
                        continue
967

    
968
                    args.append("=".join((name.replace(" ", "").lower(),
969
                                          contents)))
970
            query += "&".join(args)
971
            print query
972
            d.destroy()
973
            return query
974

    
975
        d.destroy()
976
        return query
977

    
978
    def do_przemienniki(self, do_import):
979
        url = self.do_przemienniki_prompt()
980
        if not url:
981
            return
982

    
983
        fn = tempfile.mktemp(".csv")
984
        filename, headers = urllib.urlretrieve(url, fn)
985
        if not os.path.exists(filename):
986
            print "Failed, headers were:"
987
            print str(headers)
988
            common.show_error(_("Query failed"))
989
            return
990

    
991
        class PRRadio(generic_csv.CSVRadio,
992
                      chirp_common.NetworkSourceRadio):
993
            VENDOR = "przemienniki.net"
994
            MODEL = ""
995

    
996
        try:
997
            radio = PRRadio(filename)
998
        except Exception, e:
999
            common.show_error(str(e))
1000
            return
1001

    
1002
        if do_import:
1003
            eset = self.get_current_editorset()
1004
            count = eset.do_import(filename)
1005
        else:
1006
            self.do_open_live(radio, read_only=True)
1007

    
1008
    def do_rfinder_prompt(self):
1009
        fields = {"1Email"    :      (gtk.Entry(),
1010
                                      lambda x: "@" in x),
1011
                  "2Password" :      (gtk.Entry(),
1012
                                      lambda x: x),
1013
                  "3Latitude" :      (gtk.Entry(),
1014
                                      lambda x: float(x) < 90 and \
1015
                                          float(x) > -90),
1016
                  "4Longitude":      (gtk.Entry(),
1017
                                      lambda x: float(x) < 180 and \
1018
                                          float(x) > -180),
1019
                  "5Range_in_Miles": (gtk.Entry(),
1020
                                      lambda x: int(x) > 0 and int(x) < 5000),
1021
                  }
1022

    
1023
        d = inputdialog.FieldDialog(title="RFinder Login", parent=self)
1024
        for k in sorted(fields.keys()):
1025
            d.add_field(k[1:].replace("_", " "), fields[k][0])
1026
            fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "")
1027
            fields[k][0].set_visibility(k != "2Password")
1028

    
1029
        while d.run() == gtk.RESPONSE_OK:
1030
            valid = True
1031
            for k in sorted(fields.keys()):
1032
                widget, validator = fields[k]
1033
                try:
1034
                    if validator(widget.get_text()):
1035
                        CONF.set(k[1:], widget.get_text(), "rfinder")
1036
                        continue
1037
                except Exception:
1038
                    pass
1039
                common.show_error("Invalid value for %s" % k[1:])
1040
                valid = False
1041
                break
1042

    
1043
            if valid:
1044
                d.destroy()
1045
                return True
1046

    
1047
        d.destroy()
1048
        return False
1049

    
1050
    def do_rfinder(self, do_import):
1051
        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
1052
        if not self.do_rfinder_prompt():
1053
            self.window.set_cursor(None)
1054
            return
1055

    
1056
        lat = CONF.get_float("Latitude", "rfinder")
1057
        lon = CONF.get_float("Longitude", "rfinder")
1058
        passwd = CONF.get("Password", "rfinder")
1059
        email = CONF.get("Email", "rfinder")
1060
        miles = CONF.get_int("Range_in_Miles", "rfinder")
1061

    
1062
        # Do this in case the import process is going to take a while
1063
        # to make sure we process events leading up to this
1064
        gtk.gdk.window_process_all_updates()
1065
        while gtk.events_pending():
1066
            gtk.main_iteration(False)
1067

    
1068
        if do_import:
1069
            eset = self.get_current_editorset()
1070
            count = eset.do_import("rfinder://%s/%s/%f/%f/%i" % (email, passwd, lat, lon, miles))
1071
        else:
1072
            from chirp import rfinder
1073
            radio = rfinder.RFinderRadio(None)
1074
            radio.set_params((lat, lon), miles, email, passwd)
1075
            self.do_open_live(radio, read_only=True)
1076

    
1077
        self.window.set_cursor(None)
1078

    
1079
    def do_radioreference_prompt(self):
1080
        fields = {"1Username"    : (gtk.Entry(), lambda x: x),
1081
                  "2Password"    : (gtk.Entry(), lambda x: x),
1082
                  "3Zipcode"     : (gtk.Entry(), lambda x: x),
1083
                  }
1084

    
1085
        d = inputdialog.FieldDialog(title=_("RadioReference.com Query"),
1086
                                    parent=self)
1087
        for k in sorted(fields.keys()):
1088
            d.add_field(k[1:], fields[k][0])
1089
            fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "")
1090
            fields[k][0].set_visibility(k != "2Password")
1091

    
1092
        while d.run() == gtk.RESPONSE_OK:
1093
            valid = True
1094
            for k in sorted(fields.keys()):
1095
                widget, validator = fields[k]
1096
                try:
1097
                    if validator(widget.get_text()):
1098
                        CONF.set(k[1:], widget.get_text(), "radioreference")
1099
                        continue
1100
                except Exception:
1101
                    pass
1102
                common.show_error("Invalid value for %s" % k[1:])
1103
                valid = False
1104
                break
1105

    
1106
            if valid:
1107
                d.destroy()
1108
                return True
1109

    
1110
        d.destroy()
1111
        return False
1112

    
1113
    def do_radioreference(self, do_import):
1114
        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
1115
        if not self.do_radioreference_prompt():
1116
            self.window.set_cursor(None)
1117
            return
1118

    
1119
        username = CONF.get("Username", "radioreference")
1120
        passwd = CONF.get("Password", "radioreference")
1121
        zipcode = CONF.get("Zipcode", "radioreference")
1122

    
1123
        # Do this in case the import process is going to take a while
1124
        # to make sure we process events leading up to this
1125
        gtk.gdk.window_process_all_updates()
1126
        while gtk.events_pending():
1127
            gtk.main_iteration(False)
1128

    
1129
        if do_import:
1130
            eset = self.get_current_editorset()
1131
            count = eset.do_import("radioreference://%s/%s/%s" % (zipcode, username, passwd))
1132
        else:
1133
            try:
1134
                from chirp import radioreference
1135
                radio = radioreference.RadioReferenceRadio(None)
1136
                radio.set_params(zipcode, username, passwd)
1137
                self.do_open_live(radio, read_only=True)
1138
            except errors.RadioError, e:
1139
                common.show_error(e)
1140

    
1141
        self.window.set_cursor(None)
1142

    
1143
    def do_export(self):
1144
        types = [(_("CSV Files") + " (*.csv)", "csv"),
1145
                 (_("CHIRP Files") + " (*.chirp)", "chirp"),
1146
                 ]
1147

    
1148
        eset = self.get_current_editorset()
1149

    
1150
        if os.path.exists(eset.filename):
1151
            base = os.path.basename(eset.filename)
1152
            if "." in base:
1153
                base = base[:base.rindex(".")]
1154
            defname = base
1155
        else:
1156
            defname = "radio"
1157

    
1158
        filen = platform.get_platform().gui_save_file(default_name=defname,
1159
                                                      types=types)
1160
        if not filen:
1161
            return
1162

    
1163
        if os.path.exists(filen):
1164
            dlg = inputdialog.OverwriteDialog(filen)
1165
            owrite = dlg.run()
1166
            dlg.destroy()
1167
            if owrite != gtk.RESPONSE_OK:
1168
                return
1169
            os.remove(filen)
1170

    
1171
        count = eset.do_export(filen)
1172
        reporting.report_model_usage(eset.rthread.radio, "export", count > 0)
1173

    
1174
    def do_about(self):
1175
        d = gtk.AboutDialog()
1176
        d.set_transient_for(self)
1177
        import sys
1178
        verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % ( \
1179
            ".".join([str(x) for x in gtk.gtk_version]),
1180
            ".".join([str(x) for x in gtk.pygtk_version]),
1181
            sys.version.split()[0])
1182

    
1183
        d.set_name("CHIRP")
1184
        d.set_version(CHIRP_VERSION)
1185
        d.set_copyright("Copyright 2013 Dan Smith (KK7DS)")
1186
        d.set_website("http://chirp.danplanet.com")
1187
        d.set_authors(("Dan Smith KK7DS <dsmith@danplanet.com>",
1188
                       _("With significant contributions from:"),
1189
                       "Tom KD7LXL",
1190
                       "Marco IZ3GME",
1191
                       "Jim KC9HI"
1192
                       ))
1193
        d.set_translator_credits("Polish: Grzegorz SQ2RBY" +
1194
                                 os.linesep +
1195
                                 "Italian: Fabio IZ2QDH" +
1196
                                 os.linesep +
1197
                                 "Dutch: Michael PD4MT" +
1198
                                 os.linesep +
1199
                                 "German: Benjamin HB9EUK" +
1200
                                 os.linesep +
1201
                                 "Hungarian: Attila HA7JA" +
1202
                                 os.linesep +
1203
                                 "Russian: Dmitry Slukin" +
1204
                                 os.linesep +
1205
                                 "Portuguese (BR): Crezivando PP7CJ")
1206
        d.set_comments(verinfo)
1207
        
1208
        d.run()
1209
        d.destroy()
1210

    
1211
    def do_documentation(self):
1212
        d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self,
1213
                              type=gtk.MESSAGE_INFO)
1214

    
1215
        d.set_markup("<b><big>" + _("CHIRP Documentation") + "</big></b>\r\n")
1216
        msg = _("Documentation for CHIRP, including FAQs, and help for common "
1217
                "problems is available on the CHIRP web site, please go to\n\n"
1218
                "<a href=\"http://chirp.danplanet.com/projects/chirp/wiki/"
1219
                "Documentation\">"
1220
                "http://chirp.danplanet.com/projects/chirp/wiki/"
1221
                "Documentation</a>\n")
1222
        d.format_secondary_markup(msg.replace("\n","\r\n"))
1223
        d.run()
1224
        d.destroy()
1225

    
1226
    def do_columns(self):
1227
        eset = self.get_current_editorset()
1228
        driver = directory.get_driver(eset.rthread.radio.__class__)
1229
        radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR,
1230
                                   eset.rthread.radio.MODEL,
1231
                                   eset.rthread.radio.VARIANT)
1232
        d = gtk.Dialog(title=_("Select Columns"),
1233
                       parent=self,
1234
                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
1235
                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
1236

    
1237
        vbox = gtk.VBox()
1238
        vbox.show()
1239
        sw = gtk.ScrolledWindow()
1240
        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1241
        sw.add_with_viewport(vbox)
1242
        sw.show()
1243
        d.vbox.pack_start(sw, 1, 1, 1)
1244
        d.set_size_request(-1, 300)
1245
        d.set_resizable(False)
1246

    
1247
        label = gtk.Label(_("Visible columns for {radio}").format(radio=radio_name))
1248
        label.show()
1249
        vbox.pack_start(label)
1250

    
1251
        fields = []
1252
        memedit = eset.get_current_editor() #.editors["memedit"]
1253
        unsupported = memedit.get_unsupported_columns()
1254
        for colspec in memedit.cols:
1255
            if colspec[0].startswith("_"):
1256
                continue
1257
            elif colspec[0] in unsupported:
1258
                continue
1259
            label = colspec[0]
1260
            visible = memedit.get_column_visible(memedit.col(label))
1261
            widget = gtk.CheckButton(label)
1262
            widget.set_active(visible)
1263
            fields.append(widget)
1264
            vbox.pack_start(widget, 1, 1, 1)
1265
            widget.show()
1266

    
1267
        res = d.run()
1268
        selected_columns = []
1269
        if res == gtk.RESPONSE_OK:
1270
            for widget in fields:
1271
                colnum = memedit.col(widget.get_label())
1272
                memedit.set_column_visible(colnum, widget.get_active())
1273
                if widget.get_active():
1274
                    selected_columns.append(widget.get_label())
1275
                                                
1276
        d.destroy()
1277

    
1278
        CONF.set(driver, ",".join(selected_columns), "memedit_columns")
1279

    
1280
    def do_hide_unused(self, action):
1281
        eset = self.get_current_editorset()
1282
        if eset is None:
1283
            conf = config.get("memedit")
1284
            conf.set_bool("hide_unused", action.get_active())
1285
        else:
1286
            for editortype, editor in eset.editors.iteritems():
1287
                if "memedit" in editortype:
1288
                    editor.set_hide_unused(action.get_active())
1289

    
1290
    def do_clearq(self):
1291
        eset = self.get_current_editorset()
1292
        eset.rthread.flush()
1293

    
1294
    def do_copy(self, cut):
1295
        eset = self.get_current_editorset()
1296
        eset.get_current_editor().copy_selection(cut)
1297

    
1298
    def do_paste(self):
1299
        eset = self.get_current_editorset()
1300
        eset.get_current_editor().paste_selection()
1301

    
1302
    def do_delete(self):
1303
        eset = self.get_current_editorset()
1304
        eset.get_current_editor().copy_selection(True)
1305

    
1306
    def do_toggle_report(self, action):
1307
        if not action.get_active():
1308
            d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO,
1309
                                  parent=self)
1310
            d.set_markup("<b><big>" + _("Reporting is disabled") + "</big></b>")
1311
            msg = _("The reporting feature of CHIRP is designed to help "
1312
                    "<u>improve quality</u> by allowing the authors to focus "
1313
                    "on the radio drivers used most often and errors "
1314
                    "experienced by the users. The reports contain no "
1315
                    "identifying information and are used only for statistical "
1316
                    "purposes by the authors. Your privacy is extremely "
1317
                    "important, but <u>please consider leaving this feature "
1318
                    "enabled to help make CHIRP better!</u>\n\n<b>Are you "
1319
                    "sure you want to disable this feature?</b>")
1320
            d.format_secondary_markup(msg.replace("\n", "\r\n"))
1321
            r = d.run()
1322
            d.destroy()
1323
            if r == gtk.RESPONSE_NO:
1324
                action.set_active(not action.get_active())
1325

    
1326
        conf = config.get()
1327
        conf.set_bool("no_report", not action.get_active())
1328

    
1329
    def do_toggle_no_smart_tmode(self, action):
1330
        CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit")
1331

    
1332
    def do_toggle_developer(self, action):
1333
        conf = config.get()
1334
        conf.set_bool("developer", action.get_active(), "state")
1335

    
1336
        for name in ["viewdeveloper", "loadmod"]:
1337
            devaction = self.menu_ag.get_action(name)
1338
            devaction.set_visible(action.get_active())
1339

    
1340
    def do_change_language(self):
1341
        langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German",
1342
                 "Hungarian", "Russian", "Portuguese (BR)", "French"]
1343
        d = inputdialog.ChoiceDialog(langs, parent=self,
1344
                                     title="Choose Language")
1345
        d.label.set_text(_("Choose a language or Auto to use the "
1346
                           "operating system default. You will need to "
1347
                           "restart the application before the change "
1348
                           "will take effect"))
1349
        d.label.set_line_wrap(True)
1350
        r = d.run()
1351
        if r == gtk.RESPONSE_OK:
1352
            print "Chose language %s" % d.choice.get_active_text()
1353
            conf = config.get()
1354
            conf.set("language", d.choice.get_active_text(), "state")
1355
        d.destroy()
1356

    
1357
    def load_module(self):
1358
        types = [(_("Python Modules") + "*.py", "*.py")]
1359
        filen = platform.get_platform().gui_open_file(types=types)
1360
        if not filen:
1361
            return
1362

    
1363
        # We're in development mode, so we need to tell the directory to
1364
        # allow a loaded module to override an existing driver, against
1365
        # its normal better judgement
1366
        directory.enable_reregistrations()
1367

    
1368
        try:
1369
            module = file(filen)
1370
            code = module.read()
1371
            module.close()
1372
            pyc = compile(code, filen, 'exec')
1373
            # See this for why:
1374
            # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec
1375
            exec(pyc, globals(), globals())
1376
        except Exception, e:
1377
            common.log_exception()
1378
            common.show_error("Unable to load module: %s" % e)
1379

    
1380
    def mh(self, _action, *args):
1381
        action = _action.get_name()
1382

    
1383
        if action == "quit":
1384
            gtk.main_quit()
1385
        elif action == "new":
1386
            self.do_new()
1387
        elif action == "open":
1388
            self.do_open()
1389
        elif action == "save":
1390
            self.do_save()
1391
        elif action == "saveas":
1392
            self.do_saveas()
1393
        elif action.startswith("download"):
1394
            self.do_download(*args)
1395
        elif action.startswith("upload"):
1396
            self.do_upload(*args)
1397
        elif action == "close":
1398
            self.do_close()
1399
        elif action == "import":
1400
            self.do_import()
1401
        elif action in ["qrfinder", "irfinder"]:
1402
            self.do_rfinder(action[0] == "i")
1403
        elif action in ["qradioreference", "iradioreference"]:
1404
            self.do_radioreference(action[0] == "i")
1405
        elif action == "export":
1406
            self.do_export()
1407
        elif action in ["qrbook", "irbook"]:
1408
            self.do_repeaterbook(action[0] == "i")
1409
        elif action in ["qpr", "ipr"]:
1410
            self.do_przemienniki(action[0] == "i")
1411
        elif action == "about":
1412
            self.do_about()
1413
        elif action == "documentation":
1414
            self.do_documentation()
1415
        elif action == "columns":
1416
            self.do_columns()
1417
        elif action == "hide_unused":
1418
            self.do_hide_unused(_action)
1419
        elif action == "cancelq":
1420
            self.do_clearq()
1421
        elif action == "report":
1422
            self.do_toggle_report(_action)
1423
        elif action == "channel_defaults":
1424
            # The memedit thread also has an instance of bandplans.
1425
            bp = bandplans.BandPlans(CONF)
1426
            bp.select_bandplan(self)
1427
        elif action == "no_smart_tmode":
1428
            self.do_toggle_no_smart_tmode(_action)
1429
        elif action == "developer":
1430
            self.do_toggle_developer(_action)
1431
        elif action in ["cut", "copy", "paste", "delete",
1432
                        "move_up", "move_dn", "exchange",
1433
                        "devshowraw", "devdiffraw"]:
1434
            self.get_current_editorset().get_current_editor().hotkey(_action)
1435
        elif action == "devdifftab":
1436
            self.do_diff_radio()
1437
        elif action == "language":
1438
            self.do_change_language()
1439
        elif action == "loadmod":
1440
            self.load_module()
1441
        else:
1442
            return
1443

    
1444
        self.ev_tab_switched()
1445

    
1446
    def make_menubar(self):
1447
        menu_xml = """
1448
<ui>
1449
  <menubar name="MenuBar">
1450
    <menu action="file">
1451
      <menuitem action="new"/>
1452
      <menuitem action="open"/>
1453
      <menu action="openstock" name="openstock"/>
1454
      <menu action="recent" name="recent"/>
1455
      <menuitem action="save"/>
1456
      <menuitem action="saveas"/>
1457
      <menuitem action="loadmod"/>
1458
      <separator/>
1459
      <menuitem action="import"/>
1460
      <menuitem action="export"/>
1461
      <separator/>
1462
      <menuitem action="close"/>
1463
      <menuitem action="quit"/>
1464
    </menu>
1465
    <menu action="edit">
1466
      <menuitem action="cut"/>
1467
      <menuitem action="copy"/>
1468
      <menuitem action="paste"/>
1469
      <menuitem action="delete"/>
1470
      <separator/>
1471
      <menuitem action="move_up"/>
1472
      <menuitem action="move_dn"/>
1473
      <menuitem action="exchange"/>
1474
    </menu>
1475
    <menu action="view">
1476
      <menuitem action="columns"/>
1477
      <menuitem action="hide_unused"/>
1478
      <menuitem action="no_smart_tmode"/>
1479
      <menu action="viewdeveloper">
1480
        <menuitem action="devshowraw"/>
1481
        <menuitem action="devdiffraw"/>
1482
        <menuitem action="devdifftab"/>
1483
      </menu>
1484
      <menuitem action="language"/>
1485
    </menu>
1486
    <menu action="radio" name="radio">
1487
      <menuitem action="download"/>
1488
      <menuitem action="upload"/>
1489
      <menu action="importsrc" name="importsrc">
1490
        <menuitem action="iradioreference"/>
1491
        <menuitem action="irbook"/>
1492
        <menuitem action="ipr"/>
1493
        <menuitem action="irfinder"/>
1494
      </menu>
1495
      <menu action="querysrc" name="querysrc">
1496
        <menuitem action="qradioreference"/>
1497
        <menuitem action="qrbook"/>
1498
        <menuitem action="qpr"/>
1499
        <menuitem action="qrfinder"/>
1500
      </menu>
1501
      <menu action="stock" name="stock"/>
1502
      <separator/>
1503
      <menuitem action="channel_defaults"/>
1504
      <separator/>
1505
      <menuitem action="cancelq"/>
1506
    </menu>
1507
    <menu action="help">
1508
      <menuitem action="about"/>
1509
      <menuitem action="documentation"/>
1510
      <menuitem action="report"/>
1511
      <menuitem action="developer"/>
1512
    </menu>
1513
  </menubar>
1514
</ui>
1515
"""
1516
        actions = [\
1517
            ('file', None, _("_File"), None, None, self.mh),
1518
            ('new', gtk.STOCK_NEW, None, None, None, self.mh),
1519
            ('open', gtk.STOCK_OPEN, None, None, None, self.mh),
1520
            ('openstock', None, _("Open stock config"), None, None, self.mh),
1521
            ('recent', None, _("_Recent"), None, None, self.mh),
1522
            ('save', gtk.STOCK_SAVE, None, None, None, self.mh),
1523
            ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh),
1524
            ('loadmod', None, _("Load Module"), None, None, self.mh),
1525
            ('close', gtk.STOCK_CLOSE, None, None, None, self.mh),
1526
            ('quit', gtk.STOCK_QUIT, None, None, None, self.mh),
1527
            ('edit', None, _("_Edit"), None, None, self.mh),
1528
            ('cut', None, _("_Cut"), "<Ctrl>x", None, self.mh),
1529
            ('copy', None, _("_Copy"), "<Ctrl>c", None, self.mh),
1530
            ('paste', None, _("_Paste"), "<Ctrl>v", None, self.mh),
1531
            ('delete', None, _("_Delete"), "Delete", None, self.mh),
1532
            ('move_up', None, _("Move _Up"), "<Control>Up", None, self.mh),
1533
            ('move_dn', None, _("Move Dow_n"), "<Control>Down", None, self.mh),
1534
            ('exchange', None, _("E_xchange"), "<Control><Shift>x", None, self.mh),
1535
            ('view', None, _("_View"), None, None, self.mh),
1536
            ('columns', None, _("Columns"), None, None, self.mh),
1537
            ('viewdeveloper', None, _("Developer"), None, None, self.mh),
1538
            ('devshowraw', None, _('Show raw memory'), "<Control><Shift>r", None, self.mh),
1539
            ('devdiffraw', None, _("Diff raw memories"), "<Control><Shift>d", None, self.mh),
1540
            ('devdifftab', None, _("Diff tabs"), "<Control><Shift>t", None, self.mh),
1541
            ('language', None, _("Change language"), None, None, self.mh),
1542
            ('radio', None, _("_Radio"), None, None, self.mh),
1543
            ('download', None, _("Download From Radio"), "<Alt>d", None, self.mh),
1544
            ('upload', None, _("Upload To Radio"), "<Alt>u", None, self.mh),
1545
            ('import', None, _("Import"), "<Alt>i", None, self.mh),
1546
            ('export', None, _("Export"), "<Alt>x", None, self.mh),
1547
            ('importsrc', None, _("Import from data source"), None, None, self.mh),
1548
            ('iradioreference', None, _("RadioReference.com"), None, None, self.mh),
1549
            ('irfinder', None, _("RFinder"), None, None, self.mh),
1550
            ('irbook', None, _("RepeaterBook"), None, None, self.mh),
1551
            ('ipr', None, _("przemienniki.net"), None, None, self.mh),
1552
            ('querysrc', None, _("Query data source"), None, None, self.mh),
1553
            ('qradioreference', None, _("RadioReference.com"), None, None, self.mh),
1554
            ('qrfinder', None, _("RFinder"), None, None, self.mh),
1555
            ('qpr', None, _("przemienniki.net"), None, None, self.mh),
1556
            ('qrbook', None, _("RepeaterBook"), None, None, self.mh),
1557
            ('export_chirp', None, _("CHIRP Native File"), None, None, self.mh),
1558
            ('export_csv', None, _("CSV File"), None, None, self.mh),
1559
            ('stock', None, _("Import from stock config"), None, None, self.mh),
1560
            ('channel_defaults', None, _("Channel defaults"), None, None, self.mh),
1561
            ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh),
1562
            ('help', None, _('Help'), None, None, self.mh),
1563
            ('about', gtk.STOCK_ABOUT, None, None, None, self.mh),
1564
            ('documentation', None, _("Documentation"), None, None, self.mh),
1565
            ]
1566

    
1567
        conf = config.get()
1568
        re = not conf.get_bool("no_report");
1569
        hu = conf.get_bool("hide_unused", "memedit", default=True)
1570
        dv = conf.get_bool("developer", "state")
1571
        st = not conf.get_bool("no_smart_tmode", "memedit")
1572

    
1573
        toggles = [\
1574
            ('report', None, _("Report statistics"), None, None, self.mh, re),
1575
            ('hide_unused', None, _("Hide Unused Fields"), None, None, self.mh, hu),
1576
            ('no_smart_tmode', None, _("Smart Tone Modes"), None, None, self.mh, st),
1577
            ('developer', None, _("Enable Developer Functions"), None, None, self.mh, dv),
1578
            ]
1579

    
1580
        self.menu_uim = gtk.UIManager()
1581
        self.menu_ag = gtk.ActionGroup("MenuBar")
1582
        self.menu_ag.add_actions(actions)
1583
        self.menu_ag.add_toggle_actions(toggles)
1584

    
1585
        self.menu_uim.insert_action_group(self.menu_ag, 0)
1586
        self.menu_uim.add_ui_from_string(menu_xml)
1587

    
1588
        self.add_accel_group(self.menu_uim.get_accel_group())
1589

    
1590
        self.recentmenu = self.menu_uim.get_widget("/MenuBar/file/recent")
1591

    
1592
        # Initialize
1593
        self.do_toggle_developer(self.menu_ag.get_action("developer"))
1594

    
1595
        return self.menu_uim.get_widget("/MenuBar")
1596

    
1597
    def make_tabs(self):
1598
        self.tabs = gtk.Notebook()
1599

    
1600
        return self.tabs        
1601

    
1602
    def close_out(self):
1603
        num = self.tabs.get_n_pages()
1604
        while num > 0:
1605
            num -= 1
1606
            print "Closing %i" % num
1607
            try:
1608
                self.do_close(self.tabs.get_nth_page(num))
1609
            except ModifiedError:
1610
                return False
1611

    
1612
        gtk.main_quit()
1613

    
1614
        return True
1615

    
1616
    def make_status_bar(self):
1617
        box = gtk.HBox(False, 2)
1618

    
1619
        self.sb_general = gtk.Statusbar()
1620
        self.sb_general.set_has_resize_grip(False)
1621
        self.sb_general.show()
1622
        box.pack_start(self.sb_general, 1,1,1)
1623
        
1624
        self.sb_radio = gtk.Statusbar()
1625
        self.sb_radio.set_has_resize_grip(True)
1626
        self.sb_radio.show()
1627
        box.pack_start(self.sb_radio, 1,1,1)
1628

    
1629
        box.show()
1630
        return box
1631

    
1632
    def ev_delete(self, window, event):
1633
        if not self.close_out():
1634
            return True # Don't exit
1635

    
1636
    def ev_destroy(self, window):
1637
        if not self.close_out():
1638
            return True # Don't exit
1639

    
1640
    def setup_extra_hotkeys(self):
1641
        accelg = self.menu_uim.get_accel_group()
1642

    
1643
        memedit = lambda a: self.get_current_editorset().editors["memedit"].hotkey(a)
1644

    
1645
        actions = [
1646
            # ("action_name", "key", function)
1647
            ]
1648

    
1649
        for name, key, fn in actions:
1650
            a = gtk.Action(name, name, name, "")
1651
            a.connect("activate", fn)
1652
            self.menu_ag.add_action_with_accel(a, key)
1653
            a.set_accel_group(accelg)
1654
            a.connect_accelerator()
1655
        
1656
    def _set_icon(self):
1657
        execpath = platform.get_platform().executable_path()
1658
        path = os.path.abspath(os.path.join(execpath, "share", "chirp.png"))
1659
        if not os.path.exists(path):
1660
            path = "/usr/share/pixmaps/chirp.png"
1661

    
1662
        if os.path.exists(path):
1663
            self.set_icon_from_file(path)
1664
        else:
1665
            print "Icon %s not found" % path
1666

    
1667
    def _updates(self, version):
1668
        if not version:
1669
            return
1670

    
1671
        if version == CHIRP_VERSION:
1672
            return
1673

    
1674
        print "Server reports version %s is available" % version
1675

    
1676
        # Report new updates every seven days
1677
        intv = 3600 * 24 * 7
1678

    
1679
        if CONF.is_defined("last_update_check", "state") and \
1680
             (time.time() - CONF.get_int("last_update_check", "state")) < intv:
1681
            return
1682

    
1683
        CONF.set_int("last_update_check", int(time.time()), "state")
1684
        d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self,
1685
                              type=gtk.MESSAGE_INFO)
1686
        d.set_property("text",
1687
                       _("A new version of CHIRP is available: " +
1688
                         "{ver}. ".format(ver=version) +
1689
                         "It is recommended that you upgrade, so " +
1690
                         "go to http://chirp.danplanet.com soon!"))
1691
        d.run()
1692
        d.destroy()
1693

    
1694
    def _init_macos(self, menu_bar):
1695
        try:
1696
            import gtk_osxapplication
1697
            macapp = gtk_osxapplication.OSXApplication()
1698
        except ImportError, e:
1699
            print "No MacOS support: %s" % e
1700
            return
1701
        
1702
        menu_bar.hide()
1703
        macapp.set_menu_bar(menu_bar)
1704

    
1705
        quititem = self.menu_uim.get_widget("/MenuBar/file/quit")
1706
        quititem.hide()
1707

    
1708
        aboutitem = self.menu_uim.get_widget("/MenuBar/help/about")
1709
        macapp.insert_app_menu_item(aboutitem, 0)
1710

    
1711
        documentationitem = self.menu_uim.get_widget("/MenuBar/help/documentation")
1712
        macapp.insert_app_menu_item(documentationitem, 0)
1713
        
1714
        macapp.set_use_quartz_accelerators(False)
1715
        macapp.ready()
1716

    
1717
        print "Initialized MacOS support"
1718

    
1719
    def __init__(self, *args, **kwargs):
1720
        gtk.Window.__init__(self, *args, **kwargs)
1721

    
1722
        def expose(window, event):
1723
            allocation = window.get_allocation()
1724
            CONF.set_int("window_w", allocation.width, "state")
1725
            CONF.set_int("window_h", allocation.height, "state")
1726
        self.connect("expose_event", expose)
1727

    
1728
        def state_change(window, event):
1729
            CONF.set_bool(
1730
                "window_maximized",
1731
                event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED,
1732
                "state")
1733
        self.connect("window-state-event", state_change)
1734

    
1735
        d = CONF.get("last_dir", "state")
1736
        if d and os.path.isdir(d):
1737
            platform.get_platform().set_last_dir(d)
1738

    
1739
        vbox = gtk.VBox(False, 2)
1740

    
1741
        self._recent = []
1742

    
1743
        self.menu_ag = None
1744
        mbar = self.make_menubar()
1745

    
1746
        if os.name != "nt":
1747
            self._set_icon() # Windows gets the icon from the exe
1748
            if os.uname()[0] == "Darwin":
1749
                self._init_macos(mbar)
1750

    
1751
        vbox.pack_start(mbar, 0, 0, 0)
1752

    
1753
        self.tabs = None
1754
        tabs = self.make_tabs()
1755
        tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p))
1756
        tabs.connect("page-removed", lambda *a: self.ev_tab_switched())
1757
        tabs.show()
1758
        self.ev_tab_switched()
1759
        vbox.pack_start(tabs, 1, 1, 1)
1760

    
1761
        vbox.pack_start(self.make_status_bar(), 0, 0, 0)
1762

    
1763
        vbox.show()
1764

    
1765
        self.add(vbox)
1766

    
1767
        try:
1768
            width = CONF.get_int("window_w", "state")
1769
            height = CONF.get_int("window_h", "state")
1770
        except Exception:
1771
            width = 800
1772
            height = 600
1773

    
1774
        self.set_default_size(width, height)
1775
        if CONF.get_bool("window_maximized", "state"):
1776
            self.maximize()
1777
        self.set_title("CHIRP")
1778

    
1779
        self.connect("delete_event", self.ev_delete)
1780
        self.connect("destroy", self.ev_destroy)
1781

    
1782
        if not CONF.get_bool("warned_about_reporting") and \
1783
                not CONF.get_bool("no_report"):
1784
            d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self)
1785
            d.set_markup("<b><big>" +
1786
                         _("Error reporting is enabled") +
1787
                         "</big></b>")
1788
            d.format_secondary_markup(\
1789
                _("If you wish to disable this feature you may do so in "
1790
                  "the <u>Help</u> menu"))
1791
            d.run()
1792
            d.destroy()
1793
        CONF.set_bool("warned_about_reporting", True)
1794

    
1795
        self.update_recent_files()
1796
        self.update_stock_configs()
1797
        self.setup_extra_hotkeys()
1798

    
1799
        def updates_callback(ver):
1800
            gobject.idle_add(self._updates, ver)
1801

    
1802
        if not CONF.get_bool("skip_update_check", "state"):
1803
            reporting.check_for_updates(updates_callback)