# Copyright 2008 Dan Smith # Copyright 2012 Tom Hayward # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from datetime import datetime import os import tempfile import urllib import webbrowser from glob import glob import shutil import time import logging import gtk import gobject import sys from chirp.ui import inputdialog, common from chirp import platform, directory, util from chirp.drivers import generic_xml, generic_csv, repeaterbook from chirp.drivers import ic9x, kenwood_live, idrp, vx7, vx5, vx6 from chirp.drivers import icf, ic9x_icf from chirp import CHIRP_VERSION, chirp_common, detect, errors from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips from chirp.ui import bandplans gobject.threads_init() LOG = logging.getLogger(__name__) if __name__ == "__main__": sys.path.insert(0, "..") try: import serial except ImportError, e: common.log_exception() common.show_error("\nThe Pyserial module is not installed!") CONF = config.get() KEEP_RECENT = 8 RB_BANDS = { "--All--": 0, "10 meters (29MHz)": 29, "6 meters (54MHz)": 5, "2 meters (144MHz)": 14, "1.25 meters (220MHz)": 22, "70 centimeters (440MHz)": 4, "33 centimeters (900MHz)": 9, "23 centimeters (1.2GHz)": 12, } def key_bands(band): if band.startswith("-"): return -1 amount, units, mhz = band.split(" ") scale = units == "meters" and 100 or 1 return 100000 - (float(amount) * scale) class ModifiedError(Exception): pass class ChirpMain(gtk.Window): def get_current_editorset(self): page = self.tabs.get_current_page() if page is not None: return self.tabs.get_nth_page(page) else: return None def ev_tab_switched(self, pagenum=None): def set_action_sensitive(action, sensitive): self.menu_ag.get_action(action).set_sensitive(sensitive) if pagenum is not None: eset = self.tabs.get_nth_page(pagenum) else: eset = self.get_current_editorset() upload_sens = bool(eset and isinstance(eset.radio, chirp_common.CloneModeRadio)) if not eset or isinstance(eset.radio, chirp_common.LiveRadio): save_sens = False elif isinstance(eset.radio, chirp_common.NetworkSourceRadio): save_sens = False else: save_sens = True for i in ["import", "importsrc", "stock"]: set_action_sensitive(i, eset is not None and not eset.get_read_only()) for i in ["save", "saveas"]: set_action_sensitive(i, save_sens) for i in ["upload"]: set_action_sensitive(i, upload_sens) for i in ["cancelq"]: set_action_sensitive(i, eset is not None and not save_sens) for i in ["export", "close", "columns", "irbook", "irfinder", "move_up", "move_dn", "exchange", "iradioreference", "cut", "copy", "paste", "delete", "viewdeveloper", "all", "properties"]: set_action_sensitive(i, eset is not None) def ev_status(self, editorset, msg): self.sb_radio.pop(0) self.sb_radio.push(0, msg) def ev_usermsg(self, editorset, msg): self.sb_general.pop(0) self.sb_general.push(0, msg) def ev_editor_selected(self, editorset, editortype): mappings = { "memedit": ["view", "edit"], } for _editortype, actions in mappings.items(): for _action in actions: action = self.menu_ag.get_action(_action) action.set_sensitive(editortype.startswith(_editortype)) def _connect_editorset(self, eset): eset.connect("want-close", self.do_close) eset.connect("status", self.ev_status) eset.connect("usermsg", self.ev_usermsg) eset.connect("editor-selected", self.ev_editor_selected) def do_diff_radio(self): if self.tabs.get_n_pages() < 2: common.show_error("Diff tabs requires at least two open tabs!") return esets = [] for i in range(0, self.tabs.get_n_pages()): esets.append(self.tabs.get_nth_page(i)) d = gtk.Dialog(title="Diff Radios", buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), parent=self) label = gtk.Label("") label.set_markup("-1 for either Mem # does a full-file hex " + "dump with diffs highlighted.\n" + "-2 for first Mem # shows " + "only the diffs.") d.vbox.pack_start(label, True, True, 0) label.show() choices = [] for eset in esets: choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR, eset.rthread.radio.MODEL, eset.filename)) choice_a = miscwidgets.make_choice(choices, False, choices[0]) choice_a.show() chan_a = gtk.SpinButton() chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0) chan_a.show() hbox = gtk.HBox(False, 3) hbox.pack_start(choice_a, 1, 1, 1) hbox.pack_start(chan_a, 0, 0, 0) hbox.show() d.vbox.pack_start(hbox, 0, 0, 0) choice_b = miscwidgets.make_choice(choices, False, choices[1]) choice_b.show() chan_b = gtk.SpinButton() chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0) chan_b.show() hbox = gtk.HBox(False, 3) hbox.pack_start(choice_b, 1, 1, 1) hbox.pack_start(chan_b, 0, 0, 0) hbox.show() d.vbox.pack_start(hbox, 0, 0, 0) r = d.run() sel_a = choice_a.get_active_text() sel_chan_a = chan_a.get_value() sel_b = choice_b.get_active_text() sel_chan_b = chan_b.get_value() d.destroy() if r == gtk.RESPONSE_CANCEL: return if sel_a == sel_b: common.show_error("Can't diff the same tab!") return LOG.debug("Selected %s@%i and %s@%i" % (sel_a, sel_chan_a, sel_b, sel_chan_b)) name_a = os.path.basename(sel_a) name_a = name_a[:name_a.rindex(")")] name_b = os.path.basename(sel_b) name_b = name_b[:name_b.rindex(")")] diffwintitle = "%s@%i diff %s@%i" % ( name_a, sel_chan_a, name_b, sel_chan_b) eset_a = esets[choices.index(sel_a)] eset_b = esets[choices.index(sel_b)] def _show_diff(mem_b, mem_a): # Step 3: Show the diff diff = common.simple_diff(mem_a, mem_b) common.show_diff_blob(diffwintitle, diff) def _get_mem_b(mem_a): # Step 2: Get memory b job = common.RadioJob(_show_diff, "get_raw_memory", int(sel_chan_b)) job.set_cb_args(mem_a) eset_b.rthread.submit(job) if sel_chan_a >= 0 and sel_chan_b >= 0: # Diff numbered memory # Step 1: Get memory a job = common.RadioJob(_get_mem_b, "get_raw_memory", int(sel_chan_a)) eset_a.rthread.submit(job) elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\ isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio): # Diff whole (can do this without a job, since both are clone-mode) try: addrfmt = CONF.get('hexdump_addrfmt', section='developer', raw=True) except: pass a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(), addrfmt=addrfmt) b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(), addrfmt=addrfmt) if sel_chan_a == -2: diffsonly = True else: diffsonly = False common.show_diff_blob(diffwintitle, common.simple_diff(a, b, diffsonly)) else: common.show_error("Cannot diff whole live-mode radios!") def do_new(self): eset = editorset.EditorSet(_("Untitled") + ".csv", self) self._connect_editorset(eset) eset.prime() eset.show() tab = self.tabs.append_page(eset, eset.get_tab_label()) self.tabs.set_current_page(tab) def _do_manual_select(self, filename): radiolist = {} for drv, radio in directory.DRV_TO_RADIO.items(): if not issubclass(radio, chirp_common.CloneModeRadio): continue radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv lab = gtk.Label("""Unable to detect model! 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. """) lab.set_justify(gtk.JUSTIFY_FILL) lab.set_line_wrap(True) lab.set_use_markup(True) lab.show() choice = miscwidgets.make_choice(sorted(radiolist.keys()), False, sorted(radiolist.keys())[0]) d = gtk.Dialog(title="Detection Failed", buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) d.vbox.pack_start(lab, 0, 0, 0) d.vbox.pack_start(choice, 0, 0, 0) d.vbox.set_spacing(5) choice.show() d.set_default_size(400, 200) # d.set_resizable(False) r = d.run() d.destroy() if r != gtk.RESPONSE_OK: return try: rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]] return rc(filename) except: return def do_open(self, fname=None, tempname=None): if not fname: types = [(_("All files") + " (*.*)", "*"), (_("CHIRP Radio Images") + " (*.img)", "*.img"), (_("CHIRP Files") + " (*.chirp)", "*.chirp"), (_("CSV Files") + " (*.csv)", "*.csv"), (_("DAT Files") + " (*.dat)", "*.dat"), (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), (_("ICF Files") + " (*.icf)", "*.icf"), (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"), ] fname = platform.get_platform().gui_open_file(types=types) if not fname: return self.record_recent_file(fname) if icf.is_icf_file(fname): a = common.ask_yesno_question( _("ICF files cannot be edited, only displayed or imported " "into another file. Open in read-only mode?"), self) if not a: return read_only = True else: read_only = False if icf.is_9x_icf(fname): # We have to actually instantiate the IC9xICFRadio to get its # sub-devices radio = ic9x_icf.IC9xICFRadio(fname) else: try: radio = directory.get_radio_by_image(fname) except errors.ImageDetectFailed: radio = self._do_manual_select(fname) if not radio: return LOG.debug("Manually selected %s" % radio) except Exception, e: common.log_exception() common.show_error(os.path.basename(fname) + ": " + str(e)) return first_tab = False try: eset = editorset.EditorSet(radio, self, filename=fname, tempname=tempname) except Exception, e: common.log_exception() common.show_error( _("There was an error opening {fname}: {error}").format( fname=fname, error=e)) return eset.set_read_only(read_only) self._connect_editorset(eset) eset.show() self.tabs.append_page(eset, eset.get_tab_label()) if hasattr(eset.rthread.radio, "errors") and \ eset.rthread.radio.errors: msg = _("{num} errors during open:").format( num=len(eset.rthread.radio.errors)) common.show_error_text(msg, "\r\n".join(eset.rthread.radio.errors)) def do_live_warning(self, radio): d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) d.set_markup("" + _("Note:") + "") msg = _("The {vendor} {model} operates in live mode. " "This means that any changes you make are immediately sent " "to the radio. Because of this, you cannot perform the " "Save or Upload operations. If you wish to " "edit the contents offline, please Export to a CSV " "file, using the File menu.") msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL) d.format_secondary_markup(msg) again = gtk.CheckButton(_("Don't show this again")) again.show() d.vbox.pack_start(again, 0, 0, 0) d.run() CONF.set_bool("live_mode", again.get_active(), "noconfirm") d.destroy() def do_open_live(self, radio, tempname=None, read_only=False): eset = editorset.EditorSet(radio, self, tempname=tempname) eset.connect("want-close", self.do_close) eset.connect("status", self.ev_status) eset.set_read_only(read_only) eset.show() self.tabs.append_page(eset, eset.get_tab_label()) if isinstance(radio, chirp_common.LiveRadio): reporting.report_model_usage(radio, "live", True) if not CONF.get_bool("live_mode", "noconfirm"): self.do_live_warning(radio) def do_save(self, eset=None): if not eset: eset = self.get_current_editorset() # For usability, allow Ctrl-S to short-circuit to Save-As if # we are working on a yet-to-be-saved image if not os.path.exists(eset.filename): return self.do_saveas() eset.save() def do_saveas(self): eset = self.get_current_editorset() label = _("{vendor} {model} image file").format( vendor=eset.radio.VENDOR, model=eset.radio.MODEL) defname_format = CONF.get("default_filename", "global") or \ "{vendor}_{model}_{date}" defname = defname_format.format( vendor=eset.radio.VENDOR, model=eset.radio.MODEL, date=datetime.now().strftime('%Y%m%d') ).replace('/', '_') types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION, eset.radio.FILE_EXTENSION)] if isinstance(eset.radio, vx7.VX7Radio): types += [(_("VX7 Commander") + " (*.vx7)", "vx7")] elif isinstance(eset.radio, vx6.VX6Radio): types += [(_("VX6 Commander") + " (*.vx6)", "vx6")] elif isinstance(eset.radio, vx5.VX5Radio): types += [(_("EVE") + " (*.eve)", "eve")] types += [(_("VX5 Commander") + " (*.vx5)", "vx5")] while True: fname = platform.get_platform().gui_save_file(default_name=defname, types=types) if not fname: return if os.path.exists(fname): dlg = inputdialog.OverwriteDialog(fname) owrite = dlg.run() dlg.destroy() if owrite == gtk.RESPONSE_OK: break else: break try: eset.save(fname) except Exception, e: d = inputdialog.ExceptionDialog(e) d.run() d.destroy() def cb_clonein(self, radio, emsg=None): radio.pipe.close() reporting.report_model_usage(radio, "download", bool(emsg)) if not emsg: self.do_open_live(radio, tempname="(" + _("Untitled") + ")") else: d = inputdialog.ExceptionDialog(emsg) d.run() d.destroy() def cb_cloneout(self, radio, emsg=None): radio.pipe.close() reporting.report_model_usage(radio, "upload", True) if emsg: d = inputdialog.ExceptionDialog(emsg) d.run() d.destroy() def _get_recent_list(self): recent = [] for i in range(0, KEEP_RECENT): fn = CONF.get("recent%i" % i, "state") if fn: recent.append(fn) return recent def _set_recent_list(self, recent): for fn in recent: CONF.set("recent%i" % recent.index(fn), fn, "state") def update_recent_files(self): i = 0 for fname in self._get_recent_list(): action_name = "recent%i" % i path = "/MenuBar/file/recent" old_action = self.menu_ag.get_action(action_name) if old_action: self.menu_ag.remove_action(old_action) file_basename = os.path.basename(fname).replace("_", "__") action = gtk.Action( action_name, "_%i. %s" % (i + 1, file_basename), _("Open recent file {name}").format(name=fname), "") action.connect("activate", lambda a, f: self.do_open(f), fname) mid = self.menu_uim.new_merge_id() self.menu_uim.add_ui(mid, path, action_name, action_name, gtk.UI_MANAGER_MENUITEM, False) self.menu_ag.add_action(action) i += 1 def record_recent_file(self, filename): recent_files = self._get_recent_list() if filename not in recent_files: if len(recent_files) == KEEP_RECENT: del recent_files[-1] recent_files.insert(0, filename) self._set_recent_list(recent_files) self.update_recent_files() def import_stock_config(self, action, config): eset = self.get_current_editorset() count = eset.do_import(config) def copy_shipped_stock_configs(self, stock_dir): basepath = platform.get_platform().find_resource("stock_configs") files = glob(os.path.join(basepath, "*.csv")) for fn in files: if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))): LOG.info("Skipping existing stock config") continue try: shutil.copy(fn, stock_dir) LOG.debug("Copying %s -> %s" % (fn, stock_dir)) except Exception, e: LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e)) return False return True def update_stock_configs(self): stock_dir = platform.get_platform().config_file("stock_configs") if not os.path.isdir(stock_dir): try: os.mkdir(stock_dir) except Exception, e: LOG.error("Unable to create directory: %s" % stock_dir) return if not self.copy_shipped_stock_configs(stock_dir): return def _do_import_action(config): name = os.path.splitext(os.path.basename(config))[0] action_name = "stock-%i" % configs.index(config) path = "/MenuBar/radio/stock" action = gtk.Action(action_name, name, _("Import stock " "configuration {name}").format(name=name), "") action.connect("activate", self.import_stock_config, config) mid = self.menu_uim.new_merge_id() mid = self.menu_uim.add_ui(mid, path, action_name, action_name, gtk.UI_MANAGER_MENUITEM, False) self.menu_ag.add_action(action) def _do_open_action(config): name = os.path.splitext(os.path.basename(config))[0] action_name = "openstock-%i" % configs.index(config) path = "/MenuBar/file/openstock" action = gtk.Action(action_name, name, _("Open stock " "configuration {name}").format(name=name), "") action.connect("activate", lambda a, c: self.do_open(c), config) mid = self.menu_uim.new_merge_id() mid = self.menu_uim.add_ui(mid, path, action_name, action_name, gtk.UI_MANAGER_MENUITEM, False) self.menu_ag.add_action(action) configs = glob(os.path.join(stock_dir, "*.csv")) for config in configs: _do_import_action(config) _do_open_action(config) def _confirm_experimental(self, rclass): sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass) if CONF.is_defined(sql_key, "state") and \ not CONF.get_bool(sql_key, "state"): return True title = _("Proceed with experimental driver?") text = rclass.get_prompts().experimental msg = _("This radio's driver is experimental. " "Do you want to proceed?") resp, squelch = common.show_warning(msg, text, title=title, buttons=gtk.BUTTONS_YES_NO, can_squelch=True) if resp == gtk.RESPONSE_YES: CONF.set_bool(sql_key, not squelch, "state") return resp == gtk.RESPONSE_YES def _show_information(self, radio, message): if message is None: return if CONF.get_bool("clone_information", "noconfirm"): return d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) d.set_markup("" + _("{name} Information").format( name=radio.get_name()) + "") msg = _("{information}").format(information=message) _again_msg = "Don't show information for any radio again" d.format_secondary_markup(msg) again = gtk.CheckButton(_(_again_msg)) again.show() again.connect("toggled", lambda action: self.infomenu.set_active(not action.get_active())) d.vbox.pack_start(again, 0, 0, 0) h_button_box = d.vbox.get_children()[2] try: ok_button = h_button_box.get_children()[0] ok_button.grab_default() ok_button.grab_focus() except AttributeError: # don't grab focus on GTK+ 2.0 pass d.run() d.destroy() def _show_instructions(self, radio, message): if message is None: return if CONF.get_bool("clone_instructions", "noconfirm"): return d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) d.set_markup("" + _("{name} Instructions").format( name=radio.get_name()) + "") msg = _("{instructions}").format(instructions=message) _again_msg = "Don't show instructions for any radio again" d.format_secondary_markup(msg) again = gtk.CheckButton(_(_again_msg)) again.show() again.connect("toggled", lambda action: self.clonemenu.set_active(not action.get_active())) d.vbox.pack_start(again, 0, 0, 0) h_button_box = d.vbox.get_children()[2] try: ok_button = h_button_box.get_children()[0] ok_button.grab_default() ok_button.grab_focus() except AttributeError: # don't grab focus on GTK+ 2.0 pass d.run() d.destroy() def do_download(self, port=None, rtype=None): d = clone.CloneSettingsDialog(parent=self) settings = d.run() d.destroy() if not settings: return rclass = settings.radio_class if issubclass(rclass, chirp_common.ExperimentalRadio) and \ not self._confirm_experimental(rclass): # User does not want to proceed with experimental driver return if rclass.get_prompts().display_info is True: self._show_information(rclass, rclass.get_prompts().info) self._show_instructions(rclass, rclass.get_prompts().pre_download) LOG.debug("User selected %s %s on port %s" % (rclass.VENDOR, rclass.MODEL, settings.port)) try: ser = serial.Serial(port=settings.port, baudrate=rclass.BAUD_RATE, rtscts=rclass.HARDWARE_FLOW, timeout=0.25) ser.flushInput() except serial.SerialException, e: d = inputdialog.ExceptionDialog(e) d.run() d.destroy() return radio = settings.radio_class(ser) fn = tempfile.mktemp() if isinstance(radio, chirp_common.CloneModeRadio): ct = clone.CloneThread(radio, "in", cb=self.cb_clonein, parent=self) ct.start() else: self.do_open_live(radio) def do_upload(self, port=None, rtype=None): eset = self.get_current_editorset() radio = eset.radio settings = clone.CloneSettings() settings.radio_class = radio.__class__ d = clone.CloneSettingsDialog(settings, parent=self) settings = d.run() d.destroy() if not settings: return prompts = radio.get_prompts() if prompts.display_pre_upload_prompt_before_opening_port is True: LOG.debug("Opening port after pre_upload prompt.") self._show_instructions(radio, prompts.pre_upload) if isinstance(radio, chirp_common.ExperimentalRadio) and \ not self._confirm_experimental(radio.__class__): # User does not want to proceed with experimental driver return try: ser = serial.Serial(port=settings.port, baudrate=radio.BAUD_RATE, rtscts=radio.HARDWARE_FLOW, timeout=0.25) ser.flushInput() except serial.SerialException, e: d = inputdialog.ExceptionDialog(e) d.run() d.destroy() return if prompts.display_pre_upload_prompt_before_opening_port is False: LOG.debug("Opening port before pre_upload prompt.") self._show_instructions(radio, prompts.pre_upload) radio.set_pipe(ser) ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self) ct.start() def do_close(self, tab_child=None): if tab_child: eset = tab_child else: eset = self.get_current_editorset() if not eset: return False if eset.is_modified(): dlg = miscwidgets.YesNoDialog( title=_("Save Changes?"), parent=self, buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) dlg.set_text(_("File is modified, save changes before closing?")) res = dlg.run() dlg.destroy() if res == gtk.RESPONSE_YES: self.do_save(eset) elif res != gtk.RESPONSE_NO: raise ModifiedError() eset.rthread.stop() eset.rthread.join() eset.prepare_close() if eset.radio.pipe: eset.radio.pipe.close() if isinstance(eset.radio, chirp_common.LiveRadio): action = self.menu_ag.get_action("openlive") if action: action.set_sensitive(True) page = self.tabs.page_num(eset) if page is not None: self.tabs.remove_page(page) return True def do_import(self): types = [(_("All files") + " (*.*)", "*"), (_("CHIRP Files") + " (*.chirp)", "*.chirp"), (_("CHIRP Radio Images") + " (*.img)", "*.img"), (_("CSV Files") + " (*.csv)", "*.csv"), (_("DAT Files") + " (*.dat)", "*.dat"), (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), (_("ICF Files") + " (*.icf)", "*.icf"), (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"), (_("Kenwood ITM Files") + " (*.itm)", "*.itm"), (_("Travel Plus Files") + " (*.tpe)", "*.tpe"), (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")] filen = platform.get_platform().gui_open_file(types=types) if not filen: return eset = self.get_current_editorset() count = eset.do_import(filen) reporting.report_model_usage(eset.rthread.radio, "import", count > 0) def do_dmrmarc_prompt(self): fields = {"1City": (gtk.Entry(), lambda x: x), "2State": (gtk.Entry(), lambda x: x), "3Country": (gtk.Entry(), lambda x: x), } d = inputdialog.FieldDialog(title=_("DMR-MARC Repeater Database Dump"), parent=self) for k in sorted(fields.keys()): d.add_field(k[1:], fields[k][0]) fields[k][0].set_text(CONF.get(k[1:], "dmrmarc") or "") while d.run() == gtk.RESPONSE_OK: for k in sorted(fields.keys()): widget, validator = fields[k] try: if validator(widget.get_text()): CONF.set(k[1:], widget.get_text(), "dmrmarc") continue except Exception: pass d.destroy() return True d.destroy() return False def do_dmrmarc(self, do_import): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) if not self.do_dmrmarc_prompt(): self.window.set_cursor(None) return city = CONF.get("city", "dmrmarc") state = CONF.get("state", "dmrmarc") country = CONF.get("country", "dmrmarc") # Do this in case the import process is going to take a while # to make sure we process events leading up to this gtk.gdk.window_process_all_updates() while gtk.events_pending(): gtk.main_iteration(False) if do_import: eset = self.get_current_editorset() dmrmarcstr = "dmrmarc://%s/%s/%s" % (city, state, country) eset.do_import(dmrmarcstr) else: try: from chirp import dmrmarc radio = dmrmarc.DMRMARCRadio(None) radio.set_params(city, state, country) self.do_open_live(radio, read_only=True) except errors.RadioError, e: common.show_error(e) self.window.set_cursor(None) def do_repeaterbook_political_prompt(self): if not CONF.get_bool("has_seen_credit", "repeaterbook"): d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) d.set_markup("RepeaterBook\r\n" + "North American Repeater Directory") d.format_secondary_markup("For more information about this " + "free service, please go to\r\n" + "http://www.repeaterbook.com") d.run() d.destroy() CONF.set_bool("has_seen_credit", True, "repeaterbook") default_state = "Oregon" default_county = "--All--" default_band = "--All--" try: try: code = int(CONF.get("state", "repeaterbook")) except: code = CONF.get("state", "repeaterbook") for k, v in fips.FIPS_STATES.items(): if code == v: default_state = k break code = CONF.get("county", "repeaterbook") items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items() for k, v in items: if code == v: default_county = k break code = int(CONF.get("band", "repeaterbook")) for k, v in RB_BANDS.items(): if code == v: default_band = k break except: pass state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()), False, default_state) county = miscwidgets.make_choice( sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()), False, default_county) band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands), False, default_band) def _changed(box, county): state = fips.FIPS_STATES[box.get_active_text()] county.get_model().clear() for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()): county.append_text(fips_county) county.set_active(0) state.connect("changed", _changed, county) d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self) d.add_field("State", state) d.add_field("County", county) d.add_field("Band", band) r = d.run() d.destroy() if r != gtk.RESPONSE_OK: return False code = fips.FIPS_STATES[state.get_active_text()] county_id = fips.FIPS_COUNTIES[code][county.get_active_text()] freq = RB_BANDS[band.get_active_text()] CONF.set("state", str(code), "repeaterbook") CONF.set("county", str(county_id), "repeaterbook") CONF.set("band", str(freq), "repeaterbook") return True def do_repeaterbook_political(self, do_import): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) if not self.do_repeaterbook_political_prompt(): self.window.set_cursor(None) return try: code = "%02i" % int(CONF.get("state", "repeaterbook")) except: try: code = CONF.get("state", "repeaterbook") except: code = '41' # Oregon default try: county = CONF.get("county", "repeaterbook") except: county = '%' # --All-- default try: band = int(CONF.get("band", "repeaterbook")) except: band = 14 # 2m default query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \ "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" query = query % (code, band and band or "%%", county and county or "%%") print query # Do this in case the import process is going to take a while # to make sure we process events leading up to this gtk.gdk.window_process_all_updates() while gtk.events_pending(): gtk.main_iteration(False) fn = tempfile.mktemp(".csv") filename, headers = urllib.urlretrieve(query, fn) if not os.path.exists(filename): LOG.error("Failed, headers were: %s", headers) common.show_error(_("RepeaterBook query failed")) self.window.set_cursor(None) return try: # Validate CSV radio = repeaterbook.RBRadio(filename) if radio.errors: reporting.report_misc_error("repeaterbook", ("query=%s\n" % query) + ("\n") + ("\n".join(radio.errors))) except errors.InvalidDataError, e: common.show_error(str(e)) self.window.set_cursor(None) return except Exception, e: common.log_exception() reporting.report_model_usage(radio, "import", True) self.window.set_cursor(None) if do_import: eset = self.get_current_editorset() count = eset.do_import(filename) else: self.do_open_live(radio, read_only=True) def do_repeaterbook_proximity_prompt(self): default_band = "--All--" try: code = int(CONF.get("band", "repeaterbook")) for k, v in RB_BANDS.items(): if code == v: default_band = k break except: pass fields = {"1Location": (gtk.Entry(), lambda x: x.get_text()), "2Distance": (gtk.Entry(), lambda x: x.get_text()), "3Band": (miscwidgets.make_choice( sorted(RB_BANDS.keys(), key=key_bands), False, default_band), lambda x: RB_BANDS[x.get_active_text()]), } d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self) for k in sorted(fields.keys()): d.add_field(k[1:], fields[k][0]) if isinstance(fields[k][0], gtk.Entry): fields[k][0].set_text( CONF.get(k[1:].lower(), "repeaterbook") or "") while d.run() == gtk.RESPONSE_OK: valid = True for k, (widget, fn) in fields.items(): try: CONF.set(k[1:].lower(), str(fn(widget)), "repeaterbook") continue except: pass common.show_error("Invalid value for %s" % k[1:]) valid = False break if valid: d.destroy() return True d.destroy() return False def do_repeaterbook_proximity(self, do_import): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) if not self.do_repeaterbook_proximity_prompt(): self.window.set_cursor(None) return loc = CONF.get("location", "repeaterbook") try: dist = int(CONF.get("distance", "repeaterbook")) except: dist = 20 try: band = int(CONF.get("band", "repeaterbook")) or '%' band = str(band) except: band = '%' query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \ "app_direct.php?loc=%s&band=%s&dist=%s" % (loc, band, dist) print query # Do this in case the import process is going to take a while # to make sure we process events leading up to this gtk.gdk.window_process_all_updates() while gtk.events_pending(): gtk.main_iteration(False) fn = tempfile.mktemp(".csv") filename, headers = urllib.urlretrieve(query, fn) if not os.path.exists(filename): LOG.error("Failed, headers were: %s", headers) common.show_error(_("RepeaterBook query failed")) self.window.set_cursor(None) return try: # Validate CSV radio = repeaterbook.RBRadio(filename) if radio.errors: reporting.report_misc_error("repeaterbook", ("query=%s\n" % query) + ("\n") + ("\n".join(radio.errors))) except errors.InvalidDataError, e: common.show_error(str(e)) self.window.set_cursor(None) return except Exception, e: common.log_exception() reporting.report_model_usage(radio, "import", True) self.window.set_cursor(None) if do_import: eset = self.get_current_editorset() count = eset.do_import(filename) else: self.do_open_live(radio, read_only=True) def do_przemienniki_prompt(self): d = inputdialog.FieldDialog(title='przemienniki.net query', parent=self) fields = { "Country": (miscwidgets.make_choice( ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi', 'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se', 'sk', 'ua', 'uk'], False), lambda x: str(x.get_active_text())), "Band": (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm', '23cm', '13cm', '3cm'], False, '2m'), lambda x: str(x.get_active_text())), "Mode": (miscwidgets.make_choice(['fm', 'dv'], False), lambda x: str(x.get_active_text())), "Only Working": (miscwidgets.make_choice(['', 'yes'], False), lambda x: str(x.get_active_text())), "Latitude": (gtk.Entry(), lambda x: float(x.get_text())), "Longitude": (gtk.Entry(), lambda x: float(x.get_text())), "Range": (gtk.Entry(), lambda x: int(x.get_text())), } for name in sorted(fields.keys()): value, fn = fields[name] d.add_field(name, value) while d.run() == gtk.RESPONSE_OK: query = "http://przemienniki.net/export/chirp.csv?" args = [] for name, (value, fn) in fields.items(): if isinstance(value, gtk.Entry): contents = value.get_text() else: contents = value.get_active_text() if contents: try: _value = fn(value) except ValueError: common.show_error(_("Invalid value for %s") % name) query = None continue args.append("=".join((name.replace(" ", "").lower(), contents))) query += "&".join(args) LOG.debug(query) d.destroy() return query d.destroy() return query def do_przemienniki(self, do_import): url = self.do_przemienniki_prompt() if not url: return fn = tempfile.mktemp(".csv") filename, headers = urllib.urlretrieve(url, fn) if not os.path.exists(filename): LOG.error("Failed, headers were: %s", str(headers)) common.show_error(_("Query failed")) return class PRRadio(generic_csv.CSVRadio, chirp_common.NetworkSourceRadio): VENDOR = "przemienniki.net" MODEL = "" try: radio = PRRadio(filename) except Exception, e: common.show_error(str(e)) return if do_import: eset = self.get_current_editorset() count = eset.do_import(filename) else: self.do_open_live(radio, read_only=True) def do_rfinder_prompt(self): fields = {"1Email": (gtk.Entry(), lambda x: "@" in x), "2Password": (gtk.Entry(), lambda x: x), "3Latitude": (gtk.Entry(), lambda x: float(x) < 90 and float(x) > -90), "4Longitude": (gtk.Entry(), lambda x: float(x) < 180 and float(x) > -180), "5Range_in_Miles": (gtk.Entry(), lambda x: int(x) > 0 and int(x) < 5000), } d = inputdialog.FieldDialog(title="RFinder Login", parent=self) for k in sorted(fields.keys()): d.add_field(k[1:].replace("_", " "), fields[k][0]) fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "") fields[k][0].set_visibility(k != "2Password") while d.run() == gtk.RESPONSE_OK: valid = True for k in sorted(fields.keys()): widget, validator = fields[k] try: if validator(widget.get_text()): CONF.set(k[1:], widget.get_text(), "rfinder") continue except Exception: pass common.show_error("Invalid value for %s" % k[1:]) valid = False break if valid: d.destroy() return True d.destroy() return False def do_rfinder(self, do_import): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) if not self.do_rfinder_prompt(): self.window.set_cursor(None) return lat = CONF.get_float("Latitude", "rfinder") lon = CONF.get_float("Longitude", "rfinder") passwd = CONF.get("Password", "rfinder") email = CONF.get("Email", "rfinder") miles = CONF.get_int("Range_in_Miles", "rfinder") # Do this in case the import process is going to take a while # to make sure we process events leading up to this gtk.gdk.window_process_all_updates() while gtk.events_pending(): gtk.main_iteration(False) if do_import: eset = self.get_current_editorset() rfstr = "rfinder://%s/%s/%f/%f/%i" % \ (email, passwd, lat, lon, miles) count = eset.do_import(rfstr) else: from chirp.drivers import rfinder radio = rfinder.RFinderRadio(None) radio.set_params((lat, lon), miles, email, passwd) self.do_open_live(radio, read_only=True) self.window.set_cursor(None) def do_radioreference_prompt(self): fields = {"1Username": (gtk.Entry(), lambda x: x), "2Password": (gtk.Entry(), lambda x: x), "3Zipcode": (gtk.Entry(), lambda x: x), } d = inputdialog.FieldDialog(title=_("RadioReference.com Query"), parent=self) for k in sorted(fields.keys()): d.add_field(k[1:], fields[k][0]) fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "") fields[k][0].set_visibility(k != "2Password") while d.run() == gtk.RESPONSE_OK: valid = True for k in sorted(fields.keys()): widget, validator = fields[k] try: if validator(widget.get_text()): CONF.set(k[1:], widget.get_text(), "radioreference") continue except Exception: pass common.show_error("Invalid value for %s" % k[1:]) valid = False break if valid: d.destroy() return True d.destroy() return False def do_radioreference(self, do_import): self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) if not self.do_radioreference_prompt(): self.window.set_cursor(None) return username = CONF.get("Username", "radioreference") passwd = CONF.get("Password", "radioreference") zipcode = CONF.get("Zipcode", "radioreference") # Do this in case the import process is going to take a while # to make sure we process events leading up to this gtk.gdk.window_process_all_updates() while gtk.events_pending(): gtk.main_iteration(False) if do_import: eset = self.get_current_editorset() rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd) count = eset.do_import(rrstr) else: try: from chirp import radioreference radio = radioreference.RadioReferenceRadio(None) radio.set_params(zipcode, username, passwd) self.do_open_live(radio, read_only=True) except errors.RadioError, e: common.show_error(e) self.window.set_cursor(None) def do_export(self): types = [(_("CSV Files") + " (*.csv)", "csv"), ] eset = self.get_current_editorset() if os.path.exists(eset.filename): base = os.path.basename(eset.filename) if "." in base: base = base[:base.rindex(".")] defname = base else: defname = "radio" filen = platform.get_platform().gui_save_file(default_name=defname, types=types) if not filen: return if os.path.exists(filen): dlg = inputdialog.OverwriteDialog(filen) owrite = dlg.run() dlg.destroy() if owrite != gtk.RESPONSE_OK: return os.remove(filen) count = eset.do_export(filen) reporting.report_model_usage(eset.rthread.radio, "export", count > 0) def do_about(self): d = gtk.AboutDialog() d.set_transient_for(self) import sys verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % ( ".".join([str(x) for x in gtk.gtk_version]), ".".join([str(x) for x in gtk.pygtk_version]), sys.version.split()[0]) # Set url hook to handle user activating a URL link in the about dialog gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url)) d.set_name("CHIRP") d.set_version(CHIRP_VERSION) d.set_copyright("Copyright 2015 Dan Smith (KK7DS)") d.set_website("http://chirp.danplanet.com") d.set_authors(("Dan Smith KK7DS ", _("With significant contributions from:"), "Tom KD7LXL", "Marco IZ3GME", "Jim KC9HI" )) d.set_translator_credits("Polish: Grzegorz SQ2RBY" + os.linesep + "Italian: Fabio IZ2QDH" + os.linesep + "Dutch: Michael PD4MT" + os.linesep + "German: Benjamin HB9EUK" + os.linesep + "Hungarian: Attila HA5JA" + os.linesep + "Russian: Dmitry Slukin" + os.linesep + "Portuguese (BR): Crezivando PP7CJ") d.set_comments(verinfo) d.run() d.destroy() def do_gethelp(self): webbrowser.open("http://chirp.danplanet.com") def do_columns(self): eset = self.get_current_editorset() driver = directory.get_driver(eset.rthread.radio.__class__) radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR, eset.rthread.radio.MODEL, eset.rthread.radio.VARIANT) d = gtk.Dialog(title=_("Select Columns"), parent=self, buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) vbox = gtk.VBox() vbox.show() sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.add_with_viewport(vbox) sw.show() d.vbox.pack_start(sw, 1, 1, 1) d.set_size_request(-1, 300) d.set_resizable(False) labelstr = _("Visible columns for {radio}").format(radio=radio_name) label = gtk.Label(labelstr) label.show() vbox.pack_start(label) fields = [] memedit = eset.get_current_editor() # .editors["memedit"] unsupported = memedit.get_unsupported_columns() for colspec in memedit.cols: if colspec[0].startswith("_"): continue elif colspec[0] in unsupported: continue label = colspec[0] visible = memedit.get_column_visible(memedit.col(label)) widget = gtk.CheckButton(label) widget.set_active(visible) fields.append(widget) vbox.pack_start(widget, 1, 1, 1) widget.show() res = d.run() selected_columns = [] if res == gtk.RESPONSE_OK: for widget in fields: colnum = memedit.col(widget.get_label()) memedit.set_column_visible(colnum, widget.get_active()) if widget.get_active(): selected_columns.append(widget.get_label()) d.destroy() CONF.set(driver, ",".join(selected_columns), "memedit_columns") def do_hide_unused(self, action): eset = self.get_current_editorset() if eset is None: conf = config.get("memedit") conf.set_bool("hide_unused", action.get_active()) else: for editortype, editor in eset.editors.iteritems(): if "memedit" in editortype: editor.set_hide_unused(action.get_active()) def do_clearq(self): eset = self.get_current_editorset() eset.rthread.flush() def do_copy(self, cut): eset = self.get_current_editorset() eset.get_current_editor().copy_selection(cut) def do_paste(self): eset = self.get_current_editorset() eset.get_current_editor().paste_selection() def do_delete(self): eset = self.get_current_editorset() eset.get_current_editor().copy_selection(True) def do_toggle_report(self, action): if not action.get_active(): d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self) markup = "" + _("Reporting is disabled") + "" d.set_markup(markup) msg = _("The reporting feature of CHIRP is designed to help " "improve quality by allowing the authors to focus " "on the radio drivers used most often and errors " "experienced by the users. The reports contain no " "identifying information and are used only for " "statistical purposes by the authors. Your privacy is " "extremely important, but please consider leaving " "this feature enabled to help make CHIRP better!\n\n" "Are you sure you want to disable this feature?") d.format_secondary_markup(msg.replace("\n", "\r\n")) r = d.run() d.destroy() if r == gtk.RESPONSE_NO: action.set_active(not action.get_active()) conf = config.get() conf.set_bool("no_report", not action.get_active()) def do_toggle_no_smart_tmode(self, action): CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit") def do_toggle_developer(self, action): conf = config.get() conf.set_bool("developer", action.get_active(), "state") for name in ["viewdeveloper", "loadmod"]: devaction = self.menu_ag.get_action(name) devaction.set_visible(action.get_active()) def do_toggle_clone_information(self, action): CONF.set_bool("clone_information", not action.get_active(), "noconfirm") def do_toggle_clone_instructions(self, action): CONF.set_bool("clone_instructions", not action.get_active(), "noconfirm") def do_change_language(self): langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German", "Hungarian", "Russian", "Portuguese (BR)", "French", "Spanish"] d = inputdialog.ChoiceDialog(langs, parent=self, title="Choose Language") d.label.set_text(_("Choose a language or Auto to use the " "operating system default. You will need to " "restart the application before the change " "will take effect")) d.label.set_line_wrap(True) r = d.run() if r == gtk.RESPONSE_OK: LOG.debug("Chose language %s" % d.choice.get_active_text()) conf = config.get() conf.set("language", d.choice.get_active_text(), "state") d.destroy() def load_module(self): types = [(_("Python Modules") + "*.py", "*.py")] filen = platform.get_platform().gui_open_file(types=types) if not filen: return # We're in development mode, so we need to tell the directory to # allow a loaded module to override an existing driver, against # its normal better judgement directory.enable_reregistrations() try: module = file(filen) code = module.read() module.close() pyc = compile(code, filen, 'exec') # See this for why: # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec exec(pyc, globals(), globals()) except Exception, e: common.log_exception() common.show_error("Unable to load module: %s" % e) def mh(self, _action, *args): action = _action.get_name() if action == "quit": gtk.main_quit() elif action == "new": self.do_new() elif action == "open": self.do_open() elif action == "save": self.do_save() elif action == "saveas": self.do_saveas() elif action.startswith("download"): self.do_download(*args) elif action.startswith("upload"): self.do_upload(*args) elif action == "close": self.do_close() elif action == "import": self.do_import() elif action in ["qdmrmarc", "idmrmarc"]: self.do_dmrmarc(action[0] == "i") elif action in ["qrfinder", "irfinder"]: self.do_rfinder(action[0] == "i") elif action in ["qradioreference", "iradioreference"]: self.do_radioreference(action[0] == "i") elif action == "export": self.do_export() elif action in ["qrbookpolitical", "irbookpolitical"]: self.do_repeaterbook_political(action[0] == "i") elif action in ["qrbookproximity", "irbookproximity"]: self.do_repeaterbook_proximity(action[0] == "i") elif action in ["qpr", "ipr"]: self.do_przemienniki(action[0] == "i") elif action == "about": self.do_about() elif action == "gethelp": self.do_gethelp() elif action == "columns": self.do_columns() elif action == "hide_unused": self.do_hide_unused(_action) elif action == "cancelq": self.do_clearq() elif action == "report": self.do_toggle_report(_action) elif action == "channel_defaults": # The memedit thread also has an instance of bandplans. bp = bandplans.BandPlans(CONF) bp.select_bandplan(self) elif action == "no_smart_tmode": self.do_toggle_no_smart_tmode(_action) elif action == "developer": self.do_toggle_developer(_action) elif action == "clone_information": self.do_toggle_clone_information(_action) elif action == "clone_instructions": self.do_toggle_clone_instructions(_action) elif action in ["cut", "copy", "paste", "delete", "move_up", "move_dn", "exchange", "all", "devshowraw", "devdiffraw", "properties"]: self.get_current_editorset().get_current_editor().hotkey(_action) elif action == "devdifftab": self.do_diff_radio() elif action == "language": self.do_change_language() elif action == "loadmod": self.load_module() else: return self.ev_tab_switched() def make_menubar(self): menu_xml = """ """ ALT_KEY = "" CTRL_KEY = "" if sys.platform == 'darwin': ALT_KEY = "" CTRL_KEY = "" actions = [ ('file', None, _("_File"), None, None, self.mh), ('new', gtk.STOCK_NEW, None, None, None, self.mh), ('open', gtk.STOCK_OPEN, None, None, None, self.mh), ('openstock', None, _("Open stock config"), None, None, self.mh), ('recent', None, _("_Recent"), None, None, self.mh), ('save', gtk.STOCK_SAVE, None, None, None, self.mh), ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh), ('loadmod', None, _("Load Module"), None, None, self.mh), ('close', gtk.STOCK_CLOSE, None, None, None, self.mh), ('quit', gtk.STOCK_QUIT, None, None, None, self.mh), ('edit', None, _("_Edit"), None, None, self.mh), ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh), ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh), ('paste', None, _("_Paste"), "%sv" % CTRL_KEY, None, self.mh), ('delete', None, _("_Delete"), "Delete", None, self.mh), ('all', None, _("Select _All"), None, None, self.mh), ('move_up', None, _("Move _Up"), "%sUp" % CTRL_KEY, None, self.mh), ('move_dn', None, _("Move Dow_n"), "%sDown" % CTRL_KEY, None, self.mh), ('exchange', None, _("E_xchange"), "%sx" % CTRL_KEY, None, self.mh), ('properties', None, _("P_roperties"), None, None, self.mh), ('view', None, _("_View"), None, None, self.mh), ('columns', None, _("Columns"), None, None, self.mh), ('viewdeveloper', None, _("Developer"), None, None, self.mh), ('devshowraw', None, _('Show raw memory'), "%sr" % CTRL_KEY, None, self.mh), ('devdiffraw', None, _("Diff raw memories"), "%sd" % CTRL_KEY, None, self.mh), ('devdifftab', None, _("Diff tabs"), "%st" % CTRL_KEY, None, self.mh), ('language', None, _("Change language"), None, None, self.mh), ('radio', None, _("_Radio"), None, None, self.mh), ('download', None, _("Download From Radio"), "%sd" % ALT_KEY, None, self.mh), ('upload', None, _("Upload To Radio"), "%su" % ALT_KEY, None, self.mh), ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh), ('export', None, _("Export"), "%se" % ALT_KEY, None, self.mh), ('importsrc', None, _("Import from data source"), None, None, self.mh), ('idmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), ('iradioreference', None, _("RadioReference.com"), None, None, self.mh), ('irfinder', None, _("RFinder"), None, None, self.mh), ('irbook', None, _("RepeaterBook"), None, None, self.mh), ('irbookpolitical', None, _("RepeaterBook political query"), None, None, self.mh), ('irbookproximity', None, _("RepeaterBook proximity query"), None, None, self.mh), ('ipr', None, _("przemienniki.net"), None, None, self.mh), ('querysrc', None, _("Query data source"), None, None, self.mh), ('qdmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), ('qradioreference', None, _("RadioReference.com"), None, None, self.mh), ('qrfinder', None, _("RFinder"), None, None, self.mh), ('qpr', None, _("przemienniki.net"), None, None, self.mh), ('qrbook', None, _("RepeaterBook"), None, None, self.mh), ('qrbookpolitical', None, _("RepeaterBook political query"), None, None, self.mh), ('qrbookproximity', None, _("RepeaterBook proximity query"), None, None, self.mh), ('export_chirp', None, _("CHIRP Native File"), None, None, self.mh), ('export_csv', None, _("CSV File"), None, None, self.mh), ('stock', None, _("Import from stock config"), None, None, self.mh), ('channel_defaults', None, _("Channel defaults"), None, None, self.mh), ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh), ('help', None, _('Help'), None, None, self.mh), ('about', gtk.STOCK_ABOUT, None, None, None, self.mh), ('gethelp', None, _("Get Help Online..."), None, None, self.mh), ] conf = config.get() re = not conf.get_bool("no_report") hu = conf.get_bool("hide_unused", "memedit", default=True) dv = conf.get_bool("developer", "state") cf = not conf.get_bool("clone_information", "noconfirm") ci = not conf.get_bool("clone_instructions", "noconfirm") st = not conf.get_bool("no_smart_tmode", "memedit") toggles = [('report', None, _("Report Statistics"), None, None, self.mh, re), ('hide_unused', None, _("Hide Unused Fields"), None, None, self.mh, hu), ('no_smart_tmode', None, _("Smart Tone Modes"), None, None, self.mh, st), ('clone_information', None, _("Show Information"), None, None, self.mh, cf), ('clone_instructions', None, _("Show Instructions"), None, None, self.mh, ci), ('developer', None, _("Enable Developer Functions"), None, None, self.mh, dv), ] self.menu_uim = gtk.UIManager() self.menu_ag = gtk.ActionGroup("MenuBar") self.menu_ag.add_actions(actions) self.menu_ag.add_toggle_actions(toggles) self.menu_uim.insert_action_group(self.menu_ag, 0) self.menu_uim.add_ui_from_string(menu_xml) self.add_accel_group(self.menu_uim.get_accel_group()) self.infomenu = self.menu_uim.get_widget( "/MenuBar/help/clone_information") self.clonemenu = self.menu_uim.get_widget( "/MenuBar/help/clone_instructions") # Initialize self.do_toggle_developer(self.menu_ag.get_action("developer")) return self.menu_uim.get_widget("/MenuBar") def make_tabs(self): self.tabs = gtk.Notebook() self.tabs.set_scrollable(True) return self.tabs def close_out(self): num = self.tabs.get_n_pages() while num > 0: num -= 1 LOG.debug("Closing %i" % num) try: self.do_close(self.tabs.get_nth_page(num)) except ModifiedError: return False gtk.main_quit() return True def make_status_bar(self): box = gtk.HBox(False, 2) self.sb_general = gtk.Statusbar() self.sb_general.set_has_resize_grip(False) self.sb_general.show() box.pack_start(self.sb_general, 1, 1, 1) self.sb_radio = gtk.Statusbar() self.sb_radio.set_has_resize_grip(True) self.sb_radio.show() box.pack_start(self.sb_radio, 1, 1, 1) box.show() return box def ev_delete(self, window, event): if not self.close_out(): return True # Don't exit def ev_destroy(self, window): if not self.close_out(): return True # Don't exit def setup_extra_hotkeys(self): accelg = self.menu_uim.get_accel_group() def memedit(a): self.get_current_editorset().editors["memedit"].hotkey(a) actions = [ # ("action_name", "key", function) ] for name, key, fn in actions: a = gtk.Action(name, name, name, "") a.connect("activate", fn) self.menu_ag.add_action_with_accel(a, key) a.set_accel_group(accelg) a.connect_accelerator() def _set_icon(self): this_platform = platform.get_platform() path = (this_platform.find_resource("chirp.png") or this_platform.find_resource(os.path.join("pixmaps", "chirp.png"))) if os.path.exists(path): self.set_icon_from_file(path) else: LOG.warn("Icon %s not found" % path) def _updates(self, version): if not version: return if version == CHIRP_VERSION: return LOG.info("Server reports version %s is available" % version) # Report new updates every three days intv = 3600 * 24 * 3 if CONF.is_defined("last_update_check", "state") and \ (time.time() - CONF.get_int("last_update_check", "state")) < intv: return CONF.set_int("last_update_check", int(time.time()), "state") d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self, type=gtk.MESSAGE_INFO) d.label.set_markup( _('A new version of CHIRP is available: ' + '{ver}. '.format(ver=version) + 'It is recommended that you upgrade as soon as possible. ' 'Please go to: \r\n\r\n' + 'http://chirp.danplanet.com')) response = d.run() d.destroy() if response == gtk.RESPONSE_OK: webbrowser.open('http://chirp.danplanet.com/' 'projects/chirp/wiki/Download') def _init_macos(self, menu_bar): macapp = None # for KK7DS runtime <= R10 try: import gtk_osxapplication macapp = gtk_osxapplication.OSXApplication() except ImportError: pass # for gtk-mac-integration >= 2.0.7 try: import gtkosx_application macapp = gtkosx_application.Application() except ImportError: pass if macapp is None: LOG.error("No MacOS support: %s" % e) return this_platform = platform.get_platform() icon = (this_platform.find_resource("chirp.png") or this_platform.find_resource(os.path.join("pixmaps", "chirp.png"))) if os.path.exists(icon): icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon) macapp.set_dock_icon_pixbuf(icon_pixmap) menu_bar.hide() macapp.set_menu_bar(menu_bar) quititem = self.menu_uim.get_widget("/MenuBar/file/quit") quititem.hide() aboutitem = self.menu_uim.get_widget("/MenuBar/help/about") macapp.insert_app_menu_item(aboutitem, 0) documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp") macapp.insert_app_menu_item(documentationitem, 0) macapp.set_use_quartz_accelerators(False) macapp.ready() LOG.debug("Initialized MacOS support") def __init__(self, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) def expose(window, event): allocation = window.get_allocation() CONF.set_int("window_w", allocation.width, "state") CONF.set_int("window_h", allocation.height, "state") self.connect("expose_event", expose) def state_change(window, event): CONF.set_bool( "window_maximized", event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED, "state") self.connect("window-state-event", state_change) d = CONF.get("last_dir", "state") if d and os.path.isdir(d): platform.get_platform().set_last_dir(d) vbox = gtk.VBox(False, 2) self._recent = [] self.menu_ag = None mbar = self.make_menubar() if os.name != "nt": self._set_icon() # Windows gets the icon from the exe if os.uname()[0] == "Darwin": self._init_macos(mbar) vbox.pack_start(mbar, 0, 0, 0) self.tabs = None tabs = self.make_tabs() tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p)) tabs.connect("page-removed", lambda *a: self.ev_tab_switched()) tabs.show() self.ev_tab_switched() vbox.pack_start(tabs, 1, 1, 1) vbox.pack_start(self.make_status_bar(), 0, 0, 0) vbox.show() self.add(vbox) try: width = CONF.get_int("window_w", "state") height = CONF.get_int("window_h", "state") except Exception: width = 800 height = 600 self.set_default_size(width, height) if CONF.get_bool("window_maximized", "state"): self.maximize() self.set_title("CHIRP") self.connect("delete_event", self.ev_delete) self.connect("destroy", self.ev_destroy) if not CONF.get_bool("warned_about_reporting") and \ not CONF.get_bool("no_report"): d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) d.set_markup("" + _("Error reporting is enabled") + "") d.format_secondary_markup( _("If you wish to disable this feature you may do so in " "the Help menu")) d.run() d.destroy() CONF.set_bool("warned_about_reporting", True) self.update_recent_files() try: self.update_stock_configs() except UnicodeDecodeError: LOG.exception('We hit bug #272 while working with unicode paths. ' 'Not copying stock configs so we can continue ' 'startup.') self.setup_extra_hotkeys() def updates_callback(ver): gobject.idle_add(self._updates, ver) if not CONF.get_bool("skip_update_check", "state"): reporting.check_for_updates(updates_callback)