# Copyright 2008, 2009, 2016 Free Software Foundation, Inc. # This file is part of GNU Radio # # SPDX-License-Identifier: GPL-2.0-or-later # import sys import textwrap from shutil import which as find_executable from gi.repository import Gtk, GLib, Gdk, Gio from . import Utils, Actions, Constants from ..core import Messages class SimpleTextDisplay(Gtk.TextView): """ A non user-editable gtk text view. """ def __init__(self, text=''): """ TextDisplay constructor. Args: text: the text to display (string) """ Gtk.TextView.__init__(self) self.set_text = self.get_buffer().set_text self.set_text(text) self.set_editable(False) self.set_cursor_visible(False) self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) class TextDisplay(SimpleTextDisplay): """ A non user-editable scrollable text view with popup menu. """ def __init__(self, text=''): """ TextDisplay constructor. Args: text: the text to display (string) """ SimpleTextDisplay.__init__(self, text) self.scroll_lock = True self.connect("populate-popup", self.populate_popup) def insert(self, line): """ Append text after handling backspaces and auto-scroll. Args: line: the text to append (string) """ line = self._consume_backspaces(line) self.get_buffer().insert(self.get_buffer().get_end_iter(), line) self.scroll_to_end() def _consume_backspaces(self, line): """ Removes text from the buffer if line starts with '\b' Args: line: a string which may contain backspaces Returns: The string that remains from 'line' with leading '\b's removed. """ if not line: return # for each \b delete one char from the buffer back_count = 0 start_iter = self.get_buffer().get_end_iter() while len(line) > back_count and line[back_count] == '\b': # stop at the beginning of a line if not start_iter.starts_line(): start_iter.backward_char() back_count += 1 # remove chars from buffer self.get_buffer().delete(start_iter, self.get_buffer().get_end_iter()) return line[back_count:] def scroll_to_end(self): """ Update view's scroll position. """ if self.scroll_lock: buf = self.get_buffer() mark = buf.get_insert() buf.move_mark(mark, buf.get_end_iter()) self.scroll_mark_onscreen(mark) def clear(self): """ Clear all text from buffer. """ buf = self.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) def save(self, file_path): """ Save context of buffer to the given file. Args: file_path: location to save buffer contents """ with open(file_path, 'w') as logfile: buf = self.get_buffer() logfile.write(buf.get_text(buf.get_start_iter(), buf.get_end_iter(), True)) # Action functions are set by the Application's init function def clear_cb(self, menu_item, web_view): """ Callback function to clear the text buffer """ Actions.CLEAR_CONSOLE() def scroll_back_cb(self, menu_item, web_view): """ Callback function to toggle scroll lock """ Actions.TOGGLE_SCROLL_LOCK() def save_cb(self, menu_item, web_view): """ Callback function to save the buffer """ Actions.SAVE_CONSOLE() def populate_popup(self, view, menu): """Create a popup menu for the scroll lock and clear functions""" menu.append(Gtk.SeparatorMenuItem()) lock = Gtk.CheckMenuItem(label="Scroll Lock") menu.append(lock) lock.set_active(self.scroll_lock) lock.connect('activate', self.scroll_back_cb, view) save = Gtk.ImageMenuItem(label="Save Console") menu.append(save) save.connect('activate', self.save_cb, view) clear = Gtk.ImageMenuItem(label="Clear Console") menu.append(clear) clear.connect('activate', self.clear_cb, view) menu.show_all() return False class MessageDialogWrapper(Gtk.MessageDialog): """ Run a message dialog. """ def __init__(self, parent, message_type, buttons, title=None, markup=None, default_response=None, extra_buttons=None): """ Create a modal message dialog. Args: message_type: the type of message may be one of: Gtk.MessageType.INFO Gtk.MessageType.WARNING Gtk.MessageType.QUESTION or Gtk.MessageType.ERROR buttons: the predefined set of buttons to use: Gtk.ButtonsType.NONE Gtk.ButtonsType.OK Gtk.ButtonsType.CLOSE Gtk.ButtonsType.CANCEL Gtk.ButtonsType.YES_NO Gtk.ButtonsType.OK_CANCEL title: the title of the window (string) markup: the message text with pango markup default_response: if set, determines which button is highlighted by default extra_buttons: a tuple containing pairs of values: each value is the button's text and the button's return value """ Gtk.MessageDialog.__init__( self, transient_for=parent, modal=True, destroy_with_parent=True, message_type=message_type, buttons=buttons ) self.set_keep_above(True) if title: self.set_title(title) if markup: self.set_markup(markup) if extra_buttons: self.add_buttons(*extra_buttons) if default_response: self.set_default_response(default_response) def run_and_destroy(self): response = self.run() self.hide() return response class ErrorsDialog(Gtk.Dialog): """ Display flowgraph errors. """ def __init__(self, parent, flowgraph): """Create a listview of errors""" Gtk.Dialog.__init__( self, title='Errors and Warnings', transient_for=parent, modal=True, destroy_with_parent=True, ) self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) self.set_size_request(750, Constants.MIN_DIALOG_HEIGHT) self.set_border_width(10) self.store = Gtk.ListStore(str, str, str) self.update(flowgraph) self.treeview = Gtk.TreeView(model=self.store) self.treeview.connect("button_press_event", self.mouse_click) for i, column_title in enumerate(["Block", "Aspect", "Message"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) column.set_sort_column_id(i) # liststore id matches treeview id column.set_resizable(True) self.treeview.append_column(column) self.scrollable = Gtk.ScrolledWindow() self.scrollable.set_vexpand(True) self.scrollable.add(self.treeview) self.vbox.pack_start(self.scrollable, True, True, 0) self.show_all() def update(self, flowgraph): self.store.clear() for element, message in flowgraph.iter_error_messages(): if element.is_block: src, aspect = element.name, '' elif element.is_connection: src = element.source_block.name aspect = "Connection to '{}'".format(element.sink_block.name) elif element.is_port: src = element.parent_block.name aspect = "{} '{}'".format( 'Sink' if element.is_sink else 'Source', element.name) elif element.is_param: src = element.parent_block.name aspect = "Param '{}'".format(element.name) else: src = aspect = '' self.store.append([src, aspect, message]) def run_and_destroy(self): response = self.run() self.hide() return response def mouse_click(self, _, event): """ Handle mouse click, so user can copy the error message """ if event.button == 3: path_info = self.treeview.get_path_at_pos(event.x, event.y) if path_info is not None: path, col, _, _ = path_info self.treeview.grab_focus() self.treeview.set_cursor(path, col, 0) selection = self.treeview.get_selection() (model, iterator) = selection.get_selected() self.clipboard.set_text(model[iterator][2], -1) print(model[iterator][2]) def show_about(parent, config): ad = Gtk.AboutDialog(transient_for=parent) ad.set_program_name(config.name) ad.set_name('') ad.set_license(config.license) py_version = sys.version.split()[0] ad.set_version("{} (Python {})".format(config.version, py_version)) try: ad.set_logo(Gtk.IconTheme().load_icon('gnuradio-grc', 64, 0)) except GLib.Error: Messages.send("Failed to set window logo\n") # ad.set_comments("") ad.set_copyright(config.license.splitlines()[0]) ad.set_website(config.website) ad.connect("response", lambda action, param: action.hide()) ad.show() def show_help(parent): """ Display basic usage tips. """ markup = textwrap.dedent("""\ Usage Tips \n\ Add block: drag and drop or double click a block in the block selection window. Rotate block: Select a block, press left/right on the keyboard. Change type: Select a block, press up/down on the keyboard. Edit parameters: double click on a block in the flow graph. Make connection: click on the source port of one block, then click on the sink port of another block. Remove connection: select the connection and press delete, or drag the connection. \n\ *Press Ctrl+K or see menu for Keyboard - Shortcuts \ """) markup = markup.replace("Ctrl", Utils.get_modifier_key()) MessageDialogWrapper( parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Help', markup=markup ).run_and_destroy() def show_keyboard_shortcuts(parent): """ Display keyboard shortcut-keys. """ markup = textwrap.dedent("""\ Keyboard Shortcuts \n\ Ctrl+N: Create a new flowgraph. Ctrl+O: Open an existing flowgraph. Ctrl+S: Save the current flowgraph or save as for new. Ctrl+W: Close the current flowgraph. Ctrl+Z: Undo a change to the flowgraph. Ctrl+Y: Redo a change to the flowgraph. Ctrl+A: Selects all blocks and connections. Ctrl+P: Screen Capture of the Flowgraph. Ctrl+Shift+P: Save the console output to file. Ctrl+L: Clear the console. Ctrl+E: Show variable editor. Ctrl+F: Search for a block by name. Ctrl+Q: Quit. F1 : Help menu. F5 : Generate the Flowgraph. F6 : Execute the Flowgraph. F7 : Kill the Flowgraph. Ctrl+Shift+S: Save as the current flowgraph. Ctrl+Shift+D: Create a duplicate of current flow graph. Ctrl+X/C/V: Edit-cut/copy/paste. Ctrl+D/B/R: Toggle visibility of disabled blocks or connections/block tree widget/console. Shift+T/M/B/L/C/R: Vertical Align Top/Middle/Bottom and Horizontal Align Left/Center/Right respectively of the selected block. Ctrl+0: Reset the zoom level Ctrl++/-: Zoom in and out \ """) markup = markup.replace("Ctrl", Utils.get_modifier_key()) MessageDialogWrapper( parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Keyboard - Shortcuts', markup=markup ).run_and_destroy() def show_get_involved(parent): """Get Involved Instructions""" markup = textwrap.dedent("""\ Welcome to GNU Radio Community! \n\ For more details on contributing to GNU Radio and getting engaged with our great community visit here. \n\ You can also join our Matrix chat server, IRC Channel (#gnuradio) or contact through our mailing list (discuss-gnuradio). \ """) MessageDialogWrapper( parent, Gtk.MessageType.QUESTION, Gtk.ButtonsType.CLOSE, title='Get - Involved', markup=markup ).run_and_destroy() def show_types(parent): """ Display information about standard data types. """ colors = [(name, color) for name, key, sizeof, color in Constants.CORE_TYPES] max_len = 10 + max(len(name) for name, code in colors) message = '\n'.join( '{name}' ''.format(color=color, name=Utils.encode(name).center(max_len)) for name, color in colors ) MessageDialogWrapper( parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Types - Color Mapping', markup=message ).run_and_destroy() def show_missing_xterm(parent, xterm): markup = textwrap.dedent("""\ The xterm executable {0!r} is missing. You can change this setting in your gnuradio.conf, in section [grc], 'xterm_executable'. \n\ (This message is shown only once)\ """).format(xterm) MessageDialogWrapper( parent, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, title='Warning: missing xterm executable', markup=markup ).run_and_destroy() def choose_editor(parent, config): """ Give the option to either choose an editor or use the default. """ content_type = Gio.content_type_from_mime_type("text/x-python") if content_type == "*": # fallback to plain text on Windows if no useful x-python association content_type = Gio.content_type_from_mime_type("text/plain") dialog = Gtk.AppChooserDialog.new_for_content_type( parent, Gtk.DialogFlags.MODAL, content_type, ) dialog.set_heading("Choose an editor below") widget = dialog.get_widget() widget.set_default_text("Choose an editor") widget.set_show_default(True) widget.set_show_recommended(True) widget.set_show_fallback(True) editor = None response = dialog.run() if response == Gtk.ResponseType.OK: appinfo = dialog.get_app_info() editor = config.editor = appinfo.get_executable() dialog.destroy() return editor