diff options
author | Andrej Rode <mail@andrejro.de> | 2018-06-23 23:41:42 +0200 |
---|---|---|
committer | Andrej Rode <mail@andrejro.de> | 2018-06-24 00:03:35 +0200 |
commit | 167a6152bad060fc53dd29e0fa79ef83eff1be5b (patch) | |
tree | a01049672d9d7d1bf3d295ed96698a323941f8e8 /grc/gui | |
parent | 3c8e6008b092287246234001db7cf1a4038300da (diff) | |
parent | fcd002b6ac82e1e0c1224e24506410ff0833e1aa (diff) |
Merge branch 'python3_fix' into next
Manual merge conflict resolution has been applied to following
conflicts:
* Typos:
* gnuradio-runtime/python/gnuradio/ctrlport/GrDataPlotter.py
* gr-blocks/python/blocks/qa_wavfile.py
* gr-filter/examples/gr_filtdes_api.py
* grc/blocks/parameter.xml
* gr-uhd/python/uhd/__init__.py
* ValueError -> RuntimeError:
* gr-blocks/python/blocks/qa_hier_block2.py
* relative Imports & other Py3k:
* gr-digital/python/digital/psk_constellations.py
* gr-digital/python/digital/qam_constellations.py
* gr-digital/python/digital/test_soft_decisions.py
* gr-digital/python/digital/gfsk.py
* SequenceCompleter:
* gr-utils/python/modtool/modtool_add.py
* gr-utils/python/modtool/modtool_rename.py
* gr-utils/python/modtool/modtool_rm.py
* Updated API on next:
* gr-blocks/grc/blocks_file_source.xml
* gr-blocks/python/blocks/qa_file_source_sink.py
* gr-qtgui/grc/qtgui_time_sink_x.xml
* GRC Py3k Updates:
* grc/core/Block.py
* grc/core/Constants.py
* grc/core/Platform.py
* grc/core/utils/odict.py
* grc/gui/Actions.py
* grc/gui/Block.py
* grc/gui/Executor.py
* grc/gui/Port.py
Diffstat (limited to 'grc/gui')
39 files changed, 4976 insertions, 4761 deletions
diff --git a/grc/gui/Actions.py b/grc/gui/Actions.py index 5e728a350f..14b0422764 100644 --- a/grc/gui/Actions.py +++ b/grc/gui/Actions.py @@ -17,284 +17,327 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk +from __future__ import absolute_import -import Preferences +import six +import logging -NO_MODS_MASK = 0 +from gi.repository import Gtk, Gdk, Gio, GLib, GObject -######################################################################## -# Actions API -######################################################################## -_actions_keypress_dict = dict() -_keymap = gtk.gdk.keymap_get_default() -_used_mods_mask = NO_MODS_MASK - - -def handle_key_press(event): - """ - Call the action associated with the key press event. - Both the key value and the mask must have a match. - - Args: - event: a gtk key press event - - Returns: - true if handled - """ - _used_mods_mask = reduce(lambda x, y: x | y, [mod_mask for keyval, mod_mask in _actions_keypress_dict], NO_MODS_MASK) - # extract the key value and the consumed modifiers - keyval, egroup, level, consumed = _keymap.translate_keyboard_state( - event.hardware_keycode, event.state, event.group) - # get the modifier mask and ignore irrelevant modifiers - mod_mask = event.state & ~consumed & _used_mods_mask - # look up the keypress and call the action - try: - _actions_keypress_dict[(keyval, mod_mask)]() - except KeyError: - return False # not handled - else: - return True # handled here - -_all_actions_list = list() - - -def get_all_actions(): - return _all_actions_list - -_accel_group = gtk.AccelGroup() - - -def get_accel_group(): - return _accel_group - - -class _ActionBase(object): - """ - Base class for Action and ToggleAction - Register actions and keypresses with this module. - """ - def __init__(self, label, keypresses): - _all_actions_list.append(self) - for i in range(len(keypresses)/2): - keyval, mod_mask = keypresses[i*2:(i+1)*2] - # register this keypress - if _actions_keypress_dict.has_key((keyval, mod_mask)): - raise KeyError('keyval/mod_mask pair already registered "%s"' % str((keyval, mod_mask))) - _actions_keypress_dict[(keyval, mod_mask)] = self - # set the accelerator group, and accelerator path - # register the key name and mod mask with the accelerator path - if label is None: - continue # don't register accel - accel_path = '<main>/' + self.get_name() - self.set_accel_group(get_accel_group()) - self.set_accel_path(accel_path) - gtk.accel_map_add_entry(accel_path, keyval, mod_mask) - self.args = None + +log = logging.getLogger(__name__) + + +def filter_from_dict(vars): + return filter(lambda x: isinstance(x[1], Action), vars.items()) + + +class Namespace(object): + + def __init__(self): + self._actions = {} + + def add(self, action): + key = action.props.name + self._actions[key] = action + + def connect(self, name, handler): + #log.debug("Connecting action <{}> to handler <{}>".format(name, handler.__name__)) + self._actions[name].connect('activate', handler) + + def register(self, name, parameter=None, handler=None, label=None, tooltip=None, + icon_name=None, keypresses=None, preference_name=None, default=None): + # Check types + if not isinstance(name, str): + raise TypeError("Cannot register fuction: 'name' must be a str") + if parameter and not isinstance(parameter, str): + raise TypeError("Cannot register fuction: 'parameter' must be a str") + if handler and not callable(handler): + raise TypeError("Cannot register fuction: 'handler' must be callable") + + # Check if the name has a prefix. + prefix = None + if name.startswith("app.") or name.startswith("win."): + # Set a prefix for later and remove it + prefix = name[0:3] + name = name[4:] + + if handler: + log.debug("Register action [{}, prefix={}, param={}, handler={}]".format( + name, prefix, parameter, handler.__name__)) + else: + log.debug("Register action [{}, prefix={}, param={}, handler=None]".format( + name, prefix, parameter)) + + action = Action(name, parameter, label=label, tooltip=tooltip, + icon_name=icon_name, keypresses=keypresses, prefix=prefix, + preference_name=preference_name, default=default) + if handler: + action.connect('activate', handler) + + key = name + if prefix: + key = "{}.{}".format(prefix, name) + if prefix == "app": + pass + #self.app.add_action(action) + elif prefix == "win": + pass + #self.win.add_action(action) + + #log.debug("Registering action as '{}'".format(key)) + self._actions[key] = action + return action + + + # If the actions namespace is called, trigger an action + def __call__(self, name): + # Try to parse the action string. + valid, action_name, target_value = Action.parse_detailed_name(name) + if not valid: + raise Exception("Invalid action string: '{}'".format(name)) + if action_name not in self._actions: + raise Exception("Action '{}' is not registered!".format(action_name)) + + if target_value: + self._actions[action_name].activate(target_value) + else: + self._actions[action_name].activate() + + def __getitem__(self, key): + return self._actions[key] + + def __iter__(self): + return self._actions.itervalues() + + def __repr__(self): + return str(self) + + def get_actions(self): + return self._actions def __str__(self): - """ - The string representation should be the name of the action id. - Try to find the action id for this action by searching this module. - """ - for name, value in globals().iteritems(): - if value == self: - return name - return self.get_name() - - def __repr__(self): return str(self) - - def __call__(self, *args): - """ - Emit the activate signal when called with (). - """ - self.args = args - self.emit('activate') - - -class Action(gtk.Action, _ActionBase): - """ - A custom Action class based on gtk.Action. - Pass additional arguments such as keypresses. - """ - - def __init__(self, keypresses=(), name=None, label=None, tooltip=None, - stock_id=None): - """ - Create a new Action instance. - - Args: - key_presses: a tuple of (keyval1, mod_mask1, keyval2, mod_mask2, ...) - the: regular gtk.Action parameters (defaults to None) - """ - if name is None: - name = label - gtk.Action.__init__(self, name=name, label=label, tooltip=tooltip, - stock_id=stock_id) - _ActionBase.__init__(self, label, keypresses) - - -class ToggleAction(gtk.ToggleAction, _ActionBase): - """ - A custom Action class based on gtk.ToggleAction. - Pass additional arguments such as keypresses. - """ - - def __init__(self, keypresses=(), name=None, label=None, tooltip=None, - stock_id=None, preference_name=None, default=True): - """ - Create a new ToggleAction instance. - - Args: - key_presses: a tuple of (keyval1, mod_mask1, keyval2, mod_mask2, ...) - the: regular gtk.Action parameters (defaults to None) - """ - if name is None: - name = label - gtk.ToggleAction.__init__(self, name=name, label=label, - tooltip=tooltip, stock_id=stock_id) - _ActionBase.__init__(self, label, keypresses) + s = "{Actions:" + for key in self._actions: + s += " {},".format(key) + s = s.rstrip(",") + "}" + return s + + +class Action(Gio.SimpleAction): + + # Change these to normal python properties. + #prefs_name + + def __init__(self, name, parameter=None, label=None, tooltip=None, + icon_name=None, keypresses=None, prefix=None, + preference_name=None, default=None): + self.name = name + self.label = label + self.tooltip = tooltip + self.icon_name = icon_name + self.keypresses = keypresses + self.prefix = prefix self.preference_name = preference_name self.default = default - def load_from_preferences(self): + # Don't worry about checking types here, since it's done in register() + # Save the parameter type to use for converting in __call__ + self.type = None + + variant = None + state = None + if parameter: + variant = GLib.VariantType.new(parameter) + if preference_name: + state = GLib.Variant.new_boolean(True) + Gio.SimpleAction.__init__(self, name=name, parameter_type=variant, state=state) + + def enable(self): + self.props.enabled = True + + def disable(self): + self.props.enabled = False + + def set_enabled(self, state): + if not isinstance(state, bool): + raise TypeError("State must be True/False.") + self.props.enabled = state + + def __str__(self): + return self.props.name + + def __repr__(self): + return str(self) + + def get_active(self): + if self.props.state: + return self.props.state.get_boolean() + return False + + def set_active(self, state): + if not isinstance(state, bool): + raise TypeError("State must be True/False.") + self.change_state(GLib.Variant.new_boolean(state)) + + # Allows actions to be directly called. + def __call__(self, parameter=None): + if self.type and parameter: + # Try to convert it to the correct type. + try: + param = GLib.Variant(self.type, parameter) + self.activate(param) + except TypeError: + raise TypeError("Invalid parameter type for action '{}'. Expected: '{}'".format(self.get_name(), self.type)) + else: + self.activate() + + def load_from_preferences(self, *args): + log.debug("load_from_preferences({})".format(args)) if self.preference_name is not None: - self.set_active(Preferences.entry( - self.preference_name, default=bool(self.default))) + config = Gtk.Application.get_default().config + self.set_active(config.entry(self.preference_name, default=bool(self.default))) - def save_to_preferences(self): + def save_to_preferences(self, *args): + log.debug("save_to_preferences({})".format(args)) if self.preference_name is not None: - Preferences.entry(self.preference_name, value=self.get_active()) + config = Gtk.Application.get_default().config + config.entry(self.preference_name, value=self.get_active()) + + +actions = Namespace() + + +def get_actions(): + return actions.get_actions() + + +def connect(action, handler=None): + return actions.connect(action, handler=handler) + ######################################################################## -# Actions +# Old Actions ######################################################################## -PAGE_CHANGE = Action() -EXTERNAL_UPDATE = Action() -VARIABLE_EDITOR_UPDATE = Action() -FLOW_GRAPH_NEW = Action( +PAGE_CHANGE = actions.register("win.page_change") +EXTERNAL_UPDATE = actions.register("app.external_update") +VARIABLE_EDITOR_UPDATE = actions.register("app.variable_editor_update") +FLOW_GRAPH_NEW = actions.register("app.flowgraph.new", label='_New', tooltip='Create a new flow graph', - stock_id=gtk.STOCK_NEW, - keypresses=(gtk.keysyms.n, gtk.gdk.CONTROL_MASK), + icon_name='document-new', + keypresses=["<Ctrl>n"], + parameter="s", ) -FLOW_GRAPH_OPEN = Action( +FLOW_GRAPH_OPEN = actions.register("app.flowgraph.open", label='_Open', tooltip='Open an existing flow graph', - stock_id=gtk.STOCK_OPEN, - keypresses=(gtk.keysyms.o, gtk.gdk.CONTROL_MASK), + icon_name='document-open', + keypresses=["<Ctrl>o"], ) -FLOW_GRAPH_OPEN_RECENT = Action( +FLOW_GRAPH_OPEN_RECENT = actions.register("app.flowgraph.open_recent", label='Open _Recent', tooltip='Open a recently used flow graph', - stock_id=gtk.STOCK_OPEN, + icon_name='document-open-recent', + parameter="s", ) -FLOW_GRAPH_SAVE = Action( +FLOW_GRAPH_CLEAR_RECENT = actions.register("app.flowgraph.clear_recent") +FLOW_GRAPH_SAVE = actions.register("app.flowgraph.save", label='_Save', tooltip='Save the current flow graph', - stock_id=gtk.STOCK_SAVE, - keypresses=(gtk.keysyms.s, gtk.gdk.CONTROL_MASK), + icon_name='document-save', + keypresses=["<Ctrl>s"], ) -FLOW_GRAPH_SAVE_AS = Action( +FLOW_GRAPH_SAVE_AS = actions.register("app.flowgraph.save_as", label='Save _As', tooltip='Save the current flow graph as...', - stock_id=gtk.STOCK_SAVE_AS, - keypresses=(gtk.keysyms.s, gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK), + icon_name='document-save-as', + keypresses=["<Ctrl><Shift>s"], ) -FLOW_GRAPH_SAVE_A_COPY = Action( - label='Save A Copy', - tooltip='Save the copy of current flowgraph', +FLOW_GRAPH_SAVE_COPY = actions.register("app.flowgraph.save_copy", + label='Save Copy', + tooltip='Save a copy of current flow graph', ) -FLOW_GRAPH_DUPLICATE = Action( +FLOW_GRAPH_DUPLICATE = actions.register("app.flowgraph.duplicate", label='_Duplicate', - tooltip='Create a duplicate of current flowgraph', - stock_id=gtk.STOCK_COPY, - keypresses=(gtk.keysyms.d, gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK), + tooltip='Create a duplicate of current flow graph', + #stock_id=Gtk.STOCK_COPY, + keypresses=["<Ctrl><Shift>d"], ) -FLOW_GRAPH_CLOSE = Action( +FLOW_GRAPH_CLOSE = actions.register("app.flowgraph.close", label='_Close', tooltip='Close the current flow graph', - stock_id=gtk.STOCK_CLOSE, - keypresses=(gtk.keysyms.w, gtk.gdk.CONTROL_MASK), + icon_name='window-close', + keypresses=["<Ctrl>w"], ) -APPLICATION_INITIALIZE = Action() -APPLICATION_QUIT = Action( +APPLICATION_INITIALIZE = actions.register("app.initialize") +APPLICATION_QUIT = actions.register("app.quit", label='_Quit', tooltip='Quit program', - stock_id=gtk.STOCK_QUIT, - keypresses=(gtk.keysyms.q, gtk.gdk.CONTROL_MASK), + icon_name='application-exit', + keypresses=["<Ctrl>q"], ) -FLOW_GRAPH_UNDO = Action( +FLOW_GRAPH_UNDO = actions.register("win.undo", label='_Undo', tooltip='Undo a change to the flow graph', - stock_id=gtk.STOCK_UNDO, - keypresses=(gtk.keysyms.z, gtk.gdk.CONTROL_MASK), + icon_name='edit-undo', + keypresses=["<Ctrl>z"], ) -FLOW_GRAPH_REDO = Action( +FLOW_GRAPH_REDO = actions.register("win.redo", label='_Redo', tooltip='Redo a change to the flow graph', - stock_id=gtk.STOCK_REDO, - keypresses=(gtk.keysyms.y, gtk.gdk.CONTROL_MASK), + icon_name='edit-redo', + keypresses=["<Ctrl>y"], ) -NOTHING_SELECT = Action() -SELECT_ALL = Action( +NOTHING_SELECT = actions.register("win.unselect") +SELECT_ALL = actions.register("win.select_all", label='Select _All', tooltip='Select all blocks and connections in the flow graph', - stock_id=gtk.STOCK_SELECT_ALL, - keypresses=(gtk.keysyms.a, gtk.gdk.CONTROL_MASK), + icon_name='edit-select-all', + keypresses=["<Ctrl>a"], ) -ELEMENT_SELECT = Action() -ELEMENT_CREATE = Action() -ELEMENT_DELETE = Action( +ELEMENT_SELECT = actions.register("win.select") +ELEMENT_CREATE = actions.register("win.add") +ELEMENT_DELETE = actions.register("win.delete", label='_Delete', tooltip='Delete the selected blocks', - stock_id=gtk.STOCK_DELETE, - keypresses=(gtk.keysyms.Delete, NO_MODS_MASK), + icon_name='edit-delete', + keypresses=["Delete"], ) -BLOCK_MOVE = Action() -BLOCK_ROTATE_CCW = Action( +BLOCK_MOVE = actions.register("win.block_move") +BLOCK_ROTATE_CCW = actions.register("win.block_rotate_ccw", label='Rotate Counterclockwise', tooltip='Rotate the selected blocks 90 degrees to the left', - stock_id=gtk.STOCK_GO_BACK, - keypresses=(gtk.keysyms.Left, NO_MODS_MASK), + icon_name='object-rotate-left', ) -BLOCK_ROTATE_CW = Action( +BLOCK_ROTATE_CW = actions.register("win.block_rotate", label='Rotate Clockwise', tooltip='Rotate the selected blocks 90 degrees to the right', - stock_id=gtk.STOCK_GO_FORWARD, - keypresses=(gtk.keysyms.Right, NO_MODS_MASK), + icon_name='object-rotate-right', ) -BLOCK_VALIGN_TOP = Action( +BLOCK_VALIGN_TOP = actions.register("win.block_align_top", label='Vertical Align Top', tooltip='Align tops of selected blocks', - keypresses=(gtk.keysyms.t, gtk.gdk.SHIFT_MASK), ) -BLOCK_VALIGN_MIDDLE = Action( +BLOCK_VALIGN_MIDDLE = actions.register("win.block_align_middle", label='Vertical Align Middle', tooltip='Align centers of selected blocks vertically', - keypresses=(gtk.keysyms.m, gtk.gdk.SHIFT_MASK), ) -BLOCK_VALIGN_BOTTOM = Action( +BLOCK_VALIGN_BOTTOM = actions.register("win.block_align_bottom", label='Vertical Align Bottom', tooltip='Align bottoms of selected blocks', - keypresses=(gtk.keysyms.b, gtk.gdk.SHIFT_MASK), ) -BLOCK_HALIGN_LEFT = Action( +BLOCK_HALIGN_LEFT = actions.register("win.block_align_left", label='Horizontal Align Left', tooltip='Align left edges of blocks selected blocks', - keypresses=(gtk.keysyms.l, gtk.gdk.SHIFT_MASK), ) -BLOCK_HALIGN_CENTER = Action( +BLOCK_HALIGN_CENTER = actions.register("win.block_align_center", label='Horizontal Align Center', tooltip='Align centers of selected blocks horizontally', - keypresses=(gtk.keysyms.c, gtk.gdk.SHIFT_MASK), ) -BLOCK_HALIGN_RIGHT = Action( +BLOCK_HALIGN_RIGHT = actions.register("win.block_align_right", label='Horizontal Align Right', tooltip='Align right edges of selected blocks', - keypresses=(gtk.keysyms.r, gtk.gdk.SHIFT_MASK), ) BLOCK_ALIGNMENTS = [ BLOCK_VALIGN_TOP, @@ -305,234 +348,222 @@ BLOCK_ALIGNMENTS = [ BLOCK_HALIGN_CENTER, BLOCK_HALIGN_RIGHT, ] -BLOCK_PARAM_MODIFY = Action( +BLOCK_PARAM_MODIFY = actions.register("win.block_modify", label='_Properties', tooltip='Modify params for the selected block', - stock_id=gtk.STOCK_PROPERTIES, - keypresses=(gtk.keysyms.Return, NO_MODS_MASK), + icon_name='document-properties', ) -BLOCK_ENABLE = Action( +BLOCK_ENABLE = actions.register("win.block_enable", label='E_nable', tooltip='Enable the selected blocks', - stock_id=gtk.STOCK_CONNECT, - keypresses=(gtk.keysyms.e, NO_MODS_MASK), + icon_name='network-wired', ) -BLOCK_DISABLE = Action( +BLOCK_DISABLE = actions.register("win.block_disable", label='D_isable', tooltip='Disable the selected blocks', - stock_id=gtk.STOCK_DISCONNECT, - keypresses=(gtk.keysyms.d, NO_MODS_MASK), + icon_name='network-wired-disconnected', ) -BLOCK_BYPASS = Action( +BLOCK_BYPASS = actions.register("win.block_bypass", label='_Bypass', tooltip='Bypass the selected block', - stock_id=gtk.STOCK_MEDIA_FORWARD, - keypresses=(gtk.keysyms.b, NO_MODS_MASK), + icon_name='media-seek-forward', ) -TOGGLE_SNAP_TO_GRID = ToggleAction( +TOGGLE_SNAP_TO_GRID = actions.register("win.snap_to_grid", label='_Snap to grid', tooltip='Snap blocks to a grid for an easier connection alignment', - preference_name='snap_to_grid' + preference_name='snap_to_grid', ) -TOGGLE_HIDE_DISABLED_BLOCKS = ToggleAction( +TOGGLE_HIDE_DISABLED_BLOCKS = actions.register("win.hide_disabled", label='Hide _Disabled Blocks', tooltip='Toggle visibility of disabled blocks and connections', - stock_id=gtk.STOCK_MISSING_IMAGE, - keypresses=(gtk.keysyms.d, gtk.gdk.CONTROL_MASK), + icon_name='image-missing', + keypresses=["<Ctrl>d"], + preference_name='hide_disabled', ) -TOGGLE_HIDE_VARIABLES = ToggleAction( +TOGGLE_HIDE_VARIABLES = actions.register("win.hide_variables", label='Hide Variables', tooltip='Hide all variable blocks', preference_name='hide_variables', default=False, ) -TOGGLE_FLOW_GRAPH_VAR_EDITOR = ToggleAction( +TOGGLE_FLOW_GRAPH_VAR_EDITOR = actions.register("win.toggle_variable_editor", label='Show _Variable Editor', tooltip='Show the variable editor. Modify variables and imports in this flow graph', - stock_id=gtk.STOCK_EDIT, + icon_name='accessories-text-editor', default=True, - keypresses=(gtk.keysyms.e, gtk.gdk.CONTROL_MASK), + keypresses=["<Ctrl>e"], preference_name='variable_editor_visable', ) -TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR = ToggleAction( +TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR = actions.register("win.toggle_variable_editor_sidebar", label='Move the Variable Editor to the Sidebar', tooltip='Move the variable editor to the sidebar', default=False, preference_name='variable_editor_sidebar', ) -TOGGLE_AUTO_HIDE_PORT_LABELS = ToggleAction( +TOGGLE_AUTO_HIDE_PORT_LABELS = actions.register("win.auto_hide_port_labels", label='Auto-Hide _Port Labels', tooltip='Automatically hide port labels', preference_name='auto_hide_port_labels' ) -TOGGLE_SHOW_BLOCK_COMMENTS = ToggleAction( +TOGGLE_SHOW_BLOCK_COMMENTS = actions.register("win.show_block_comments", label='Show Block Comments', tooltip="Show comment beneath each block", preference_name='show_block_comments' ) -TOGGLE_SHOW_CODE_PREVIEW_TAB = ToggleAction( +TOGGLE_SHOW_CODE_PREVIEW_TAB = actions.register("win.toggle_code_preview", label='Generated Code Preview', tooltip="Show a preview of the code generated for each Block in its " "Properties Dialog", preference_name='show_generated_code_tab', default=False, ) -TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY = ToggleAction( +TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY = actions.register("win.show_flowgraph_complexity", label='Show Flowgraph Complexity', tooltip="How many Balints is the flowgraph...", preference_name='show_flowgraph_complexity', default=False, ) -BLOCK_CREATE_HIER = Action( +BLOCK_CREATE_HIER = actions.register("win.block_create_hier", label='C_reate Hier', tooltip='Create hier block from selected blocks', - stock_id=gtk.STOCK_CONNECT, -# keypresses=(gtk.keysyms.c, NO_MODS_MASK), + icon_name='document-new', ) -BLOCK_CUT = Action( +BLOCK_CUT = actions.register("win.block_cut", label='Cu_t', tooltip='Cut', - stock_id=gtk.STOCK_CUT, - keypresses=(gtk.keysyms.x, gtk.gdk.CONTROL_MASK), + icon_name='edit-cut', + keypresses=["<Ctrl>x"], ) -BLOCK_COPY = Action( +BLOCK_COPY = actions.register("win.block_copy", label='_Copy', tooltip='Copy', - stock_id=gtk.STOCK_COPY, - keypresses=(gtk.keysyms.c, gtk.gdk.CONTROL_MASK), + icon_name='edit-copy', + keypresses=["<Ctrl>c"], ) -BLOCK_PASTE = Action( +BLOCK_PASTE = actions.register("win.block_paste", label='_Paste', tooltip='Paste', - stock_id=gtk.STOCK_PASTE, - keypresses=(gtk.keysyms.v, gtk.gdk.CONTROL_MASK), + icon_name='edit-paste', + keypresses=["<Ctrl>v"], ) -ERRORS_WINDOW_DISPLAY = Action( +ERRORS_WINDOW_DISPLAY = actions.register("app.errors", label='Flowgraph _Errors', tooltip='View flow graph errors', - stock_id=gtk.STOCK_DIALOG_ERROR, + icon_name='dialog-error', ) -TOGGLE_CONSOLE_WINDOW = ToggleAction( +TOGGLE_CONSOLE_WINDOW = actions.register("win.toggle_console_window", label='Show _Console Panel', tooltip='Toggle visibility of the console', - keypresses=(gtk.keysyms.r, gtk.gdk.CONTROL_MASK), + keypresses=["<Ctrl>r"], preference_name='console_window_visible' ) -TOGGLE_BLOCKS_WINDOW = ToggleAction( +# TODO: Might be able to convert this to a Gio.PropertyAction eventually. +# actions would need to be defined in the correct class and not globally +TOGGLE_BLOCKS_WINDOW = actions.register("win.toggle_blocks_window", label='Show _Block Tree Panel', tooltip='Toggle visibility of the block tree widget', - keypresses=(gtk.keysyms.b, gtk.gdk.CONTROL_MASK), + keypresses=["<Ctrl>b"], preference_name='blocks_window_visible' ) -TOGGLE_SCROLL_LOCK = ToggleAction( +TOGGLE_SCROLL_LOCK = actions.register("win.console.scroll_lock", label='Console Scroll _Lock', tooltip='Toggle scroll lock for the console window', preference_name='scroll_lock' ) -ABOUT_WINDOW_DISPLAY = Action( +ABOUT_WINDOW_DISPLAY = actions.register("app.about", label='_About', tooltip='About this program', - stock_id=gtk.STOCK_ABOUT, + icon_name='help-about', ) -HELP_WINDOW_DISPLAY = Action( +HELP_WINDOW_DISPLAY = actions.register("app.help", label='_Help', tooltip='Usage tips', - stock_id=gtk.STOCK_HELP, - keypresses=(gtk.keysyms.F1, NO_MODS_MASK), + icon_name='help-contents', + keypresses=["F1"], ) -TYPES_WINDOW_DISPLAY = Action( +TYPES_WINDOW_DISPLAY = actions.register("app.types", label='_Types', tooltip='Types color mapping', - stock_id=gtk.STOCK_DIALOG_INFO, + icon_name='dialog-information', ) -FLOW_GRAPH_GEN = Action( +FLOW_GRAPH_GEN = actions.register("app.flowgraph.generate", label='_Generate', tooltip='Generate the flow graph', - stock_id=gtk.STOCK_CONVERT, - keypresses=(gtk.keysyms.F5, NO_MODS_MASK), + icon_name='insert-object', + keypresses=["F5"], ) -FLOW_GRAPH_EXEC = Action( +FLOW_GRAPH_EXEC = actions.register("app.flowgraph.execute", label='_Execute', tooltip='Execute the flow graph', - stock_id=gtk.STOCK_MEDIA_PLAY, - keypresses=(gtk.keysyms.F6, NO_MODS_MASK), + icon_name='media-playback-start', + keypresses=["F6"], ) -FLOW_GRAPH_KILL = Action( +FLOW_GRAPH_KILL = actions.register("app.flowgraph.kill", label='_Kill', tooltip='Kill the flow graph', - stock_id=gtk.STOCK_STOP, - keypresses=(gtk.keysyms.F7, NO_MODS_MASK), + icon_name='media-playback-stop', + keypresses=["F7"], ) -FLOW_GRAPH_SCREEN_CAPTURE = Action( +FLOW_GRAPH_SCREEN_CAPTURE = actions.register("app.flowgraph.screen_capture", label='Screen Ca_pture', tooltip='Create a screen capture of the flow graph', - stock_id=gtk.STOCK_PRINT, - keypresses=(gtk.keysyms.Print, NO_MODS_MASK), -) -PORT_CONTROLLER_DEC = Action( - keypresses=(gtk.keysyms.minus, NO_MODS_MASK, gtk.keysyms.KP_Subtract, NO_MODS_MASK), -) -PORT_CONTROLLER_INC = Action( - keypresses=(gtk.keysyms.plus, NO_MODS_MASK, gtk.keysyms.KP_Add, NO_MODS_MASK), -) -BLOCK_INC_TYPE = Action( - keypresses=(gtk.keysyms.Down, NO_MODS_MASK), -) -BLOCK_DEC_TYPE = Action( - keypresses=(gtk.keysyms.Up, NO_MODS_MASK), -) -RELOAD_BLOCKS = Action( + icon_name='printer', + keypresses=["<Ctrl>p"], +) +PORT_CONTROLLER_DEC = actions.register("win.port_controller_dec") +PORT_CONTROLLER_INC = actions.register("win.port_controller_inc") +BLOCK_INC_TYPE = actions.register("win.block_inc_type") +BLOCK_DEC_TYPE = actions.register("win.block_dec_type") +RELOAD_BLOCKS = actions.register("app.reload_blocks", label='Reload _Blocks', tooltip='Reload Blocks', - stock_id=gtk.STOCK_REFRESH + icon_name='view-refresh' ) -FIND_BLOCKS = Action( +FIND_BLOCKS = actions.register("win.find_blocks", label='_Find Blocks', tooltip='Search for a block by name (and key)', - stock_id=gtk.STOCK_FIND, - keypresses=(gtk.keysyms.f, gtk.gdk.CONTROL_MASK, - gtk.keysyms.slash, NO_MODS_MASK), + icon_name='edit-find', + keypresses=["<Ctrl>f", "slash"], ) -CLEAR_CONSOLE = Action( +CLEAR_CONSOLE = actions.register("win.console.clear", label='_Clear Console', tooltip='Clear Console', - stock_id=gtk.STOCK_CLEAR, + icon_name='edit-clear', ) -SAVE_CONSOLE = Action( +SAVE_CONSOLE = actions.register("win.console.save", label='_Save Console', tooltip='Save Console', - stock_id=gtk.STOCK_SAVE, + icon_name='edit-save', ) -OPEN_HIER = Action( +OPEN_HIER = actions.register("win.open_hier", label='Open H_ier', tooltip='Open the source of the selected hierarchical block', - stock_id=gtk.STOCK_JUMP_TO, + icon_name='go-jump', ) -BUSSIFY_SOURCES = Action( +BUSSIFY_SOURCES = actions.register("win.bussify_sources", label='Toggle So_urce Bus', tooltip='Gang source ports into a single bus port', - stock_id=gtk.STOCK_JUMP_TO, + icon_name='go-jump', ) -BUSSIFY_SINKS = Action( +BUSSIFY_SINKS = actions.register("win.bussify_sinks", label='Toggle S_ink Bus', tooltip='Gang sink ports into a single bus port', - stock_id=gtk.STOCK_JUMP_TO, + icon_name='go-jump', ) -XML_PARSER_ERRORS_DISPLAY = Action( +XML_PARSER_ERRORS_DISPLAY = actions.register("app.xml_errors", label='_Parser Errors', tooltip='View errors that occurred while parsing XML files', - stock_id=gtk.STOCK_DIALOG_ERROR, + icon_name='dialog-error', ) -FLOW_GRAPH_OPEN_QSS_THEME = Action( +FLOW_GRAPH_OPEN_QSS_THEME = actions.register("app.open_qss_theme", label='Set Default QT GUI _Theme', tooltip='Set a default QT Style Sheet file to use for QT GUI', - stock_id=gtk.STOCK_OPEN, + icon_name='document-open', ) -TOOLS_RUN_FDESIGN = Action( +TOOLS_RUN_FDESIGN = actions.register("app.filter_design", label='Filter Design Tool', tooltip='Execute gr_filter_design', - stock_id=gtk.STOCK_EXECUTE, -) -TOOLS_MORE_TO_COME = Action( - label='More to come', + icon_name='media-playback-start', ) +POST_HANDLER = actions.register("app.post_handler") +READY = actions.register("app.ready") diff --git a/grc/gui/ActionHandler.py b/grc/gui/Application.py index 017dab3346..70cf9b78b2 100644 --- a/grc/gui/ActionHandler.py +++ b/grc/gui/Application.py @@ -18,33 +18,36 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import gobject -import gtk +from __future__ import absolute_import, print_function + +import logging import os import subprocess -from . import Dialogs, Preferences, Actions, Executor, Constants, Utils -from .FileDialogs import (OpenFlowGraphFileDialog, SaveFlowGraphFileDialog, - SaveConsoleFileDialog, SaveScreenShotDialog, - OpenQSSFileDialog) +from gi.repository import Gtk, Gio, GLib, GObject + +from . import Constants, Dialogs, Actions, Executor, FileDialogs, Utils, Bars + from .MainWindow import MainWindow -from .ParserErrorsDialog import ParserErrorsDialog +# from .ParserErrorsDialog import ParserErrorsDialog from .PropsDialog import PropsDialog -from ..core import ParseXML, Messages +from ..core import Messages -gobject.threads_init() +log = logging.getLogger(__name__) -class ActionHandler: + +class Application(Gtk.Application): """ The action handler will setup all the major window components, and handle button presses and flow graph operations from the GUI. """ def __init__(self, file_paths, platform): + Gtk.Application.__init__(self) """ - ActionHandler constructor. + Application constructor. Create the main window, setup the message handler, import the preferences, and connect all of the action handlers. Finally, enter the gtk main loop and block. @@ -54,41 +57,57 @@ class ActionHandler: """ self.clipboard = None self.dialog = None - for action in Actions.get_all_actions(): action.connect('activate', self._handle_action) - #setup the main window + + # Setup the main window self.platform = platform - self.main_window = MainWindow(platform, self._handle_action) + self.config = platform.config + + log.debug("Application()") + # Connect all actions to _handle_action + for x in Actions.get_actions(): + Actions.connect(x, handler=self._handle_action) + Actions.actions[x].enable() + if x.startswith("app."): + self.add_action(Actions.actions[x]) + # Setup the shortcut keys + # These are the globally defined shortcuts + keypress = Actions.actions[x].keypresses + if keypress: + self.set_accels_for_action(x, keypress) + + # Initialize + self.init_file_paths = [os.path.abspath(file_path) for file_path in file_paths] + self.init = False + + def do_startup(self): + Gtk.Application.do_startup(self) + log.debug("Application.do_startup()") + + # Setup the menu + log.debug("Creating menu") + ''' + self.menu = Bars.Menu() + self.set_menu() + if self.prefers_app_menu(): + self.set_app_menu(self.menu) + else: + self.set_menubar(self.menu) + ''' + + def do_activate(self): + Gtk.Application.do_activate(self) + log.debug("Application.do_activate()") + + self.main_window = MainWindow(self, self.platform) self.main_window.connect('delete-event', self._quit) - self.main_window.connect('key-press-event', self._handle_key_press) - self.get_page = self.main_window.get_page self.get_focus_flag = self.main_window.get_focus_flag + #setup the messages Messages.register_messenger(self.main_window.add_console_line) - Messages.send_init(platform) - #initialize - self.init_file_paths = [os.path.abspath(file_path) for file_path in file_paths] - self.init = False - Actions.APPLICATION_INITIALIZE() + Messages.send_init(self.platform) - def _handle_key_press(self, widget, event): - """ - Handle key presses from the keyboard and translate key combinations into actions. - This key press handler is called prior to the gtk key press handler. - This handler bypasses built in accelerator key handling when in focus because - * some keys are ignored by the accelerators like the direction keys, - * some keys are not registered to any accelerators but are still used. - When not in focus, gtk and the accelerators handle the the key press. - - Returns: - false to let gtk handle the key action - """ - # prevent key event stealing while the search box is active - # .has_focus() only in newer versions 2.17+? - # .is_focus() seems to work, but exactly the same - if self.main_window.btwin.search_entry.flags() & gtk.HAS_FOCUS: - return False - if not self.get_focus_flag(): return False - return Actions.handle_key_press(event) + log.debug("Calling Actions.APPLICATION_INITIALIZE") + Actions.APPLICATION_INITIALIZE() def _quit(self, window, event): """ @@ -103,61 +122,111 @@ class ActionHandler: return True def _handle_action(self, action, *args): - #print action + log.debug("_handle_action({0}, {1})".format(action, args)) main = self.main_window - page = main.get_page() - flow_graph = page.get_flow_graph() if page else None + page = main.current_page + flow_graph = page.flow_graph if page else None def flow_graph_update(fg=flow_graph): - main.vars.update_gui() + main.vars.update_gui(fg.blocks) fg.update() ################################################## # Initialize/Quit ################################################## if action == Actions.APPLICATION_INITIALIZE: - file_path_to_show = Preferences.file_open() - for file_path in (self.init_file_paths or Preferences.get_open_files()): + log.debug("APPLICATION_INITIALIZE") + file_path_to_show = self.config.file_open() + for file_path in (self.init_file_paths or self.config.get_open_files()): if os.path.exists(file_path): main.new_page(file_path, show=file_path_to_show == file_path) - if not self.get_page(): + if not main.current_page: main.new_page() # ensure that at least a blank page exists main.btwin.search_entry.hide() - # Disable all actions, then re-enable a few - for action in Actions.get_all_actions(): - action.set_sensitive(False) # set all actions disabled + """ + Only disable certain actions on startup. Each of these actions are + conditionally enabled in _handle_action, so disable them first. + - FLOW_GRAPH_UNDO/REDO are set in gui/StateCache.py + - XML_PARSER_ERRORS_DISPLAY is set in RELOAD_BLOCKS + + TODO: These 4 should probably be included, but they are not currently + enabled anywhere else: + - PORT_CONTROLLER_DEC, PORT_CONTROLLER_INC + - BLOCK_INC_TYPE, BLOCK_DEC_TYPE + + TODO: These should be handled better. They are set in + update_exec_stop(), but not anywhere else + - FLOW_GRAPH_GEN, FLOW_GRAPH_EXEC, FLOW_GRAPH_KILL + """ + for action in ( + Actions.ERRORS_WINDOW_DISPLAY, + Actions.ELEMENT_DELETE, + Actions.BLOCK_PARAM_MODIFY, + Actions.BLOCK_ROTATE_CCW, + Actions.BLOCK_ROTATE_CW, + Actions.BLOCK_VALIGN_TOP, + Actions.BLOCK_VALIGN_MIDDLE, + Actions.BLOCK_VALIGN_BOTTOM, + Actions.BLOCK_HALIGN_LEFT, + Actions.BLOCK_HALIGN_CENTER, + Actions.BLOCK_HALIGN_RIGHT, + Actions.BLOCK_CUT, + Actions.BLOCK_COPY, + Actions.BLOCK_PASTE, + Actions.BLOCK_ENABLE, + Actions.BLOCK_DISABLE, + Actions.BLOCK_BYPASS, + Actions.BLOCK_CREATE_HIER, + Actions.OPEN_HIER, + Actions.BUSSIFY_SOURCES, + Actions.BUSSIFY_SINKS, + Actions.FLOW_GRAPH_SAVE, + Actions.FLOW_GRAPH_UNDO, + Actions.FLOW_GRAPH_REDO, + Actions.XML_PARSER_ERRORS_DISPLAY + ): + action.disable() + + # Load preferences for action in ( - Actions.APPLICATION_QUIT, Actions.FLOW_GRAPH_NEW, - Actions.FLOW_GRAPH_OPEN, Actions.FLOW_GRAPH_SAVE_AS, - Actions.FLOW_GRAPH_DUPLICATE, Actions.FLOW_GRAPH_SAVE_A_COPY, - Actions.FLOW_GRAPH_CLOSE, Actions.ABOUT_WINDOW_DISPLAY, - Actions.FLOW_GRAPH_SCREEN_CAPTURE, Actions.HELP_WINDOW_DISPLAY, - Actions.TYPES_WINDOW_DISPLAY, Actions.TOGGLE_BLOCKS_WINDOW, - Actions.TOGGLE_CONSOLE_WINDOW, Actions.TOGGLE_HIDE_DISABLED_BLOCKS, - Actions.TOOLS_RUN_FDESIGN, Actions.TOGGLE_SCROLL_LOCK, - Actions.CLEAR_CONSOLE, Actions.SAVE_CONSOLE, - Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, Actions.TOGGLE_SNAP_TO_GRID, + Actions.TOGGLE_BLOCKS_WINDOW, + Actions.TOGGLE_CONSOLE_WINDOW, + Actions.TOGGLE_HIDE_DISABLED_BLOCKS, + Actions.TOGGLE_SCROLL_LOCK, + Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, + Actions.TOGGLE_SNAP_TO_GRID, Actions.TOGGLE_SHOW_BLOCK_COMMENTS, Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB, Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY, - Actions.FLOW_GRAPH_OPEN_QSS_THEME, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR, Actions.TOGGLE_HIDE_VARIABLES, - Actions.SELECT_ALL, ): - action.set_sensitive(True) + action.set_enabled(True) if hasattr(action, 'load_from_preferences'): action.load_from_preferences() - if ParseXML.xml_failures: - Messages.send_xml_errors_if_any(ParseXML.xml_failures) - Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(True) + + # Hide the panels *IF* it's saved in preferences + main.update_panel_visibility(main.BLOCKS, Actions.TOGGLE_BLOCKS_WINDOW.get_active()) + main.update_panel_visibility(main.CONSOLE, Actions.TOGGLE_CONSOLE_WINDOW.get_active()) + main.update_panel_visibility(main.VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR.get_active()) + + #if ParseXML.xml_failures: + # Messages.send_xml_errors_if_any(ParseXML.xml_failures) + # Actions.XML_PARSER_ERRORS_DISPLAY.set_enabled(True) + + # Force an update on the current page to match loaded preferences. + # In the future, change the __init__ order to load preferences first + page = main.current_page + if page: + page.flow_graph.update() + self.init = True elif action == Actions.APPLICATION_QUIT: if main.close_pages(): - gtk.main_quit() + Gtk.main_quit() exit(0) ################################################## # Selections @@ -171,21 +240,16 @@ class ActionHandler: ################################################## # Enable/Disable ################################################## - elif action == Actions.BLOCK_ENABLE: - if flow_graph.enable_selected(True): + elif action in (Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS): + changed = flow_graph.change_state_selected(new_state={ + Actions.BLOCK_ENABLE: 'enabled', + Actions.BLOCK_DISABLE: 'disabled', + Actions.BLOCK_BYPASS: 'bypassed', + }[action]) + if changed: flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) - elif action == Actions.BLOCK_DISABLE: - if flow_graph.enable_selected(False): - flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) - elif action == Actions.BLOCK_BYPASS: - if flow_graph.bypass_selected(): - flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False ################################################## # Cut/Copy/Paste ################################################## @@ -198,15 +262,15 @@ class ActionHandler: if self.clipboard: flow_graph.paste_from_clipboard(self.clipboard) flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False ################################################## # Create heir block ################################################## elif action == Actions.BLOCK_CREATE_HIER: # keeping track of coordinates for pasting later - coords = flow_graph.get_selected_blocks()[0].get_coordinate() + coords = flow_graph.selected_blocks()[0].coordinate x,y = coords x_min = x y_min = y @@ -215,22 +279,22 @@ class ActionHandler: params = []; # Save the state of the leaf blocks - for block in flow_graph.get_selected_blocks(): + for block in flow_graph.selected_blocks(): # Check for string variables within the blocks - for param in block.get_params(): + for param in block.params.values(): for variable in flow_graph.get_variables(): # If a block parameter exists that is a variable, create a parameter for it - if param.get_value() == variable.get_id(): + if param.get_value() == variable.name: params.append(param.get_value()) for flow_param in flow_graph.get_parameters(): # If a block parameter exists that is a parameter, create a parameter for it - if param.get_value() == flow_param.get_id(): + if param.get_value() == flow_param.name: params.append(param.get_value()) # keep track of x,y mins for pasting later - (x,y) = block.get_coordinate() + (x,y) = block.coordinate if x < x_min: x_min = x if y < y_min: @@ -239,15 +303,15 @@ class ActionHandler: for connection in block.connections: # Get id of connected blocks - source_id = connection.get_source().get_parent().get_id() - sink_id = connection.get_sink().get_parent().get_id() + source_id = connection.source_block.name + sink_id = connection.sink_block.name # If connected block is not in the list of selected blocks create a pad for it - if flow_graph.get_block(source_id) not in flow_graph.get_selected_blocks(): - pads.append({'key': connection.get_sink().get_key(), 'coord': connection.get_source().get_coordinate(), 'block_id' : block.get_id(), 'direction': 'source'}) + if flow_graph.get_block(source_id) not in flow_graph.selected_blocks(): + pads.append({'key': connection.sink_port.key, 'coord': connection.source_port.coordinate, 'block_id' : block.name, 'direction': 'source'}) - if flow_graph.get_block(sink_id) not in flow_graph.get_selected_blocks(): - pads.append({'key': connection.get_source().get_key(), 'coord': connection.get_sink().get_coordinate(), 'block_id' : block.get_id(), 'direction': 'sink'}) + if flow_graph.get_block(sink_id) not in flow_graph.selected_blocks(): + pads.append({'key': connection.source_port.key, 'coord': connection.sink_port.coordinate, 'block_id' : block.name, 'direction': 'sink'}) # Copy the selected blocks and paste them into a new page @@ -261,10 +325,10 @@ class ActionHandler: # Set flow graph to heir block type top_block = flow_graph.get_block("top_block") - top_block.get_param('generate_options').set_value('hb') + top_block.params['generate_options'].set_value('hb') # this needs to be a unique name - top_block.get_param('id').set_value('new_heir') + top_block.params['id'].set_value('new_heir') # Remove the default samp_rate variable block that is created remove_me = flow_graph.get_block("samp_rate") @@ -276,7 +340,7 @@ class ActionHandler: for param in params: param_id = flow_graph.add_new_block('parameter',(x_pos,10)) param_block = flow_graph.get_block(param_id) - param_block.get_param('id').set_value(param) + param_block.params['id'].set_value(param) x_pos = x_pos + 100 for pad in pads: @@ -288,16 +352,16 @@ class ActionHandler: # setup the references to the sink and source pad_block = flow_graph.get_block(pad_id) - pad_sink = pad_block.get_sinks()[0] + pad_sink = pad_block.sinks[0] source_block = flow_graph.get_block(pad['block_id']) source = source_block.get_source(pad['key']) # Ensure the port types match - while pad_sink.get_type() != source.get_type(): + while pad_sink.dtype != source.dtype: # Special case for some blocks that have non-standard type names, e.g. uhd - if pad_sink.get_type() == 'complex' and source.get_type() == 'fc32': + if pad_sink.dtype == 'complex' and source.dtype == 'fc32': break; pad_block.type_controller_modify(1) @@ -309,15 +373,15 @@ class ActionHandler: # setup the references to the sink and source pad_block = flow_graph.get_block(pad_id) - pad_source = pad_block.get_sources()[0] + pad_source = pad_block.sources[0] sink_block = flow_graph.get_block(pad['block_id']) sink = sink_block.get_sink(pad['key']) # Ensure the port types match - while sink.get_type() != pad_source.get_type(): + while sink.dtype != pad_source.dtype: # Special case for some blocks that have non-standard type names, e.g. uhd - if pad_source.get_type() == 'complex' and sink.get_type() == 'fc32': + if pad_source.dtype == 'complex' and sink.dtype == 'fc32': break; pad_block.type_controller_modify(1) @@ -332,311 +396,327 @@ class ActionHandler: # Move/Rotate/Delete/Create ################################################## elif action == Actions.BLOCK_MOVE: - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action in Actions.BLOCK_ALIGNMENTS: if flow_graph.align_selected(action): - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.BLOCK_ROTATE_CCW: if flow_graph.rotate_selected(90): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.BLOCK_ROTATE_CW: if flow_graph.rotate_selected(-90): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.ELEMENT_DELETE: if flow_graph.remove_selected(): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) + page.state_cache.save_new_state(flow_graph.export_data()) Actions.NOTHING_SELECT() - page.set_saved(False) + page.saved = False elif action == Actions.ELEMENT_CREATE: flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) + page.state_cache.save_new_state(flow_graph.export_data()) Actions.NOTHING_SELECT() - page.set_saved(False) + page.saved = False elif action == Actions.BLOCK_INC_TYPE: if flow_graph.type_controller_modify_selected(1): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.BLOCK_DEC_TYPE: if flow_graph.type_controller_modify_selected(-1): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.PORT_CONTROLLER_INC: if flow_graph.port_controller_modify_selected(1): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False elif action == Actions.PORT_CONTROLLER_DEC: if flow_graph.port_controller_modify_selected(-1): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False ################################################## # Window stuff ################################################## elif action == Actions.ABOUT_WINDOW_DISPLAY: - platform = flow_graph.get_parent() - Dialogs.AboutDialog(platform.config) + Dialogs.show_about(main, self.platform.config) elif action == Actions.HELP_WINDOW_DISPLAY: - Dialogs.HelpDialog() + Dialogs.show_help(main) elif action == Actions.TYPES_WINDOW_DISPLAY: - Dialogs.TypesDialog(flow_graph.get_parent()) + Dialogs.show_types(main) elif action == Actions.ERRORS_WINDOW_DISPLAY: - Dialogs.ErrorsDialog(flow_graph) + Dialogs.ErrorsDialog(main, flow_graph).run_and_destroy() elif action == Actions.TOGGLE_CONSOLE_WINDOW: + action.set_active(not action.get_active()) main.update_panel_visibility(main.CONSOLE, action.get_active()) action.save_to_preferences() elif action == Actions.TOGGLE_BLOCKS_WINDOW: + # This would be better matched to a Gio.PropertyAction, but to do + # this, actions would have to be defined in the window not globally + action.set_active(not action.get_active()) main.update_panel_visibility(main.BLOCKS, action.get_active()) action.save_to_preferences() elif action == Actions.TOGGLE_SCROLL_LOCK: + action.set_active(not action.get_active()) active = action.get_active() - main.text_display.scroll_lock = active + main.console.text_display.scroll_lock = active if active: - main.text_display.scroll_to_end() + main.console.text_display.scroll_to_end() action.save_to_preferences() elif action == Actions.CLEAR_CONSOLE: - main.text_display.clear() + main.console.text_display.clear() elif action == Actions.SAVE_CONSOLE: - file_path = SaveConsoleFileDialog(page.get_file_path()).run() + file_path = FileDialogs.SaveConsole(main, page.file_path).run() if file_path is not None: - main.text_display.save(file_path) + main.console.text_display.save(file_path) elif action == Actions.TOGGLE_HIDE_DISABLED_BLOCKS: + action.set_active(not action.get_active()) Actions.NOTHING_SELECT() elif action == Actions.TOGGLE_AUTO_HIDE_PORT_LABELS: + action.set_active(not action.get_active()) action.save_to_preferences() for page in main.get_pages(): - page.get_flow_graph().create_shapes() + page.flow_graph.create_shapes() elif action in (Actions.TOGGLE_SNAP_TO_GRID, Actions.TOGGLE_SHOW_BLOCK_COMMENTS, Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB): + action.set_active(not action.get_active()) action.save_to_preferences() elif action == Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY: + action.set_active(not action.get_active()) action.save_to_preferences() for page in main.get_pages(): - flow_graph_update(page.get_flow_graph()) + flow_graph_update(page.flow_graph) elif action == Actions.TOGGLE_HIDE_VARIABLES: - # Call the variable editor TOGGLE in case it needs to be showing - Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR() + action.set_active(not action.get_active()) + active = action.get_active() + # Either way, triggering this should simply trigger the variable editor + # to be visible. + varedit = Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR + if active: + log.debug("Variables are hidden. Forcing the variable panel to be visible.") + varedit.disable() + else: + varedit.enable() + # Just force it to show. + varedit.set_active(True) + main.update_panel_visibility(main.VARIABLES) Actions.NOTHING_SELECT() action.save_to_preferences() + varedit.save_to_preferences() + flow_graph_update() elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR: - # See if the variables are hidden - if Actions.TOGGLE_HIDE_VARIABLES.get_active(): - # Force this to be shown - main.update_panel_visibility(main.VARIABLES, True) - action.set_active(True) - action.set_sensitive(False) - else: - if action.get_sensitive(): - main.update_panel_visibility(main.VARIABLES, action.get_active()) - else: # This is occurring after variables are un-hidden - # Leave it enabled - action.set_sensitive(True) - action.set_active(True) - #Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR.set_sensitive(action.get_active()) + # TODO: There may be issues at startup since these aren't triggered + # the same was as Gtk.Actions when loading preferences. + action.set_active(not action.get_active()) + # Just assume this was triggered because it was enabled. + main.update_panel_visibility(main.VARIABLES, action.get_active()) + action.save_to_preferences() + + # Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR.set_enabled(action.get_active()) action.save_to_preferences() elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR: + action.set_active(not action.get_active()) if self.init: - md = gtk.MessageDialog(main, - gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, - gtk.BUTTONS_CLOSE, "Moving the variable editor requires a restart of GRC.") - md.run() - md.destroy() + Dialogs.MessageDialogWrapper( + main, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, + markup="Moving the variable editor requires a restart of GRC." + ).run_and_destroy() action.save_to_preferences() ################################################## # Param Modifications ################################################## elif action == Actions.BLOCK_PARAM_MODIFY: - if action.args: - selected_block = action.args[0] - else: - selected_block = flow_graph.get_selected_block() + selected_block = args[0] if args[0] else flow_graph.selected_block if selected_block: - self.dialog = PropsDialog(selected_block) - response = gtk.RESPONSE_APPLY - while response == gtk.RESPONSE_APPLY: # rerun the dialog if Apply was hit + self.dialog = PropsDialog(self.main_window, selected_block) + response = Gtk.ResponseType.APPLY + while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit response = self.dialog.run() - if response in (gtk.RESPONSE_APPLY, gtk.RESPONSE_ACCEPT): + if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT): flow_graph_update() - page.get_state_cache().save_new_state(flow_graph.export_data()) - page.set_saved(False) + page.state_cache.save_new_state(flow_graph.export_data()) + page.saved = False else: # restore the current state - n = page.get_state_cache().get_current_state() + n = page.state_cache.get_current_state() flow_graph.import_data(n) flow_graph_update() - if response == gtk.RESPONSE_APPLY: + if response == Gtk.ResponseType.APPLY: # null action, that updates the main window Actions.ELEMENT_SELECT() self.dialog.destroy() self.dialog = None elif action == Actions.EXTERNAL_UPDATE: - page.get_state_cache().save_new_state(flow_graph.export_data()) + page.state_cache.save_new_state(flow_graph.export_data()) flow_graph_update() if self.dialog is not None: self.dialog.update_gui(force=True) - page.set_saved(False) + page.saved = False elif action == Actions.VARIABLE_EDITOR_UPDATE: - page.get_state_cache().save_new_state(flow_graph.export_data()) + page.state_cache.save_new_state(flow_graph.export_data()) flow_graph_update() - page.set_saved(False) + page.saved = False ################################################## # View Parser Errors ################################################## elif action == Actions.XML_PARSER_ERRORS_DISPLAY: - ParserErrorsDialog(ParseXML.xml_failures).run() + # ParserErrorsDialog(ParseXML.xml_failures).run() + pass ################################################## # Undo/Redo ################################################## elif action == Actions.FLOW_GRAPH_UNDO: - n = page.get_state_cache().get_prev_state() + n = page.state_cache.get_prev_state() if n: flow_graph.unselect() flow_graph.import_data(n) flow_graph_update() - page.set_saved(False) + page.saved = False elif action == Actions.FLOW_GRAPH_REDO: - n = page.get_state_cache().get_next_state() + n = page.state_cache.get_next_state() if n: flow_graph.unselect() flow_graph.import_data(n) flow_graph_update() - page.set_saved(False) + page.saved = False ################################################## # New/Open/Save/Close ################################################## elif action == Actions.FLOW_GRAPH_NEW: main.new_page() if args: - flow_graph = main.get_flow_graph() - flow_graph._options_block.get_param('generate_options').set_value(args[0]) + flow_graph = main.current_page.flow_graph + flow_graph._options_block.params['generate_options'].set_value(str(args[0])[1:-1]) flow_graph_update(flow_graph) elif action == Actions.FLOW_GRAPH_OPEN: - file_paths = args if args else OpenFlowGraphFileDialog(page.get_file_path()).run() - if file_paths: #open a new page for each file, show only the first + file_paths = args[0] if args[0] else FileDialogs.OpenFlowGraph(main, page.file_path).run() + if file_paths: # Open a new page for each file, show only the first for i,file_path in enumerate(file_paths): main.new_page(file_path, show=(i==0)) - Preferences.add_recent_file(file_path) + self.config.add_recent_file(file_path) main.tool_bar.refresh_submenus() - main.menu_bar.refresh_submenus() - main.vars.update_gui() - + #main.menu_bar.refresh_submenus() elif action == Actions.FLOW_GRAPH_OPEN_QSS_THEME: - file_paths = OpenQSSFileDialog(self.platform.config.install_prefix + - '/share/gnuradio/themes/').run() + file_paths = FileDialogs.OpenQSS(main, self.platform.config.install_prefix + + '/share/gnuradio/themes/').run() if file_paths: - try: - prefs = self.platform.config.prefs - prefs.set_string("qtgui", "qss", file_paths[0]) - prefs.save() - except Exception as e: - Messages.send("Failed to save QSS preference: " + str(e)) + self.platform.config.default_qss_theme = file_paths[0] elif action == Actions.FLOW_GRAPH_CLOSE: main.close_page() elif action == Actions.FLOW_GRAPH_SAVE: #read-only or undefined file path, do save-as - if page.get_read_only() or not page.get_file_path(): + if page.get_read_only() or not page.file_path: Actions.FLOW_GRAPH_SAVE_AS() #otherwise try to save else: try: - ParseXML.to_file(flow_graph.export_data(), page.get_file_path()) - flow_graph.grc_file_path = page.get_file_path() - page.set_saved(True) + self.platform.save_flow_graph(page.file_path, flow_graph) + flow_graph.grc_file_path = page.file_path + page.saved = True except IOError: - Messages.send_fail_save(page.get_file_path()) - page.set_saved(False) + Messages.send_fail_save(page.file_path) + page.saved = False elif action == Actions.FLOW_GRAPH_SAVE_AS: - file_path = SaveFlowGraphFileDialog(page.get_file_path()).run() + file_path = FileDialogs.SaveFlowGraph(main, page.file_path).run() if file_path is not None: - page.set_file_path(file_path) + page.file_path = os.path.abspath(file_path) Actions.FLOW_GRAPH_SAVE() - Preferences.add_recent_file(file_path) + self.config.add_recent_file(file_path) main.tool_bar.refresh_submenus() - main.menu_bar.refresh_submenus() - elif action == Actions.FLOW_GRAPH_SAVE_A_COPY: + #TODO + #main.menu_bar.refresh_submenus() + elif action == Actions.FLOW_GRAPH_SAVE_COPY: try: - if not page.get_file_path(): + if not page.file_path: + # Make sure the current flowgraph has been saved Actions.FLOW_GRAPH_SAVE_AS() else: - dup_file_path = page.get_file_path() + dup_file_path = page.file_path dup_file_name = '.'.join(dup_file_path.split('.')[:-1]) + "_copy" # Assuming .grc extension at the end of file_path - dup_file_path_temp = dup_file_name+'.grc' + dup_file_path_temp = dup_file_name + Constants.FILE_EXTENSION count = 1 while os.path.exists(dup_file_path_temp): - dup_file_path_temp = dup_file_name+'('+str(count)+').grc' + dup_file_path_temp = '{}({}){}'.format(dup_file_name, count, Constants.FILE_EXTENSION) count += 1 - dup_file_path_user = SaveFlowGraphFileDialog(dup_file_path_temp).run() + dup_file_path_user = FileDialogs.SaveFlowGraph(main, dup_file_path_temp).run() if dup_file_path_user is not None: - ParseXML.to_file(flow_graph.export_data(), dup_file_path_user) + self.platform.save_flow_graph(dup_file_path_user, flow_graph) Messages.send('Saved Copy to: "' + dup_file_path_user + '"\n') except IOError: Messages.send_fail_save("Can not create a copy of the flowgraph\n") elif action == Actions.FLOW_GRAPH_DUPLICATE: - flow_graph = main.get_flow_graph() + previous = flow_graph + # Create a new page main.new_page() - curr_page = main.get_page() - new_flow_graph = main.get_flow_graph() - new_flow_graph.import_data(flow_graph.export_data()) + page = main.current_page + new_flow_graph = page.flow_graph + # Import the old data and mark the current as not saved + new_flow_graph.import_data(previous.export_data()) flow_graph_update(new_flow_graph) - curr_page.set_saved(False) + page.saved = False elif action == Actions.FLOW_GRAPH_SCREEN_CAPTURE: - file_path, background_transparent = SaveScreenShotDialog(page.get_file_path()).run() + file_path, background_transparent = SaveScreenShotDialog(main, page.get_file_path()).run() if file_path is not None: - pixbuf = flow_graph.get_drawing_area().get_screenshot(background_transparent) - pixbuf.save(file_path, Constants.IMAGE_FILE_EXTENSION[1:]) + try: + Utils.make_screenshot(flow_graph, file_path, background_transparent) + except ValueError: + Messages.send('Failed to generate screen shot\n') ################################################## # Gen/Exec/Stop ################################################## elif action == Actions.FLOW_GRAPH_GEN: - if not page.get_proc(): - if not page.get_saved() or not page.get_file_path(): - Actions.FLOW_GRAPH_SAVE() #only save if file path missing or not saved - if page.get_saved() and page.get_file_path(): + if not page.process: + if not page.saved or not page.file_path: + Actions.FLOW_GRAPH_SAVE() # only save if file path missing or not saved + if page.saved and page.file_path: generator = page.get_generator() try: - Messages.send_start_gen(generator.get_file_path()) + Messages.send_start_gen(generator.file_path) generator.write() except Exception as e: Messages.send_fail_gen(e) else: self.generator = None elif action == Actions.FLOW_GRAPH_EXEC: - if not page.get_proc(): + if not page.process: Actions.FLOW_GRAPH_GEN() xterm = self.platform.config.xterm_executable - if Preferences.xterm_missing() != xterm: + Dialogs.show_missing_xterm(main, xterm) + if self.config.xterm_missing() != xterm: if not os.path.exists(xterm): - Dialogs.MissingXTermDialog(xterm) - Preferences.xterm_missing(xterm) - if page.get_saved() and page.get_file_path(): + Dialogs.show_missing_xterm(main, xterm) + self.config.xterm_missing(xterm) + if page.saved and page.file_path: Executor.ExecFlowGraphThread( flow_graph_page=page, xterm_executable=xterm, callback=self.update_exec_stop ) elif action == Actions.FLOW_GRAPH_KILL: - if page.get_proc(): + if page.process: try: page.term_proc() except: - print "could not terminate process: %d" % page.get_proc().pid + print("could not terminate process: %d" % page.get_proc().pid) + elif action == Actions.PAGE_CHANGE: # pass and run the global actions pass elif action == Actions.RELOAD_BLOCKS: - self.platform.build_block_library() + self.platform.build_library() main.btwin.repopulate() - Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(bool( - ParseXML.xml_failures)) - Messages.send_xml_errors_if_any(ParseXML.xml_failures) + + #todo: implement parser error dialog for YAML + #Actions.XML_PARSER_ERRORS_DISPLAY.set_enabled(bool(ParseXML.xml_failures)) + #Messages.send_xml_errors_if_any(ParseXML.xml_failures) + # Force a redraw of the graph, by getting the current state and re-importing it main.update_pages() @@ -645,21 +725,20 @@ class ActionHandler: main.btwin.search_entry.show() main.btwin.search_entry.grab_focus() elif action == Actions.OPEN_HIER: - for b in flow_graph.get_selected_blocks(): - if b._grc_source: - main.new_page(b._grc_source, show=True) + for b in flow_graph.selected_blocks(): + grc_source = b.extra_data.get('grc_source', '') + if grc_source: + main.new_page(b.grc_source, show=True) elif action == Actions.BUSSIFY_SOURCES: - n = {'name':'bus', 'type':'bus'} - for b in flow_graph.get_selected_blocks(): - b.bussify(n, 'source') + for b in flow_graph.selected_blocks(): + b.bussify('source') flow_graph._old_selected_port = None flow_graph._new_selected_port = None Actions.ELEMENT_CREATE() elif action == Actions.BUSSIFY_SINKS: - n = {'name':'bus', 'type':'bus'} - for b in flow_graph.get_selected_blocks(): - b.bussify(n, 'sink') + for b in flow_graph.selected_blocks(): + b.bussify('sink') flow_graph._old_selected_port = None flow_graph._new_selected_port = None Actions.ELEMENT_CREATE() @@ -669,71 +748,67 @@ class ActionHandler: shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) else: - print '!!! Action "%s" not handled !!!' % action + log.warning('!!! Action "%s" not handled !!!' % action) ################################################## # Global Actions for all States ################################################## - page = main.get_page() # page and flowgraph might have changed - flow_graph = page.get_flow_graph() if page else None + page = main.current_page # page and flow graph might have changed + flow_graph = page.flow_graph if page else None - selected_blocks = flow_graph.get_selected_blocks() + selected_blocks = list(flow_graph.selected_blocks()) selected_block = selected_blocks[0] if selected_blocks else None #update general buttons - Actions.ERRORS_WINDOW_DISPLAY.set_sensitive(not flow_graph.is_valid()) - Actions.ELEMENT_DELETE.set_sensitive(bool(flow_graph.get_selected_elements())) - Actions.BLOCK_PARAM_MODIFY.set_sensitive(bool(selected_block)) - Actions.BLOCK_ROTATE_CCW.set_sensitive(bool(selected_blocks)) - Actions.BLOCK_ROTATE_CW.set_sensitive(bool(selected_blocks)) + Actions.ERRORS_WINDOW_DISPLAY.set_enabled(not flow_graph.is_valid()) + Actions.ELEMENT_DELETE.set_enabled(bool(flow_graph.selected_elements)) + Actions.BLOCK_PARAM_MODIFY.set_enabled(bool(selected_block)) + Actions.BLOCK_ROTATE_CCW.set_enabled(bool(selected_blocks)) + Actions.BLOCK_ROTATE_CW.set_enabled(bool(selected_blocks)) #update alignment options for act in Actions.BLOCK_ALIGNMENTS: if act: - act.set_sensitive(len(selected_blocks) > 1) + act.set_enabled(len(selected_blocks) > 1) #update cut/copy/paste - Actions.BLOCK_CUT.set_sensitive(bool(selected_blocks)) - Actions.BLOCK_COPY.set_sensitive(bool(selected_blocks)) - Actions.BLOCK_PASTE.set_sensitive(bool(self.clipboard)) + Actions.BLOCK_CUT.set_enabled(bool(selected_blocks)) + Actions.BLOCK_COPY.set_enabled(bool(selected_blocks)) + Actions.BLOCK_PASTE.set_enabled(bool(self.clipboard)) #update enable/disable/bypass - can_enable = any(block.get_state() != Constants.BLOCK_ENABLED + can_enable = any(block.state != 'enabled' for block in selected_blocks) - can_disable = any(block.get_state() != Constants.BLOCK_DISABLED + can_disable = any(block.state != 'disabled' for block in selected_blocks) - can_bypass_all = all(block.can_bypass() for block in selected_blocks) \ - and any (not block.get_bypassed() for block in selected_blocks) - Actions.BLOCK_ENABLE.set_sensitive(can_enable) - Actions.BLOCK_DISABLE.set_sensitive(can_disable) - Actions.BLOCK_BYPASS.set_sensitive(can_bypass_all) - - Actions.BLOCK_CREATE_HIER.set_sensitive(bool(selected_blocks)) - Actions.OPEN_HIER.set_sensitive(bool(selected_blocks)) - Actions.BUSSIFY_SOURCES.set_sensitive(bool(selected_blocks)) - Actions.BUSSIFY_SINKS.set_sensitive(bool(selected_blocks)) - Actions.RELOAD_BLOCKS.set_sensitive(True) - Actions.FIND_BLOCKS.set_sensitive(True) - #set the exec and stop buttons + can_bypass_all = ( + all(block.can_bypass() for block in selected_blocks) and + any(not block.get_bypassed() for block in selected_blocks) + ) + Actions.BLOCK_ENABLE.set_enabled(can_enable) + Actions.BLOCK_DISABLE.set_enabled(can_disable) + Actions.BLOCK_BYPASS.set_enabled(can_bypass_all) + + Actions.BLOCK_CREATE_HIER.set_enabled(bool(selected_blocks)) + Actions.OPEN_HIER.set_enabled(bool(selected_blocks)) + #Actions.BUSSIFY_SOURCES.set_enabled(bool(selected_blocks)) + #Actions.BUSSIFY_SINKS.set_enabled(bool(selected_blocks)) + Actions.RELOAD_BLOCKS.enable() + Actions.FIND_BLOCKS.enable() + self.update_exec_stop() - #saved status - Actions.FLOW_GRAPH_SAVE.set_sensitive(not page.get_saved()) + + Actions.FLOW_GRAPH_SAVE.set_enabled(not page.saved) main.update() - try: #set the size of the flow graph area (if changed) - new_size = Utils.scale( - flow_graph.get_option('window_size') or - self.platform.config.default_canvas_size - ) - if flow_graph.get_size() != tuple(new_size): - flow_graph.set_size(*new_size) - except: pass - #draw the flow graph + flow_graph.update_selected() - flow_graph.queue_draw() - return True #action was handled + page.drawing_area.queue_draw() + + return True # Action was handled def update_exec_stop(self): """ Update the exec and stop buttons. Lock and unlock the mutex for race conditions with exec flow graph threads. """ - sensitive = self.main_window.get_flow_graph().is_valid() and not self.get_page().get_proc() - Actions.FLOW_GRAPH_GEN.set_sensitive(sensitive) - Actions.FLOW_GRAPH_EXEC.set_sensitive(sensitive) - Actions.FLOW_GRAPH_KILL.set_sensitive(self.get_page().get_proc() is not None) + page = self.main_window.current_page + sensitive = page.flow_graph.is_valid() and not page.process + Actions.FLOW_GRAPH_GEN.set_enabled(sensitive) + Actions.FLOW_GRAPH_EXEC.set_enabled(sensitive) + Actions.FLOW_GRAPH_KILL.set_enabled(page.process is not None) diff --git a/grc/gui/Bars.py b/grc/gui/Bars.py index d9bc2aedb7..2a8040f5d5 100644 --- a/grc/gui/Bars.py +++ b/grc/gui/Bars.py @@ -1,5 +1,5 @@ """ -Copyright 2007, 2008, 2009, 2015 Free Software Foundation, Inc. +Copyright 2007, 2008, 2009, 2015, 2016 Free Software Foundation, Inc. This file is part of GNU Radio GNU Radio Companion is free software; you can redistribute it and/or @@ -17,304 +17,301 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk +from __future__ import absolute_import + +import logging + +from gi.repository import Gtk, GObject, Gio, GLib from . import Actions +log = logging.getLogger(__name__) + + +''' +# Menu/Toolbar Lists: +# +# Sub items can be 1 of 3 types +# - List Creates a section within the current menu +# - Tuple Creates a submenu using a string or action as the parent. The child +# can be another menu list or an identifier used to call a helper function. +# - Action Appends a new menu item to the current menu +# + +LIST_NAME = [ + [Action1, Action2], # New section + (Action3, [Action4, Action5]), # Submenu with action as parent + ("Label", [Action6, Action7]), # Submenu with string as parent + ("Label2", "helper") # Submenu with helper function. Calls 'create_helper()' +] +''' + + # The list of actions for the toolbar. -TOOLBAR_LIST = ( - (Actions.FLOW_GRAPH_NEW, 'flow_graph_new'), - (Actions.FLOW_GRAPH_OPEN, 'flow_graph_recent'), - Actions.FLOW_GRAPH_SAVE, - Actions.FLOW_GRAPH_CLOSE, - None, - Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, - Actions.FLOW_GRAPH_SCREEN_CAPTURE, - None, - Actions.BLOCK_CUT, - Actions.BLOCK_COPY, - Actions.BLOCK_PASTE, - Actions.ELEMENT_DELETE, - None, - Actions.FLOW_GRAPH_UNDO, - Actions.FLOW_GRAPH_REDO, - None, - Actions.ERRORS_WINDOW_DISPLAY, - Actions.FLOW_GRAPH_GEN, - Actions.FLOW_GRAPH_EXEC, - Actions.FLOW_GRAPH_KILL, - None, - Actions.BLOCK_ROTATE_CCW, - Actions.BLOCK_ROTATE_CW, - None, - Actions.BLOCK_ENABLE, - Actions.BLOCK_DISABLE, - Actions.BLOCK_BYPASS, - Actions.TOGGLE_HIDE_DISABLED_BLOCKS, - None, - Actions.FIND_BLOCKS, - Actions.RELOAD_BLOCKS, - Actions.OPEN_HIER, -) +TOOLBAR_LIST = [ + [(Actions.FLOW_GRAPH_NEW, 'flow_graph_new'), Actions.FLOW_GRAPH_OPEN, + (Actions.FLOW_GRAPH_OPEN_RECENT, 'flow_graph_recent'), Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_CLOSE], + [Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.FLOW_GRAPH_SCREEN_CAPTURE], + [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE], + [Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO], + [Actions.ERRORS_WINDOW_DISPLAY, Actions.FLOW_GRAPH_GEN, Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL], + [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW], + [Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS, Actions.TOGGLE_HIDE_DISABLED_BLOCKS], + [Actions.FIND_BLOCKS, Actions.RELOAD_BLOCKS, Actions.OPEN_HIER] +] + # The list of actions and categories for the menu bar. -MENU_BAR_LIST = ( - (gtk.Action('File', '_File', None, None), [ - 'flow_graph_new', - Actions.FLOW_GRAPH_DUPLICATE, - Actions.FLOW_GRAPH_OPEN, - 'flow_graph_recent', - None, - Actions.FLOW_GRAPH_SAVE, - Actions.FLOW_GRAPH_SAVE_AS, - Actions.FLOW_GRAPH_SAVE_A_COPY, - None, - Actions.FLOW_GRAPH_SCREEN_CAPTURE, - None, - Actions.FLOW_GRAPH_CLOSE, - Actions.APPLICATION_QUIT, - ]), - (gtk.Action('Edit', '_Edit', None, None), [ - Actions.FLOW_GRAPH_UNDO, - Actions.FLOW_GRAPH_REDO, - None, - Actions.BLOCK_CUT, - Actions.BLOCK_COPY, - Actions.BLOCK_PASTE, - Actions.ELEMENT_DELETE, - Actions.SELECT_ALL, - None, - Actions.BLOCK_ROTATE_CCW, - Actions.BLOCK_ROTATE_CW, - (gtk.Action('Align', '_Align', None, None), Actions.BLOCK_ALIGNMENTS), - None, - Actions.BLOCK_ENABLE, - Actions.BLOCK_DISABLE, - Actions.BLOCK_BYPASS, - None, - Actions.BLOCK_PARAM_MODIFY, - ]), - (gtk.Action('View', '_View', None, None), [ - Actions.TOGGLE_BLOCKS_WINDOW, - None, - Actions.TOGGLE_CONSOLE_WINDOW, - Actions.TOGGLE_SCROLL_LOCK, - Actions.SAVE_CONSOLE, - Actions.CLEAR_CONSOLE, - None, - Actions.TOGGLE_HIDE_VARIABLES, - Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, - Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR, - None, - Actions.TOGGLE_HIDE_DISABLED_BLOCKS, - Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, - Actions.TOGGLE_SNAP_TO_GRID, - Actions.TOGGLE_SHOW_BLOCK_COMMENTS, - None, - Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB, - None, - Actions.ERRORS_WINDOW_DISPLAY, - Actions.FIND_BLOCKS, - ]), - (gtk.Action('Run', '_Run', None, None), [ - Actions.FLOW_GRAPH_GEN, - Actions.FLOW_GRAPH_EXEC, - Actions.FLOW_GRAPH_KILL, - ]), - (gtk.Action('Tools', '_Tools', None, None), [ - Actions.TOOLS_RUN_FDESIGN, - Actions.FLOW_GRAPH_OPEN_QSS_THEME, - None, - Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY, - None, - Actions.TOOLS_MORE_TO_COME, - ]), - (gtk.Action('Help', '_Help', None, None), [ - Actions.HELP_WINDOW_DISPLAY, - Actions.TYPES_WINDOW_DISPLAY, - Actions.XML_PARSER_ERRORS_DISPLAY, - None, - Actions.ABOUT_WINDOW_DISPLAY, - ]), -) +MENU_BAR_LIST = [ + ('_File', [ + [(Actions.FLOW_GRAPH_NEW, 'flow_graph_new'), Actions.FLOW_GRAPH_DUPLICATE, + Actions.FLOW_GRAPH_OPEN, (Actions.FLOW_GRAPH_OPEN_RECENT, 'flow_graph_recent')], + [Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_SAVE_AS, Actions.FLOW_GRAPH_SAVE_COPY], + [Actions.FLOW_GRAPH_SCREEN_CAPTURE], + [Actions.FLOW_GRAPH_CLOSE, Actions.APPLICATION_QUIT] + ]), + ('_Edit', [ + [Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO], + [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE, Actions.SELECT_ALL], + [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW, ('_Align', Actions.BLOCK_ALIGNMENTS)], + [Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS], + [Actions.BLOCK_PARAM_MODIFY] + ]), + ('_View', [ + [Actions.TOGGLE_BLOCKS_WINDOW], + [Actions.TOGGLE_CONSOLE_WINDOW, Actions.TOGGLE_SCROLL_LOCK, Actions.SAVE_CONSOLE, Actions.CLEAR_CONSOLE], + [Actions.TOGGLE_HIDE_VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR], + [Actions.TOGGLE_HIDE_DISABLED_BLOCKS, Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, Actions.TOGGLE_SNAP_TO_GRID, Actions.TOGGLE_SHOW_BLOCK_COMMENTS], + [Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB], + [Actions.ERRORS_WINDOW_DISPLAY, Actions.FIND_BLOCKS], + ]), + ('_Run', [ + Actions.FLOW_GRAPH_GEN, Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL + ]), + ('_Tools', [ + [Actions.TOOLS_RUN_FDESIGN, Actions.FLOW_GRAPH_OPEN_QSS_THEME], + [Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY] + ]), + ('_Help', [ + [Actions.HELP_WINDOW_DISPLAY, Actions.TYPES_WINDOW_DISPLAY, Actions.XML_PARSER_ERRORS_DISPLAY], + [Actions.ABOUT_WINDOW_DISPLAY] + ])] + # The list of actions for the context menu. CONTEXT_MENU_LIST = [ - Actions.BLOCK_CUT, - Actions.BLOCK_COPY, - Actions.BLOCK_PASTE, - Actions.ELEMENT_DELETE, - None, - Actions.BLOCK_ROTATE_CCW, - Actions.BLOCK_ROTATE_CW, - Actions.BLOCK_ENABLE, - Actions.BLOCK_DISABLE, - Actions.BLOCK_BYPASS, - None, - (gtk.Action('More', '_More', None, None), [ - Actions.BLOCK_CREATE_HIER, - Actions.OPEN_HIER, - None, - Actions.BUSSIFY_SOURCES, - Actions.BUSSIFY_SINKS, - ]), - Actions.BLOCK_PARAM_MODIFY + [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE], + [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW, Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS], + [("_More", [ + [Actions.BLOCK_CREATE_HIER, Actions.OPEN_HIER], + [Actions.BUSSIFY_SOURCES, Actions.BUSSIFY_SINKS]] + )], + [Actions.BLOCK_PARAM_MODIFY], ] -class SubMenuCreator(object): +class SubMenuHelper(object): + ''' Generates custom submenus for the main menu or toolbar. ''' - def __init__(self, generate_modes, action_handler_callback): - self.generate_modes = generate_modes - self.action_handler_callback = action_handler_callback - self.submenus = [] + def __init__(self): + self.submenus = {} - def create_submenu(self, action_tuple, item): - func = getattr(self, '_fill_' + action_tuple[1] + "_submenu") - self.submenus.append((action_tuple[0], func, item)) - self.refresh_submenus() + def build_submenu(self, name, obj, set_func): + # Get the correct helper function + create_func = getattr(self, "create_{}".format(name)) + # Save the helper functions for rebuilding the menu later + self.submenus[name] = (create_func, obj, set_func) + # Actually build the menu + set_func(obj, create_func()) def refresh_submenus(self): - for action, func, item in self.submenus: - try: - item.set_property("menu", func(action)) - except TypeError: - item.set_property("submenu", func(action)) - item.set_property('sensitive', True) - - def callback_adaptor(self, item, action_key): - action, key = action_key - self.action_handler_callback(action, key) - - def _fill_flow_graph_new_submenu(self, action): - """Sub menu to create flow-graph with pre-set generate mode""" - menu = gtk.Menu() - for key, name, default in self.generate_modes: - if default: - item = Actions.FLOW_GRAPH_NEW.create_menu_item() - item.set_label(name) - else: - item = gtk.MenuItem(name, use_underline=False) - item.connect('activate', self.callback_adaptor, (action, key)) - menu.append(item) - menu.show_all() + for name in self.submenus: + create_func, obj, set_func = self.submenus[name] + print ("refresh", create_func, obj, set_func) + set_func(obj, create_func()) + + def create_flow_graph_new(self): + """ Different flowgraph types """ + menu = Gio.Menu() + platform = Gtk.Application.get_default().platform + generate_modes = platform.get_generate_options() + for key, name, default in generate_modes: + target = "app.flowgraph.new::{}".format(key) + menu.append(name, target) return menu - def _fill_flow_graph_recent_submenu(self, action): - """menu showing recent flow-graphs""" - import Preferences - menu = gtk.Menu() - recent_files = Preferences.get_recent_files() + def create_flow_graph_recent(self): + """ Recent flow graphs """ + + config = Gtk.Application.get_default().config + recent_files = config.get_recent_files() + menu = Gio.Menu() if len(recent_files) > 0: + files = Gio.Menu() for i, file_name in enumerate(recent_files): - item = gtk.MenuItem("%d. %s" % (i+1, file_name), use_underline=False) - item.connect('activate', self.callback_adaptor, - (action, file_name)) - menu.append(item) - menu.show_all() - return menu - return None + target = "app.flowgraph.open_recent::{}".format(file_name) + files.append(file_name, target) + menu.append_section(None, files) + #clear = Gio.Menu() + #clear.append("Clear recent files", "app.flowgraph.clear_recent") + #menu.append_section(None, clear) + else: + # Show an empty menu + menuitem = Gio.MenuItem.new("No items found", "app.none") + menu.append_item(menuitem) + return menu -class Toolbar(gtk.Toolbar, SubMenuCreator): - """The gtk toolbar with actions added from the toolbar list.""" +class MenuHelper(SubMenuHelper): + """ + Recursively builds a menu from a given list of actions. - def __init__(self, generate_modes, action_handler_callback): - """ - Parse the list of action names in the toolbar list. - Look up the action for each name in the action list and add it to the - toolbar. - """ - gtk.Toolbar.__init__(self) - self.set_style(gtk.TOOLBAR_ICONS) - SubMenuCreator.__init__(self, generate_modes, action_handler_callback) - - for action in TOOLBAR_LIST: - if isinstance(action, tuple) and isinstance(action[1], str): - # create a button with a sub-menu - action[0].set_tool_item_type(gtk.MenuToolButton) - item = action[0].create_tool_item() - self.create_submenu(action, item) - self.refresh_submenus() - - elif action is None: - item = gtk.SeparatorToolItem() - - else: - action.set_tool_item_type(gtk.ToolButton) - item = action.create_tool_item() - # this reset of the tooltip property is required - # (after creating the tool item) for the tooltip to show - action.set_property('tooltip', action.get_property('tooltip')) - self.add(item) - - -class MenuHelperMixin(object): - """Mixin class to help build menus from the above action lists""" - - def _fill_menu(self, actions, menu=None): - """Create a menu from list of actions""" - menu = menu or gtk.Menu() + Args: + - actions: List of actions to build the menu + - menu: Current menu being built + + Notes: + - Tuple: Create a new submenu from the parent (1st) and child (2nd) elements + - Action: Append to current menu + - List: Start a new section + """ + + def __init__(self): + SubMenuHelper.__init__(self) + + def build_menu(self, actions, menu): for item in actions: if isinstance(item, tuple): - menu_item = self._make_sub_menu(*item) - elif isinstance(item, str): - menu_item = getattr(self, 'create_' + item)() - elif item is None: - menu_item = gtk.SeparatorMenuItem() - else: - menu_item = item.create_menu_item() - menu.append(menu_item) - menu.show_all() - return menu + # Create a new submenu + parent, child = (item[0], item[1]) + + # Create the parent + label, target = (parent, None) + if isinstance(parent, Actions.Action): + label = parent.label + target = "{}.{}".format(parent.prefix, parent.name) + menuitem = Gio.MenuItem.new(label, None) + if hasattr(parent, "icon_name"): + menuitem.set_icon(Gio.Icon.new_for_string(parent.icon_name)) + + # Create the new submenu + if isinstance(child, list): + submenu = Gio.Menu() + self.build_menu(child, submenu) + menuitem.set_submenu(submenu) + elif isinstance(child, str): + # Child is the name of the submenu to create + def set_func(obj, menu): + obj.set_submenu(menu) + self.build_submenu(child, menuitem, set_func) + menu.append_item(menuitem) + + elif isinstance(item, list): + # Create a new section + section = Gio.Menu() + self.build_menu(item, section) + menu.append_section(None, section) + + elif isinstance(item, Actions.Action): + # Append a new menuitem + target = "{}.{}".format(item.prefix, item.name) + menuitem = Gio.MenuItem.new(item.label, target) + if item.icon_name: + menuitem.set_icon(Gio.Icon.new_for_string(item.icon_name)) + menu.append_item(menuitem) + + +class ToolbarHelper(SubMenuHelper): + """ + Builds a toolbar from a given list of actions. + + Args: + - actions: List of actions to build the menu + - item: Current menu being built + + Notes: + - Tuple: Create a new submenu from the parent (1st) and child (2nd) elements + - Action: Append to current menu + - List: Start a new section + """ - def _make_sub_menu(self, main, actions): - """Create a submenu from a main action and a list of actions""" - main = main.create_menu_item() - main.set_submenu(self._fill_menu(actions)) - return main + def __init__(self): + SubMenuHelper.__init__(self) + def build_toolbar(self, actions, current): + for item in actions: + if isinstance(item, list): + # Toolbar's don't have sections like menus, so call this function + # recursively with the "section" and just append a separator. + self.build_toolbar(item, self) + current.insert(Gtk.SeparatorToolItem.new(), -1) + + elif isinstance(item, tuple): + parent, child = (item[0], item[1]) + # Create an item with a submenu + # Generate the submenu and add to the item. + # Add the item to the toolbar + button = Gtk.MenuToolButton.new() + # The tuple should be made up of an Action and something. + button.set_label(parent.label) + button.set_tooltip_text(parent.tooltip) + button.set_icon_name(parent.icon_name) + + target = "{}.{}".format(parent.prefix, parent.name) + button.set_action_name(target) + + def set_func(obj, menu): + obj.set_menu(Gtk.Menu.new_from_model(menu)) + + self.build_submenu(child, button, set_func) + current.insert(button, -1) + + elif isinstance(item, Actions.Action): + button = Gtk.ToolButton.new() + button.set_label(item.label) + button.set_tooltip_text(item.tooltip) + button.set_icon_name(item.icon_name) + target = "{}.{}".format(item.prefix, item.name) + button.set_action_name(target) + current.insert(button, -1) + + +class Menu(Gio.Menu, MenuHelper): + """ Main Menu """ -class MenuBar(gtk.MenuBar, MenuHelperMixin, SubMenuCreator): - """The gtk menu bar with actions added from the menu bar list.""" + def __init__(self): + GObject.GObject.__init__(self) + MenuHelper.__init__(self) - def __init__(self, generate_modes, action_handler_callback): - """ - Parse the list of submenus from the menubar list. - For each submenu, get a list of action names. - Look up the action for each name in the action list and add it to the - submenu. Add the submenu to the menu bar. - """ - gtk.MenuBar.__init__(self) - SubMenuCreator.__init__(self, generate_modes, action_handler_callback) - for main_action, actions in MENU_BAR_LIST: - self.append(self._make_sub_menu(main_action, actions)) + log.debug("Building the main menu") + self.build_menu(MENU_BAR_LIST, self) - def create_flow_graph_new(self): - main = gtk.ImageMenuItem(gtk.STOCK_NEW) - main.set_label(Actions.FLOW_GRAPH_NEW.get_label()) - func = self._fill_flow_graph_new_submenu - self.submenus.append((Actions.FLOW_GRAPH_NEW, func, main)) - self.refresh_submenus() - return main - def create_flow_graph_recent(self): - main = gtk.ImageMenuItem(gtk.STOCK_OPEN) - main.set_label(Actions.FLOW_GRAPH_OPEN_RECENT.get_label()) - func = self._fill_flow_graph_recent_submenu - self.submenus.append((Actions.FLOW_GRAPH_OPEN, func, main)) - self.refresh_submenus() - if main.get_submenu() is None: - main.set_property('sensitive', False) - return main +class ContextMenu(Gio.Menu, MenuHelper): + """ Context menu for the drawing area """ + + def __init__(self): + GObject.GObject.__init__(self) + log.debug("Building the context menu") + self.build_menu(CONTEXT_MENU_LIST, self) -class ContextMenu(gtk.Menu, MenuHelperMixin): - """The gtk menu with actions added from the context menu list.""" + +class Toolbar(Gtk.Toolbar, ToolbarHelper): + """ The gtk toolbar with actions added from the toolbar list. """ def __init__(self): - gtk.Menu.__init__(self) - self._fill_menu(CONTEXT_MENU_LIST, self) + """ + Parse the list of action names in the toolbar list. + Look up the action for each name in the action list and add it to the + toolbar. + """ + GObject.GObject.__init__(self) + ToolbarHelper.__init__(self) + + self.set_style(Gtk.ToolbarStyle.ICONS) + #self.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR) + + #SubMenuCreator.__init__(self) + self.build_toolbar(TOOLBAR_LIST, self) diff --git a/grc/gui/Block.py b/grc/gui/Block.py deleted file mode 100644 index 510ea4b624..0000000000 --- a/grc/gui/Block.py +++ /dev/null @@ -1,350 +0,0 @@ -""" -Copyright 2007, 2008, 2009 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import pygtk -pygtk.require('2.0') -import gtk -import pango - -from . import Actions, Colors, Utils, Constants - -from . Element import Element -from ..core.Param import num_to_str -from ..core.utils import odict -from ..core.utils.complexity import calculate_flowgraph_complexity -from ..core.Block import Block as _Block - -BLOCK_MARKUP_TMPL="""\ -#set $foreground = $block.is_valid() and 'black' or 'red' -<span foreground="$foreground" font_desc="$font"><b>$encode($block.get_name())</b></span>""" - -# Includes the additional complexity markup if enabled -COMMENT_COMPLEXITY_MARKUP_TMPL="""\ -#set $foreground = $block.get_enabled() and '#444' or '#888' -#if $complexity -<span foreground="#444" size="medium" font_desc="$font"><b>$encode($complexity)</b></span>#slurp -#end if -#if $complexity and $comment -<span></span> -#end if -#if $comment -<span foreground="$foreground" font_desc="$font">$encode($comment)</span>#slurp -#end if -""" - - -class Block(Element, _Block): - """The graphical signal block.""" - - def __init__(self, flow_graph, n): - """ - Block constructor. - Add graphics related params to the block. - """ - _Block.__init__(self, flow_graph, n) - - self.W = 0 - self.H = 0 - #add the position param - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'GUI Coordinate', - 'key': '_coordinate', - 'type': 'raw', - 'value': '(0, 0)', - 'hide': 'all', - }) - )) - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'GUI Rotation', - 'key': '_rotation', - 'type': 'raw', - 'value': '0', - 'hide': 'all', - }) - )) - Element.__init__(self) - self._comment_pixmap = None - self.has_busses = [False, False] # source, sink - - def get_coordinate(self): - """ - Get the coordinate from the position param. - - Returns: - the coordinate tuple (x, y) or (0, 0) if failure - """ - proximity = Constants.BORDER_PROXIMITY_SENSITIVITY - try: #should evaluate to tuple - x, y = Utils.scale(eval(self.get_param('_coordinate').get_value())) - fgW, fgH = self.get_parent().get_size() - if x <= 0: - x = 0 - elif x >= fgW - proximity: - x = fgW - proximity - if y <= 0: - y = 0 - elif y >= fgH - proximity: - y = fgH - proximity - return (x, y) - except: - self.set_coordinate((0, 0)) - return (0, 0) - - def set_coordinate(self, coor): - """ - Set the coordinate into the position param. - - Args: - coor: the coordinate tuple (x, y) - """ - if Actions.TOGGLE_SNAP_TO_GRID.get_active(): - offset_x, offset_y = (0, self.H/2) if self.is_horizontal() else (self.H/2, 0) - coor = ( - Utils.align_to_grid(coor[0] + offset_x) - offset_x, - Utils.align_to_grid(coor[1] + offset_y) - offset_y - ) - self.get_param('_coordinate').set_value(str(Utils.scale(coor, reverse=True))) - - def bound_move_delta(self, delta_coor): - """ - Limit potential moves from exceeding the bounds of the canvas - - Args: - delta_coor: requested delta coordinate (dX, dY) to move - - Returns: - The delta coordinate possible to move while keeping the block on the canvas - or the input (dX, dY) on failure - """ - dX, dY = delta_coor - - try: - fgW, fgH = self.get_parent().get_size() - x, y = Utils.scale(eval(self.get_param('_coordinate').get_value())) - if self.is_horizontal(): - sW, sH = self.W, self.H - else: - sW, sH = self.H, self.W - - if x + dX < 0: - dX = -x - elif dX + x + sW >= fgW: - dX = fgW - x - sW - if y + dY < 0: - dY = -y - elif dY + y + sH >= fgH: - dY = fgH - y - sH - except: - pass - - return ( dX, dY ) - - def get_rotation(self): - """ - Get the rotation from the position param. - - Returns: - the rotation in degrees or 0 if failure - """ - try: #should evaluate to dict - rotation = eval(self.get_param('_rotation').get_value()) - return int(rotation) - except: - self.set_rotation(Constants.POSSIBLE_ROTATIONS[0]) - return Constants.POSSIBLE_ROTATIONS[0] - - def set_rotation(self, rot): - """ - Set the rotation into the position param. - - Args: - rot: the rotation in degrees - """ - self.get_param('_rotation').set_value(str(rot)) - - def create_shapes(self): - """Update the block, parameters, and ports when a change occurs.""" - Element.create_shapes(self) - if self.is_horizontal(): self.add_area((0, 0), (self.W, self.H)) - elif self.is_vertical(): self.add_area((0, 0), (self.H, self.W)) - - def create_labels(self): - """Create the labels for the signal block.""" - Element.create_labels(self) - self._bg_color = self.is_dummy_block and Colors.MISSING_BLOCK_BACKGROUND_COLOR or \ - self.get_bypassed() and Colors.BLOCK_BYPASSED_COLOR or \ - self.get_enabled() and Colors.BLOCK_ENABLED_COLOR or Colors.BLOCK_DISABLED_COLOR - - layouts = list() - #create the main layout - layout = gtk.DrawingArea().create_pango_layout('') - layouts.append(layout) - layout.set_markup(Utils.parse_template(BLOCK_MARKUP_TMPL, block=self, font=Constants.BLOCK_FONT)) - self.label_width, self.label_height = layout.get_pixel_size() - #display the params - if self.is_dummy_block: - markups = [ - '<span foreground="black" font_desc="{font}"><b>key: </b>{key}</span>' - ''.format(font=Constants.PARAM_FONT, key=self._key) - ] - else: - markups = [param.get_markup() for param in self.get_params() if param.get_hide() not in ('all', 'part')] - if markups: - layout = gtk.DrawingArea().create_pango_layout('') - layout.set_spacing(Constants.LABEL_SEPARATION * pango.SCALE) - layout.set_markup('\n'.join(markups)) - layouts.append(layout) - w, h = layout.get_pixel_size() - self.label_width = max(w, self.label_width) - self.label_height += h + Constants.LABEL_SEPARATION - width = self.label_width - height = self.label_height - #setup the pixmap - pixmap = self.get_parent().new_pixmap(width, height) - gc = pixmap.new_gc() - gc.set_foreground(self._bg_color) - pixmap.draw_rectangle(gc, True, 0, 0, width, height) - #draw the layouts - h_off = 0 - for i,layout in enumerate(layouts): - w,h = layout.get_pixel_size() - if i == 0: w_off = (width-w)/2 - else: w_off = 0 - pixmap.draw_layout(gc, w_off, h_off, layout) - h_off = h + h_off + Constants.LABEL_SEPARATION - #create vertical and horizontal pixmaps - self.horizontal_label = pixmap - if self.is_vertical(): - self.vertical_label = self.get_parent().new_pixmap(height, width) - Utils.rotate_pixmap(gc, self.horizontal_label, self.vertical_label) - #calculate width and height needed - W = self.label_width + 2 * Constants.BLOCK_LABEL_PADDING - - def get_min_height_for_ports(): - visible_ports = filter(lambda p: not p.get_hide(), ports) - min_height = 2*Constants.PORT_BORDER_SEPARATION + len(visible_ports) * Constants.PORT_SEPARATION - if visible_ports: - min_height -= ports[0].H - return min_height - H = max( - [ # labels - self.label_height + 2 * Constants.BLOCK_LABEL_PADDING - ] + - [ # ports - get_min_height_for_ports() for ports in (self.get_sources_gui(), self.get_sinks_gui()) - ] + - [ # bus ports only - 2 * Constants.PORT_BORDER_SEPARATION + - sum([port.H + Constants.PORT_SPACING for port in ports if port.get_type() == 'bus']) - Constants.PORT_SPACING - for ports in (self.get_sources_gui(), self.get_sinks_gui()) - ] - ) - self.W, self.H = Utils.align_to_grid((W, H)) - self.has_busses = [ - any(port.get_type() == 'bus' for port in ports) - for ports in (self.get_sources_gui(), self.get_sinks_gui()) - ] - self.create_comment_label() - - def create_comment_label(self): - comment = self.get_comment() # Returns None if there are no comments - complexity = None - - # Show the flowgraph complexity on the top block if enabled - if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.get_key() == "options": - complexity = calculate_flowgraph_complexity(self.get_parent()) - complexity = "Complexity: {}bal".format(num_to_str(complexity)) - - layout = gtk.DrawingArea().create_pango_layout('') - layout.set_markup(Utils.parse_template(COMMENT_COMPLEXITY_MARKUP_TMPL, - block=self, - comment=comment, - complexity=complexity, - font=Constants.BLOCK_FONT)) - - # Setup the pixel map. Make sure that layout not empty - width, height = layout.get_pixel_size() - if width and height: - padding = Constants.BLOCK_LABEL_PADDING - pixmap = self.get_parent().new_pixmap(width + 2 * padding, - height + 2 * padding) - gc = pixmap.new_gc() - gc.set_foreground(Colors.COMMENT_BACKGROUND_COLOR) - pixmap.draw_rectangle( - gc, True, 0, 0, width + 2 * padding, height + 2 * padding) - pixmap.draw_layout(gc, padding, padding, layout) - self._comment_pixmap = pixmap - else: - self._comment_pixmap = None - - def draw(self, gc, window): - """ - Draw the signal block with label and inputs/outputs. - - Args: - gc: the graphics context - window: the gtk window to draw on - """ - # draw ports - for port in self.get_ports_gui(): - port.draw(gc, window) - # draw main block - x, y = self.get_coordinate() - Element.draw( - self, gc, window, bg_color=self._bg_color, - border_color=self.is_highlighted() and Colors.HIGHLIGHT_COLOR or - self.is_dummy_block and Colors.MISSING_BLOCK_BORDER_COLOR or Colors.BORDER_COLOR, - ) - #draw label image - if self.is_horizontal(): - window.draw_drawable(gc, self.horizontal_label, 0, 0, x+Constants.BLOCK_LABEL_PADDING, y+(self.H-self.label_height)/2, -1, -1) - elif self.is_vertical(): - window.draw_drawable(gc, self.vertical_label, 0, 0, x+(self.H-self.label_height)/2, y+Constants.BLOCK_LABEL_PADDING, -1, -1) - - def what_is_selected(self, coor, coor_m=None): - """ - Get the element that is selected. - - Args: - coor: the (x,y) tuple - coor_m: the (x_m, y_m) tuple - - Returns: - this block, a port, or None - """ - for port in self.get_ports_gui(): - port_selected = port.what_is_selected(coor, coor_m) - if port_selected: return port_selected - return Element.what_is_selected(self, coor, coor_m) - - def draw_comment(self, gc, window): - if not self._comment_pixmap: - return - x, y = self.get_coordinate() - - if self.is_horizontal(): - y += self.H + Constants.BLOCK_LABEL_PADDING - else: - x += self.H + Constants.BLOCK_LABEL_PADDING - - window.draw_drawable(gc, self._comment_pixmap, 0, 0, x, y, -1, -1) diff --git a/grc/gui/BlockTreeWindow.py b/grc/gui/BlockTreeWindow.py index 900cbd3151..3b9b8642f9 100644 --- a/grc/gui/BlockTreeWindow.py +++ b/grc/gui/BlockTreeWindow.py @@ -1,5 +1,5 @@ """ -Copyright 2007, 2008, 2009 Free Software Foundation, Inc. +Copyright 2007, 2008, 2009, 2016 Free Software Foundation, Inc. This file is part of GNU Radio GNU Radio Companion is free software; you can redistribute it and/or @@ -17,71 +17,56 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk -import gobject - -from . import Actions, Utils -from . import Constants - - -NAME_INDEX = 0 -KEY_INDEX = 1 -DOC_INDEX = 2 - -DOC_MARKUP_TMPL = """\ -#set $docs = [] -#if $doc.get('') - #set $docs += $doc.pop('').splitlines() + [''] -#end if -#for b, d in $doc.iteritems() - #set $docs += ['--- {0} ---'.format(b)] + d.splitlines() + [''] -#end for -#set $len_out = 0 -#for $n, $line in $enumerate($docs[:-1]) -#if $n - -#end if -$encode($line)#slurp -#set $len_out += $len($line) -#if $n > 10 or $len_out > 500 - -...#slurp -#break -#end if -#end for -#if $len_out == 0 -undocumented#slurp -#end if""" - -CAT_MARKUP_TMPL = """ -#set $name = $cat[-1] -#if len($cat) > 1 -Category: $cat[-1] -## -#elif $name == 'Core' -Module: Core - -This subtree is meant for blocks included with GNU Radio (in-tree). -## -#elif $name == $default_module -This subtree holds all blocks (from OOT modules) that specify no module name. \ -The module name is the root category enclosed in square brackets. - -Please consider contacting OOT module maintainer for any block in here \ -and kindly ask to update their GRC Block Descriptions or Block Tree to include a module name. -#else -Module: $name -## -#end if -""".strip() - - -class BlockTreeWindow(gtk.VBox): +from __future__ import absolute_import +import six + +from gi.repository import Gtk, Gdk, GObject + +from . import Actions, Utils, Constants + + +NAME_INDEX, KEY_INDEX, DOC_INDEX = range(3) + + +def _format_doc(doc): + docs = [] + if doc.get(''): + docs += doc.pop('').splitlines() + [''] + for block_name, docstring in six.iteritems(doc): + docs.append('--- {0} ---'.format(block_name)) + docs += docstring.splitlines() + docs.append('') + out = '' + for n, line in enumerate(docs[:-1]): + if n: + out += '\n' + out += Utils.encode(line) + if n > 10 or len(out) > 500: + out += '\n...' + break + return out or 'undocumented' + + +def _format_cat_tooltip(category): + tooltip = '{}: {}'.format('Category' if len(category) > 1 else 'Module', category[-1]) + + if category == ('Core',): + tooltip += '\n\nThis subtree is meant for blocks included with GNU Radio (in-tree).' + + elif category == (Constants.DEFAULT_BLOCK_MODULE_NAME,): + tooltip += '\n\n' + Constants.DEFAULT_BLOCK_MODULE_TOOLTIP + + return tooltip + + +class BlockTreeWindow(Gtk.VBox): """The block selection panel.""" - def __init__(self, platform, get_flow_graph): + __gsignals__ = { + 'create_new_block': (GObject.SignalFlags.RUN_FIRST, None, (str,)) + } + + def __init__(self, platform): """ BlockTreeWindow constructor. Create a tree view of the possible blocks in the platform. @@ -90,58 +75,52 @@ class BlockTreeWindow(gtk.VBox): Args: platform: the particular platform will all block prototypes - get_flow_graph: get the selected flow graph """ - gtk.VBox.__init__(self) + Gtk.VBox.__init__(self) self.platform = platform - self.get_flow_graph = get_flow_graph # search entry - self.search_entry = gtk.Entry() + self.search_entry = Gtk.Entry() try: - self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_PRIMARY, gtk.STOCK_FIND) - self.search_entry.set_icon_activatable(gtk.ENTRY_ICON_PRIMARY, False) - self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, gtk.STOCK_CLOSE) + self.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, 'edit-find') + self.search_entry.set_icon_activatable(Gtk.EntryIconPosition.PRIMARY, False) + self.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'window-close') self.search_entry.connect('icon-release', self._handle_icon_event) except AttributeError: pass # no icon for old pygtk self.search_entry.connect('changed', self._update_search_tree) self.search_entry.connect('key-press-event', self._handle_search_key_press) - self.pack_start(self.search_entry, False) + self.pack_start(self.search_entry, False, False, 0) # make the tree model for holding blocks and a temporary one for search results - self.treestore = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) - self.treestore_search = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) + self.treestore = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) + self.treestore_search = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) - self.treeview = gtk.TreeView(self.treestore) + self.treeview = Gtk.TreeView(model=self.treestore) self.treeview.set_enable_search(False) # disable pop up search box self.treeview.set_search_column(-1) # really disable search self.treeview.set_headers_visible(False) - self.treeview.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.treeview.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.treeview.connect('button-press-event', self._handle_mouse_button_press) self.treeview.connect('key-press-event', self._handle_search_key_press) - self.treeview.get_selection().set_mode('single') - renderer = gtk.CellRendererText() - column = gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX) + self.treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX) self.treeview.append_column(column) - # try to enable the tooltips (available in pygtk 2.12 and above) - try: - self.treeview.set_tooltip_column(DOC_INDEX) - except: - pass + self.treeview.set_tooltip_column(DOC_INDEX) # setup sort order column.set_sort_column_id(0) - self.treestore.set_sort_column_id(0, gtk.SORT_ASCENDING) + self.treestore.set_sort_column_id(0, Gtk.SortType.ASCENDING) # setup drag and drop - self.treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, Constants.DND_TARGETS, gtk.gdk.ACTION_COPY) + self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, Constants.DND_TARGETS, Gdk.DragAction.COPY) self.treeview.connect('drag-data-get', self._handle_drag_get_data) # make the scrolled window to hold the tree view - scrolled_window = gtk.ScrolledWindow() - scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrolled_window.add_with_viewport(self.treeview) + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.add(self.treeview) scrolled_window.set_size_request(Constants.DEFAULT_BLOCKS_WINDOW_WIDTH, -1) - self.pack_start(scrolled_window) + self.pack_start(scrolled_window, True, True, 0) # map categories to iters, automatic mapping for root self._categories = {tuple(): None} self._categories_search = {tuple(): None} @@ -154,7 +133,7 @@ class BlockTreeWindow(gtk.VBox): def repopulate(self): self.clear() - for block in self.platform.blocks.itervalues(): + for block in six.itervalues(self.platform.blocks): if block.category: self.add_block(block) self.expand_module_in_tree() @@ -188,15 +167,13 @@ class BlockTreeWindow(gtk.VBox): iter_ = treestore.insert_before(categories[parent_category[:-1]], None) treestore.set_value(iter_, NAME_INDEX, parent_cat_name) treestore.set_value(iter_, KEY_INDEX, '') - treestore.set_value(iter_, DOC_INDEX, Utils.parse_template( - CAT_MARKUP_TMPL, cat=parent_category, default_module=Constants.DEFAULT_BLOCK_MODULE_NAME)) + treestore.set_value(iter_, DOC_INDEX, _format_cat_tooltip(parent_cat_name)) categories[parent_category] = iter_ - # add block iter_ = treestore.insert_before(categories[category], None) - treestore.set_value(iter_, NAME_INDEX, block.get_name()) - treestore.set_value(iter_, KEY_INDEX, block.get_key()) - treestore.set_value(iter_, DOC_INDEX, Utils.parse_template(DOC_MARKUP_TMPL, doc=block.get_doc())) + treestore.set_value(iter_, KEY_INDEX, block.key) + treestore.set_value(iter_, NAME_INDEX, block.label) + treestore.set_value(iter_, DOC_INDEX, _format_doc(block.documentation)) def update_docs(self): """Update the documentation column of every block""" @@ -206,8 +183,7 @@ class BlockTreeWindow(gtk.VBox): if not key: return # category node, no doc string block = self.platform.blocks[key] - doc = Utils.parse_template(DOC_MARKUP_TMPL, doc=block.get_doc()) - model.set_value(iter_, DOC_INDEX, doc) + model.set_value(iter_, DOC_INDEX, _format_doc(block.documentation)) self.treestore.foreach(update_doc) self.treestore_search.foreach(update_doc) @@ -226,16 +202,6 @@ class BlockTreeWindow(gtk.VBox): treestore, iter = selection.get_selected() return iter and treestore.get_value(iter, KEY_INDEX) or '' - def _add_selected_block(self): - """ - Add the selected block with the given key to the flow graph. - """ - key = self._get_selected_block_key() - if key: - self.get_flow_graph().add_new_block(key) - return True - return False - def _expand_category(self): treestore, iter = self.treeview.get_selection().get_selected() if iter and treestore.iter_has_child(iter): @@ -246,9 +212,9 @@ class BlockTreeWindow(gtk.VBox): ## Event Handlers ############################################################ def _handle_icon_event(self, widget, icon, event): - if icon == gtk.ENTRY_ICON_PRIMARY: + if icon == Gtk.EntryIconPosition.PRIMARY: pass - elif icon == gtk.ENTRY_ICON_SECONDARY: + elif icon == Gtk.EntryIconPosition.SECONDARY: widget.set_text('') self.search_entry.hide() @@ -258,8 +224,8 @@ class BlockTreeWindow(gtk.VBox): self.treeview.set_model(self.treestore) self.expand_module_in_tree() else: - matching_blocks = filter(lambda b: key in b.get_key().lower() or key in b.get_name().lower(), - self.platform.blocks.values()) + matching_blocks = [b for b in list(self.platform.blocks.values()) + if key in b.key.lower() or key in b.label.lower()] self.treestore_search.clear() self._categories_search = {tuple(): None} @@ -270,7 +236,7 @@ class BlockTreeWindow(gtk.VBox): def _handle_search_key_press(self, widget, event): """Handle Return and Escape key events in search entry and treeview""" - if event.keyval == gtk.keysyms.Return: + if event.keyval == Gdk.KEY_Return: # add block on enter if widget == self.search_entry: @@ -280,24 +246,28 @@ class BlockTreeWindow(gtk.VBox): selected = self.treestore_search.iter_children(selected) if selected is not None: key = self.treestore_search.get_value(selected, KEY_INDEX) - if key: self.get_flow_graph().add_new_block(key) + if key: self.emit('create_new_block', key) elif widget == self.treeview: - self._add_selected_block() or self._expand_category() + key = self._get_selected_block_key() + if key: + self.emit('create_new_block', key) + else: + self._expand_category() else: return False # propagate event - elif event.keyval == gtk.keysyms.Escape: + elif event.keyval == Gdk.KEY_Escape: # reset the search self.search_entry.set_text('') self.search_entry.hide() - elif (event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.f) \ - or event.keyval == gtk.keysyms.slash: + elif (event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_f) \ + or event.keyval == Gdk.KEY_slash: # propagation doesn't work although treeview search is disabled =( # manually trigger action... Actions.FIND_BLOCKS.activate() - elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.b: + elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_b: # ugly... Actions.TOGGLE_BLOCKS_WINDOW.activate() @@ -313,12 +283,15 @@ class BlockTreeWindow(gtk.VBox): Only call set when the key is valid to ignore DND from categories. """ key = self._get_selected_block_key() - if key: selection_data.set(selection_data.target, 8, key) + if key: + selection_data.set_text(key, len(key)) def _handle_mouse_button_press(self, widget, event): """ Handle the mouse button press. If a left double click is detected, call add selected block. """ - if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: - self._add_selected_block() + if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS: + key = self._get_selected_block_key() + if key: + self.emit('create_new_block', key) diff --git a/grc/gui/CMakeLists.txt b/grc/gui/CMakeLists.txt deleted file mode 100644 index dc661c44ed..0000000000 --- a/grc/gui/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2011 Free Software Foundation, Inc. -# -# This file is part of GNU Radio -# -# GNU Radio 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, or (at your option) -# any later version. -# -# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to -# the Free Software Foundation, Inc., 51 Franklin Street, -# Boston, MA 02110-1301, USA. - -file(GLOB py_files "*.py") - -GR_PYTHON_INSTALL( - FILES ${py_files} - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/gui -) - -install( - FILES - ${CMAKE_CURRENT_SOURCE_DIR}/icon.png - DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/gui - COMPONENT "grc" -) diff --git a/grc/gui/Colors.py b/grc/gui/Colors.py deleted file mode 100644 index d322afa410..0000000000 --- a/grc/gui/Colors.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Copyright 2008,2013 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" -try: - import pygtk - pygtk.require('2.0') - import gtk - - _COLORMAP = gtk.gdk.colormap_get_system() #create all of the colors - def get_color(color_code): return _COLORMAP.alloc_color(color_code, True, True) - - HIGHLIGHT_COLOR = get_color('#00FFFF') - BORDER_COLOR = get_color('#444444') - # missing blocks stuff - MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2') - MISSING_BLOCK_BORDER_COLOR = get_color('red') - #param entry boxes - PARAM_ENTRY_TEXT_COLOR = get_color('black') - ENTRYENUM_CUSTOM_COLOR = get_color('#EEEEEE') - #flow graph color constants - FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF') - COMMENT_BACKGROUND_COLOR = get_color('#F3F3F3') - FLOWGRAPH_EDGE_COLOR = COMMENT_BACKGROUND_COLOR - #block color constants - BLOCK_ENABLED_COLOR = get_color('#F1ECFF') - BLOCK_DISABLED_COLOR = get_color('#CCCCCC') - BLOCK_BYPASSED_COLOR = get_color('#F4FF81') - #connection color constants - CONNECTION_ENABLED_COLOR = get_color('black') - CONNECTION_DISABLED_COLOR = get_color('#BBBBBB') - CONNECTION_ERROR_COLOR = get_color('red') -except: - print 'Unable to import Colors' - -DEFAULT_DOMAIN_COLOR_CODE = '#777777' diff --git a/grc/gui/Config.py b/grc/gui/Config.py index 9b0c5d4afe..6135296660 100644 --- a/grc/gui/Config.py +++ b/grc/gui/Config.py @@ -17,13 +17,26 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ +from __future__ import absolute_import, print_function + import sys import os -from ..core.Config import Config as _Config + +from ..core.Config import Config as CoreConfig from . import Constants +from six.moves import configparser -class Config(_Config): +HEADER = """\ +# This contains only GUI settings for GRC and is not meant for users to edit. +# +# GRC settings not accessible through the GUI are in gnuradio.conf under +# section [grc]. + +""" + + +class Config(CoreConfig): name = 'GNU Radio Companion' @@ -31,44 +44,163 @@ class Config(_Config): 'GRC_PREFS_PATH', os.path.expanduser('~/.gnuradio/grc.conf')) def __init__(self, install_prefix, *args, **kwargs): - _Config.__init__(self, *args, **kwargs) + CoreConfig.__init__(self, *args, **kwargs) self.install_prefix = install_prefix Constants.update_font_size(self.font_size) + self.parser = configparser.ConfigParser() + for section in ['main', 'files_open', 'files_recent']: + try: + self.parser.add_section(section) + except Exception as e: + print(e) + try: + self.parser.read(self.gui_prefs_file) + except Exception as err: + print(err, file=sys.stderr) + + def save(self): + try: + with open(self.gui_prefs_file, 'w') as fp: + fp.write(HEADER) + self.parser.write(fp) + except Exception as err: + print(err, file=sys.stderr) + + def entry(self, key, value=None, default=None): + if value is not None: + self.parser.set('main', key, str(value)) + result = value + else: + _type = type(default) if default is not None else str + getter = { + bool: self.parser.getboolean, + int: self.parser.getint, + }.get(_type, self.parser.get) + try: + result = getter('main', key) + except (AttributeError, configparser.Error): + result = _type() if default is None else default + return result + @property def editor(self): - return self.prefs.get_string('grc', 'editor', '') + return self._gr_prefs.get_string('grc', 'editor', '') @editor.setter def editor(self, value): - self.prefs.get_string('grc', 'editor', value) - self.prefs.save() + self._gr_prefs.get_string('grc', 'editor', value) + self._gr_prefs.save() @property def xterm_executable(self): - return self.prefs.get_string('grc', 'xterm_executable', 'xterm') + return self._gr_prefs.get_string('grc', 'xterm_executable', 'xterm') @property def default_canvas_size(self): try: # ugly, but matches current code style - raw = self.prefs.get_string('grc', 'canvas_default_size', '1280, 1024') + raw = self._gr_prefs.get_string('grc', 'canvas_default_size', '1280, 1024') value = tuple(int(x.strip('() ')) for x in raw.split(',')) if len(value) != 2 or not all(300 < x < 4096 for x in value): raise Exception() return value except: - print >> sys.stderr, "Error: invalid 'canvas_default_size' setting." + print("Error: invalid 'canvas_default_size' setting.", file=sys.stderr) return Constants.DEFAULT_CANVAS_SIZE_DEFAULT @property def font_size(self): try: # ugly, but matches current code style - font_size = self.prefs.get_long('grc', 'canvas_font_size', - Constants.DEFAULT_FONT_SIZE) + font_size = self._gr_prefs.get_long('grc', 'canvas_font_size', + Constants.DEFAULT_FONT_SIZE) if font_size <= 0: raise Exception() except: font_size = Constants.DEFAULT_FONT_SIZE - print >> sys.stderr, "Error: invalid 'canvas_font_size' setting." + print("Error: invalid 'canvas_font_size' setting.", file=sys.stderr) return font_size + + @property + def default_qss_theme(self): + return self._gr_prefs.get_string('qtgui', 'qss', '') + + @default_qss_theme.setter + def default_qss_theme(self, value): + self._gr_prefs.set_string("qtgui", "qss", value) + self._gr_prefs.save() + + ##### Originally from Preferences.py ##### + def main_window_size(self, size=None): + if size is None: + size = [None, None] + w = self.entry('main_window_width', size[0], default=1) + h = self.entry('main_window_height', size[1], default=1) + return w, h + + def file_open(self, filename=None): + return self.entry('file_open', filename, default='') + + def set_file_list(self, key, files): + self.parser.remove_section(key) # clear section + self.parser.add_section(key) + for i, filename in enumerate(files): + self.parser.set(key, '%s_%d' % (key, i), filename) + + def get_file_list(self, key): + try: + files = [value for name, value in self.parser.items(key) + if name.startswith('%s_' % key)] + except (AttributeError, configparser.Error): + files = [] + return files + + def get_open_files(self): + return self.get_file_list('files_open') + + def set_open_files(self, files): + return self.set_file_list('files_open', files) + + def get_recent_files(self): + """ Gets recent files, removes any that do not exist and re-saves it """ + files = list(filter(os.path.exists, self.get_file_list('files_recent'))) + self.set_recent_files(files) + return files + + def set_recent_files(self, files): + return self.set_file_list('files_recent', files) + + def add_recent_file(self, file_name): + # double check file_name + if os.path.exists(file_name): + recent_files = self.get_recent_files() + if file_name in recent_files: + recent_files.remove(file_name) # Attempt removal + recent_files.insert(0, file_name) # Insert at start + self.set_recent_files(recent_files[:10]) # Keep up to 10 files + + def console_window_position(self, pos=None): + return self.entry('console_window_position', pos, default=-1) or 1 + + def blocks_window_position(self, pos=None): + return self.entry('blocks_window_position', pos, default=-1) or 1 + + def variable_editor_position(self, pos=None, sidebar=False): + # Figure out default + if sidebar: + w, h = self.main_window_size() + return self.entry('variable_editor_sidebar_position', pos, default=int(h*0.7)) + else: + return self.entry('variable_editor_position', pos, default=int(self.blocks_window_position()*0.5)) + + def variable_editor_sidebar(self, pos=None): + return self.entry('variable_editor_sidebar', pos, default=False) + + def variable_editor_confirm_delete(self, pos=None): + return self.entry('variable_editor_confirm_delete', pos, default=True) + + def xterm_missing(self, cmd=None): + return self.entry('xterm_missing', cmd, default='INVALID_XTERM_SETTING') + + def screen_shot_background_transparent(self, transparent=None): + return self.entry('screen_shot_background_transparent', transparent, default=False) diff --git a/grc/gui/Connection.py b/grc/gui/Connection.py deleted file mode 100644 index 50361c19d0..0000000000 --- a/grc/gui/Connection.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Copyright 2007, 2008, 2009 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import gtk - -import Colors -import Utils -from Constants import CONNECTOR_ARROW_BASE, CONNECTOR_ARROW_HEIGHT -from Element import Element - -from ..core.Constants import GR_MESSAGE_DOMAIN -from ..core.Connection import Connection as _Connection - - -class Connection(Element, _Connection): - """ - A graphical connection for ports. - The connection has 2 parts, the arrow and the wire. - The coloring of the arrow and wire exposes the status of 3 states: - enabled/disabled, valid/invalid, highlighted/non-highlighted. - The wire coloring exposes the enabled and highlighted states. - The arrow coloring exposes the enabled and valid states. - """ - - def __init__(self, **kwargs): - Element.__init__(self) - _Connection.__init__(self, **kwargs) - # can't use Colors.CONNECTION_ENABLED_COLOR here, might not be defined (grcc) - self._bg_color = self._arrow_color = self._color = None - - def get_coordinate(self): - """ - Get the 0,0 coordinate. - Coordinates are irrelevant in connection. - - Returns: - 0, 0 - """ - return 0, 0 - - def get_rotation(self): - """ - Get the 0 degree rotation. - Rotations are irrelevant in connection. - - Returns: - 0 - """ - return 0 - - def create_shapes(self): - """Precalculate relative coordinates.""" - Element.create_shapes(self) - self._sink_rot = None - self._source_rot = None - self._sink_coor = None - self._source_coor = None - #get the source coordinate - try: - connector_length = self.get_source().get_connector_length() - except: - return - self.x1, self.y1 = Utils.get_rotated_coordinate((connector_length, 0), self.get_source().get_rotation()) - #get the sink coordinate - connector_length = self.get_sink().get_connector_length() + CONNECTOR_ARROW_HEIGHT - self.x2, self.y2 = Utils.get_rotated_coordinate((-connector_length, 0), self.get_sink().get_rotation()) - #build the arrow - self.arrow = [(0, 0), - Utils.get_rotated_coordinate((-CONNECTOR_ARROW_HEIGHT, -CONNECTOR_ARROW_BASE/2), self.get_sink().get_rotation()), - Utils.get_rotated_coordinate((-CONNECTOR_ARROW_HEIGHT, CONNECTOR_ARROW_BASE/2), self.get_sink().get_rotation()), - ] - source_domain = self.get_source().get_domain() - sink_domain = self.get_sink().get_domain() - self.line_attributes[0] = 2 if source_domain != sink_domain else 0 - self.line_attributes[1] = gtk.gdk.LINE_DOUBLE_DASH \ - if not source_domain == sink_domain == GR_MESSAGE_DOMAIN \ - else gtk.gdk.LINE_ON_OFF_DASH - get_domain_color = lambda d: Colors.get_color(( - self.get_parent().get_parent().domains.get(d, {}) - ).get('color') or Colors.DEFAULT_DOMAIN_COLOR_CODE) - self._color = get_domain_color(source_domain) - self._bg_color = get_domain_color(sink_domain) - self._arrow_color = self._bg_color if self.is_valid() else Colors.CONNECTION_ERROR_COLOR - self._update_after_move() - - def _update_after_move(self): - """Calculate coordinates.""" - self.clear() #FIXME do i want this here? - #source connector - source = self.get_source() - X, Y = source.get_connector_coordinate() - x1, y1 = self.x1 + X, self.y1 + Y - self.add_line((x1, y1), (X, Y)) - #sink connector - sink = self.get_sink() - X, Y = sink.get_connector_coordinate() - x2, y2 = self.x2 + X, self.y2 + Y - self.add_line((x2, y2), (X, Y)) - #adjust arrow - self._arrow = [(x+X, y+Y) for x,y in self.arrow] - #add the horizontal and vertical lines in this connection - if abs(source.get_connector_direction() - sink.get_connector_direction()) == 180: - #2 possible point sets to create a 3-line connector - mid_x, mid_y = (x1 + x2)/2.0, (y1 + y2)/2.0 - points = [((mid_x, y1), (mid_x, y2)), ((x1, mid_y), (x2, mid_y))] - #source connector -> points[0][0] should be in the direction of source (if possible) - if Utils.get_angle_from_coordinates((x1, y1), points[0][0]) != source.get_connector_direction(): points.reverse() - #points[0][0] -> sink connector should not be in the direction of sink - if Utils.get_angle_from_coordinates(points[0][0], (x2, y2)) == sink.get_connector_direction(): points.reverse() - #points[0][0] -> source connector should not be in the direction of source - if Utils.get_angle_from_coordinates(points[0][0], (x1, y1)) == source.get_connector_direction(): points.reverse() - #create 3-line connector - p1, p2 = map(int, points[0][0]), map(int, points[0][1]) - self.add_line((x1, y1), p1) - self.add_line(p1, p2) - self.add_line((x2, y2), p2) - else: - #2 possible points to create a right-angled connector - points = [(x1, y2), (x2, y1)] - #source connector -> points[0] should be in the direction of source (if possible) - if Utils.get_angle_from_coordinates((x1, y1), points[0]) != source.get_connector_direction(): points.reverse() - #points[0] -> sink connector should not be in the direction of sink - if Utils.get_angle_from_coordinates(points[0], (x2, y2)) == sink.get_connector_direction(): points.reverse() - #points[0] -> source connector should not be in the direction of source - if Utils.get_angle_from_coordinates(points[0], (x1, y1)) == source.get_connector_direction(): points.reverse() - #create right-angled connector - self.add_line((x1, y1), points[0]) - self.add_line((x2, y2), points[0]) - - def draw(self, gc, window): - """ - Draw the connection. - - Args: - gc: the graphics context - window: the gtk window to draw on - """ - sink = self.get_sink() - source = self.get_source() - #check for changes - if self._sink_rot != sink.get_rotation() or self._source_rot != source.get_rotation(): self.create_shapes() - elif self._sink_coor != sink.get_coordinate() or self._source_coor != source.get_coordinate(): - try: - self._update_after_move() - except: - return - #cache values - self._sink_rot = sink.get_rotation() - self._source_rot = source.get_rotation() - self._sink_coor = sink.get_coordinate() - self._source_coor = source.get_coordinate() - #draw - mod_color = lambda color: ( - Colors.HIGHLIGHT_COLOR if self.is_highlighted() else - Colors.CONNECTION_DISABLED_COLOR if not self.get_enabled() else - color - ) - Element.draw(self, gc, window, mod_color(self._color), mod_color(self._bg_color)) - # draw arrow on sink port - try: - gc.set_foreground(mod_color(self._arrow_color)) - gc.set_line_attributes(0, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER) - window.draw_polygon(gc, True, self._arrow) - except: - pass diff --git a/grc/gui/Console.py b/grc/gui/Console.py new file mode 100644 index 0000000000..0ae862493d --- /dev/null +++ b/grc/gui/Console.py @@ -0,0 +1,57 @@ +""" +Copyright 2008, 2009, 2011 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import + +import os +import logging + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, GObject + +from .Constants import DEFAULT_CONSOLE_WINDOW_WIDTH +from .Dialogs import TextDisplay, MessageDialogWrapper + +from ..core import Messages + + +log = logging.getLogger(__name__) + + +class Console(Gtk.ScrolledWindow): + def __init__(self): + Gtk.ScrolledWindow.__init__(self) + log.debug("console()") + self.app = Gtk.Application.get_default() + + self.text_display = TextDisplay() + + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.add(self.text_display) + self.set_size_request(-1, DEFAULT_CONSOLE_WINDOW_WIDTH) + + def add_line(self, line): + """ + Place line at the end of the text buffer, then scroll its window all the way down. + + Args: + line: the new text + """ + self.text_display.insert(line) diff --git a/grc/gui/Constants.py b/grc/gui/Constants.py index f77221e52d..a3d08cbe38 100644 --- a/grc/gui/Constants.py +++ b/grc/gui/Constants.py @@ -17,17 +17,16 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import gtk +from __future__ import absolute_import + +from gi.repository import Gtk, Gdk from ..core.Constants import * # default path for the open/save dialogs DEFAULT_FILE_PATH = os.getcwd() if os.name != 'nt' else os.path.expanduser("~/Documents") - -# file extensions -IMAGE_FILE_EXTENSION = '.png' -TEXT_FILE_EXTENSION = '.txt' +FILE_EXTENSION = '.grc' # name for new/unsaved flow graphs NEW_FLOGRAPH_TITLE = 'untitled' @@ -36,7 +35,7 @@ NEW_FLOGRAPH_TITLE = 'untitled' MIN_WINDOW_WIDTH = 600 MIN_WINDOW_HEIGHT = 400 # dialog constraints -MIN_DIALOG_WIDTH = 500 +MIN_DIALOG_WIDTH = 600 MIN_DIALOG_HEIGHT = 500 # default sizes DEFAULT_BLOCKS_WINDOW_WIDTH = 100 @@ -53,7 +52,7 @@ PARAM_FONT = "Sans 7.5" STATE_CACHE_SIZE = 42 # Shared targets for drag and drop of blocks -DND_TARGETS = [('STRING', gtk.TARGET_SAME_APP, 0)] +DND_TARGETS = [('STRING', Gtk.TargetFlags.SAME_APP, 0)] # label constraint dimensions LABEL_SEPARATION = 3 @@ -70,6 +69,7 @@ PORT_SEPARATION = 32 PORT_MIN_WIDTH = 20 PORT_LABEL_HIDDEN_WIDTH = 10 +PORT_EXTRA_BUS_HEIGHT = 40 # minimal length of connector CONNECTOR_EXTENSION_MINIMAL = 11 @@ -78,17 +78,14 @@ CONNECTOR_EXTENSION_MINIMAL = 11 CONNECTOR_EXTENSION_INCREMENT = 11 # connection arrow dimensions -CONNECTOR_ARROW_BASE = 13 -CONNECTOR_ARROW_HEIGHT = 17 +CONNECTOR_ARROW_BASE = 10 +CONNECTOR_ARROW_HEIGHT = 13 # possible rotations in degrees POSSIBLE_ROTATIONS = (0, 90, 180, 270) -# How close can the mouse get to the window border before mouse events are ignored. -BORDER_PROXIMITY_SENSITIVITY = 50 - # How close the mouse can get to the edge of the visible window before scrolling is invoked. -SCROLL_PROXIMITY_SENSITIVITY = 30 +SCROLL_PROXIMITY_SENSITIVITY = 50 # When the window has to be scrolled, move it this distance in the required direction. SCROLL_DISTANCE = 15 @@ -96,8 +93,18 @@ SCROLL_DISTANCE = 15 # How close the mouse click can be to a line and register a connection select. LINE_SELECT_SENSITIVITY = 5 -_SCREEN_RESOLUTION = gtk.gdk.screen_get_default().get_resolution() -DPI_SCALING = _SCREEN_RESOLUTION / 96.0 if _SCREEN_RESOLUTION > 0 else 1.0 +DEFAULT_BLOCK_MODULE_TOOLTIP = """\ +This subtree holds all blocks (from OOT modules) that specify no module name. \ +The module name is the root category enclosed in square brackets. + +Please consider contacting OOT module maintainer for any block in here \ +and kindly ask to update their GRC Block Descriptions or Block Tree to include a module name.""" + + +# _SCREEN = Gdk.Screen.get_default() +# _SCREEN_RESOLUTION = _SCREEN.get_resolution() if _SCREEN else -1 +# DPI_SCALING = _SCREEN_RESOLUTION / 96.0 if _SCREEN_RESOLUTION > 0 else 1.0 +DPI_SCALING = 1.0 # todo: figure out the GTK3 way (maybe cairo does this for us def update_font_size(font_size): diff --git a/grc/gui/Dialogs.py b/grc/gui/Dialogs.py index 4c89810bb6..f58ea78ca2 100644 --- a/grc/gui/Dialogs.py +++ b/grc/gui/Dialogs.py @@ -1,33 +1,36 @@ -""" -Copyright 2008, 2009 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import gtk +# Copyright 2008, 2009, 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion 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 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +from __future__ import absolute_import import sys +import textwrap from distutils.spawn import find_executable -from . import Utils, Actions +from gi.repository import Gtk + +from . import Utils, Actions, Constants from ..core import Messages -class SimpleTextDisplay(gtk.TextView): - """A non editable gtk text view.""" +class SimpleTextDisplay(Gtk.TextView): + """ + A non user-editable gtk text view. + """ def __init__(self, text=''): """ @@ -36,16 +39,18 @@ class SimpleTextDisplay(gtk.TextView): Args: text: the text to display (string) """ - text_buffer = gtk.TextBuffer() - text_buffer.set_text(text) - self.set_text = text_buffer.set_text - gtk.TextView.__init__(self, text_buffer) + 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.WRAP_WORD_CHAR) + 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=''): """ @@ -59,220 +64,314 @@ class TextDisplay(SimpleTextDisplay): self.connect("populate-popup", self.populate_popup) def insert(self, line): - # make backspaces work + """ + Append text after handling backspaces and auto-scroll. + + Args: + line: the text to append (string) + """ line = self._consume_backspaces(line) - # add the remaining text to buffer self.get_buffer().insert(self.get_buffer().get_end_iter(), line) - # Automatically scroll on insert self.scroll_to_end() def _consume_backspaces(self, line): - """removes text from the buffer if line starts with \b*""" - if not line: return + """ + 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() + if not start_iter.starts_line(): + start_iter.backward_char() back_count += 1 - # remove chars + # remove chars from buffer self.get_buffer().delete(start_iter, self.get_buffer().get_end_iter()) - # return remaining text return line[back_count:] def scroll_to_end(self): + """ Update view's scroll position. """ if self.scroll_lock: - buffer = self.get_buffer() - buffer.move_mark(buffer.get_insert(), buffer.get_end_iter()) - self.scroll_to_mark(buffer.get_insert(), 0.0) + buf = self.get_buffer() + mark = buf.get_insert() + buf.move_mark(mark, buf.get_end_iter()) + self.scroll_mark_onscreen(mark) def clear(self): - buffer = self.get_buffer() - buffer.delete(buffer.get_start_iter(), buffer.get_end_iter()) + """ Clear all text from buffer. """ + buf = self.get_buffer() + buf.delete(buf.get_start_iter(), buf.get_end_iter()) def save(self, file_path): - console_file = open(file_path, 'w') - buffer = self.get_buffer() - console_file.write(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)) - console_file.close() + """ + Save context of buffer to the given file. - # Callback functions to handle the scrolling lock and clear context menus options - # Action functions are set by the ActionHandler's init function + 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()) + menu.append(Gtk.SeparatorMenuItem()) - lock = gtk.CheckMenuItem("Scroll Lock") + lock = Gtk.CheckMenuItem("Scroll Lock") menu.append(lock) lock.set_active(self.scroll_lock) lock.connect('activate', self.scroll_back_cb, view) - save = gtk.ImageMenuItem(gtk.STOCK_SAVE) + save = Gtk.ImageMenuItem(Gtk.STOCK_SAVE) menu.append(save) save.connect('activate', self.save_cb, view) - clear = gtk.ImageMenuItem(gtk.STOCK_CLEAR) + clear = Gtk.ImageMenuItem(Gtk.STOCK_CLEAR) menu.append(clear) clear.connect('activate', self.clear_cb, view) menu.show_all() return False -def MessageDialogHelper(type, buttons, title=None, markup=None, default_response=None, extra_buttons=None): - """ - Create a modal message dialog and run it. +class MessageDialogWrapper(Gtk.MessageDialog): + """ Run a message dialog. """ - Args: - type: the type of message: gtk.MESSAGE_INFO, gtk.MESSAGE_WARNING, gtk.MESSAGE_QUESTION or gtk.MESSAGE_ERROR - buttons: the predefined set of buttons to use: - gtk.BUTTONS_NONE, gtk.BUTTONS_OK, gtk.BUTTONS_CLOSE, gtk.BUTTONS_CANCEL, gtk.BUTTONS_YES_NO, gtk.BUTTONS_OK_CANCEL + def __init__(self, parent, message_type, buttons, title=None, markup=None, + default_response=None, extra_buttons=None): + """ + Create a modal message dialog. - Args: - 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 + 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 - Returns: - the gtk response from run() - """ - message_dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, type, buttons) - if title: message_dialog.set_title(title) - if markup: message_dialog.set_markup(markup) - if extra_buttons: message_dialog.add_buttons(*extra_buttons) - if default_response: message_dialog.set_default_response(default_response) - response = message_dialog.run() - message_dialog.destroy() - return response - - -ERRORS_MARKUP_TMPL="""\ -#for $i, $err_msg in enumerate($errors) -<b>Error $i:</b> -$encode($err_msg.replace('\t', ' ')) - -#end for""" - - -def ErrorsDialog(flowgraph): MessageDialogHelper( - type=gtk.MESSAGE_ERROR, - buttons=gtk.BUTTONS_CLOSE, - title='Flow Graph Errors', - markup=Utils.parse_template(ERRORS_MARKUP_TMPL, errors=flowgraph.get_error_messages()), -) - - -class AboutDialog(gtk.AboutDialog): - """A cute little about dialog.""" - - def __init__(self, config): - """AboutDialog constructor.""" - gtk.AboutDialog.__init__(self) - self.set_name(config.name) - self.set_version(config.version) - self.set_license(config.license) - self.set_copyright(config.license.splitlines()[0]) - self.set_website(config.website) - self.run() - self.destroy() - - -def HelpDialog(): MessageDialogHelper( - type=gtk.MESSAGE_INFO, - buttons=gtk.BUTTONS_CLOSE, - title='Help', - markup="""\ -<b>Usage Tips</b> - -<u>Add block</u>: drag and drop or double click a block in the block selection window. -<u>Rotate block</u>: Select a block, press left/right on the keyboard. -<u>Change type</u>: Select a block, press up/down on the keyboard. -<u>Edit parameters</u>: double click on a block in the flow graph. -<u>Make connection</u>: click on the source port of one block, then click on the sink port of another block. -<u>Remove connection</u>: select the connection and press delete, or drag the connection. - -* See the menu for other keyboard shortcuts.""") - -COLORS_DIALOG_MARKUP_TMPL = """\ -<b>Color Mapping</b> - -#if $colors - #set $max_len = max([len(color[0]) for color in $colors]) + 10 - #for $title, $color_spec in $colors -<span background="$color_spec"><tt>$($encode($title).center($max_len))</tt></span> - #end for -#end if -""" - - -def TypesDialog(platform): - MessageDialogHelper( - type=gtk.MESSAGE_INFO, - buttons=gtk.BUTTONS_CLOSE, - title='Types', - markup=Utils.parse_template(COLORS_DIALOG_MARKUP_TMPL, - colors=platform.get_colors()) + """ + Gtk.MessageDialog.__init__( + self, transient_for=parent, modal=True, destroy_with_parent=True, + message_type=message_type, buttons=buttons + ) + 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.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) + 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 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: + pass + + #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("""\ + <b>Usage Tips</b> + \n\ + <u>Add block</u>: drag and drop or double click a block in the block selection window. + <u>Rotate block</u>: Select a block, press left/right on the keyboard. + <u>Change type</u>: Select a block, press up/down on the keyboard. + <u>Edit parameters</u>: double click on a block in the flow graph. + <u>Make connection</u>: click on the source port of one block, then click on the sink port of another block. + <u>Remove connection</u>: select the connection and press delete, or drag the connection. + \n\ + * See the menu for other keyboard shortcuts.\ + """) + + MessageDialogWrapper( + parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Help', 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( + '<span background="{color}"><tt>{name}</tt></span>' + ''.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 MissingXTermDialog(xterm): - MessageDialogHelper( - type=gtk.MESSAGE_WARNING, - buttons=gtk.BUTTONS_OK, - title='Warning: missing xterm executable', - markup=("The xterm executable {0!r} is missing.\n\n" - "You can change this setting in your gnuradio.conf, in " - "section [grc], 'xterm_executable'.\n" - "\n" - "(This message is shown only once)").format(xterm) - ) +def show_missing_xterm(parent, xterm): + markup = textwrap.dedent("""\ + The xterm executable {0!r} is missing. + You can change this setting in your gnurado.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. + """ + if config.editor and find_executable(config.editor): + return config.editor -def ChooseEditorDialog(config): - # Give the option to either choose an editor or use the default - # Always return true/false so the caller knows it was successful buttons = ( - 'Choose Editor', gtk.RESPONSE_YES, - 'Use Default', gtk.RESPONSE_NO, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL - ) - response = MessageDialogHelper( - gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, 'Choose Editor', - 'Would you like to choose the editor to use?', gtk.RESPONSE_YES, buttons + 'Choose Editor', Gtk.ResponseType.YES, + 'Use Default', Gtk.ResponseType.NO, + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL ) + response = MessageDialogWrapper( + parent, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.NONE, + title='Choose Editor', markup='Would you like to choose the editor to use?', + default_response=Gtk.ResponseType.YES, extra_buttons=buttons + ).run_and_destroy() # Handle the initial default/choose/cancel response # User wants to choose the editor to use - if response == gtk.RESPONSE_YES: - file_dialog = gtk.FileChooserDialog( + editor = '' + if response == Gtk.ResponseType.YES: + file_dialog = Gtk.FileChooserDialog( 'Select an Editor...', None, - gtk.FILE_CHOOSER_ACTION_OPEN, - ('gtk-cancel', gtk.RESPONSE_CANCEL, 'gtk-open', gtk.RESPONSE_OK) + Gtk.FileChooserAction.OPEN, + ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-open', Gtk.ResponseType.OK), + transient_for=parent ) file_dialog.set_select_multiple(False) file_dialog.set_local_only(True) file_dialog.set_current_folder('/usr/bin') try: - if file_dialog.run() == gtk.RESPONSE_OK: - config.editor = file_path = file_dialog.get_filename() - file_dialog.destroy() - return file_path + if file_dialog.run() == Gtk.ResponseType.OK: + editor = file_dialog.get_filename() finally: - file_dialog.destroy() + file_dialog.hide() # Go with the default editor - elif response == gtk.RESPONSE_NO: - # Determine the platform + elif response == Gtk.ResponseType.NO: try: process = None if sys.platform.startswith('linux'): @@ -282,13 +381,10 @@ def ChooseEditorDialog(config): if process is None: raise ValueError("Can't find default editor executable") # Save - config.editor = process - return process + editor = config.editor = process except Exception: Messages.send('>>> Unable to load the default editor. Please choose an editor.\n') - # Just reset of the constant and force the user to select an editor the next time - config.editor = '' - return - Messages.send('>>> No editor selected.\n') - return + if editor == '': + Messages.send('>>> No editor selected.\n') + return editor diff --git a/grc/gui/DrawingArea.py b/grc/gui/DrawingArea.py index 64862ce6d8..3a77d062f4 100644 --- a/grc/gui/DrawingArea.py +++ b/grc/gui/DrawingArea.py @@ -17,15 +17,16 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk +from __future__ import absolute_import -from Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, DND_TARGETS -import Colors +from gi.repository import Gtk, Gdk +from .canvas.colors import FLOWGRAPH_BACKGROUND_COLOR +from . import Constants +from . import Actions -class DrawingArea(gtk.DrawingArea): + +class DrawingArea(Gtk.DrawingArea): """ DrawingArea is the gtk pixel map that graphical elements may draw themselves on. The drawing area also responds to mouse and key events. @@ -39,137 +40,225 @@ class DrawingArea(gtk.DrawingArea): Args: main_window: the main_window containing all flow graphs """ + Gtk.DrawingArea.__init__(self) + + self._flow_graph = flow_graph + self.set_property('can_focus', True) + + self.zoom_factor = 1.0 + self._update_after_zoom = False self.ctrl_mask = False self.mod1_mask = False - self._flow_graph = flow_graph - gtk.DrawingArea.__init__(self) - self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) + self.button_state = [False] * 10 + + # self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) self.connect('realize', self._handle_window_realize) - self.connect('configure-event', self._handle_window_configure) - self.connect('expose-event', self._handle_window_expose) + self.connect('draw', self.draw) self.connect('motion-notify-event', self._handle_mouse_motion) self.connect('button-press-event', self._handle_mouse_button_press) self.connect('button-release-event', self._handle_mouse_button_release) self.connect('scroll-event', self._handle_mouse_scroll) self.add_events( - gtk.gdk.BUTTON_PRESS_MASK | \ - gtk.gdk.POINTER_MOTION_MASK | \ - gtk.gdk.BUTTON_RELEASE_MASK | \ - gtk.gdk.LEAVE_NOTIFY_MASK | \ - gtk.gdk.ENTER_NOTIFY_MASK | \ - gtk.gdk.FOCUS_CHANGE_MASK + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.SCROLL_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + Gdk.EventMask.ENTER_NOTIFY_MASK + # Gdk.EventMask.FOCUS_CHANGE_MASK ) - #setup drag and drop - self.drag_dest_set(gtk.DEST_DEFAULT_ALL, DND_TARGETS, gtk.gdk.ACTION_COPY) + + # This may not be the correct place to be handling the user events + # Should this be in the page instead? + # Or should more of the page functionality move here? + self.connect('key_press_event', self._handle_key_press) + + # setup drag and drop + self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) self.connect('drag-data-received', self._handle_drag_data_received) - #setup the focus flag + self.drag_dest_set_target_list(None) + self.drag_dest_add_text_targets() + + # setup the focus flag self._focus_flag = False self.get_focus_flag = lambda: self._focus_flag - def _handle_notify_event(widget, event, focus_flag): self._focus_flag = focus_flag + + def _handle_notify_event(widget, event, focus_flag): + self._focus_flag = focus_flag + self.connect('leave-notify-event', _handle_notify_event, False) self.connect('enter-notify-event', _handle_notify_event, True) - self.set_flags(gtk.CAN_FOCUS) # self.set_can_focus(True) - self.connect('focus-out-event', self._handle_focus_lost_event) - - def new_pixmap(self, width, height): - return gtk.gdk.Pixmap(self.window, width, height, -1) + # todo: fix +# self.set_flags(Gtk.CAN_FOCUS) # self.set_can_focus(True) +# self.connect('focus-out-event', self._handle_focus_lost_event) - def get_screenshot(self, transparent_bg=False): - pixmap = self._pixmap - W, H = pixmap.get_size() - pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, W, H) - pixbuf.fill(0xFF + Colors.FLOWGRAPH_BACKGROUND_COLOR.pixel << 8) - pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), 0, 0, 0, 0, W-1, H-1) - if transparent_bg: - bgc = Colors.FLOWGRAPH_BACKGROUND_COLOR - pixbuf = pixbuf.add_alpha(True, bgc.red, bgc.green, bgc.blue) - return pixbuf + # Setup a map of the accelerator keys to the action to trigger + self.accels = { + Gtk.accelerator_parse('d'): Actions.BLOCK_DISABLE, + Gtk.accelerator_parse('e'): Actions.BLOCK_ENABLE, + Gtk.accelerator_parse('b'): Actions.BLOCK_BYPASS, + Gtk.accelerator_parse('c'): Actions.BLOCK_CREATE_HIER, + Gtk.accelerator_parse('Up'): Actions.BLOCK_DEC_TYPE, + Gtk.accelerator_parse('Down'): Actions.BLOCK_INC_TYPE, + Gtk.accelerator_parse('Left'): Actions.BLOCK_ROTATE_CCW, + Gtk.accelerator_parse('Right'): Actions.BLOCK_ROTATE_CW, + Gtk.accelerator_parse('minus'): Actions.PORT_CONTROLLER_DEC, + Gtk.accelerator_parse('plus'): Actions.PORT_CONTROLLER_INC, + Gtk.accelerator_parse('Add'): Actions.PORT_CONTROLLER_INC, + Gtk.accelerator_parse('Subtract'): Actions.PORT_CONTROLLER_DEC, + Gtk.accelerator_parse('Return'): Actions.BLOCK_PARAM_MODIFY, + Gtk.accelerator_parse('<Shift>t'): Actions.BLOCK_VALIGN_TOP, + Gtk.accelerator_parse('<Shift>m'): Actions.BLOCK_VALIGN_MIDDLE, + Gtk.accelerator_parse('<Shift>b'): Actions.BLOCK_VALIGN_BOTTOM, + Gtk.accelerator_parse('<Shift>l'): Actions.BLOCK_HALIGN_LEFT, + Gtk.accelerator_parse('<Shift>c'): Actions.BLOCK_HALIGN_CENTER, + Gtk.accelerator_parse('<Shift>r'): Actions.BLOCK_HALIGN_RIGHT, + } ########################################################################## - ## Handlers + # Handlers ########################################################################## def _handle_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time): """ Handle a drag and drop by adding a block at the given coordinate. """ - self._flow_graph.add_new_block(selection_data.data, (x, y)) + coords = x / self.zoom_factor, y / self.zoom_factor + self._flow_graph.add_new_block(selection_data.get_text(), coords) def _handle_mouse_scroll(self, widget, event): - if event.state & gtk.gdk.SHIFT_MASK: - if event.direction == gtk.gdk.SCROLL_UP: - event.direction = gtk.gdk.SCROLL_LEFT - else: - event.direction = gtk.gdk.SCROLL_RIGHT + if event.get_state() & Gdk.ModifierType.CONTROL_MASK: + change = 1.2 if event.direction == Gdk.ScrollDirection.UP else 1/1.2 + zoom_factor = min(max(self.zoom_factor * change, 0.1), 5.0) + + if zoom_factor != self.zoom_factor: + self.zoom_factor = zoom_factor + self._update_after_zoom = True + self.queue_draw() + return True + + return False def _handle_mouse_button_press(self, widget, event): """ Forward button click information to the flow graph. """ self.grab_focus() - self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK - self.mod1_mask = event.state & gtk.gdk.MOD1_MASK - if event.button == 1: self._flow_graph.handle_mouse_selector_press( - double_click=(event.type == gtk.gdk._2BUTTON_PRESS), - coordinate=(event.x, event.y), - ) - if event.button == 3: self._flow_graph.handle_mouse_context_press( - coordinate=(event.x, event.y), - event=event, - ) + self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK + self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK + self.button_state[event.button] = True + + if event.button == 1: + double_click = (event.type == Gdk.EventType._2BUTTON_PRESS) + self.button_state[1] = not double_click + self._flow_graph.handle_mouse_selector_press( + double_click=double_click, + coordinate=self._translate_event_coords(event), + ) + elif event.button == 3: + self._flow_graph.handle_mouse_context_press( + coordinate=self._translate_event_coords(event), + event=event, + ) def _handle_mouse_button_release(self, widget, event): """ Forward button release information to the flow graph. """ - self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK - self.mod1_mask = event.state & gtk.gdk.MOD1_MASK - if event.button == 1: self._flow_graph.handle_mouse_selector_release( - coordinate=(event.x, event.y), - ) + self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK + self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK + self.button_state[event.button] = False + if event.button == 1: + self._flow_graph.handle_mouse_selector_release( + coordinate=self._translate_event_coords(event), + ) def _handle_mouse_motion(self, widget, event): """ Forward mouse motion information to the flow graph. """ - self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK - self.mod1_mask = event.state & gtk.gdk.MOD1_MASK + self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK + self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK + + if self.button_state[1]: + self._auto_scroll(event) + self._flow_graph.handle_mouse_motion( - coordinate=(event.x, event.y), + coordinate=self._translate_event_coords(event), ) + def _handle_key_press(self, widget, event): + """ + Handle specific keypresses when the drawing area has focus that + triggers actions by the user. + """ + key = event.keyval + mod = event.state + + try: + action = self.accels[(key, mod)] + action() + return True + except KeyError: + return False + + def _update_size(self): + w, h = self._flow_graph.get_extents()[2:] + self.set_size_request(w * self.zoom_factor + 100, h * self.zoom_factor + 100) + + def _auto_scroll(self, event): + x, y = event.x, event.y + scrollbox = self.get_parent().get_parent() + + self._update_size() + + def scroll(pos, adj): + """scroll if we moved near the border""" + adj_val = adj.get_value() + adj_len = adj.get_page_size() + if pos - adj_val > adj_len - Constants.SCROLL_PROXIMITY_SENSITIVITY: + adj.set_value(adj_val + Constants.SCROLL_DISTANCE) + adj.emit('changed') + elif pos - adj_val < Constants.SCROLL_PROXIMITY_SENSITIVITY: + adj.set_value(adj_val - Constants.SCROLL_DISTANCE) + adj.emit('changed') + + scroll(x, scrollbox.get_hadjustment()) + scroll(y, scrollbox.get_vadjustment()) + def _handle_window_realize(self, widget): """ Called when the window is realized. Update the flowgraph, which calls new pixmap. """ self._flow_graph.update() + self._update_size() - def _handle_window_configure(self, widget, event): - """ - Called when the window is resized. - Create a new pixmap for background buffer. - """ - self._pixmap = self.new_pixmap(*self.get_size_request()) + def draw(self, widget, cr): + width = widget.get_allocated_width() + height = widget.get_allocated_height() - def _handle_window_expose(self, widget, event): - """ - Called when window is exposed, or queue_draw is called. - Double buffering: draw to pixmap, then draw pixmap to window. - """ - gc = self.window.new_gc() - self._flow_graph.draw(gc, self._pixmap) - self.window.draw_drawable(gc, self._pixmap, 0, 0, 0, 0, -1, -1) - # draw a light grey line on the bottom and right end of the canvas. - # this is useful when the theme uses the same panel bg color as the canvas - W, H = self._pixmap.get_size() - gc.set_foreground(Colors.FLOWGRAPH_EDGE_COLOR) - self.window.draw_line(gc, 0, H-1, W, H-1) - self.window.draw_line(gc, W-1, 0, W-1, H) + cr.set_source_rgba(*FLOWGRAPH_BACKGROUND_COLOR) + cr.rectangle(0, 0, width, height) + cr.fill() + + cr.scale(self.zoom_factor, self.zoom_factor) + cr.set_line_width(2.0 / self.zoom_factor) + + if self._update_after_zoom: + self._flow_graph.create_labels(cr) + self._flow_graph.create_shapes() + self._update_size() + self._update_after_zoom = False + + self._flow_graph.draw(cr) + + def _translate_event_coords(self, event): + return event.x / self.zoom_factor, event.y / self.zoom_factor def _handle_focus_lost_event(self, widget, event): # don't clear selection while context menu is active - if not self._flow_graph.get_context_menu().flags() & gtk.VISIBLE: + if not self._flow_graph.context_menu.get_take_focus(): self._flow_graph.unselect() self._flow_graph.update_selected() self._flow_graph.queue_draw() diff --git a/grc/gui/Element.py b/grc/gui/Element.py deleted file mode 100644 index 9385424772..0000000000 --- a/grc/gui/Element.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Copyright 2007, 2008, 2009 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -from Constants import LINE_SELECT_SENSITIVITY -from Constants import POSSIBLE_ROTATIONS - -import gtk - - -class Element(object): - """ - GraphicalElement is the base class for all graphical elements. - It contains an X,Y coordinate, a list of rectangular areas that the element occupies, - and methods to detect selection of those areas. - """ - - def __init__(self): - """ - Make a new list of rectangular areas and lines, and set the coordinate and the rotation. - """ - self.set_rotation(POSSIBLE_ROTATIONS[0]) - self.set_coordinate((0, 0)) - self.clear() - self.set_highlighted(False) - self.line_attributes = [ - 0, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER - ] - - def is_horizontal(self, rotation=None): - """ - Is this element horizontal? - If rotation is None, use this element's rotation. - - Args: - rotation: the optional rotation - - Returns: - true if rotation is horizontal - """ - rotation = rotation or self.get_rotation() - return rotation in (0, 180) - - def is_vertical(self, rotation=None): - """ - Is this element vertical? - If rotation is None, use this element's rotation. - - Args: - rotation: the optional rotation - - Returns: - true if rotation is vertical - """ - rotation = rotation or self.get_rotation() - return rotation in (90, 270) - - def create_labels(self): - """ - Create labels (if applicable) and call on all children. - Call this base method before creating labels in the element. - """ - for child in self.get_children():child.create_labels() - - def create_shapes(self): - """ - Create shapes (if applicable) and call on all children. - Call this base method before creating shapes in the element. - """ - self.clear() - for child in self.get_children(): child.create_shapes() - - def draw(self, gc, window, border_color, bg_color): - """ - Draw in the given window. - - Args: - gc: the graphics context - window: the gtk window to draw on - border_color: the color for lines and rectangle borders - bg_color: the color for the inside of the rectangle - """ - X, Y = self.get_coordinate() - gc.set_line_attributes(*self.line_attributes) - for (rX, rY), (W, H) in self._areas_list: - aX = X + rX - aY = Y + rY - gc.set_foreground(bg_color) - window.draw_rectangle(gc, True, aX, aY, W, H) - gc.set_foreground(border_color) - window.draw_rectangle(gc, False, aX, aY, W, H) - for (x1, y1), (x2, y2) in self._lines_list: - gc.set_foreground(border_color) - gc.set_background(bg_color) - window.draw_line(gc, X+x1, Y+y1, X+x2, Y+y2) - - def rotate(self, rotation): - """ - Rotate all of the areas by 90 degrees. - - Args: - rotation: multiple of 90 degrees - """ - self.set_rotation((self.get_rotation() + rotation)%360) - - def clear(self): - """Empty the lines and areas.""" - self._areas_list = list() - self._lines_list = list() - - def set_coordinate(self, coor): - """ - Set the reference coordinate. - - Args: - coor: the coordinate tuple (x,y) - """ - self.coor = coor - - # def get_parent(self): - # """ - # Get the parent of this element. - # - # Returns: - # the parent - # """ - # return self.parent - - def set_highlighted(self, highlighted): - """ - Set the highlight status. - - Args: - highlighted: true to enable highlighting - """ - self.highlighted = highlighted - - def is_highlighted(self): - """ - Get the highlight status. - - Returns: - true if highlighted - """ - return self.highlighted - - def get_coordinate(self): - """Get the coordinate. - - Returns: - the coordinate tuple (x,y) - """ - return self.coor - - def move(self, delta_coor): - """ - Move the element by adding the delta_coor to the current coordinate. - - Args: - delta_coor: (delta_x,delta_y) tuple - """ - deltaX, deltaY = delta_coor - X, Y = self.get_coordinate() - self.set_coordinate((X+deltaX, Y+deltaY)) - - def add_area(self, rel_coor, area): - """ - Add an area to the area list. - An area is actually a coordinate relative to the main coordinate - with a width/height pair relative to the area coordinate. - A positive width is to the right of the coordinate. - A positive height is above the coordinate. - The area is associated with a rotation. - - Args: - rel_coor: (x,y) offset from this element's coordinate - area: (width,height) tuple - """ - self._areas_list.append((rel_coor, area)) - - def add_line(self, rel_coor1, rel_coor2): - """ - Add a line to the line list. - A line is defined by 2 relative coordinates. - Lines must be horizontal or vertical. - The line is associated with a rotation. - - Args: - rel_coor1: relative (x1,y1) tuple - rel_coor2: relative (x2,y2) tuple - """ - self._lines_list.append((rel_coor1, rel_coor2)) - - def what_is_selected(self, coor, coor_m=None): - """ - One coordinate specified: - Is this element selected at given coordinate? - ie: is the coordinate encompassed by one of the areas or lines? - Both coordinates specified: - Is this element within the rectangular region defined by both coordinates? - ie: do any area corners or line endpoints fall within the region? - - Args: - coor: the selection coordinate, tuple x, y - coor_m: an additional selection coordinate. - - Returns: - self if one of the areas/lines encompasses coor, else None. - """ - #function to test if p is between a and b (inclusive) - in_between = lambda p, a, b: p >= min(a, b) and p <= max(a, b) - #relative coordinate - x, y = [a-b for a,b in zip(coor, self.get_coordinate())] - if coor_m: - x_m, y_m = [a-b for a,b in zip(coor_m, self.get_coordinate())] - #handle rectangular areas - for (x1,y1), (w,h) in self._areas_list: - if in_between(x1, x, x_m) and in_between(y1, y, y_m) or \ - in_between(x1+w, x, x_m) and in_between(y1, y, y_m) or \ - in_between(x1, x, x_m) and in_between(y1+h, y, y_m) or \ - in_between(x1+w, x, x_m) and in_between(y1+h, y, y_m): - return self - #handle horizontal or vertical lines - for (x1, y1), (x2, y2) in self._lines_list: - if in_between(x1, x, x_m) and in_between(y1, y, y_m) or \ - in_between(x2, x, x_m) and in_between(y2, y, y_m): - return self - return None - else: - #handle rectangular areas - for (x1,y1), (w,h) in self._areas_list: - if in_between(x, x1, x1+w) and in_between(y, y1, y1+h): return self - #handle horizontal or vertical lines - for (x1, y1), (x2, y2) in self._lines_list: - if x1 == x2: x1, x2 = x1-LINE_SELECT_SENSITIVITY, x2+LINE_SELECT_SENSITIVITY - if y1 == y2: y1, y2 = y1-LINE_SELECT_SENSITIVITY, y2+LINE_SELECT_SENSITIVITY - if in_between(x, x1, x2) and in_between(y, y1, y2): return self - return None - - def get_rotation(self): - """ - Get the rotation in degrees. - - Returns: - the rotation - """ - return self.rotation - - def set_rotation(self, rotation): - """ - Set the rotation in degrees. - - Args: - rotation: the rotation""" - if rotation not in POSSIBLE_ROTATIONS: - raise Exception('"%s" is not one of the possible rotations: (%s)'%(rotation, POSSIBLE_ROTATIONS)) - self.rotation = rotation - - def mouse_over(self): - pass - - def mouse_out(self): - pass diff --git a/grc/gui/Executor.py b/grc/gui/Executor.py index 4082cea18c..7db0c955cb 100644 --- a/grc/gui/Executor.py +++ b/grc/gui/Executor.py @@ -15,14 +15,16 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +from __future__ import absolute_import + +import os +import shlex import subprocess import threading from distutils.spawn import find_executable -import gobject -import os +from gi.repository import GLib -from ..core.utils import shlex from ..core import Messages @@ -32,20 +34,16 @@ class ExecFlowGraphThread(threading.Thread): def __init__(self, flow_graph_page, xterm_executable, callback): """ ExecFlowGraphThread constructor. - - Args: - action_handler: an instance of an ActionHandler """ threading.Thread.__init__(self) self.page = flow_graph_page # store page and don't use main window calls in run - self.flow_graph = self.page.get_flow_graph() + self.flow_graph = self.page.flow_graph self.xterm_executable = xterm_executable self.update_callback = callback try: - self.process = self._popen() - self.page.set_proc(self.process) + self.process = self.page.process = self._popen() self.update_callback() self.start() except Exception as e: @@ -79,18 +77,18 @@ class ExecFlowGraphThread(threading.Thread): def run(self): """ Wait on the executing process by reading from its stdout. - Use gobject.idle_add when calling functions that modify gtk objects. + Use GObject.idle_add when calling functions that modify gtk objects. """ # handle completion r = "\n" while r: - gobject.idle_add(Messages.send_verbose_exec, r) + GLib.idle_add(Messages.send_verbose_exec, r) r = os.read(self.process.stdout.fileno(), 1024) self.process.poll() - gobject.idle_add(self.done) + GLib.idle_add(self.done) def done(self): """Perform end of execution tasks.""" Messages.send_end_exec(self.process.returncode) - self.page.set_proc(None) + self.page.process = None self.update_callback() diff --git a/grc/gui/FileDialogs.py b/grc/gui/FileDialogs.py index 30978bbf9d..e8c5800d6a 100644 --- a/grc/gui/FileDialogs.py +++ b/grc/gui/FileDialogs.py @@ -17,140 +17,99 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk -from Dialogs import MessageDialogHelper -from Constants import \ - DEFAULT_FILE_PATH, IMAGE_FILE_EXTENSION, TEXT_FILE_EXTENSION, \ - NEW_FLOGRAPH_TITLE -import Preferences -from os import path -import Utils - -################################################## -# Constants -################################################## -OPEN_FLOW_GRAPH = 'open flow graph' -SAVE_FLOW_GRAPH = 'save flow graph' -SAVE_CONSOLE = 'save console' -SAVE_IMAGE = 'save image' -OPEN_QSS_THEME = 'open qss theme' - -FILE_OVERWRITE_MARKUP_TMPL="""\ -File <b>$encode($filename)</b> Exists!\nWould you like to overwrite the existing file?""" - -FILE_DNE_MARKUP_TMPL="""\ -File <b>$encode($filename)</b> Does not Exist!""" - - - -# File Filters -def get_flow_graph_files_filter(): - filter = gtk.FileFilter() - filter.set_name('Flow Graph Files') - filter.add_pattern('*'+Preferences.file_extension()) - return filter - - -def get_text_files_filter(): - filter = gtk.FileFilter() - filter.set_name('Text Files') - filter.add_pattern('*'+TEXT_FILE_EXTENSION) - return filter - - -def get_image_files_filter(): - filter = gtk.FileFilter() - filter.set_name('Image Files') - filter.add_pattern('*'+IMAGE_FILE_EXTENSION) - return filter +from __future__ import absolute_import +from os import path -def get_all_files_filter(): - filter = gtk.FileFilter() - filter.set_name('All Files') - filter.add_pattern('*') - return filter - +from gi.repository import Gtk -def get_qss_themes_filter(): - filter = gtk.FileFilter() - filter.set_name('QSS Themes') - filter.add_pattern('*.qss') - return filter +from . import Constants, Utils, Dialogs -# File Dialogs -class FileDialogHelper(gtk.FileChooserDialog): +class FileDialogHelper(Gtk.FileChooserDialog, object): """ A wrapper class for the gtk file chooser dialog. Implement a file chooser dialog with only necessary parameters. """ + title = '' + action = Gtk.FileChooserAction.OPEN + filter_label = '' + filter_ext = '' - def __init__(self, action, title): + def __init__(self, parent, current_file_path): """ FileDialogHelper constructor. Create a save or open dialog with cancel and ok buttons. Use standard settings: no multiple selection, local files only, and the * filter. Args: - action: gtk.FILE_CHOOSER_ACTION_OPEN or gtk.FILE_CHOOSER_ACTION_SAVE + action: Gtk.FileChooserAction.OPEN or Gtk.FileChooserAction.SAVE title: the title of the dialog (string) """ - ok_stock = {gtk.FILE_CHOOSER_ACTION_OPEN : 'gtk-open', gtk.FILE_CHOOSER_ACTION_SAVE : 'gtk-save'}[action] - gtk.FileChooserDialog.__init__(self, title, None, action, ('gtk-cancel', gtk.RESPONSE_CANCEL, ok_stock, gtk.RESPONSE_OK)) + ok_stock = { + Gtk.FileChooserAction.OPEN: 'gtk-open', + Gtk.FileChooserAction.SAVE: 'gtk-save' + }[self.action] + + Gtk.FileChooserDialog.__init__(self, title=self.title, action=self.action, + transient_for=parent) + self.add_buttons('gtk-cancel', Gtk.ResponseType.CANCEL, ok_stock, Gtk.ResponseType.OK) self.set_select_multiple(False) self.set_local_only(True) - self.add_filter(get_all_files_filter()) + + self.parent = parent + self.current_file_path = current_file_path or path.join( + Constants.DEFAULT_FILE_PATH, Constants.NEW_FLOGRAPH_TITLE + Constants.FILE_EXTENSION) + + self.set_current_folder(path.dirname(current_file_path)) # current directory + self.setup_filters() + + def setup_filters(self, filters=None): + set_default = True + filters = filters or ([(self.filter_label, self.filter_ext)] if self.filter_label else []) + filters.append(('All Files', '')) + for label, ext in filters: + if not label: + continue + f = Gtk.FileFilter() + f.set_name(label) + f.add_pattern('*' + ext) + self.add_filter(f) + if not set_default: + self.set_filter(f) + set_default = True + + def run(self): + """Get the filename and destroy the dialog.""" + response = Gtk.FileChooserDialog.run(self) + filename = self.get_filename() if response == Gtk.ResponseType.OK else None + self.destroy() + return filename -class FileDialog(FileDialogHelper): +class SaveFileDialog(FileDialogHelper): """A dialog box to save or open flow graph files. This is a base class, do not use.""" + action = Gtk.FileChooserAction.SAVE - def __init__(self, current_file_path=''): - """ - FileDialog constructor. + def __init__(self, parent, current_file_path): + super(SaveFileDialog, self).__init__(parent, current_file_path) + self.set_current_name(path.splitext(path.basename(self.current_file_path))[0] + self.filter_ext) + self.set_create_folders(True) + self.set_do_overwrite_confirmation(True) - Args: - current_file_path: the current directory or path to the open flow graph - """ - if not current_file_path: current_file_path = path.join(DEFAULT_FILE_PATH, NEW_FLOGRAPH_TITLE + Preferences.file_extension()) - if self.type == OPEN_FLOW_GRAPH: - FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, 'Open a Flow Graph from a File...') - self.add_and_set_filter(get_flow_graph_files_filter()) - self.set_select_multiple(True) - elif self.type == SAVE_FLOW_GRAPH: - FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save a Flow Graph to a File...') - self.add_and_set_filter(get_flow_graph_files_filter()) - self.set_current_name(path.basename(current_file_path)) - elif self.type == SAVE_CONSOLE: - FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save Console to a File...') - self.add_and_set_filter(get_text_files_filter()) - file_path = path.splitext(path.basename(current_file_path))[0] - self.set_current_name(file_path) #show the current filename - elif self.type == SAVE_IMAGE: - FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save a Flow Graph Screen Shot...') - self.add_and_set_filter(get_image_files_filter()) - current_file_path = current_file_path + IMAGE_FILE_EXTENSION - self.set_current_name(path.basename(current_file_path)) #show the current filename - elif self.type == OPEN_QSS_THEME: - FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, 'Open a QSS theme...') - self.add_and_set_filter(get_qss_themes_filter()) - self.set_select_multiple(False) - self.set_current_folder(path.dirname(current_file_path)) #current directory - - def add_and_set_filter(self, filter): - """ - Add the gtk file filter to the list of filters and set it as the default file filter. - Args: - filter: a gtk file filter. - """ - self.add_filter(filter) - self.set_filter(filter) +class OpenFileDialog(FileDialogHelper): + """A dialog box to save or open flow graph files. This is a base class, do not use.""" + action = Gtk.FileChooserAction.OPEN + + def show_missing_message(self, filename): + Dialogs.MessageDialogWrapper( + self.parent, + Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, 'Cannot Open!', + 'File <b>{filename}</b> Does not Exist!'.format(filename=Utils.encode(filename)), + ).run_and_destroy() - def get_rectified_filename(self): + def get_filename(self): """ Run the dialog and get the filename. If this is a save dialog and the file name is missing the extension, append the file extension. @@ -160,82 +119,82 @@ class FileDialog(FileDialogHelper): Returns: the complete file path """ - if gtk.FileChooserDialog.run(self) != gtk.RESPONSE_OK: return None #response was cancel - ############################################# - # Handle Save Dialogs - ############################################# - if self.type in (SAVE_FLOW_GRAPH, SAVE_CONSOLE, SAVE_IMAGE): - filename = self.get_filename() - extension = { - SAVE_FLOW_GRAPH: Preferences.file_extension(), - SAVE_CONSOLE: TEXT_FILE_EXTENSION, - SAVE_IMAGE: IMAGE_FILE_EXTENSION, - }[self.type] - #append the missing file extension if the filter matches - if path.splitext(filename)[1].lower() != extension: filename += extension - self.set_current_name(path.basename(filename)) #show the filename with extension - if path.exists(filename): #ask the user to confirm overwrite - if MessageDialogHelper( - gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, 'Confirm Overwrite!', - Utils.parse_template(FILE_OVERWRITE_MARKUP_TMPL, filename=filename), - ) == gtk.RESPONSE_NO: return self.get_rectified_filename() - return filename - ############################################# - # Handle Open Dialogs - ############################################# - elif self.type in (OPEN_FLOW_GRAPH, OPEN_QSS_THEME): - filenames = self.get_filenames() - for filename in filenames: - if not path.exists(filename): #show a warning and re-run - MessageDialogHelper( - gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'Cannot Open!', - Utils.parse_template(FILE_DNE_MARKUP_TMPL, filename=filename), - ) - return self.get_rectified_filename() - return filenames + filenames = Gtk.FileChooserDialog.get_filenames(self) + for filename in filenames: + if not path.exists(filename): + self.show_missing_message(filename) + return None # rerun + return filenames - def run(self): - """ - Get the filename and destroy the dialog. - - Returns: - the filename or None if a close/cancel occurred. - """ - filename = self.get_rectified_filename() - self.destroy() - return filename +class OpenFlowGraph(OpenFileDialog): + title = 'Open a Flow Graph from a File...' + filter_label = 'Flow Graph Files' + filter_ext = Constants.FILE_EXTENSION -class OpenFlowGraphFileDialog(FileDialog): - type = OPEN_FLOW_GRAPH + def __init__(self, parent, current_file_path=''): + super(OpenFlowGraph, self).__init__(parent, current_file_path) + self.set_select_multiple(True) -class SaveFlowGraphFileDialog(FileDialog): - type = SAVE_FLOW_GRAPH +class OpenQSS(OpenFileDialog): + title = 'Open a QSS theme...' + filter_label = 'QSS Themes' + filter_ext = '.qss' -class OpenQSSFileDialog(FileDialog): - type = OPEN_QSS_THEME +class SaveFlowGraph(SaveFileDialog): + title = 'Save a Flow Graph to a File...' + filter_label = 'Flow Graph Files' + filter_ext = Constants.FILE_EXTENSION -class SaveConsoleFileDialog(FileDialog): - type = SAVE_CONSOLE +class SaveConsole(SaveFileDialog): + title = 'Save Console to a File...' + filter_label = 'Test Files' + filter_ext = '.txt' -class SaveImageFileDialog(FileDialog): - type = SAVE_IMAGE +class SaveScreenShot(SaveFileDialog): + title = 'Save a Flow Graph Screen Shot...' + filters = [('PDF Files', '.pdf'), ('PNG Files', '.png'), ('SVG Files', '.svg')] + filter_ext = '.pdf' # the default + def __init__(self, parent, current_file_path=''): + super(SaveScreenShot, self).__init__(parent, current_file_path) -class SaveScreenShotDialog(SaveImageFileDialog): + self.config = Gtk.Application.get_default().config - def __init__(self, current_file_path=''): - SaveImageFileDialog.__init__(self, current_file_path) - self._button = button = gtk.CheckButton('_Background transparent') - self._button.set_active(Preferences.screen_shot_background_transparent()) + self._button = button = Gtk.CheckButton(label='Background transparent') + self._button.set_active(self.config.screen_shot_background_transparent()) self.set_extra_widget(button) + def setup_filters(self, filters=None): + super(SaveScreenShot, self).setup_filters(self.filters) + + def show_missing_message(self, filename): + Dialogs.MessageDialogWrapper( + self.parent, + Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, 'Can not Save!', + 'File Extention of <b>{filename}</b> not supported!'.format(filename=Utils.encode(filename)), + ).run_and_destroy() + def run(self): - filename = SaveImageFileDialog.run(self) + valid_exts = {ext for label, ext in self.filters} + filename = None + while True: + response = Gtk.FileChooserDialog.run(self) + if response != Gtk.ResponseType.OK: + filename = None + break + + filename = self.get_filename() + if path.splitext(filename)[1] in valid_exts: + break + + self.show_missing_message(filename) + bg_transparent = self._button.get_active() - Preferences.screen_shot_background_transparent(bg_transparent) + self.config.screen_shot_background_transparent(bg_transparent) + self.destroy() return filename, bg_transparent diff --git a/grc/gui/FlowGraph.py b/grc/gui/FlowGraph.py deleted file mode 100644 index 5bcf018120..0000000000 --- a/grc/gui/FlowGraph.py +++ /dev/null @@ -1,765 +0,0 @@ -""" -Copyright 2007-2011 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import functools -import random -from distutils.spawn import find_executable -from itertools import chain, count -from operator import methodcaller - -import gobject - -from . import Actions, Colors, Constants, Utils, Bars, Dialogs -from .Constants import SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE -from .Element import Element -from .external_editor import ExternalEditor - -from ..core.FlowGraph import FlowGraph as _Flowgraph -from ..core import Messages - - -class FlowGraph(Element, _Flowgraph): - """ - FlowGraph is the data structure to store graphical signal blocks, - graphical inputs and outputs, - and the connections between inputs and outputs. - """ - - def __init__(self, **kwargs): - """ - FlowGraph constructor. - Create a list for signal blocks and connections. Connect mouse handlers. - """ - Element.__init__(self) - _Flowgraph.__init__(self, **kwargs) - #when is the flow graph selected? (used by keyboard event handler) - self.is_selected = lambda: bool(self.get_selected_elements()) - #important vars dealing with mouse event tracking - self.element_moved = False - self.mouse_pressed = False - self._selected_elements = [] - self.press_coor = (0, 0) - #selected ports - self._old_selected_port = None - self._new_selected_port = None - # current mouse hover element - self.element_under_mouse = None - #context menu - self._context_menu = Bars.ContextMenu() - self.get_context_menu = lambda: self._context_menu - - self._external_updaters = {} - - def _get_unique_id(self, base_id=''): - """ - Get a unique id starting with the base id. - - Args: - base_id: the id starts with this and appends a count - - Returns: - a unique id - """ - for index in count(): - block_id = '{}_{}'.format(base_id, index) - if block_id not in (b.get_id() for b in self.blocks): - break - return block_id - - def install_external_editor(self, param): - target = (param.get_parent().get_id(), param.get_key()) - - if target in self._external_updaters: - editor = self._external_updaters[target] - else: - config = self.get_parent().config - editor = (find_executable(config.editor) or - Dialogs.ChooseEditorDialog(config)) - if not editor: - return - updater = functools.partial( - self.handle_external_editor_change, target=target) - editor = self._external_updaters[target] = ExternalEditor( - editor=editor, - name=target[0], value=param.get_value(), - callback=functools.partial(gobject.idle_add, updater) - ) - editor.start() - try: - editor.open_editor() - except Exception as e: - # Problem launching the editor. Need to select a new editor. - Messages.send('>>> Error opening an external editor. Please select a different editor.\n') - # Reset the editor to force the user to select a new one. - self.get_parent().config.editor = '' - - def handle_external_editor_change(self, new_value, target): - try: - block_id, param_key = target - self.get_block(block_id).get_param(param_key).set_value(new_value) - - except (IndexError, ValueError): # block no longer exists - self._external_updaters[target].stop() - del self._external_updaters[target] - return - Actions.EXTERNAL_UPDATE() - - - ########################################################################### - # Access Drawing Area - ########################################################################### - def get_drawing_area(self): return self.drawing_area - def queue_draw(self): self.get_drawing_area().queue_draw() - def get_size(self): return self.get_drawing_area().get_size_request() - def set_size(self, *args): self.get_drawing_area().set_size_request(*args) - def get_scroll_pane(self): return self.drawing_area.get_parent() - def get_ctrl_mask(self): return self.drawing_area.ctrl_mask - def get_mod1_mask(self): return self.drawing_area.mod1_mask - def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args) - - def add_new_block(self, key, coor=None): - """ - Add a block of the given key to this flow graph. - - Args: - key: the block key - coor: an optional coordinate or None for random - """ - id = self._get_unique_id(key) - #calculate the position coordinate - W, H = self.get_size() - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - if coor is None: coor = ( - int(random.uniform(.25, .75) * min(h_adj.page_size, W) + - h_adj.get_value()), - int(random.uniform(.25, .75) * min(v_adj.page_size, H) + - v_adj.get_value()), - ) - #get the new block - block = self.new_block(key) - block.set_coordinate(coor) - block.set_rotation(0) - block.get_param('id').set_value(id) - Actions.ELEMENT_CREATE() - return id - - ########################################################################### - # Copy Paste - ########################################################################### - def copy_to_clipboard(self): - """ - Copy the selected blocks and connections into the clipboard. - - Returns: - the clipboard - """ - #get selected blocks - blocks = self.get_selected_blocks() - if not blocks: return None - #calc x and y min - x_min, y_min = blocks[0].get_coordinate() - for block in blocks: - x, y = block.get_coordinate() - x_min = min(x, x_min) - y_min = min(y, y_min) - #get connections between selected blocks - connections = filter( - lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks, - self.connections, - ) - clipboard = ( - (x_min, y_min), - [block.export_data() for block in blocks], - [connection.export_data() for connection in connections], - ) - return clipboard - - def paste_from_clipboard(self, clipboard): - """ - Paste the blocks and connections from the clipboard. - - Args: - clipboard: the nested data of blocks, connections - """ - selected = set() - (x_min, y_min), blocks_n, connections_n = clipboard - old_id2block = dict() - #recalc the position - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - x_off = h_adj.get_value() - x_min + h_adj.page_size/4 - y_off = v_adj.get_value() - y_min + v_adj.page_size/4 - if len(self.get_elements()) <= 1: - x_off, y_off = 0, 0 - #create blocks - for block_n in blocks_n: - block_key = block_n.find('key') - if block_key == 'options': continue - block = self.new_block(block_key) - if not block: - continue # unknown block was pasted (e.g. dummy block) - selected.add(block) - #set params - params = dict((n.find('key'), n.find('value')) - for n in block_n.findall('param')) - if block_key == 'epy_block': - block.get_param('_io_cache').set_value(params.pop('_io_cache')) - block.get_param('_source_code').set_value(params.pop('_source_code')) - block.rewrite() # this creates the other params - for param_key, param_value in params.iteritems(): - #setup id parameter - if param_key == 'id': - old_id2block[param_value] = block - #if the block id is not unique, get a new block id - if param_value in (blk.get_id() for blk in self.blocks): - param_value = self._get_unique_id(param_value) - #set value to key - block.get_param(param_key).set_value(param_value) - #move block to offset coordinate - block.move((x_off, y_off)) - #update before creating connections - self.update() - #create connections - for connection_n in connections_n: - source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key')) - sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('sink_key')) - self.connect(source, sink) - #set all pasted elements selected - for block in selected: selected = selected.union(set(block.get_connections())) - self._selected_elements = list(selected) - - ########################################################################### - # Modify Selected - ########################################################################### - def type_controller_modify_selected(self, direction): - """ - Change the registered type controller for the selected signal blocks. - - Args: - direction: +1 or -1 - - Returns: - true for change - """ - return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()]) - - def port_controller_modify_selected(self, direction): - """ - Change port controller for the selected signal blocks. - - Args: - direction: +1 or -1 - - Returns: - true for changed - """ - return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()]) - - def enable_selected(self, enable): - """ - Enable/disable the selected blocks. - - Args: - enable: true to enable - - Returns: - true if changed - """ - changed = False - for selected_block in self.get_selected_blocks(): - if selected_block.set_enabled(enable): changed = True - return changed - - def bypass_selected(self): - """ - Bypass the selected blocks. - - Args: - None - Returns: - true if changed - """ - changed = False - for selected_block in self.get_selected_blocks(): - if selected_block.set_bypassed(): changed = True - return changed - - def move_selected(self, delta_coordinate): - """ - Move the element and by the change in coordinates. - - Args: - delta_coordinate: the change in coordinates - """ - for selected_block in self.get_selected_blocks(): - delta_coordinate = selected_block.bound_move_delta(delta_coordinate) - - for selected_block in self.get_selected_blocks(): - selected_block.move(delta_coordinate) - self.element_moved = True - - def align_selected(self, calling_action=None): - """ - Align the selected blocks. - - Args: - calling_action: the action initiating the alignment - - Returns: - True if changed, otherwise False - """ - blocks = self.get_selected_blocks() - if calling_action is None or not blocks: - return False - - # compute common boundary of selected objects - min_x, min_y = max_x, max_y = blocks[0].get_coordinate() - for selected_block in blocks: - x, y = selected_block.get_coordinate() - min_x, min_y = min(min_x, x), min(min_y, y) - x += selected_block.W - y += selected_block.H - max_x, max_y = max(max_x, x), max(max_y, y) - ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 - - # align the blocks as requested - transform = { - Actions.BLOCK_VALIGN_TOP: lambda x, y, w, h: (x, min_y), - Actions.BLOCK_VALIGN_MIDDLE: lambda x, y, w, h: (x, ctr_y - h/2), - Actions.BLOCK_VALIGN_BOTTOM: lambda x, y, w, h: (x, max_y - h), - Actions.BLOCK_HALIGN_LEFT: lambda x, y, w, h: (min_x, y), - Actions.BLOCK_HALIGN_CENTER: lambda x, y, w, h: (ctr_x-w/2, y), - Actions.BLOCK_HALIGN_RIGHT: lambda x, y, w, h: (max_x - w, y), - }.get(calling_action, lambda *args: args) - - for selected_block in blocks: - x, y = selected_block.get_coordinate() - w, h = selected_block.W, selected_block.H - selected_block.set_coordinate(transform(x, y, w, h)) - - return True - - def rotate_selected(self, rotation): - """ - Rotate the selected blocks by multiples of 90 degrees. - - Args: - rotation: the rotation in degrees - - Returns: - true if changed, otherwise false. - """ - if not self.get_selected_blocks(): - return False - #initialize min and max coordinates - min_x, min_y = self.get_selected_block().get_coordinate() - max_x, max_y = self.get_selected_block().get_coordinate() - #rotate each selected block, and find min/max coordinate - for selected_block in self.get_selected_blocks(): - selected_block.rotate(rotation) - #update the min/max coordinate - x, y = selected_block.get_coordinate() - min_x, min_y = min(min_x, x), min(min_y, y) - max_x, max_y = max(max_x, x), max(max_y, y) - #calculate center point of slected blocks - ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 - #rotate the blocks around the center point - for selected_block in self.get_selected_blocks(): - x, y = selected_block.get_coordinate() - x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation) - selected_block.set_coordinate((x + ctr_x, y + ctr_y)) - return True - - def remove_selected(self): - """ - Remove selected elements - - Returns: - true if changed. - """ - changed = False - for selected_element in self.get_selected_elements(): - self.remove_element(selected_element) - changed = True - return changed - - def draw(self, gc, window): - """ - Draw the background and grid if enabled. - Draw all of the elements in this flow graph onto the pixmap. - Draw the pixmap to the drawable window of this flow graph. - """ - - W, H = self.get_size() - hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active() - hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active() - - #draw the background - gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR) - window.draw_rectangle(gc, True, 0, 0, W, H) - - # draw comments first - if Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active(): - for block in self.blocks: - if hide_variables and (block.is_variable or block.is_import): - continue # skip hidden disabled blocks and connections - if block.get_enabled(): - block.draw_comment(gc, window) - #draw multi select rectangle - if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()): - #coordinates - x1, y1 = self.press_coor - x2, y2 = self.get_coordinate() - #calculate top-left coordinate and width/height - x, y = int(min(x1, x2)), int(min(y1, y2)) - w, h = int(abs(x1 - x2)), int(abs(y1 - y2)) - #draw - gc.set_foreground(Colors.HIGHLIGHT_COLOR) - window.draw_rectangle(gc, True, x, y, w, h) - gc.set_foreground(Colors.BORDER_COLOR) - window.draw_rectangle(gc, False, x, y, w, h) - #draw blocks on top of connections - blocks = sorted(self.blocks, key=methodcaller('get_enabled')) - for element in chain(self.connections, blocks): - if hide_disabled_blocks and not element.get_enabled(): - continue # skip hidden disabled blocks and connections - if hide_variables and (element.is_variable or element.is_import): - continue # skip hidden disabled blocks and connections - element.draw(gc, window) - #draw selected blocks on top of selected connections - for selected_element in self.get_selected_connections() + self.get_selected_blocks(): - selected_element.draw(gc, window) - - def update_selected(self): - """ - Remove deleted elements from the selected elements list. - Update highlighting so only the selected are highlighted. - """ - selected_elements = self.get_selected_elements() - elements = self.get_elements() - #remove deleted elements - for selected in selected_elements: - if selected in elements: continue - selected_elements.remove(selected) - if self._old_selected_port and self._old_selected_port.get_parent() not in elements: - self._old_selected_port = None - if self._new_selected_port and self._new_selected_port.get_parent() not in elements: - self._new_selected_port = None - #update highlighting - for element in elements: - element.set_highlighted(element in selected_elements) - - def update(self): - """ - Call the top level rewrite and validate. - Call the top level create labels and shapes. - """ - self.rewrite() - self.validate() - self.create_labels() - self.create_shapes() - - def reload(self): - """ - Reload flow-graph (with updated blocks) - - Args: - page: the page to reload (None means current) - Returns: - False if some error occurred during import - """ - success = False - data = self.export_data() - if data: - self.unselect() - success = self.import_data(data) - self.update() - return success - - ########################################################################## - ## Get Selected - ########################################################################## - def unselect(self): - """ - Set selected elements to an empty set. - """ - self._selected_elements = [] - - def select_all(self): - """Select all blocks in the flow graph""" - self._selected_elements = list(self.get_elements()) - - def what_is_selected(self, coor, coor_m=None): - """ - What is selected? - At the given coordinate, return the elements found to be selected. - If coor_m is unspecified, return a list of only the first element found to be selected: - Iterate though the elements backwards since top elements are at the end of the list. - If an element is selected, place it at the end of the list so that is is drawn last, - and hence on top. Update the selected port information. - - Args: - coor: the coordinate of the mouse click - coor_m: the coordinate for multi select - - Returns: - the selected blocks and connections or an empty list - """ - selected_port = None - selected = set() - #check the elements - hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active() - hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active() - for element in reversed(self.get_elements()): - if hide_disabled_blocks and not element.get_enabled(): - continue # skip hidden disabled blocks and connections - if hide_variables and (element.is_variable or element.is_import): - continue # skip hidden disabled blocks and connections - selected_element = element.what_is_selected(coor, coor_m) - if not selected_element: - continue - #update the selected port information - if selected_element.is_port: - if not coor_m: selected_port = selected_element - selected_element = selected_element.get_parent() - selected.add(selected_element) - #place at the end of the list - self.get_elements().remove(element) - self.get_elements().append(element) - #single select mode, break - if not coor_m: break - #update selected ports - if selected_port is not self._new_selected_port: - self._old_selected_port = self._new_selected_port - self._new_selected_port = selected_port - return list(selected) - - def get_selected_connections(self): - """ - Get a group of selected connections. - - Returns: - sub set of connections in this flow graph - """ - selected = set() - for selected_element in self.get_selected_elements(): - if selected_element.is_connection: - selected.add(selected_element) - return list(selected) - - def get_selected_blocks(self): - """ - Get a group of selected blocks. - - Returns: - sub set of blocks in this flow graph - """ - selected = set() - for selected_element in self.get_selected_elements(): - if selected_element.is_block: - selected.add(selected_element) - return list(selected) - - def get_selected_block(self): - """ - Get the selected block when a block or port is selected. - - Returns: - a block or None - """ - selected_blocks = self.get_selected_blocks() - return selected_blocks[0] if selected_blocks else None - - def get_selected_elements(self): - """ - Get the group of selected elements. - - Returns: - sub set of elements in this flow graph - """ - return self._selected_elements - - def get_selected_element(self): - """ - Get the selected element. - - Returns: - a block, port, or connection or None - """ - selected_elements = self.get_selected_elements() - return selected_elements[0] if selected_elements else None - - def update_selected_elements(self): - """ - Update the selected elements. - The update behavior depends on the state of the mouse button. - When the mouse button pressed the selection will change when - the control mask is set or the new selection is not in the current group. - When the mouse button is released the selection will change when - the mouse has moved and the control mask is set or the current group is empty. - Attempt to make a new connection if the old and ports are filled. - If the control mask is set, merge with the current elements. - """ - selected_elements = None - if self.mouse_pressed: - new_selections = self.what_is_selected(self.get_coordinate()) - #update the selections if the new selection is not in the current selections - #allows us to move entire selected groups of elements - if self.get_ctrl_mask() or not ( - new_selections and new_selections[0] in self.get_selected_elements() - ): selected_elements = new_selections - if self._old_selected_port: - self._old_selected_port.force_label_unhidden(False) - self.create_shapes() - self.queue_draw() - elif self._new_selected_port: - self._new_selected_port.force_label_unhidden() - else: # called from a mouse release - if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()): - selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor) - #this selection and the last were ports, try to connect them - if self._old_selected_port and self._new_selected_port: - try: - self.connect(self._old_selected_port, self._new_selected_port) - Actions.ELEMENT_CREATE() - except: Messages.send_fail_connection() - self._old_selected_port = None - self._new_selected_port = None - return - #update selected elements - if selected_elements is None: return - old_elements = set(self.get_selected_elements()) - self._selected_elements = list(set(selected_elements)) - new_elements = set(self.get_selected_elements()) - #if ctrl, set the selected elements to the union - intersection of old and new - if self.get_ctrl_mask(): - self._selected_elements = list( - set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements) - ) - Actions.ELEMENT_SELECT() - - ########################################################################## - ## Event Handlers - ########################################################################## - def handle_mouse_context_press(self, coordinate, event): - """ - The context mouse button was pressed: - If no elements were selected, perform re-selection at this coordinate. - Then, show the context menu at the mouse click location. - """ - selections = self.what_is_selected(coordinate) - if not set(selections).intersection(self.get_selected_elements()): - self.set_coordinate(coordinate) - self.mouse_pressed = True - self.update_selected_elements() - self.mouse_pressed = False - self._context_menu.popup(None, None, None, event.button, event.time) - - def handle_mouse_selector_press(self, double_click, coordinate): - """ - The selector mouse button was pressed: - Find the selected element. Attempt a new connection if possible. - Open the block params window on a double click. - Update the selection state of the flow graph. - """ - self.press_coor = coordinate - self.set_coordinate(coordinate) - self.time = 0 - self.mouse_pressed = True - if double_click: self.unselect() - self.update_selected_elements() - #double click detected, bring up params dialog if possible - if double_click and self.get_selected_block(): - self.mouse_pressed = False - Actions.BLOCK_PARAM_MODIFY() - - def handle_mouse_selector_release(self, coordinate): - """ - The selector mouse button was released: - Update the state, handle motion (dragging). - And update the selected flowgraph elements. - """ - self.set_coordinate(coordinate) - self.time = 0 - self.mouse_pressed = False - if self.element_moved: - Actions.BLOCK_MOVE() - self.element_moved = False - self.update_selected_elements() - - def handle_mouse_motion(self, coordinate): - """ - The mouse has moved, respond to mouse dragging or notify elements - Move a selected element to the new coordinate. - Auto-scroll the scroll bars at the boundaries. - """ - #to perform a movement, the mouse must be pressed - # (no longer checking pending events via gtk.events_pending() - always true in Windows) - if not self.mouse_pressed: - # only continue if mouse-over stuff is enabled (just the auto-hide port label stuff for now) - if not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active(): return - redraw = False - for element in reversed(self.get_elements()): - over_element = element.what_is_selected(coordinate) - if not over_element: continue - if over_element != self.element_under_mouse: # over sth new - if self.element_under_mouse: - redraw |= self.element_under_mouse.mouse_out() or False - self.element_under_mouse = over_element - redraw |= over_element.mouse_over() or False - break - else: - if self.element_under_mouse: - redraw |= self.element_under_mouse.mouse_out() or False - self.element_under_mouse = None - if redraw: - #self.create_labels() - self.create_shapes() - self.queue_draw() - else: - #perform auto-scrolling - width, height = self.get_size() - x, y = coordinate - h_adj = self.get_scroll_pane().get_hadjustment() - v_adj = self.get_scroll_pane().get_vadjustment() - for pos, length, adj, adj_val, adj_len in ( - (x, width, h_adj, h_adj.get_value(), h_adj.page_size), - (y, height, v_adj, v_adj.get_value(), v_adj.page_size), - ): - #scroll if we moved near the border - if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len: - adj.set_value(adj_val+SCROLL_DISTANCE) - adj.emit('changed') - elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY: - adj.set_value(adj_val-SCROLL_DISTANCE) - adj.emit('changed') - #remove the connection if selected in drag event - if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection: - Actions.ELEMENT_DELETE() - #move the selected elements and record the new coordinate - if not self.get_ctrl_mask(): - X, Y = self.get_coordinate() - dX, dY = int(x - X), int(y - Y) - active = Actions.TOGGLE_SNAP_TO_GRID.get_active() or self.get_mod1_mask() - if not active or abs(dX) >= Utils.CANVAS_GRID_SIZE or abs(dY) >= Utils.CANVAS_GRID_SIZE: - self.move_selected((dX, dY)) - self.set_coordinate((x, y)) - #queue draw for animation - self.queue_draw() diff --git a/grc/gui/MainWindow.py b/grc/gui/MainWindow.py index 9a2823a7ab..e737610a79 100644 --- a/grc/gui/MainWindow.py +++ b/grc/gui/MainWindow.py @@ -17,52 +17,32 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ +from __future__ import absolute_import + import os +import logging -import gtk +from gi.repository import Gtk, Gdk, GObject -from . import Bars, Actions, Preferences, Utils +from . import Bars, Actions, Utils from .BlockTreeWindow import BlockTreeWindow +from .Console import Console from .VariableEditor import VariableEditor from .Constants import \ NEW_FLOGRAPH_TITLE, DEFAULT_CONSOLE_WINDOW_WIDTH -from .Dialogs import TextDisplay, MessageDialogHelper -from .NotebookPage import NotebookPage +from .Dialogs import TextDisplay, MessageDialogWrapper +from .Notebook import Notebook, Page from ..core import Messages -MAIN_WINDOW_TITLE_TMPL = """\ -#if not $saved -*#slurp -#end if -#if $basename -$basename#slurp -#else -$new_flowgraph_title#slurp -#end if -#if $read_only - (read only)#slurp -#end if -#if $dirname - - $dirname#slurp -#end if - - $platform_name#slurp -""" -PAGE_TITLE_MARKUP_TMPL = """\ -#set $foreground = $saved and 'black' or 'red' -<span foreground="$foreground">$encode($title or $new_flowgraph_title)</span>#slurp -#if $read_only - (ro)#slurp -#end if -""" +log = logging.getLogger(__name__) ############################################################ # Main window ############################################################ - -class MainWindow(gtk.Window): +class MainWindow(Gtk.ApplicationWindow): """The topmost window with menus, the tool bar, and other major windows.""" # Constants the action handler can use to indicate which panel visibility to change. @@ -70,105 +50,114 @@ class MainWindow(gtk.Window): CONSOLE = 1 VARIABLES = 2 - def __init__(self, platform, action_handler_callback): + def __init__(self, app, platform): """ MainWindow constructor Setup the menu, toolbar, flow graph editor notebook, block selection window... """ - self._platform = platform + Gtk.ApplicationWindow.__init__(self, title="GNU Radio Companion", application=app) + log.debug("__init__()") - gen_opts = platform.blocks['options'].get_param('generate_options') - generate_mode_default = gen_opts.get_value() - generate_modes = [ - (o.get_key(), o.get_name(), o.get_key() == generate_mode_default) - for o in gen_opts.get_options()] + self._platform = platform + self.app = app + self.config = platform.config - # Load preferences - Preferences.load(platform) + # Add all "win" actions to the local + for x in Actions.get_actions(): + if x.startswith("win."): + self.add_action(Actions.actions[x]) # Setup window - gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) - vbox = gtk.VBox() + vbox = Gtk.VBox() self.add(vbox) - icon_theme = gtk.icon_theme_get_default() + icon_theme = Gtk.IconTheme.get_default() icon = icon_theme.lookup_icon("gnuradio-grc", 48, 0) if not icon: # Set window icon self.set_icon_from_file(os.path.dirname(os.path.abspath(__file__)) + "/icon.png") # Create the menu bar and toolbar - self.add_accel_group(Actions.get_accel_group()) - self.menu_bar = Bars.MenuBar(generate_modes, action_handler_callback) - vbox.pack_start(self.menu_bar, False) - self.tool_bar = Bars.Toolbar(generate_modes, action_handler_callback) - vbox.pack_start(self.tool_bar, False) + generate_modes = platform.get_generate_options() + + # This needs to be replaced + # Have an option for either the application menu or this menu + self.menu_bar = Gtk.MenuBar.new_from_model(Bars.Menu()) + vbox.pack_start(self.menu_bar, False, False, 0) + + self.tool_bar = Bars.Toolbar() + self.tool_bar.set_hexpand(True) + # Show the toolbar + self.tool_bar.show() + vbox.pack_start(self.tool_bar, False, False, 0) # Main parent container for the different panels - self.container = gtk.HPaned() - vbox.pack_start(self.container) + self.main = Gtk.HPaned() #(orientation=Gtk.Orientation.HORIZONTAL) + vbox.pack_start(self.main, True, True, 0) # Create the notebook - self.notebook = gtk.Notebook() + self.notebook = Notebook() self.page_to_be_closed = None - self.current_page = None - self.notebook.set_show_border(False) - self.notebook.set_scrollable(True) # scroll arrows for page tabs - self.notebook.connect('switch-page', self._handle_page_change) + + self.current_page = None # type: Page # Create the console window - self.text_display = TextDisplay() - self.console_window = gtk.ScrolledWindow() - self.console_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - self.console_window.add(self.text_display) - self.console_window.set_size_request(-1, DEFAULT_CONSOLE_WINDOW_WIDTH) + self.console = Console() # Create the block tree and variable panels - self.btwin = BlockTreeWindow(platform, self.get_flow_graph) - self.vars = VariableEditor(platform, self.get_flow_graph) + self.btwin = BlockTreeWindow(platform) + self.btwin.connect('create_new_block', self._add_block_to_current_flow_graph) + self.vars = VariableEditor() + self.vars.connect('create_new_block', self._add_block_to_current_flow_graph) + self.vars.connect('remove_block', self._remove_block_from_current_flow_graph) # Figure out which place to put the variable editor - self.left = gtk.VPaned() - self.right = gtk.VPaned() - self.left_subpanel = gtk.HPaned() + self.left = Gtk.VPaned() #orientation=Gtk.Orientation.VERTICAL) + self.right = Gtk.VPaned() #orientation=Gtk.Orientation.VERTICAL) + self.left_subpanel = Gtk.HPaned() #orientation=Gtk.Orientation.HORIZONTAL) - self.variable_panel_sidebar = Preferences.variable_editor_sidebar() + self.variable_panel_sidebar = self.config.variable_editor_sidebar() if self.variable_panel_sidebar: self.left.pack1(self.notebook) - self.left.pack2(self.console_window, False) + self.left.pack2(self.console, False) self.right.pack1(self.btwin) self.right.pack2(self.vars, False) else: # Put the variable editor in a panel with the console self.left.pack1(self.notebook) - self.left_subpanel.pack1(self.console_window, shrink=False) + self.left_subpanel.pack1(self.console, shrink=False) self.left_subpanel.pack2(self.vars, resize=False, shrink=True) self.left.pack2(self.left_subpanel, False) # Create the right panel self.right.pack1(self.btwin) - self.container.pack1(self.left) - self.container.pack2(self.right, False) + self.main.pack1(self.left) + self.main.pack2(self.right, False) - # load preferences and show the main window - self.resize(*Preferences.main_window_size()) - self.container.set_position(Preferences.blocks_window_position()) - self.left.set_position(Preferences.console_window_position()) + # Load preferences and show the main window + self.resize(*self.config.main_window_size()) + self.main.set_position(self.config.blocks_window_position()) + self.left.set_position(self.config.console_window_position()) if self.variable_panel_sidebar: - self.right.set_position(Preferences.variable_editor_position(sidebar=True)) + self.right.set_position(self.config.variable_editor_position(sidebar=True)) else: - self.left_subpanel.set_position(Preferences.variable_editor_position()) + self.left_subpanel.set_position(self.config.variable_editor_position()) self.show_all() - self.console_window.hide() - self.vars.hide() - self.btwin.hide() + log.debug("Main window ready") ############################################################ # Event Handlers ############################################################ + def _add_block_to_current_flow_graph(self, widget, key): + self.current_flow_graph.add_new_block(key) + + def _remove_block_from_current_flow_graph(self, widget, key): + block = self.current_flow_graph.get_block(key) + self.current_flow_graph.remove_element(block) + def _quit(self, window, event): """ Handle the delete event from the main window. @@ -181,20 +170,6 @@ class MainWindow(gtk.Window): Actions.APPLICATION_QUIT() return True - def _handle_page_change(self, notebook, page, page_num): - """ - Handle a page change. When the user clicks on a new tab, - reload the flow graph to update the vars window and - call handle states (select nothing) to update the buttons. - - Args: - notebook: the notebook - page: new page - page_num: new page number - """ - self.current_page = self.notebook.get_nth_page(page_num) - Actions.PAGE_CHANGE() - def update_panel_visibility(self, panel, visibility=True): """ Handles changing visibility of panels. @@ -204,19 +179,19 @@ class MainWindow(gtk.Window): if panel == self.BLOCKS: if visibility: - self.btwin.show() + self.btwin.show() else: - self.btwin.hide() + self.btwin.hide() elif panel == self.CONSOLE: if visibility: - self.console_window.show() + self.console.show() else: - self.console_window.hide() + self.console.hide() elif panel == self.VARIABLES: if visibility: - self.vars.show() + self.vars.show() else: - self.vars.hide() + self.vars.hide() else: return @@ -231,7 +206,7 @@ class MainWindow(gtk.Window): self.right.hide() else: self.right.show() - if not (self.vars.get_property('visible')) and not (self.console_window.get_property('visible')): + if not (self.vars.get_property('visible')) and not (self.console.get_property('visible')): self.left_subpanel.hide() else: self.left_subpanel.show() @@ -240,6 +215,14 @@ class MainWindow(gtk.Window): # Console Window ############################################################ + @property + def current_page(self): + return self.notebook.current_page + + @current_page.setter + def current_page(self, page): + self.notebook.current_page = page + def add_console_line(self, line): """ Place line at the end of the text buffer, then scroll its window all the way down. @@ -247,7 +230,7 @@ class MainWindow(gtk.Window): Args: line: the new text """ - self.text_display.insert(line) + self.console.add_line(line) ############################################################ # Pages: create and close @@ -269,26 +252,24 @@ class MainWindow(gtk.Window): return try: #try to load from file if file_path: Messages.send_start_load(file_path) - flow_graph = self._platform.get_new_flow_graph() + flow_graph = self._platform.make_flow_graph() flow_graph.grc_file_path = file_path #print flow_graph - page = NotebookPage( + page = Page( self, flow_graph=flow_graph, file_path=file_path, ) if file_path: Messages.send_end_load() - except Exception, e: #return on failure + except Exception as e: #return on failure Messages.send_fail_load(e) if isinstance(e, KeyError) and str(e) == "'options'": # This error is unrecoverable, so crash gracefully exit(-1) return #add this page to the notebook - self.notebook.append_page(page, page.get_tab()) - try: self.notebook.set_tab_reorderable(page, True) - except: pass #gtk too old - self.notebook.set_tab_label_packing(page, False, False, gtk.PACK_START) + self.notebook.append_page(page, page.tab) + self.notebook.set_tab_reorderable(page, True) #only show if blank or manual if not file_path or show: self._set_page(page) @@ -299,26 +280,26 @@ class MainWindow(gtk.Window): Returns: true if all closed """ - open_files = filter(lambda file: file, self._get_files()) #filter blank files - open_file = self.get_page().get_file_path() + open_files = [file for file in self._get_files() if file] #filter blank files + open_file = self.current_page.file_path #close each page - for page in sorted(self.get_pages(), key=lambda p: p.get_saved()): + for page in sorted(self.get_pages(), key=lambda p: p.saved): self.page_to_be_closed = page closed = self.close_page(False) if not closed: break if self.notebook.get_n_pages(): return False #save state before closing - Preferences.set_open_files(open_files) - Preferences.file_open(open_file) - Preferences.main_window_size(self.get_size()) - Preferences.console_window_position(self.left.get_position()) - Preferences.blocks_window_position(self.container.get_position()) + self.config.set_open_files(open_files) + self.config.file_open(open_file) + self.config.main_window_size(self.get_size()) + self.config.console_window_position(self.left.get_position()) + self.config.blocks_window_position(self.main.get_position()) if self.variable_panel_sidebar: - Preferences.variable_editor_position(self.right.get_position(), sidebar=True) + self.config.variable_editor_position(self.right.get_position(), sidebar=True) else: - Preferences.variable_editor_position(self.left_subpanel.get_position()) - Preferences.save() + self.config.variable_editor_position(self.left_subpanel.get_position()) + self.config.save() return True def close_page(self, ensure=True): @@ -330,23 +311,24 @@ class MainWindow(gtk.Window): Args: ensure: boolean """ - if not self.page_to_be_closed: self.page_to_be_closed = self.get_page() + if not self.page_to_be_closed: self.page_to_be_closed = self.current_page #show the page if it has an executing flow graph or is unsaved - if self.page_to_be_closed.get_proc() or not self.page_to_be_closed.get_saved(): + if self.page_to_be_closed.process or not self.page_to_be_closed.saved: self._set_page(self.page_to_be_closed) #unsaved? ask the user - if not self.page_to_be_closed.get_saved(): + if not self.page_to_be_closed.saved: response = self._save_changes() # return value is either OK, CLOSE, or CANCEL - if response == gtk.RESPONSE_OK: + if response == Gtk.ResponseType.OK: Actions.FLOW_GRAPH_SAVE() #try to save - if not self.page_to_be_closed.get_saved(): #still unsaved? + if not self.page_to_be_closed.saved: #still unsaved? self.page_to_be_closed = None #set the page to be closed back to None return False - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: self.page_to_be_closed = None return False #stop the flow graph if executing - if self.page_to_be_closed.get_proc(): Actions.FLOW_GRAPH_KILL() + if self.page_to_be_closed.process: + Actions.FLOW_GRAPH_KILL() #remove the page self.notebook.remove_page(self.notebook.page_num(self.page_to_be_closed)) if ensure and self.notebook.get_n_pages() == 0: self.new_page() #no pages, make a new one @@ -362,69 +344,49 @@ class MainWindow(gtk.Window): Set the title of the main window. Set the titles on the page tabs. Show/hide the console window. - - Args: - title: the window title """ - gtk.Window.set_title(self, Utils.parse_template(MAIN_WINDOW_TITLE_TMPL, - basename=os.path.basename(self.get_page().get_file_path()), - dirname=os.path.dirname(self.get_page().get_file_path()), - new_flowgraph_title=NEW_FLOGRAPH_TITLE, - read_only=self.get_page().get_read_only(), - saved=self.get_page().get_saved(), - platform_name=self._platform.config.name, - ) - ) - #set tab titles - for page in self.get_pages(): page.set_markup( - Utils.parse_template(PAGE_TITLE_MARKUP_TMPL, - #get filename and strip out file extension - title=os.path.splitext(os.path.basename(page.get_file_path()))[0], - read_only=page.get_read_only(), saved=page.get_saved(), - new_flowgraph_title=NEW_FLOGRAPH_TITLE, - ) - ) - #show/hide notebook tabs + page = self.current_page + + basename = os.path.basename(page.file_path) + dirname = os.path.dirname(page.file_path) + Gtk.Window.set_title(self, ''.join(( + '*' if not page.saved else '', basename if basename else NEW_FLOGRAPH_TITLE, + '(read only)' if page.get_read_only() else '', ' - ', + dirname if dirname else self._platform.config.name, + ))) + # set tab titles + for page in self.get_pages(): + file_name = os.path.splitext(os.path.basename(page.file_path))[0] + page.set_markup('<span foreground="{foreground}">{title}{ro}</span>'.format( + foreground='black' if page.saved else 'red', ro=' (ro)' if page.get_read_only() else '', + title=Utils.encode(file_name or NEW_FLOGRAPH_TITLE), + )) + # show/hide notebook tabs self.notebook.set_show_tabs(len(self.get_pages()) > 1) - # Need to update the variable window when changing - self.vars.update_gui() + # Need to update the variable window when changing + self.vars.update_gui(self.current_flow_graph.blocks) def update_pages(self): """ Forces a reload of all the pages in this notebook. """ for page in self.get_pages(): - success = page.get_flow_graph().reload() + success = page.flow_graph.reload() if success: # Only set saved if errors occurred during import - page.set_saved(False) - - def get_page(self): - """ - Get the selected page. + page.saved = False - Returns: - the selected page - """ - return self.current_page - - def get_flow_graph(self): - """ - Get the selected flow graph. - - Returns: - the selected flow graph - """ - return self.get_page().get_flow_graph() + @property + def current_flow_graph(self): + return self.current_page.flow_graph def get_focus_flag(self): """ Get the focus flag from the current page. - Returns: the focus flag """ - return self.get_page().get_drawing_area().get_focus_flag() + return self.current_page.drawing_area.get_focus_flag() ############################################################ # Helpers @@ -448,14 +410,14 @@ class MainWindow(gtk.Window): the response_id (see buttons variable below) """ buttons = ( - 'Close without saving', gtk.RESPONSE_CLOSE, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK - ) - return MessageDialogHelper( - gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, 'Unsaved Changes!', - 'Would you like to save changes before closing?', gtk.RESPONSE_OK, buttons + 'Close without saving', Gtk.ResponseType.CLOSE, + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK ) + return MessageDialogWrapper( + self, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, 'Unsaved Changes!', + 'Would you like to save changes before closing?', Gtk.ResponseType.OK, buttons + ).run_and_destroy() def _get_files(self): """ @@ -464,7 +426,7 @@ class MainWindow(gtk.Window): Returns: list of file paths """ - return map(lambda page: page.get_file_path(), self.get_pages()) + return [page.file_path for page in self.get_pages()] def get_pages(self): """ @@ -473,4 +435,5 @@ class MainWindow(gtk.Window): Returns: list of pages """ - return [self.notebook.get_nth_page(page_num) for page_num in range(self.notebook.get_n_pages())] + return [self.notebook.get_nth_page(page_num) + for page_num in range(self.notebook.get_n_pages())] diff --git a/grc/gui/Notebook.py b/grc/gui/Notebook.py new file mode 100644 index 0000000000..9f63190b31 --- /dev/null +++ b/grc/gui/Notebook.py @@ -0,0 +1,187 @@ +""" +Copyright 2008, 2009, 2011 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import +import os +import logging + +from gi.repository import Gtk, Gdk, GObject + +from . import Actions +from .StateCache import StateCache +from .Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT +from .DrawingArea import DrawingArea + + +log = logging.getLogger(__name__) + + +class Notebook(Gtk.Notebook): + def __init__(self): + Gtk.Notebook.__init__(self) + log.debug("notebook()") + self.app = Gtk.Application.get_default() + self.current_page = None + + self.set_show_border(False) + self.set_scrollable(True) + self.connect('switch-page', self._handle_page_change) + + self.add_events(Gdk.EventMask.SCROLL_MASK) + self.connect('scroll-event', self._handle_scroll) + self._ignore_consecutive_scrolls = 0 + + def _handle_page_change(self, notebook, page, page_num): + """ + Handle a page change. When the user clicks on a new tab, + reload the flow graph to update the vars window and + call handle states (select nothing) to update the buttons. + + Args: + notebook: the notebook + page: new page + page_num: new page number + """ + self.current_page = self.get_nth_page(page_num) + Actions.PAGE_CHANGE() + + def _handle_scroll(self, widget, event): + # Not sure how to handle this at the moment. + natural = True + # Slow it down + if self._ignore_consecutive_scrolls == 0: + if event.direction in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.LEFT): + if natural: + self.prev_page() + else: + self.next_page() + elif event.direction in (Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.RIGHT): + if natural: + self.next_page() + else: + self.prev_page() + self._ignore_consecutive_scrolls = 3 + else: + self._ignore_consecutive_scrolls -= 1 + return False + + +class Page(Gtk.HBox): + """A page in the notebook.""" + + def __init__(self, main_window, flow_graph, file_path=''): + """ + Page constructor. + + Args: + main_window: main window + file_path: path to a flow graph file + """ + Gtk.HBox.__init__(self) + + self.main_window = main_window + self.flow_graph = flow_graph + self.file_path = file_path + + self.process = None + self.saved = True + + # import the file + initial_state = flow_graph.parent_platform.parse_flow_graph(file_path) + flow_graph.import_data(initial_state) + self.state_cache = StateCache(initial_state) + + # tab box to hold label and close button + self.label = Gtk.Label() + image = Gtk.Image.new_from_icon_name('window-close', Gtk.IconSize.MENU) + image_box = Gtk.HBox(homogeneous=False, spacing=0) + image_box.pack_start(image, True, False, 0) + button = Gtk.Button() + button.connect("clicked", self._handle_button) + button.set_relief(Gtk.ReliefStyle.NONE) + button.add(image_box) + + tab = self.tab = Gtk.HBox(homogeneous=False, spacing=0) + tab.pack_start(self.label, False, False, 0) + tab.pack_start(button, False, False, 0) + tab.show_all() + + # setup scroll window and drawing area + self.drawing_area = DrawingArea(flow_graph) + flow_graph.drawing_area = self.drawing_area + + self.scrolled_window = Gtk.ScrolledWindow() + self.scrolled_window.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) + self.scrolled_window.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS) + self.scrolled_window.connect('key-press-event', self._handle_scroll_window_key_press) + + self.scrolled_window.add(self.drawing_area) + self.pack_start(self.scrolled_window, True, True, 0) + self.show_all() + + def _handle_scroll_window_key_press(self, widget, event): + is_ctrl_pg = ( + event.state & Gdk.ModifierType.CONTROL_MASK and + event.keyval in (Gdk.KEY_Page_Up, Gdk.KEY_Page_Down) + ) + if is_ctrl_pg: + return self.get_parent().event(event) + + def get_generator(self): + """ + Get the generator object for this flow graph. + + Returns: + generator + """ + platform = self.flow_graph.parent_platform + return platform.Generator(self.flow_graph, os.path.dirname(self.file_path)) + + def _handle_button(self, button): + """ + The button was clicked. + Make the current page selected, then close. + + Args: + the: button + """ + self.main_window.page_to_be_closed = self + Actions.FLOW_GRAPH_CLOSE() + + def set_markup(self, markup): + """ + Set the markup in this label. + + Args: + markup: the new markup text + """ + self.label.set_markup(markup) + + def get_read_only(self): + """ + Get the read-only state of the file. + Always false for empty path. + + Returns: + true for read-only + """ + if not self.file_path: + return False + return (os.path.exists(self.file_path) and + not os.access(self.file_path, os.W_OK)) diff --git a/grc/gui/NotebookPage.py b/grc/gui/NotebookPage.py deleted file mode 100644 index 79ad8bf207..0000000000 --- a/grc/gui/NotebookPage.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Copyright 2008, 2009, 2011 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import pygtk -pygtk.require('2.0') -import gtk -import gobject -import Actions -from StateCache import StateCache -from Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT -from DrawingArea import DrawingArea -import os - - -class NotebookPage(gtk.HBox): - """A page in the notebook.""" - - def __init__(self, main_window, flow_graph, file_path=''): - """ - Page constructor. - - Args: - main_window: main window - file_path: path to a flow graph file - """ - self._flow_graph = flow_graph - self.process = None - #import the file - self.main_window = main_window - self.file_path = file_path - initial_state = flow_graph.get_parent().parse_flow_graph(file_path) - self.state_cache = StateCache(initial_state) - self.saved = True - #import the data to the flow graph - self.get_flow_graph().import_data(initial_state) - #initialize page gui - gtk.HBox.__init__(self, False, 0) - self.show() - #tab box to hold label and close button - self.tab = gtk.HBox(False, 0) - #setup tab label - self.label = gtk.Label() - self.tab.pack_start(self.label, False) - #setup button image - image = gtk.Image() - image.set_from_stock('gtk-close', gtk.ICON_SIZE_MENU) - #setup image box - image_box = gtk.HBox(False, 0) - image_box.pack_start(image, True, False, 0) - #setup the button - button = gtk.Button() - button.connect("clicked", self._handle_button) - button.set_relief(gtk.RELIEF_NONE) - button.add(image_box) - #button size - w, h = gtk.icon_size_lookup_for_settings(button.get_settings(), gtk.ICON_SIZE_MENU) - button.set_size_request(w+6, h+6) - self.tab.pack_start(button, False) - self.tab.show_all() - #setup scroll window and drawing area - self.scrolled_window = gtk.ScrolledWindow() - self.scrolled_window.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) - self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - self.scrolled_window.connect('key-press-event', self._handle_scroll_window_key_press) - self.drawing_area = DrawingArea(self.get_flow_graph()) - self.scrolled_window.add_with_viewport(self.get_drawing_area()) - self.pack_start(self.scrolled_window) - #inject drawing area into flow graph - self.get_flow_graph().drawing_area = self.get_drawing_area() - self.show_all() - - def get_drawing_area(self): return self.drawing_area - - def _handle_scroll_window_key_press(self, widget, event): - """forward Ctrl-PgUp/Down to NotebookPage (switch fg instead of horiz. scroll""" - is_ctrl_pg = ( - event.state & gtk.gdk.CONTROL_MASK and - event.keyval in (gtk.keysyms.Page_Up, gtk.keysyms.Page_Down) - ) - if is_ctrl_pg: - return self.get_parent().event(event) - - def get_generator(self): - """ - Get the generator object for this flow graph. - - Returns: - generator - """ - platform = self.get_flow_graph().get_parent() - return platform.Generator(self.get_flow_graph(), self.get_file_path()) - - def _handle_button(self, button): - """ - The button was clicked. - Make the current page selected, then close. - - Args: - the: button - """ - self.main_window.page_to_be_closed = self - Actions.FLOW_GRAPH_CLOSE() - - def set_markup(self, markup): - """ - Set the markup in this label. - - Args: - markup: the new markup text - """ - self.label.set_markup(markup) - - def get_tab(self): - """ - Get the gtk widget for this page's tab. - - Returns: - gtk widget - """ - return self.tab - - def get_proc(self): - """ - Get the subprocess for the flow graph. - - Returns: - the subprocess object - """ - return self.process - - def set_proc(self, process): - """ - Set the subprocess object. - - Args: - process: the new subprocess - """ - self.process = process - - def term_proc(self): - """ - Terminate the subprocess object - - Add a callback to kill the process - after 2 seconds if not already terminated - """ - def kill(process): - """ - Kill process if not already terminated - - Called by gobject.timeout_add - - Returns: - False to stop timeout_add periodic calls - """ - is_terminated = process.poll() - if is_terminated is None: - process.kill() - return False - - self.get_proc().terminate() - gobject.timeout_add(2000, kill, self.get_proc()) - - def get_flow_graph(self): - """ - Get the flow graph. - - Returns: - the flow graph - """ - return self._flow_graph - - def get_read_only(self): - """ - Get the read-only state of the file. - Always false for empty path. - - Returns: - true for read-only - """ - if not self.get_file_path(): return False - return os.path.exists(self.get_file_path()) and \ - not os.access(self.get_file_path(), os.W_OK) - - def get_file_path(self): - """ - Get the file path for the flow graph. - - Returns: - the file path or '' - """ - return self.file_path - - def set_file_path(self, file_path=''): - """ - Set the file path, '' for no file path. - - Args: - file_path: file path string - """ - self.file_path = os.path.abspath(file_path) if file_path else '' - - def get_saved(self): - """ - Get the saved status for the flow graph. - - Returns: - true if saved - """ - return self.saved - - def set_saved(self, saved=True): - """ - Set the saved status. - - Args: - saved: boolean status - """ - self.saved = saved - - def get_state_cache(self): - """ - Get the state cache for the flow graph. - - Returns: - the state cache - """ - return self.state_cache diff --git a/grc/gui/Param.py b/grc/gui/Param.py deleted file mode 100644 index c71e1c0aa5..0000000000 --- a/grc/gui/Param.py +++ /dev/null @@ -1,442 +0,0 @@ -""" -Copyright 2007-2011 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import os - -import pygtk -pygtk.require('2.0') -import gtk - -from . import Colors, Utils, Constants -from .Element import Element -from . import Utils - -from ..core.Param import Param as _Param - - -class InputParam(gtk.HBox): - """The base class for an input parameter inside the input parameters dialog.""" - expand = False - - def __init__(self, param, changed_callback=None, editing_callback=None): - gtk.HBox.__init__(self) - self.param = param - self._changed_callback = changed_callback - self._editing_callback = editing_callback - self.label = gtk.Label() #no label, markup is added by set_markup - self.label.set_size_request(Utils.scale_scalar(150), -1) - self.pack_start(self.label, False) - self.set_markup = lambda m: self.label.set_markup(m) - self.tp = None - self._have_pending_changes = False - #connect events - self.connect('show', self._update_gui) - - def set_color(self, color): - pass - - def set_tooltip_text(self, text): - pass - - def get_text(self): - raise NotImplementedError() - - def _update_gui(self, *args): - """ - Set the markup, color, tooltip, show/hide. - """ - #set the markup - has_cb = \ - hasattr(self.param.get_parent(), 'get_callbacks') and \ - filter(lambda c: self.param.get_key() in c, self.param.get_parent()._callbacks) - self.set_markup(Utils.parse_template(PARAM_LABEL_MARKUP_TMPL, - param=self.param, has_cb=has_cb, - modified=self._have_pending_changes)) - #set the color - self.set_color(self.param.get_color()) - #set the tooltip - self.set_tooltip_text( - Utils.parse_template(TIP_MARKUP_TMPL, param=self.param).strip(), - ) - #show/hide - if self.param.get_hide() == 'all': self.hide_all() - else: self.show_all() - - def _mark_changed(self, *args): - """ - Mark this param as modified on change, but validate only on focus-lost - """ - self._have_pending_changes = True - self._update_gui() - if self._editing_callback: - self._editing_callback(self, None) - - def _apply_change(self, *args): - """ - Handle a gui change by setting the new param value, - calling the callback (if applicable), and updating. - """ - #set the new value - self.param.set_value(self.get_text()) - #call the callback - if self._changed_callback: - self._changed_callback(self, None) - else: - self.param.validate() - #gui update - self._have_pending_changes = False - self._update_gui() - - def _handle_key_press(self, widget, event): - if event.keyval == gtk.keysyms.Return and event.state & gtk.gdk.CONTROL_MASK: - self._apply_change(widget, event) - return True - return False - - def apply_pending_changes(self): - if self._have_pending_changes: - self._apply_change() - - -class EntryParam(InputParam): - """Provide an entry box for strings and numbers.""" - - def __init__(self, *args, **kwargs): - InputParam.__init__(self, *args, **kwargs) - self._input = gtk.Entry() - self._input.set_text(self.param.get_value()) - self._input.connect('changed', self._mark_changed) - self._input.connect('focus-out-event', self._apply_change) - self._input.connect('key-press-event', self._handle_key_press) - self.pack_start(self._input, True) - - def get_text(self): - return self._input.get_text() - - def set_color(self, color): - need_status_color = self.label not in self.get_children() - text_color = ( - Colors.PARAM_ENTRY_TEXT_COLOR if not need_status_color else - gtk.gdk.color_parse('blue') if self._have_pending_changes else - gtk.gdk.color_parse('red') if not self.param.is_valid() else - Colors.PARAM_ENTRY_TEXT_COLOR) - base_color = ( - Colors.BLOCK_DISABLED_COLOR - if need_status_color and not self.param.get_parent().get_enabled() - else gtk.gdk.color_parse(color) - ) - self._input.modify_base(gtk.STATE_NORMAL, base_color) - self._input.modify_text(gtk.STATE_NORMAL, text_color) - - def set_tooltip_text(self, text): - try: - self._input.set_tooltip_text(text) - except AttributeError: - pass # no tooltips for old GTK - - -class MultiLineEntryParam(InputParam): - """Provide an multi-line box for strings.""" - expand = True - - def __init__(self, *args, **kwargs): - InputParam.__init__(self, *args, **kwargs) - self._buffer = gtk.TextBuffer() - self._buffer.set_text(self.param.get_value()) - self._buffer.connect('changed', self._mark_changed) - - self._view = gtk.TextView(self._buffer) - self._view.connect('focus-out-event', self._apply_change) - self._view.connect('key-press-event', self._handle_key_press) - - self._sw = gtk.ScrolledWindow() - self._sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - self._sw.add_with_viewport(self._view) - - self.pack_start(self._sw, True) - - def get_text(self): - buf = self._buffer - return buf.get_text(buf.get_start_iter(), - buf.get_end_iter()).strip() - - def set_color(self, color): - self._view.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) - self._view.modify_text(gtk.STATE_NORMAL, Colors.PARAM_ENTRY_TEXT_COLOR) - - def set_tooltip_text(self, text): - try: - self._view.set_tooltip_text(text) - except AttributeError: - pass # no tooltips for old GTK - - -# try: -# import gtksourceview -# lang_manager = gtksourceview.SourceLanguagesManager() -# py_lang = lang_manager.get_language_from_mime_type('text/x-python') -# -# class PythonEditorParam(InputParam): -# expand = True -# -# def __init__(self, *args, **kwargs): -# InputParam.__init__(self, *args, **kwargs) -# -# buf = self._buffer = gtksourceview.SourceBuffer() -# buf.set_language(py_lang) -# buf.set_highlight(True) -# buf.set_text(self.param.get_value()) -# buf.connect('changed', self._mark_changed) -# -# view = self._view = gtksourceview.SourceView(self._buffer) -# view.connect('focus-out-event', self._apply_change) -# view.connect('key-press-event', self._handle_key_press) -# view.set_tabs_width(4) -# view.set_insert_spaces_instead_of_tabs(True) -# view.set_auto_indent(True) -# view.set_border_width(2) -# -# scroll = gtk.ScrolledWindow() -# scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) -# scroll.add_with_viewport(view) -# self.pack_start(scroll, True) -# -# def get_text(self): -# buf = self._buffer -# return buf.get_text(buf.get_start_iter(), -# buf.get_end_iter()).strip() -# -# except ImportError: -# print "Package 'gtksourceview' not found. No Syntax highlighting." -# PythonEditorParam = MultiLineEntryParam - -class PythonEditorParam(InputParam): - - def __init__(self, *args, **kwargs): - InputParam.__init__(self, *args, **kwargs) - button = self._button = gtk.Button('Open in Editor') - button.connect('clicked', self.open_editor) - self.pack_start(button, True) - - def open_editor(self, widget=None): - flowgraph = self.param.get_parent().get_parent() - flowgraph.install_external_editor(self.param) - - def get_text(self): - pass # we never update the value from here - - def set_color(self, color): - # self._button.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) - self._button.modify_text(gtk.STATE_NORMAL, Colors.PARAM_ENTRY_TEXT_COLOR) - - def _apply_change(self, *args): - pass - - -class EnumParam(InputParam): - """Provide an entry box for Enum types with a drop down menu.""" - - def __init__(self, *args, **kwargs): - InputParam.__init__(self, *args, **kwargs) - self._input = gtk.combo_box_new_text() - for option in self.param.get_options(): self._input.append_text(option.get_name()) - self._input.set_active(self.param.get_option_keys().index(self.param.get_value())) - self._input.connect('changed', self._editing_callback) - self._input.connect('changed', self._apply_change) - self.pack_start(self._input, False) - - def get_text(self): - return self.param.get_option_keys()[self._input.get_active()] - - def set_tooltip_text(self, text): - try: - self._input.set_tooltip_text(text) - except AttributeError: - pass # no tooltips for old GTK - - -class EnumEntryParam(InputParam): - """Provide an entry box and drop down menu for Raw Enum types.""" - - def __init__(self, *args, **kwargs): - InputParam.__init__(self, *args, **kwargs) - self._input = gtk.combo_box_entry_new_text() - for option in self.param.get_options(): self._input.append_text(option.get_name()) - try: self._input.set_active(self.param.get_option_keys().index(self.param.get_value())) - except: - self._input.set_active(-1) - self._input.get_child().set_text(self.param.get_value()) - self._input.connect('changed', self._apply_change) - self._input.get_child().connect('changed', self._mark_changed) - self._input.get_child().connect('focus-out-event', self._apply_change) - self._input.get_child().connect('key-press-event', self._handle_key_press) - self.pack_start(self._input, False) - - def get_text(self): - if self._input.get_active() == -1: return self._input.get_child().get_text() - return self.param.get_option_keys()[self._input.get_active()] - - def set_tooltip_text(self, text): - try: - if self._input.get_active() == -1: #custom entry - self._input.get_child().set_tooltip_text(text) - else: - self._input.set_tooltip_text(text) - except AttributeError: - pass # no tooltips for old GTK - - def set_color(self, color): - if self._input.get_active() == -1: #custom entry, use color - self._input.get_child().modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) - self._input.get_child().modify_text(gtk.STATE_NORMAL, Colors.PARAM_ENTRY_TEXT_COLOR) - else: #from enum, make pale background - self._input.get_child().modify_base(gtk.STATE_NORMAL, Colors.ENTRYENUM_CUSTOM_COLOR) - self._input.get_child().modify_text(gtk.STATE_NORMAL, Colors.PARAM_ENTRY_TEXT_COLOR) - - -class FileParam(EntryParam): - """Provide an entry box for filename and a button to browse for a file.""" - - def __init__(self, *args, **kwargs): - EntryParam.__init__(self, *args, **kwargs) - input = gtk.Button('...') - input.connect('clicked', self._handle_clicked) - self.pack_start(input, False) - - def _handle_clicked(self, widget=None): - """ - If the button was clicked, open a file dialog in open/save format. - Replace the text in the entry with the new filename from the file dialog. - """ - #get the paths - file_path = self.param.is_valid() and self.param.get_evaluated() or '' - (dirname, basename) = os.path.isfile(file_path) and os.path.split(file_path) or (file_path, '') - # check for qss theme default directory - if self.param.get_key() == 'qt_qss_theme': - dirname = os.path.dirname(dirname) # trim filename - if not os.path.exists(dirname): - platform = self.param.get_parent().get_parent().get_parent() - dirname = os.path.join(platform.config.install_prefix, - '/share/gnuradio/themes') - if not os.path.exists(dirname): - dirname = os.getcwd() # fix bad paths - - #build the dialog - if self.param.get_type() == 'file_open': - file_dialog = gtk.FileChooserDialog('Open a Data File...', None, - gtk.FILE_CHOOSER_ACTION_OPEN, ('gtk-cancel',gtk.RESPONSE_CANCEL,'gtk-open',gtk.RESPONSE_OK)) - elif self.param.get_type() == 'file_save': - file_dialog = gtk.FileChooserDialog('Save a Data File...', None, - gtk.FILE_CHOOSER_ACTION_SAVE, ('gtk-cancel',gtk.RESPONSE_CANCEL, 'gtk-save',gtk.RESPONSE_OK)) - file_dialog.set_do_overwrite_confirmation(True) - file_dialog.set_current_name(basename) #show the current filename - else: - raise ValueError("Can't open file chooser dialog for type " + repr(self.param.get_type())) - file_dialog.set_current_folder(dirname) #current directory - file_dialog.set_select_multiple(False) - file_dialog.set_local_only(True) - if gtk.RESPONSE_OK == file_dialog.run(): #run the dialog - file_path = file_dialog.get_filename() #get the file path - self._input.set_text(file_path) - self._editing_callback() - self._apply_change() - file_dialog.destroy() #destroy the dialog - - -PARAM_MARKUP_TMPL="""\ -#set $foreground = $param.is_valid() and 'black' or 'red' -<span foreground="$foreground" font_desc="$font"><b>$encode($param.get_name()): </b>$encode(repr($param).replace('\\n',' '))</span>""" - -PARAM_LABEL_MARKUP_TMPL="""\ -#set $foreground = $modified and 'blue' or $param.is_valid() and 'black' or 'red' -#set $underline = $has_cb and 'low' or 'none' -<span underline="$underline" foreground="$foreground" font_desc="Sans 9">$encode($param.get_name())</span>""" - -TIP_MARKUP_TMPL="""\ -######################################## -#def truncate(string) - #set $max_len = 100 - #set $string = str($string) - #if len($string) > $max_len -$('%s...%s'%($string[:$max_len/2], $string[-$max_len/2:]))#slurp - #else -$string#slurp - #end if -#end def -######################################## -Key: $param.get_key() -Type: $param.get_type() -#if $param.is_valid() -Value: $truncate($param.get_evaluated()) -#elif len($param.get_error_messages()) == 1 -Error: $(param.get_error_messages()[0]) -#else -Error: - #for $error_msg in $param.get_error_messages() - * $error_msg - #end for -#end if""" - - -class Param(Element, _Param): - """The graphical parameter.""" - - def __init__(self, **kwargs): - Element.__init__(self) - _Param.__init__(self, **kwargs) - - def get_input(self, *args, **kwargs): - """ - Get the graphical gtk class to represent this parameter. - An enum requires and combo parameter. - A non-enum with options gets a combined entry/combo parameter. - All others get a standard entry parameter. - - Returns: - gtk input class - """ - if self.get_type() in ('file_open', 'file_save'): - input_widget = FileParam(self, *args, **kwargs) - - elif self.is_enum(): - input_widget = EnumParam(self, *args, **kwargs) - - elif self.get_options(): - input_widget = EnumEntryParam(self, *args, **kwargs) - - elif self.get_type() == '_multiline': - input_widget = MultiLineEntryParam(self, *args, **kwargs) - - elif self.get_type() == '_multiline_python_external': - input_widget = PythonEditorParam(self, *args, **kwargs) - - else: - input_widget = EntryParam(self, *args, **kwargs) - - return input_widget - - def get_markup(self): - """ - Get the markup for this param. - - Returns: - a pango markup string - """ - return Utils.parse_template(PARAM_MARKUP_TMPL, - param=self, font=Constants.PARAM_FONT) diff --git a/grc/gui/ParamWidgets.py b/grc/gui/ParamWidgets.py new file mode 100644 index 0000000000..747c3ffec5 --- /dev/null +++ b/grc/gui/ParamWidgets.py @@ -0,0 +1,330 @@ +# Copyright 2007-2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion 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 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +from __future__ import absolute_import +import os + +from gi.repository import Gtk, Gdk + +from . import Utils + + +style_provider = Gtk.CssProvider() + +style_provider.load_from_data(b""" + #dtype_complex { background-color: #3399FF; } + #dtype_real { background-color: #FF8C69; } + #dtype_float { background-color: #FF8C69; } + #dtype_int { background-color: #00FF99; } + + #dtype_complex_vector { background-color: #3399AA; } + #dtype_real_vector { background-color: #CC8C69; } + #dtype_float_vector { background-color: #CC8C69; } + #dtype_int_vector { background-color: #00CC99; } + + #dtype_bool { background-color: #00FF99; } + #dtype_hex { background-color: #00FF99; } + #dtype_string { background-color: #CC66CC; } + #dtype_id { background-color: #DDDDDD; } + #dtype_stream_id { background-color: #DDDDDD; } + #dtype_raw { background-color: #FFFFFF; } + + #enum_custom { background-color: #EEEEEE; } +""") + +Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION +) + + +class InputParam(Gtk.HBox): + """The base class for an input parameter inside the input parameters dialog.""" + expand = False + + def __init__(self, param, changed_callback=None, editing_callback=None, transient_for=None): + Gtk.HBox.__init__(self) + + self.param = param + self._changed_callback = changed_callback + self._editing_callback = editing_callback + self._transient_for = transient_for + + self.label = Gtk.Label() + self.label.set_size_request(Utils.scale_scalar(150), -1) + self.label.show() + self.pack_start(self.label, False, False, 0) + + self.tp = None + self._have_pending_changes = False + + self.connect('show', self._update_gui) + + def set_color(self, css_name): + pass + + def set_tooltip_text(self, text): + pass + + def get_text(self): + raise NotImplementedError() + + def _update_gui(self, *args): + """ + Set the markup, color, tooltip, show/hide. + """ + self.label.set_markup(self.param.format_label_markup(self._have_pending_changes)) + self.set_color('dtype_' + self.param.dtype) + + self.set_tooltip_text(self.param.format_tooltip_text()) + + if self.param.hide == 'all': + self.hide() + else: + self.show_all() + + def _mark_changed(self, *args): + """ + Mark this param as modified on change, but validate only on focus-lost + """ + self._have_pending_changes = True + self._update_gui() + if self._editing_callback: + self._editing_callback(self, None) + + def _apply_change(self, *args): + """ + Handle a gui change by setting the new param value, + calling the callback (if applicable), and updating. + """ + #set the new value + self.param.set_value(self.get_text()) + #call the callback + if self._changed_callback: + self._changed_callback(self, None) + else: + self.param.validate() + #gui update + self._have_pending_changes = False + self._update_gui() + + def _handle_key_press(self, widget, event): + if event.keyval == Gdk.KEY_Return and event.get_state() & Gdk.ModifierType.CONTROL_MASK: + self._apply_change(widget, event) + return True + return False + + def apply_pending_changes(self): + if self._have_pending_changes: + self._apply_change() + + +class EntryParam(InputParam): + """Provide an entry box for strings and numbers.""" + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + self._input = Gtk.Entry() + self._input.set_text(self.param.get_value()) + self._input.connect('changed', self._mark_changed) + self._input.connect('focus-out-event', self._apply_change) + self._input.connect('key-press-event', self._handle_key_press) + self.pack_start(self._input, True, True, 0) + + def get_text(self): + return self._input.get_text() + + def set_color(self, css_name): + self._input.set_name(css_name) + + def set_tooltip_text(self, text): + self._input.set_tooltip_text(text) + + +class MultiLineEntryParam(InputParam): + """Provide an multi-line box for strings.""" + expand = True + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + self._buffer = Gtk.TextBuffer() + self._buffer.set_text(self.param.get_value()) + self._buffer.connect('changed', self._mark_changed) + + self._view = Gtk.TextView() + self._view.set_buffer(self._buffer) + self._view.connect('focus-out-event', self._apply_change) + self._view.connect('key-press-event', self._handle_key_press) + + self._sw = Gtk.ScrolledWindow() + self._sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._sw.set_shadow_type(type=Gtk.ShadowType.IN) + self._sw.add(self._view) + + self.pack_start(self._sw, True, True, True) + + def get_text(self): + buf = self._buffer + text = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), + include_hidden_chars=False) + return text.strip() + + def set_color(self, css_name): + self._view.set_name(css_name) + + def set_tooltip_text(self, text): + self._view.set_tooltip_text(text) + + +class PythonEditorParam(InputParam): + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + button = self._button = Gtk.Button(label='Open in Editor') + button.connect('clicked', self.open_editor) + self.pack_start(button, True, True, True) + + def open_editor(self, widget=None): + self.param.parent_flowgraph.install_external_editor(self.param, parent=self._transient_for) + + def get_text(self): + pass # we never update the value from here + + def _apply_change(self, *args): + pass + + +class EnumParam(InputParam): + """Provide an entry box for Enum types with a drop down menu.""" + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + self._input = Gtk.ComboBoxText() + for option_name in self.param.options.values(): + self._input.append_text(option_name) + + self.param_values = list(self.param.options) + self._input.set_active(self.param_values.index(self.param.get_value())) + self._input.connect('changed', self._editing_callback) + self._input.connect('changed', self._apply_change) + self.pack_start(self._input, False, False, 0) + + def get_text(self): + return self.param_values[self._input.get_active()] + + def set_tooltip_text(self, text): + self._input.set_tooltip_text(text) + + +class EnumEntryParam(InputParam): + """Provide an entry box and drop down menu for Raw Enum types.""" + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + self._input = Gtk.ComboBoxText.new_with_entry() + for option_name in self.param.options.values(): + self._input.append_text(option_name) + + self.param_values = list(self.param.options) + value = self.param.get_value() + try: + self._input.set_active(self.param_values.index(value)) + except ValueError: + self._input.set_active(-1) + self._input.get_child().set_text(value) + + self._input.connect('changed', self._apply_change) + self._input.get_child().connect('changed', self._mark_changed) + self._input.get_child().connect('focus-out-event', self._apply_change) + self._input.get_child().connect('key-press-event', self._handle_key_press) + self.pack_start(self._input, False, False, 0) + + @property + def has_custom_value(self): + return self._input.get_active() == -1 + + def get_text(self): + if self.has_custom_value: + return self._input.get_child().get_text() + else: + return self.param_values[self._input.get_active()] + + def set_tooltip_text(self, text): + if self.has_custom_value: # custom entry + self._input.get_child().set_tooltip_text(text) + else: + self._input.set_tooltip_text(text) + + def set_color(self, css_name): + self._input.get_child().set_name( + css_name if not self.has_custom_value else 'enum_custom' + ) + + +class FileParam(EntryParam): + """Provide an entry box for filename and a button to browse for a file.""" + + def __init__(self, *args, **kwargs): + EntryParam.__init__(self, *args, **kwargs) + self._open_button = Gtk.Button(label='...') + self._open_button.connect('clicked', self._handle_clicked) + self.pack_start(self._open_button, False, False, 0) + + def _handle_clicked(self, widget=None): + """ + If the button was clicked, open a file dialog in open/save format. + Replace the text in the entry with the new filename from the file dialog. + """ + # get the paths + file_path = self.param.is_valid() and self.param.get_evaluated() or '' + (dirname, basename) = os.path.isfile(file_path) and os.path.split(file_path) or (file_path, '') + # check for qss theme default directory + if self.param.key == 'qt_qss_theme': + dirname = os.path.dirname(dirname) # trim filename + if not os.path.exists(dirname): + config = self.param.parent_platform.config + dirname = os.path.join(config.install_prefix, '/share/gnuradio/themes') + if not os.path.exists(dirname): + dirname = os.getcwd() # fix bad paths + + # build the dialog + if self.param.dtype == 'file_open': + file_dialog = Gtk.FileChooserDialog( + 'Open a Data File...', None, Gtk.FileChooserAction.OPEN, + ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-open', Gtk.ResponseType.OK), + transient_for=self._transient_for, + ) + elif self.param.dtype == 'file_save': + file_dialog = Gtk.FileChooserDialog( + 'Save a Data File...', None, Gtk.FileChooserAction.SAVE, + ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-save', Gtk.ResponseType.OK), + transient_for=self._transient_for, + ) + file_dialog.set_do_overwrite_confirmation(True) + file_dialog.set_current_name(basename) # show the current filename + else: + raise ValueError("Can't open file chooser dialog for type " + repr(self.param.dtype)) + file_dialog.set_current_folder(dirname) # current directory + file_dialog.set_select_multiple(False) + file_dialog.set_local_only(True) + if Gtk.ResponseType.OK == file_dialog.run(): # run the dialog + file_path = file_dialog.get_filename() # get the file path + self._input.set_text(file_path) + self._editing_callback() + self._apply_change() + file_dialog.destroy() # destroy the dialog diff --git a/grc/gui/ParserErrorsDialog.py b/grc/gui/ParserErrorsDialog.py index 68ee459414..050b9a4f98 100644 --- a/grc/gui/ParserErrorsDialog.py +++ b/grc/gui/ParserErrorsDialog.py @@ -17,14 +17,16 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk +from __future__ import absolute_import -from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT +import six +from gi.repository import Gtk, GObject -class ParserErrorsDialog(gtk.Dialog): +from .Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT + + +class ParserErrorsDialog(Gtk.Dialog): """ A dialog for viewing parser errors """ @@ -36,32 +38,32 @@ class ParserErrorsDialog(gtk.Dialog): Args: block: a block instance """ - gtk.Dialog.__init__(self, title='Parser Errors', buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) + GObject.GObject.__init__(self, title='Parser Errors', buttons=(Gtk.STOCK_CLOSE, Gtk.ResponseType.ACCEPT)) self._error_logs = None - self.tree_store = gtk.TreeStore(str) + self.tree_store = Gtk.TreeStore(str) self.update_tree_store(error_logs) - column = gtk.TreeViewColumn('XML Parser Errors by Filename') - renderer = gtk.CellRendererText() + column = Gtk.TreeViewColumn('XML Parser Errors by Filename') + renderer = Gtk.CellRendererText() column.pack_start(renderer, True) column.add_attribute(renderer, 'text', 0) column.set_sort_column_id(0) - self.tree_view = tree_view = gtk.TreeView(self.tree_store) + self.tree_view = tree_view = Gtk.TreeView(self.tree_store) tree_view.set_enable_search(False) # disable pop up search box tree_view.set_search_column(-1) # really disable search tree_view.set_reorderable(False) tree_view.set_headers_visible(False) - tree_view.get_selection().set_mode(gtk.SELECTION_NONE) + tree_view.get_selection().set_mode(Gtk.SelectionMode.NONE) tree_view.append_column(column) for row in self.tree_store: tree_view.expand_row(row.path, False) - scrolled_window = gtk.ScrolledWindow() - scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrolled_window.add_with_viewport(tree_view) + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.add(tree_view) self.vbox.pack_start(scrolled_window, True) self.set_size_request(2*MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT) @@ -71,7 +73,7 @@ class ParserErrorsDialog(gtk.Dialog): """set up data model""" self.tree_store.clear() self._error_logs = error_logs - for filename, errors in error_logs.iteritems(): + for filename, errors in six.iteritems(error_logs): parent = self.tree_store.append(None, [str(filename)]) try: with open(filename, 'r') as fp: @@ -95,6 +97,6 @@ class ParserErrorsDialog(gtk.Dialog): Returns: true if the response was accept """ - response = gtk.Dialog.run(self) + response = Gtk.Dialog.run(self) self.destroy() - return response == gtk.RESPONSE_ACCEPT + return response == Gtk.ResponseType.ACCEPT diff --git a/grc/gui/Platform.py b/grc/gui/Platform.py index 500df1cce4..8eb79f3459 100644 --- a/grc/gui/Platform.py +++ b/grc/gui/Platform.py @@ -17,25 +17,21 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import os -import sys +from __future__ import absolute_import, print_function -from ..core.Platform import Platform as _Platform +import sys +import os -from .Config import Config as _Config -from .Block import Block as _Block -from .Connection import Connection as _Connection -from .Element import Element -from .FlowGraph import FlowGraph as _FlowGraph -from .Param import Param as _Param -from .Port import Port as _Port +from .Config import Config +from . import canvas +from ..core.platform import Platform as CorePlatform +from ..core.utils.backports import ChainMap -class Platform(Element, _Platform): +class Platform(CorePlatform): def __init__(self, *args, **kwargs): - Element.__init__(self) - _Platform.__init__(self, *args, **kwargs) + CorePlatform.__init__(self, *args, **kwargs) # Ensure conf directories gui_prefs_file = self.config.gui_prefs_file @@ -58,14 +54,24 @@ class Platform(Element, _Platform): import shutil shutil.move(old_gui_prefs_file, gui_prefs_file) except Exception as e: - print >> sys.stderr, e + print(e, file=sys.stderr) ############################################## - # Constructors + # Factories ############################################## - FlowGraph = _FlowGraph - Connection = _Connection - Block = _Block - Port = _Port - Param = _Param - Config = _Config + Config = Config + FlowGraph = canvas.FlowGraph + Connection = canvas.Connection + + def new_block_class(self, **data): + cls = CorePlatform.new_block_class(self, **data) + return canvas.Block.make_cls_with_base(cls) + + block_classes_build_in = {key: canvas.Block.make_cls_with_base(cls) + for key, cls in CorePlatform.block_classes_build_in.items()} + block_classes = ChainMap({}, block_classes_build_in) + + port_classes = {key: canvas.Port.make_cls_with_base(cls) + for key, cls in CorePlatform.port_classes.items()} + param_classes = {key: canvas.Param.make_cls_with_base(cls) + for key, cls in CorePlatform.param_classes.items()} diff --git a/grc/gui/Port.py b/grc/gui/Port.py deleted file mode 100644 index 690b1087e3..0000000000 --- a/grc/gui/Port.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -Copyright 2007, 2008, 2009 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import pygtk -pygtk.require('2.0') -import gtk - -from . import Actions, Colors, Utils -from .Constants import ( - PORT_SEPARATION, PORT_SPACING, CONNECTOR_EXTENSION_MINIMAL, - CONNECTOR_EXTENSION_INCREMENT, PORT_LABEL_PADDING, PORT_MIN_WIDTH, PORT_LABEL_HIDDEN_WIDTH, PORT_FONT -) -from .Element import Element -from ..core.Constants import DEFAULT_DOMAIN, GR_MESSAGE_DOMAIN - -from ..core.Port import Port as _Port - -PORT_MARKUP_TMPL="""\ -<span foreground="black" font_desc="$font">$encode($port.get_name())</span>""" - - -class Port(_Port, Element): - """The graphical port.""" - - def __init__(self, block, n, dir): - """ - Port constructor. - Create list of connector coordinates. - """ - _Port.__init__(self, block, n, dir) - Element.__init__(self) - self.W = self.H = self.w = self.h = 0 - self._connector_coordinate = (0, 0) - self._connector_length = 0 - self._hovering = True - self._force_label_unhidden = False - - def create_shapes(self): - """Create new areas and labels for the port.""" - Element.create_shapes(self) - if self.get_hide(): - return # this port is hidden, no need to create shapes - if self.get_domain() == GR_MESSAGE_DOMAIN: - pass - elif self.get_domain() != DEFAULT_DOMAIN: - self.line_attributes[0] = 2 - #get current rotation - rotation = self.get_rotation() - #get all sibling ports - ports = self.get_parent().get_sources_gui() \ - if self.is_source else self.get_parent().get_sinks_gui() - ports = filter(lambda p: not p.get_hide(), ports) - #get the max width - self.W = max([port.W for port in ports] + [PORT_MIN_WIDTH]) - W = self.W if not self._label_hidden() else PORT_LABEL_HIDDEN_WIDTH - #get a numeric index for this port relative to its sibling ports - try: - index = ports.index(self) - except: - if hasattr(self, '_connector_length'): - del self._connector_length - return - length = len(filter(lambda p: not p.get_hide(), ports)) - #reverse the order of ports for these rotations - if rotation in (180, 270): - index = length-index-1 - - port_separation = PORT_SEPARATION \ - if not self.get_parent().has_busses[self.is_source] \ - else max([port.H for port in ports]) + PORT_SPACING - - offset = (self.get_parent().H - (length-1)*port_separation - self.H)/2 - #create areas and connector coordinates - if (self.is_sink and rotation == 0) or (self.is_source and rotation == 180): - x = -W - y = port_separation*index+offset - self.add_area((x, y), (W, self.H)) - self._connector_coordinate = (x-1, y+self.H/2) - elif (self.is_source and rotation == 0) or (self.is_sink and rotation == 180): - x = self.get_parent().W - y = port_separation*index+offset - self.add_area((x, y), (W, self.H)) - self._connector_coordinate = (x+1+W, y+self.H/2) - elif (self.is_source and rotation == 90) or (self.is_sink and rotation == 270): - y = -W - x = port_separation*index+offset - self.add_area((x, y), (self.H, W)) - self._connector_coordinate = (x+self.H/2, y-1) - elif (self.is_sink and rotation == 90) or (self.is_source and rotation == 270): - y = self.get_parent().W - x = port_separation*index+offset - self.add_area((x, y), (self.H, W)) - self._connector_coordinate = (x+self.H/2, y+1+W) - #the connector length - self._connector_length = CONNECTOR_EXTENSION_MINIMAL + CONNECTOR_EXTENSION_INCREMENT*index - - def create_labels(self): - """Create the labels for the socket.""" - Element.create_labels(self) - self._bg_color = Colors.get_color(self.get_color()) - # create the layout - layout = gtk.DrawingArea().create_pango_layout('') - layout.set_markup(Utils.parse_template(PORT_MARKUP_TMPL, port=self, font=PORT_FONT)) - self.w, self.h = layout.get_pixel_size() - self.W = 2 * PORT_LABEL_PADDING + self.w - self.H = 2 * PORT_LABEL_PADDING + self.h * ( - 3 if self.get_type() == 'bus' else 1) - self.H += self.H % 2 - # create the pixmap - pixmap = self.get_parent().get_parent().new_pixmap(self.w, self.h) - gc = pixmap.new_gc() - gc.set_foreground(self._bg_color) - pixmap.draw_rectangle(gc, True, 0, 0, self.w, self.h) - pixmap.draw_layout(gc, 0, 0, layout) - # create vertical and horizontal pixmaps - self.horizontal_label = pixmap - if self.is_vertical(): - self.vertical_label = self.get_parent().get_parent().new_pixmap(self.h, self.w) - Utils.rotate_pixmap(gc, self.horizontal_label, self.vertical_label) - - def draw(self, gc, window): - """ - Draw the socket with a label. - - Args: - gc: the graphics context - window: the gtk window to draw on - """ - Element.draw( - self, gc, window, bg_color=self._bg_color, - border_color=self.is_highlighted() and Colors.HIGHLIGHT_COLOR or - self.get_parent().is_dummy_block and Colors.MISSING_BLOCK_BORDER_COLOR or - Colors.BORDER_COLOR, - ) - if not self._areas_list or self._label_hidden(): - return # this port is either hidden (no areas) or folded (no label) - X, Y = self.get_coordinate() - (x, y), (w, h) = self._areas_list[0] # use the first area's sizes to place the labels - if self.is_horizontal(): - window.draw_drawable(gc, self.horizontal_label, 0, 0, x+X+(self.W-self.w)/2, y+Y+(self.H-self.h)/2, -1, -1) - elif self.is_vertical(): - window.draw_drawable(gc, self.vertical_label, 0, 0, x+X+(self.H-self.h)/2, y+Y+(self.W-self.w)/2, -1, -1) - - def get_connector_coordinate(self): - """ - Get the coordinate where connections may attach to. - - Returns: - the connector coordinate (x, y) tuple - """ - x, y = self._connector_coordinate - X, Y = self.get_coordinate() - return (x + X, y + Y) - - def get_connector_direction(self): - """ - Get the direction that the socket points: 0,90,180,270. - This is the rotation degree if the socket is an output or - the rotation degree + 180 if the socket is an input. - - Returns: - the direction in degrees - """ - if self.is_source: return self.get_rotation() - elif self.is_sink: return (self.get_rotation() + 180)%360 - - def get_connector_length(self): - """ - Get the length of the connector. - The connector length increases as the port index changes. - - Returns: - the length in pixels - """ - return self._connector_length - - def get_rotation(self): - """ - Get the parent's rotation rather than self. - - Returns: - the parent's rotation - """ - return self.get_parent().get_rotation() - - def move(self, delta_coor): - """ - Move the parent rather than self. - - Args: - delta_corr: the (delta_x, delta_y) tuple - """ - self.get_parent().move(delta_coor) - - def rotate(self, direction): - """ - Rotate the parent rather than self. - - Args: - direction: degrees to rotate - """ - self.get_parent().rotate(direction) - - def get_coordinate(self): - """ - Get the parent's coordinate rather than self. - - Returns: - the parents coordinate - """ - return self.get_parent().get_coordinate() - - def set_highlighted(self, highlight): - """ - Set the parent highlight rather than self. - - Args: - highlight: true to enable highlighting - """ - self.get_parent().set_highlighted(highlight) - - def is_highlighted(self): - """ - Get the parent's is highlight rather than self. - - Returns: - the parent's highlighting status - """ - return self.get_parent().is_highlighted() - - def _label_hidden(self): - """ - Figure out if the label should be hidden - - Returns: - true if the label should not be shown - """ - return self._hovering and not self._force_label_unhidden and Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() - - def force_label_unhidden(self, enable=True): - """ - Disable showing the label on mouse-over for this port - - Args: - enable: true to override the mouse-over behaviour - """ - self._force_label_unhidden = enable - - def mouse_over(self): - """ - Called from flow graph on mouse-over - """ - self._hovering = False - return Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() # only redraw if necessary - - def mouse_out(self): - """ - Called from flow graph on mouse-out - """ - self._hovering = True - return Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() # only redraw if necessary diff --git a/grc/gui/Preferences.py b/grc/gui/Preferences.py deleted file mode 100644 index d377018eb4..0000000000 --- a/grc/gui/Preferences.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Copyright 2008 Free Software Foundation, Inc. -This file is part of GNU Radio - -GNU Radio Companion 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 2 -of the License, or (at your option) any later version. - -GNU Radio Companion 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -""" - -import os -import sys -import ConfigParser - - -HEADER = """\ -# This contains only GUI settings for GRC and is not meant for users to edit. -# -# GRC settings not accessible through the GUI are in gnuradio.conf under -# section [grc]. - -""" - -_platform = None -_config_parser = ConfigParser.SafeConfigParser() - - -def file_extension(): - return '.grc' - - -def load(platform): - global _platform - _platform = platform - # create sections - for section in ['main', 'files_open', 'files_recent']: - try: - _config_parser.add_section(section) - except Exception, e: - print e - try: - _config_parser.read(_platform.get_prefs_file()) - except Exception as err: - print >> sys.stderr, err - - -def save(): - try: - with open(_platform.get_prefs_file(), 'w') as fp: - fp.write(HEADER) - _config_parser.write(fp) - except Exception as err: - print >> sys.stderr, err - - -def entry(key, value=None, default=None): - if value is not None: - _config_parser.set('main', key, str(value)) - result = value - else: - _type = type(default) if default is not None else str - getter = { - bool: _config_parser.getboolean, - int: _config_parser.getint, - }.get(_type, _config_parser.get) - try: - result = getter('main', key) - except (AttributeError, ConfigParser.Error): - result = _type() if default is None else default - return result - - -########################################################################### -# Special methods for specific program functionalities -########################################################################### - -def main_window_size(size=None): - if size is None: - size = [None, None] - w = entry('main_window_width', size[0], default=1) - h = entry('main_window_height', size[1], default=1) - return w, h - - -def file_open(filename=None): - return entry('file_open', filename, default='') - - -def set_file_list(key, files): - _config_parser.remove_section(key) # clear section - _config_parser.add_section(key) - for i, filename in enumerate(files): - _config_parser.set(key, '%s_%d' % (key, i), filename) - - -def get_file_list(key): - try: - files = [value for name, value in _config_parser.items(key) - if name.startswith('%s_' % key)] - except (AttributeError, ConfigParser.Error): - files = [] - return files - - -def get_open_files(): - return get_file_list('files_open') - - -def set_open_files(files): - return set_file_list('files_open', files) - - -def get_recent_files(): - """ Gets recent files, removes any that do not exist and re-saves it """ - files = filter(os.path.exists, get_file_list('files_recent')) - set_recent_files(files) - return files - - -def set_recent_files(files): - return set_file_list('files_recent', files) - - -def add_recent_file(file_name): - # double check file_name - if os.path.exists(file_name): - recent_files = get_recent_files() - if file_name in recent_files: - recent_files.remove(file_name) # Attempt removal - recent_files.insert(0, file_name) # Insert at start - set_recent_files(recent_files[:10]) # Keep up to 10 files - - -def console_window_position(pos=None): - return entry('console_window_position', pos, default=-1) or 1 - - -def blocks_window_position(pos=None): - return entry('blocks_window_position', pos, default=-1) or 1 - - -def variable_editor_position(pos=None, sidebar=False): - # Figure out default - if sidebar: - w, h = main_window_size() - return entry('variable_editor_sidebar_position', pos, default=int(h*0.7)) - else: - return entry('variable_editor_position', pos, default=int(blocks_window_position()*0.5)) - - -def variable_editor_sidebar(pos=None): - return entry('variable_editor_sidebar', pos, default=False) - - -def variable_editor_confirm_delete(pos=None): - return entry('variable_editor_confirm_delete', pos, default=True) - - -def xterm_missing(cmd=None): - return entry('xterm_missing', cmd, default='INVALID_XTERM_SETTING') - - -def screen_shot_background_transparent(transparent=None): - return entry('screen_shot_background_transparent', transparent, default=False) diff --git a/grc/gui/PropsDialog.py b/grc/gui/PropsDialog.py index a5b46cbbac..ac4506a3d8 100644 --- a/grc/gui/PropsDialog.py +++ b/grc/gui/PropsDialog.py @@ -17,116 +17,91 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk +from __future__ import absolute_import +from gi.repository import Gtk, Gdk, GObject, Pango -import Actions -from Dialogs import SimpleTextDisplay -from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT, FONT_SIZE -import Utils -import pango +from . import Actions, Utils, Constants +from .Dialogs import SimpleTextDisplay +import six -TAB_LABEL_MARKUP_TMPL="""\ -#set $foreground = $valid and 'black' or 'red' -<span foreground="$foreground">$encode($tab)</span>""" - -def get_title_label(title): - """ - Get a title label for the params window. - The title will be bold, underlined, and left justified. - - Args: - title: the text of the title - - Returns: - a gtk object - """ - label = gtk.Label() - label.set_markup('\n<b><span underline="low">%s</span>:</b>\n'%title) - hbox = gtk.HBox() - hbox.pack_start(label, False, False, padding=11) - return hbox - - -class PropsDialog(gtk.Dialog): +class PropsDialog(Gtk.Dialog): """ A dialog to set block parameters, view errors, and view documentation. """ - def __init__(self, block): + def __init__(self, parent, block): """ Properties dialog constructor. - Args: + Args:% block: a block instance """ - self._hash = 0 - gtk.Dialog.__init__( + Gtk.Dialog.__init__( self, - title='Properties: %s' % block.get_name(), - buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, - gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, - gtk.STOCK_APPLY, gtk.RESPONSE_APPLY) + title='Properties: ' + block.label, + transient_for=parent, + modal=True, + destroy_with_parent=True, + ) + self.add_buttons( + Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT, + Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, + Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY, ) - self.set_response_sensitive(gtk.RESPONSE_APPLY, False) + self.set_response_sensitive(Gtk.ResponseType.APPLY, False) self.set_size_request(*Utils.scale( - (MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT) + (Constants.MIN_DIALOG_WIDTH, Constants.MIN_DIALOG_HEIGHT) )) + self._block = block + self._hash = 0 - vpaned = gtk.VPaned() - self.vbox.pack_start(vpaned) + vpaned = Gtk.VPaned() + self.vbox.pack_start(vpaned, True, True, 0) # Notebook to hold param boxes - notebook = gtk.Notebook() + notebook = self.notebook = Gtk.Notebook() notebook.set_show_border(False) notebook.set_scrollable(True) # scroll arrows for page tabs - notebook.set_tab_pos(gtk.POS_TOP) + notebook.set_tab_pos(Gtk.PositionType.TOP) vpaned.pack1(notebook, True) # Params boxes for block parameters - self._params_boxes = list() - for tab in block.get_param_tab_labels(): - label = gtk.Label() - vbox = gtk.VBox() - scroll_box = gtk.ScrolledWindow() - scroll_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scroll_box.add_with_viewport(vbox) - notebook.append_page(scroll_box, label) - self._params_boxes.append((tab, label, vbox)) + self._params_boxes = [] + self._build_param_tab_boxes() # Docs for the block self._docs_text_display = doc_view = SimpleTextDisplay() - doc_view.get_buffer().create_tag('b', weight=pango.WEIGHT_BOLD) - self._docs_box = gtk.ScrolledWindow() - self._docs_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - self._docs_box.add_with_viewport(self._docs_text_display) - notebook.append_page(self._docs_box, gtk.Label("Documentation")) + doc_view.get_buffer().create_tag('b', weight=Pango.Weight.BOLD) + self._docs_box = Gtk.ScrolledWindow() + self._docs_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._docs_box.add(self._docs_text_display) + notebook.append_page(self._docs_box, Gtk.Label(label="Documentation")) # Generated code for the block if Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB.get_active(): self._code_text_display = code_view = SimpleTextDisplay() - code_view.set_wrap_mode(gtk.WRAP_NONE) - code_view.get_buffer().create_tag('b', weight=pango.WEIGHT_BOLD) - code_view.modify_font(pango.FontDescription( - 'monospace %d' % FONT_SIZE)) - code_box = gtk.ScrolledWindow() - code_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - code_box.add_with_viewport(self._code_text_display) - notebook.append_page(code_box, gtk.Label("Generated Code")) + code_view.set_wrap_mode(Gtk.WrapMode.NONE) + code_view.get_buffer().create_tag('b', weight=Pango.Weight.BOLD) + code_view.set_monospace(True) + # todo: set font size in non-deprecated way + # code_view.override_font(Pango.FontDescription('monospace %d' % Constants.FONT_SIZE)) + code_box = Gtk.ScrolledWindow() + code_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + code_box.add(self._code_text_display) + notebook.append_page(code_box, Gtk.Label(label="Generated Code")) else: self._code_text_display = None # Error Messages for the block self._error_messages_text_display = SimpleTextDisplay() - self._error_box = gtk.ScrolledWindow() - self._error_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - self._error_box.add_with_viewport(self._error_messages_text_display) + self._error_box = Gtk.ScrolledWindow() + self._error_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self._error_box.add(self._error_messages_text_display) vpaned.pack2(self._error_box) - vpaned.set_position(int(0.65 * MIN_DIALOG_HEIGHT)) + vpaned.set_position(int(0.65 * Constants.MIN_DIALOG_HEIGHT)) # Connect events self.connect('key-press-event', self._handle_key_press) @@ -134,6 +109,27 @@ class PropsDialog(gtk.Dialog): self.connect('response', self._handle_response) self.show_all() # show all (performs initial gui update) + def _build_param_tab_boxes(self): + categories = (p.category for p in self._block.params.values()) + + def unique_categories(): + seen = {Constants.DEFAULT_PARAM_TAB} + yield Constants.DEFAULT_PARAM_TAB + for cat in categories: + if cat in seen: + continue + yield cat + seen.add(cat) + + for category in unique_categories(): + label = Gtk.Label() + vbox = Gtk.VBox() + scroll_box = Gtk.ScrolledWindow() + scroll_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll_box.add(vbox) + self.notebook.append_page(scroll_box, label) + self._params_boxes.append((category, label, vbox)) + def _params_changed(self): """ Have the params in this dialog changed? @@ -146,25 +142,23 @@ class PropsDialog(gtk.Dialog): true if changed """ old_hash = self._hash - # create a tuple of things from each param that affects the params box - self._hash = hash(tuple([( - hash(param), param.get_name(), param.get_type(), - param.get_hide() == 'all', - ) for param in self._block.get_params()])) - return self._hash != old_hash + new_hash = self._hash = hash(tuple( + (hash(param), param.name, param.dtype, param.hide == 'all',) + for param in self._block.params.values() + )) + return new_hash != old_hash def _handle_changed(self, *args): """ A change occurred within a param: Rewrite/validate the block and update the gui. """ - # update for the block self._block.rewrite() self._block.validate() self.update_gui() def _activate_apply(self, *args): - self.set_response_sensitive(gtk.RESPONSE_APPLY, True) + self.set_response_sensitive(Gtk.ResponseType.APPLY, True) def update_gui(self, widget=None, force=False): """ @@ -175,45 +169,49 @@ class PropsDialog(gtk.Dialog): Update the documentation block. Hide the box if there are no docs. """ - # update the params box if force or self._params_changed(): # hide params box before changing - for tab, label, vbox in self._params_boxes: - vbox.hide_all() + for category, label, vbox in self._params_boxes: + vbox.hide() # empty the params box for child in vbox.get_children(): vbox.remove(child) - child.destroy() + # child.destroy() # disabled because it throws errors... # repopulate the params box box_all_valid = True - for param in filter(lambda p: p.get_tab_label() == tab, self._block.get_params()): - if param.get_hide() == 'all': + for param in self._block.params.values(): + # todo: why do we even rebuild instead of really hiding params? + if param.category != category or param.hide == 'all': continue box_all_valid = box_all_valid and param.is_valid() - input_widget = param.get_input(self._handle_changed, self._activate_apply) - vbox.pack_start(input_widget, input_widget.expand) - label.set_markup(Utils.parse_template(TAB_LABEL_MARKUP_TMPL, valid=box_all_valid, tab=tab)) - # show params box with new params - vbox.show_all() - # update the errors box + + input_widget = param.get_input(self._handle_changed, self._activate_apply, + transient_for=self.get_transient_for()) + input_widget.show_all() + vbox.pack_start(input_widget, input_widget.expand, True, 1) + + label.set_markup('<span {color}>{name}</span>'.format( + color='foreground="red"' if not box_all_valid else '', name=Utils.encode(category) + )) + vbox.show() # show params box with new params + if self._block.is_valid(): self._error_box.hide() else: self._error_box.show() messages = '\n\n'.join(self._block.get_error_messages()) self._error_messages_text_display.set_text(messages) - # update the docs box + self._update_docs_page() - # update the generated code self._update_generated_code_page() def _update_docs_page(self): """Show documentation from XML and try to display best matching docstring""" - buffer = self._docs_text_display.get_buffer() - buffer.delete(buffer.get_start_iter(), buffer.get_end_iter()) - pos = buffer.get_end_iter() + buf = self._docs_text_display.get_buffer() + buf.delete(buf.get_start_iter(), buf.get_end_iter()) + pos = buf.get_end_iter() - docstrings = self._block.get_doc() + docstrings = self._block.documentation if not docstrings: return @@ -221,77 +219,71 @@ class PropsDialog(gtk.Dialog): from_xml = docstrings.pop('', '') for line in from_xml.splitlines(): if line.lstrip() == line and line.endswith(':'): - buffer.insert_with_tags_by_name(pos, line + '\n', 'b') + buf.insert_with_tags_by_name(pos, line + '\n', 'b') else: - buffer.insert(pos, line + '\n') + buf.insert(pos, line + '\n') if from_xml: - buffer.insert(pos, '\n') + buf.insert(pos, '\n') # if given the current parameters an exact match can be made - block_constructor = self._block.get_make().rsplit('.', 2)[-1] + block_constructor = self._block.templates.render('make').rsplit('.', 2)[-1] block_class = block_constructor.partition('(')[0].strip() if block_class in docstrings: docstrings = {block_class: docstrings[block_class]} # show docstring(s) extracted from python sources - for cls_name, docstring in docstrings.iteritems(): - buffer.insert_with_tags_by_name(pos, cls_name + '\n', 'b') - buffer.insert(pos, docstring + '\n\n') + for cls_name, docstring in six.iteritems(docstrings): + buf.insert_with_tags_by_name(pos, cls_name + '\n', 'b') + buf.insert(pos, docstring + '\n\n') pos.backward_chars(2) - buffer.delete(pos, buffer.get_end_iter()) + buf.delete(pos, buf.get_end_iter()) def _update_generated_code_page(self): if not self._code_text_display: return # user disabled code preview - buffer = self._code_text_display.get_buffer() + buf = self._code_text_display.get_buffer() block = self._block - key = block.get_key() + key = block.key if key == 'epy_block': - src = block.get_param('_source_code').get_value() + src = block.params['_source_code'].get_value() elif key == 'epy_module': - src = block.get_param('source_code').get_value() + src = block.params['source_code'].get_value() else: src = '' def insert(header, text): if not text: return - buffer.insert_with_tags_by_name(buffer.get_end_iter(), header, 'b') - buffer.insert(buffer.get_end_iter(), text) - - buffer.delete(buffer.get_start_iter(), buffer.get_end_iter()) - insert('# Imports\n', '\n'.join(block.get_imports())) - if key.startswith('variable'): - insert('\n\n# Variables\n', block.get_var_make()) - insert('\n\n# Blocks\n', block.get_make()) + buf.insert_with_tags_by_name(buf.get_end_iter(), header, 'b') + buf.insert(buf.get_end_iter(), text) + + buf.delete(buf.get_start_iter(), buf.get_end_iter()) + insert('# Imports\n', block.templates.render('imports').strip('\n')) + if block.is_variable: + insert('\n\n# Variables\n', block.templates.render('var_make')) + insert('\n\n# Blocks\n', block.templates.render('make')) if src: - insert('\n\n# External Code ({}.py)\n'.format(block.get_id()), src) + insert('\n\n# External Code ({}.py)\n'.format(block.name), src) def _handle_key_press(self, widget, event): - """ - Handle key presses from the keyboard. - Call the ok response when enter is pressed. - - Returns: - false to forward the keypress - """ - if (event.keyval == gtk.keysyms.Return and - event.state & gtk.gdk.CONTROL_MASK == 0 and - not isinstance(widget.get_focus(), gtk.TextView) - ): - self.response(gtk.RESPONSE_ACCEPT) + close_dialog = ( + event.keyval == Gdk.KEY_Return and + event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0 and + not isinstance(widget.get_focus(), Gtk.TextView) + ) + if close_dialog: + self.response(Gtk.ResponseType.ACCEPT) return True # handled here + return False # forward the keypress def _handle_response(self, widget, response): - if response in (gtk.RESPONSE_APPLY, gtk.RESPONSE_ACCEPT): + if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT): for tab, label, vbox in self._params_boxes: for child in vbox.get_children(): child.apply_pending_changes() - self.set_response_sensitive(gtk.RESPONSE_APPLY, False) + self.set_response_sensitive(Gtk.ResponseType.APPLY, False) return True return False - - diff --git a/grc/gui/StateCache.py b/grc/gui/StateCache.py index 12ec9305b0..8159d71246 100644 --- a/grc/gui/StateCache.py +++ b/grc/gui/StateCache.py @@ -17,8 +17,9 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import Actions -from Constants import STATE_CACHE_SIZE +from __future__ import absolute_import +from . import Actions +from .Constants import STATE_CACHE_SIZE class StateCache(object): """ @@ -98,5 +99,5 @@ class StateCache(object): """ Update the undo and redo actions based on the number of next and prev states. """ - Actions.FLOW_GRAPH_REDO.set_sensitive(self.num_next_states != 0) - Actions.FLOW_GRAPH_UNDO.set_sensitive(self.num_prev_states != 0) + Actions.FLOW_GRAPH_REDO.set_enabled(self.num_next_states != 0) + Actions.FLOW_GRAPH_UNDO.set_enabled(self.num_prev_states != 0) diff --git a/grc/gui/Utils.py b/grc/gui/Utils.py index 3ab8d2009e..1b32e91439 100644 --- a/grc/gui/Utils.py +++ b/grc/gui/Utils.py @@ -17,37 +17,16 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk -import gobject +from __future__ import absolute_import -from Cheetah.Template import Template +import numbers -from Constants import POSSIBLE_ROTATIONS, CANVAS_GRID_SIZE, DPI_SCALING +from gi.repository import GLib +import cairo +import six - -def rotate_pixmap(gc, src_pixmap, dst_pixmap, angle=gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE): - """ - Load the destination pixmap with a rotated version of the source pixmap. - The source pixmap will be loaded into a pixbuf, rotated, and drawn to the destination pixmap. - The pixbuf is a client-side drawable, where a pixmap is a server-side drawable. - - Args: - gc: the graphics context - src_pixmap: the source pixmap - dst_pixmap: the destination pixmap - angle: the angle to rotate by - """ - width, height = src_pixmap.get_size() - pixbuf = gtk.gdk.Pixbuf( - colorspace=gtk.gdk.COLORSPACE_RGB, - has_alpha=False, bits_per_sample=8, - width=width, height=height, - ) - pixbuf.get_from_drawable(src_pixmap, src_pixmap.get_colormap(), 0, 0, 0, 0, -1, -1) - pixbuf = pixbuf.rotate_simple(angle) - dst_pixmap.draw_pixbuf(gc, pixbuf, 0, 0, 0, 0) +from .canvas.colors import FLOWGRAPH_BACKGROUND_COLOR +from . import Constants def get_rotated_coordinate(coor, rotation): @@ -62,8 +41,8 @@ def get_rotated_coordinate(coor, rotation): the rotated coordinates """ # handles negative angles - rotation = (rotation + 360)%360 - if rotation not in POSSIBLE_ROTATIONS: + rotation = (rotation + 360) % 360 + if rotation not in Constants.POSSIBLE_ROTATIONS: raise ValueError('unusable rotation angle "%s"'%str(rotation)) # determine the number of degrees to rotate cos_r, sin_r = { @@ -73,7 +52,7 @@ def get_rotated_coordinate(coor, rotation): return x * cos_r + y * sin_r, -x * sin_r + y * cos_r -def get_angle_from_coordinates((x1, y1), (x2, y2)): +def get_angle_from_coordinates(p1, p2): """ Given two points, calculate the vector direction from point1 to point2, directions are multiples of 90 degrees. @@ -84,59 +63,103 @@ def get_angle_from_coordinates((x1, y1), (x2, y2)): Returns: the direction in degrees """ + (x1, y1) = p1 + (x2, y2) = p2 if y1 == y2: # 0 or 180 return 0 if x2 > x1 else 180 else: # 90 or 270 return 270 if y2 > y1 else 90 +def align_to_grid(coor, mode=round): + def align(value): + return int(mode(value / (1.0 * Constants.CANVAS_GRID_SIZE)) * Constants.CANVAS_GRID_SIZE) + try: + return [align(c) for c in coor] + except TypeError: + x = coor + return align(coor) + + +def num_to_str(num): + """ Display logic for numbers """ + def eng_notation(value, fmt='g'): + """Convert a number to a string in engineering notation. E.g., 5e-9 -> 5n""" + template = '{:' + fmt + '}{}' + magnitude = abs(value) + for exp, symbol in zip(range(9, -15-1, -3), 'GMk munpf'): + factor = 10 ** exp + if magnitude >= factor: + return template.format(value / factor, symbol.strip()) + return template.format(value, '') + + if isinstance(num, numbers.Complex): + num = complex(num) # Cast to python complex + if num == 0: + return '0' + output = eng_notation(num.real) if num.real else '' + output += eng_notation(num.imag, '+g' if output else 'g') + 'j' if num.imag else '' + return output + else: + return str(num) + + def encode(value): """Make sure that we pass only valid utf-8 strings into markup_escape_text. Older versions of glib seg fault if the last byte starts a multi-byte character. """ + if six.PY2: + valid_utf8 = value.decode('utf-8', errors='replace').encode('utf-8') + else: + valid_utf8 = value + return GLib.markup_escape_text(valid_utf8) - valid_utf8 = value.decode('utf-8', errors='replace').encode('utf-8') - return gobject.markup_escape_text(valid_utf8) +def make_screenshot(flow_graph, file_path, transparent_bg=False): + if not file_path: + return -class TemplateParser(object): - def __init__(self): - self.cache = {} + x_min, y_min, x_max, y_max = flow_graph.get_extents() + padding = Constants.CANVAS_GRID_SIZE + width = x_max - x_min + 2 * padding + height = y_max - y_min + 2 * padding - def __call__(self, tmpl_str, **kwargs): - """ - Parse the template string with the given args. - Pass in the xml encode method for pango escape chars. + if file_path.endswith('.png'): + psurf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + elif file_path.endswith('.pdf'): + psurf = cairo.PDFSurface(file_path, width, height) + elif file_path.endswith('.svg'): + psurf = cairo.SVGSurface(file_path, width, height) + else: + raise ValueError('Unknown file format') - Args: - tmpl_str: the template as a string + cr = cairo.Context(psurf) - Returns: - a string of the parsed template - """ - kwargs['encode'] = encode - template = self.cache.setdefault(tmpl_str, Template.compile(tmpl_str)) - return str(template(namespaces=kwargs)) + if not transparent_bg: + cr.set_source_rgba(*FLOWGRAPH_BACKGROUND_COLOR) + cr.rectangle(0, 0, width, height) + cr.fill() -parse_template = TemplateParser() + cr.translate(padding - x_min, padding - y_min) + flow_graph.create_labels(cr) + flow_graph.create_shapes() + flow_graph.draw(cr) -def align_to_grid(coor, mode=round): - def align(value): - return int(mode(value / (1.0 * CANVAS_GRID_SIZE)) * CANVAS_GRID_SIZE) - try: - return map(align, coor) - except TypeError: - x = coor - return align(coor) + if file_path.endswith('.png'): + psurf.write_to_png(file_path) + if file_path.endswith('.pdf') or file_path.endswith('.svg'): + cr.show_page() + psurf.finish() def scale(coor, reverse=False): - factor = DPI_SCALING if not reverse else 1 / DPI_SCALING + factor = Constants.DPI_SCALING if not reverse else 1 / Constants.DPI_SCALING return tuple(int(x * factor) for x in coor) + def scale_scalar(coor, reverse=False): - factor = DPI_SCALING if not reverse else 1 / DPI_SCALING + factor = Constants.DPI_SCALING if not reverse else 1 / Constants.DPI_SCALING return int(coor * factor) diff --git a/grc/gui/VariableEditor.py b/grc/gui/VariableEditor.py index 45f0bb75fc..c179c8bc84 100644 --- a/grc/gui/VariableEditor.py +++ b/grc/gui/VariableEditor.py @@ -1,5 +1,5 @@ """ -Copyright 2015 Free Software Foundation, Inc. +Copyright 2015, 2016 Free Software Foundation, Inc. This file is part of GNU Radio GNU Radio Companion is free software; you can redistribute it and/or @@ -17,50 +17,45 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -from operator import attrgetter +from __future__ import absolute_import -import pygtk -pygtk.require('2.0') -import gtk -import gobject +from gi.repository import Gtk, Gdk, GObject -from . import Actions -from . import Preferences -from . import Utils -from .Constants import DEFAULT_BLOCKS_WINDOW_WIDTH +from . import Actions, Constants, Utils BLOCK_INDEX = 0 ID_INDEX = 1 -class VariableEditorContextMenu(gtk.Menu): +class VariableEditorContextMenu(Gtk.Menu): """ A simple context menu for our variable editor """ + def __init__(self, var_edit): - gtk.Menu.__init__(self) + Gtk.Menu.__init__(self) - self.imports = gtk.MenuItem("Add _Import") + self.imports = Gtk.MenuItem(label="Add _Import") self.imports.connect('activate', var_edit.handle_action, var_edit.ADD_IMPORT) self.add(self.imports) - self.variables = gtk.MenuItem("Add _Variable") + self.variables = Gtk.MenuItem(label="Add _Variable") self.variables.connect('activate', var_edit.handle_action, var_edit.ADD_VARIABLE) self.add(self.variables) - self.add(gtk.SeparatorMenuItem()) + self.add(Gtk.SeparatorMenuItem()) - self.enable = gtk.MenuItem("_Enable") + self.enable = Gtk.MenuItem(label="_Enable") self.enable.connect('activate', var_edit.handle_action, var_edit.ENABLE_BLOCK) - self.disable = gtk.MenuItem("_Disable") + self.disable = Gtk.MenuItem(label="_Disable") self.disable.connect('activate', var_edit.handle_action, var_edit.DISABLE_BLOCK) self.add(self.enable) self.add(self.disable) - self.add(gtk.SeparatorMenuItem()) + self.add(Gtk.SeparatorMenuItem()) - self.delete = gtk.MenuItem("_Delete") + self.delete = Gtk.MenuItem(label="_Delete") self.delete.connect('activate', var_edit.handle_action, var_edit.DELETE_BLOCK) self.add(self.delete) - self.add(gtk.SeparatorMenuItem()) + self.add(Gtk.SeparatorMenuItem()) - self.properties = gtk.MenuItem("_Properties...") + self.properties = Gtk.MenuItem(label="_Properties...") self.properties.connect('activate', var_edit.handle_action, var_edit.OPEN_PROPERTIES) self.add(self.properties) self.show_all() @@ -72,7 +67,7 @@ class VariableEditorContextMenu(gtk.Menu): self.disable.set_sensitive(selected and enabled) -class VariableEditor(gtk.VBox): +class VariableEditor(Gtk.VBox): # Actions that are handled by the editor ADD_IMPORT = 0 @@ -83,23 +78,30 @@ class VariableEditor(gtk.VBox): ENABLE_BLOCK = 5 DISABLE_BLOCK = 6 - def __init__(self, platform, get_flow_graph): - gtk.VBox.__init__(self) - self.platform = platform - self.get_flow_graph = get_flow_graph + __gsignals__ = { + 'create_new_block': (GObject.SignalFlags.RUN_FIRST, None, (str,)), + 'remove_block': (GObject.SignalFlags.RUN_FIRST, None, (str,)) + } + + def __init__(self): + Gtk.VBox.__init__(self) + config = Gtk.Application.get_default().config + self._block = None self._mouse_button_pressed = False + self._imports = [] + self._variables = [] # Only use the model to store the block reference and name. # Generate everything else dynamically - self.treestore = gtk.TreeStore(gobject.TYPE_PYOBJECT, # Block reference - gobject.TYPE_STRING) # Category and block name - self.treeview = gtk.TreeView(self.treestore) + self.treestore = Gtk.TreeStore(GObject.TYPE_PYOBJECT, # Block reference + GObject.TYPE_STRING) # Category and block name + self.treeview = Gtk.TreeView(model=self.treestore) self.treeview.set_enable_search(False) self.treeview.set_search_column(-1) #self.treeview.set_enable_search(True) #self.treeview.set_search_column(ID_INDEX) - self.treeview.get_selection().set_mode('single') + self.treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE) self.treeview.set_headers_visible(True) self.treeview.connect('button-press-event', self._handle_mouse_button_press) self.treeview.connect('button-release-event', self._handle_mouse_button_release) @@ -107,67 +109,63 @@ class VariableEditor(gtk.VBox): self.treeview.connect('key-press-event', self._handle_key_button_press) # Block Name or Category - self.id_cell = gtk.CellRendererText() + self.id_cell = Gtk.CellRendererText() self.id_cell.connect('edited', self._handle_name_edited_cb) - id_column = gtk.TreeViewColumn("Id", self.id_cell, text=ID_INDEX) + id_column = Gtk.TreeViewColumn("Id", self.id_cell, text=ID_INDEX) id_column.set_name("id") id_column.set_resizable(True) id_column.set_max_width(Utils.scale_scalar(300)) id_column.set_min_width(Utils.scale_scalar(80)) id_column.set_fixed_width(Utils.scale_scalar(100)) - id_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + id_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) id_column.set_cell_data_func(self.id_cell, self.set_properties) self.id_column = id_column self.treeview.append_column(id_column) - self.treestore.set_sort_column_id(ID_INDEX, gtk.SORT_ASCENDING) + self.treestore.set_sort_column_id(ID_INDEX, Gtk.SortType.ASCENDING) # For forcing resize self._col_width = 0 # Block Value - self.value_cell = gtk.CellRendererText() + self.value_cell = Gtk.CellRendererText() self.value_cell.connect('edited', self._handle_value_edited_cb) - value_column = gtk.TreeViewColumn("Value", self.value_cell) + value_column = Gtk.TreeViewColumn("Value", self.value_cell) value_column.set_name("value") value_column.set_resizable(False) value_column.set_expand(True) value_column.set_min_width(Utils.scale_scalar(100)) - value_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) + value_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) value_column.set_cell_data_func(self.value_cell, self.set_value) self.value_column = value_column self.treeview.append_column(value_column) # Block Actions (Add, Remove) - self.action_cell = gtk.CellRendererPixbuf() + self.action_cell = Gtk.CellRendererPixbuf() value_column.pack_start(self.action_cell, False) value_column.set_cell_data_func(self.action_cell, self.set_icon) # Make the scrolled window to hold the tree view - scrolled_window = gtk.ScrolledWindow() - scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrolled_window.add_with_viewport(self.treeview) - scrolled_window.set_size_request(DEFAULT_BLOCKS_WINDOW_WIDTH, -1) - self.pack_start(scrolled_window) + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.add(self.treeview) + scrolled_window.set_size_request(Constants.DEFAULT_BLOCKS_WINDOW_WIDTH, -1) + self.pack_start(scrolled_window, True, True, 0) # Context menus self._context_menu = VariableEditorContextMenu(self) - self._confirm_delete = Preferences.variable_editor_confirm_delete() + self._confirm_delete = config.variable_editor_confirm_delete() # Sets cell contents - def set_icon(self, col, cell, model, iter): + def set_icon(self, col, cell, model, iter, data): block = model.get_value(iter, BLOCK_INDEX) - if block: - pb = self.treeview.render_icon(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU, None) - else: - pb = self.treeview.render_icon(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU, None) - cell.set_property('pixbuf', pb) + cell.set_property('icon-name', 'window-close' if block else 'list-add') - def set_value(self, col, cell, model, iter): + def set_value(self, col, cell, model, iter, data): sp = cell.set_property block = model.get_value(iter, BLOCK_INDEX) # Set the default properties for this column first. # Some set in set_properties() may be overridden (editable for advanced variable blocks) - self.set_properties(col, cell, model, iter) + self.set_properties(col, cell, model, iter, data) # Set defaults value = None @@ -175,14 +173,14 @@ class VariableEditor(gtk.VBox): # Block specific values if block: - if block.get_key() == 'import': - value = block.get_param('import').get_value() - elif block.get_key() != "variable": + if block.key == 'import': + value = block.params['import'].get_value() + elif block.key != "variable": value = "<Open Properties>" sp('editable', False) sp('foreground', '#0D47A1') else: - value = block.get_param('value').get_value() + value = block.params['value'].get_value() # Check if there are errors in the blocks. # Show the block error as a tooltip @@ -193,13 +191,13 @@ class VariableEditor(gtk.VBox): self.set_tooltip_text(error_message[-1]) else: # Evaluate and show the value (if it is a variable) - if block.get_key() == "variable": - evaluated = str(block.get_param('value').evaluate()) + if block.key == "variable": + evaluated = str(block.params['value'].evaluate()) self.set_tooltip_text(evaluated) # Always set the text value. sp('text', value) - def set_properties(self, col, cell, model, iter): + def set_properties(self, col, cell, model, iter, data): sp = cell.set_property block = model.get_value(iter, BLOCK_INDEX) # Set defaults @@ -209,7 +207,7 @@ class VariableEditor(gtk.VBox): # Block specific changes if block: - if not block.get_enabled(): + if not block.enabled: # Disabled block. But, this should still be editable sp('editable', True) sp('foreground', 'gray') @@ -218,39 +216,32 @@ class VariableEditor(gtk.VBox): if block.get_error_messages(): sp('foreground', 'red') - def update_gui(self): - if not self.get_flow_graph(): - return - self._update_blocks() + def update_gui(self, blocks): + self._imports = [block for block in blocks if block.is_import] + self._variables = [block for block in blocks if block.is_variable] self._rebuild() self.treeview.expand_all() - def _update_blocks(self): - self._imports = filter(attrgetter('is_import'), - self.get_flow_graph().blocks) - self._variables = filter(attrgetter('is_variable'), - self.get_flow_graph().blocks) - def _rebuild(self, *args): self.treestore.clear() imports = self.treestore.append(None, [None, 'Imports']) variables = self.treestore.append(None, [None, 'Variables']) for block in self._imports: - self.treestore.append(imports, [block, block.get_param('id').get_value()]) - for block in sorted(self._variables, key=lambda v: v.get_id()): - self.treestore.append(variables, [block, block.get_param('id').get_value()]) + self.treestore.append(imports, [block, block.params['id'].get_value()]) + for block in sorted(self._variables, key=lambda v: v.name): + self.treestore.append(variables, [block, block.params['id'].get_value()]) def _handle_name_edited_cb(self, cell, path, new_text): block = self.treestore[path][BLOCK_INDEX] - block.get_param('id').set_value(new_text) + block.params['id'].set_value(new_text) Actions.VARIABLE_EDITOR_UPDATE() def _handle_value_edited_cb(self, cell, path, new_text): block = self.treestore[path][BLOCK_INDEX] if block.is_import: - block.get_param('import').set_value(new_text) + block.params['import'].set_value(new_text) else: - block.get_param('value').set_value(new_text) + block.params['value'].set_value(new_text) Actions.VARIABLE_EDITOR_UPDATE() def handle_action(self, item, key, event=None): @@ -259,29 +250,31 @@ class VariableEditor(gtk.VBox): key presses or mouse clicks. Also triggers an update of the flow graph and editor. """ if key == self.ADD_IMPORT: - self.get_flow_graph().add_new_block('import') + self.emit('create_new_block', 'import') elif key == self.ADD_VARIABLE: - self.get_flow_graph().add_new_block('variable') + self.emit('create_new_block', 'variable') elif key == self.OPEN_PROPERTIES: - Actions.BLOCK_PARAM_MODIFY(self._block) + # TODO: This probably isn't working because the action doesn't expect a parameter + #Actions.BLOCK_PARAM_MODIFY() + pass elif key == self.DELETE_BLOCK: - self.get_flow_graph().remove_element(self._block) + self.emit('remove_block', self._block.name) elif key == self.DELETE_CONFIRM: if self._confirm_delete: # Create a context menu to confirm the delete operation - confirmation_menu = gtk.Menu() - block_id = self._block.get_param('id').get_value().replace("_", "__") - confirm = gtk.MenuItem("Delete {}".format(block_id)) + confirmation_menu = Gtk.Menu() + block_id = self._block.params['id'].get_value().replace("_", "__") + confirm = Gtk.MenuItem(label="Delete {}".format(block_id)) confirm.connect('activate', self.handle_action, self.DELETE_BLOCK) confirmation_menu.add(confirm) confirmation_menu.show_all() - confirmation_menu.popup(None, None, None, event.button, event.time) + confirmation_menu.popup(None, None, None, None, event.button, event.time) else: self.handle_action(None, self.DELETE_BLOCK, None) elif key == self.ENABLE_BLOCK: - self._block.set_enabled(True) + self._block.state = 'enabled' elif key == self.DISABLE_BLOCK: - self._block.set_enabled(False) + self._block.state = 'disabled' Actions.VARIABLE_EDITOR_UPDATE() def _handle_mouse_button_press(self, widget, event): @@ -303,12 +296,12 @@ class VariableEditor(gtk.VBox): if event.button == 1 and col.get_name() == "value": # Make sure this has a block (not the import/variable rows) - if self._block and event.type == gtk.gdk._2BUTTON_PRESS: + if self._block and event.type == Gdk.EventType._2BUTTON_PRESS: # Open the advanced dialog if it is a gui variable - if self._block.get_key() not in ("variable", "import"): + if self._block.key not in ("variable", "import"): self.handle_action(None, self.OPEN_PROPERTIES, event=event) return True - if event.type == gtk.gdk.BUTTON_PRESS: + if event.type == Gdk.EventType.BUTTON_PRESS: # User is adding/removing blocks # Make sure this is the action cell (Add/Remove Icons) if path[2] > col.cell_get_position(self.action_cell)[0]: @@ -321,15 +314,15 @@ class VariableEditor(gtk.VBox): else: self.handle_action(None, self.DELETE_CONFIRM, event=event) return True - elif event.button == 3 and event.type == gtk.gdk.BUTTON_PRESS: + elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: if self._block: - self._context_menu.update_sensitive(True, enabled=self._block.get_enabled()) + self._context_menu.update_sensitive(True, enabled=self._block.enabled) else: self._context_menu.update_sensitive(False) - self._context_menu.popup(None, None, None, event.button, event.time) + self._context_menu.popup(None, None, None, None, event.button, event.time) # Null handler. Stops the treeview from handling double click events. - if event.type == gtk.gdk._2BUTTON_PRESS: + if event.type == Gdk.EventType._2BUTTON_PRESS: return True return False @@ -346,10 +339,10 @@ class VariableEditor(gtk.VBox): def _handle_key_button_press(self, widget, event): model, path = self.treeview.get_selection().get_selected_rows() if path and self._block: - if self._block.get_enabled() and event.string == "d": + if self._block.enabled and event.string == "d": self.handle_action(None, self.DISABLE_BLOCK, None) return True - elif not self._block.get_enabled() and event.string == "e": + elif not self._block.enabled and event.string == "e": self.handle_action(None, self.ENABLE_BLOCK, None) return True return False diff --git a/grc/gui/canvas/__init__.py b/grc/gui/canvas/__init__.py new file mode 100644 index 0000000000..f90d10c4e6 --- /dev/null +++ b/grc/gui/canvas/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion 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 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +from .block import Block +from .connection import Connection +from .flowgraph import FlowGraph +from .param import Param +from .port import Port diff --git a/grc/gui/canvas/block.py b/grc/gui/canvas/block.py new file mode 100644 index 0000000000..33edf988c2 --- /dev/null +++ b/grc/gui/canvas/block.py @@ -0,0 +1,400 @@ +""" +Copyright 2007, 2008, 2009 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import, division + +import math + +import six +from gi.repository import Gtk, Pango, PangoCairo + +from . import colors +from .drawable import Drawable +from .. import Actions, Utils, Constants +from ..Constants import ( + BLOCK_LABEL_PADDING, PORT_SPACING, PORT_SEPARATION, LABEL_SEPARATION, + PORT_BORDER_SEPARATION, BLOCK_FONT, PARAM_FONT +) +from ...core import utils +from ...core.blocks import Block as CoreBlock + + +class Block(CoreBlock, Drawable): + """The graphical signal block.""" + + def __init__(self, parent, **n): + """ + Block constructor. + Add graphics related params to the block. + """ + super(self.__class__, self).__init__(parent, **n) + + self.states.update(coordinate=(0, 0), rotation=0) + self.width = self.height = 0 + Drawable.__init__(self) # needs the states and initial sizes + + self._surface_layouts = [ + None, # title + None, # params + ] + self._surface_layouts_offsets = 0, 0 + self._comment_layout = None + + self._area = [] + self._border_color = self._bg_color = colors.BLOCK_ENABLED_COLOR + self._font_color = list(colors.FONT_COLOR) + + @property + def coordinate(self): + """ + Get the coordinate from the position param. + + Returns: + the coordinate tuple (x, y) or (0, 0) if failure + """ + return Utils.scale(self.states['coordinate']) + + @coordinate.setter + def coordinate(self, coor): + """ + Set the coordinate into the position param. + + Args: + coor: the coordinate tuple (x, y) + """ + coor = Utils.scale(coor, reverse=True) + if Actions.TOGGLE_SNAP_TO_GRID.get_active(): + offset_x, offset_y = (0, self.height / 2) if self.is_horizontal() else (self.height / 2, 0) + coor = ( + Utils.align_to_grid(coor[0] + offset_x) - offset_x, + Utils.align_to_grid(coor[1] + offset_y) - offset_y + ) + self.states['coordinate'] = coor + + @property + def rotation(self): + """ + Get the rotation from the position param. + + Returns: + the rotation in degrees or 0 if failure + """ + return self.states['rotation'] + + @rotation.setter + def rotation(self, rot): + """ + Set the rotation into the position param. + + Args: + rot: the rotation in degrees + """ + self.states['rotation'] = rot + + def _update_colors(self): + self._bg_color = ( + colors.MISSING_BLOCK_BACKGROUND_COLOR if self.is_dummy_block else + colors.BLOCK_BYPASSED_COLOR if self.state == 'bypassed' else + colors.BLOCK_ENABLED_COLOR if self.state == 'enabled' else + colors.BLOCK_DISABLED_COLOR + ) + self._font_color[-1] = 1.0 if self.state == 'enabled' else 0.4 + self._border_color = ( + colors.MISSING_BLOCK_BORDER_COLOR if self.is_dummy_block else + colors.BORDER_COLOR_DISABLED if not self.state == 'enabled' else colors.BORDER_COLOR + ) + + def create_shapes(self): + """Update the block, parameters, and ports when a change occurs.""" + if self.is_horizontal(): + self._area = (0, 0, self.width, self.height) + elif self.is_vertical(): + self._area = (0, 0, self.height, self.width) + self.bounds_from_area(self._area) + + # bussified = self.current_bus_structure['source'], self.current_bus_structure['sink'] + bussified = False, False + for ports, has_busses in zip((self.active_sources, self.active_sinks), bussified): + if not ports: + continue + port_separation = PORT_SEPARATION if not has_busses else ports[0].height + PORT_SPACING + offset = (self.height - (len(ports) - 1) * port_separation - ports[0].height) / 2 + for port in ports: + port.create_shapes() + + port.coordinate = { + 0: (+self.width, offset), + 90: (offset, -port.width), + 180: (-port.width, offset), + 270: (offset, +self.width), + }[port.connector_direction] + + offset += PORT_SEPARATION if not has_busses else port.height + PORT_SPACING + + def create_labels(self, cr=None): + """Create the labels for the signal block.""" + + # (Re-)creating layouts here, because layout.context_changed() doesn't seems to work (after zoom) + title_layout, params_layout = self._surface_layouts = [ + Gtk.DrawingArea().create_pango_layout(''), # title + Gtk.DrawingArea().create_pango_layout(''), # params + ] + + if cr: # to fix up extents after zooming + PangoCairo.update_layout(cr, title_layout) + PangoCairo.update_layout(cr, params_layout) + + title_layout.set_markup( + '<span {foreground} font_desc="{font}"><b>{label}</b></span>'.format( + foreground='foreground="red"' if not self.is_valid() else '', font=BLOCK_FONT, + label=Utils.encode(self.label) + ) + ) + title_width, title_height = title_layout.get_size() + + # update the params layout + if not self.is_dummy_block: + markups = [param.format_block_surface_markup() + for param in self.params.values() if param.hide not in ('all', 'part')] + else: + markups = ['<span font_desc="{font}"><b>key: </b>{key}</span>'.format(font=PARAM_FONT, key=self.key)] + + params_layout.set_spacing(LABEL_SEPARATION * Pango.SCALE) + params_layout.set_markup('\n'.join(markups)) + params_width, params_height = params_layout.get_size() if markups else (0, 0) + + label_width = max(title_width, params_width) / Pango.SCALE + label_height = title_height / Pango.SCALE + if markups: + label_height += LABEL_SEPARATION + params_height / Pango.SCALE + + # calculate width and height needed + width = label_width + 2 * BLOCK_LABEL_PADDING + height = label_height + 2 * BLOCK_LABEL_PADDING + + self._update_colors() + self.create_port_labels() + + def get_min_height_for_ports(ports): + min_height = 2 * PORT_BORDER_SEPARATION + len(ports) * PORT_SEPARATION + if ports: + min_height -= ports[-1].height + return min_height + + height = max(height, + get_min_height_for_ports(self.active_sinks), + get_min_height_for_ports(self.active_sources)) + + # def get_min_height_for_bus_ports(ports): + # return 2 * PORT_BORDER_SEPARATION + sum( + # port.height + PORT_SPACING for port in ports if port.dtype == 'bus' + # ) - PORT_SPACING + # + # if self.current_bus_structure['sink']: + # height = max(height, get_min_height_for_bus_ports(self.active_sinks)) + # if self.current_bus_structure['source']: + # height = max(height, get_min_height_for_bus_ports(self.active_sources)) + + self.width, self.height = width, height = Utils.align_to_grid((width, height)) + + self._surface_layouts_offsets = [ + (0, (height - label_height) / 2.0), + (0, (height - label_height) / 2.0 + LABEL_SEPARATION + title_height / Pango.SCALE) + ] + + title_layout.set_width(width * Pango.SCALE) + title_layout.set_alignment(Pango.Alignment.CENTER) + params_layout.set_indent((width - label_width) / 2.0 * Pango.SCALE) + + self.create_comment_layout() + + def create_port_labels(self): + for ports in (self.active_sinks, self.active_sources): + max_width = 0 + for port in ports: + port.create_labels() + max_width = max(max_width, port.width_with_label) + for port in ports: + port.width = max_width + + def create_comment_layout(self): + markups = [] + + # Show the flow graph complexity on the top block if enabled + if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.key == "options": + complexity = utils.flow_graph_complexity.calculate(self.parent) + markups.append( + '<span foreground="#444" size="medium" font_desc="{font}">' + '<b>Complexity: {num}bal</b></span>'.format(num=Utils.num_to_str(complexity), font=BLOCK_FONT) + ) + comment = self.comment # Returns None if there are no comments + if comment: + if markups: + markups.append('<span></span>') + + markups.append('<span foreground="{foreground}" font_desc="{font}">{comment}</span>'.format( + foreground='#444' if self.enabled else '#888', font=BLOCK_FONT, comment=Utils.encode(comment) + )) + if markups: + layout = self._comment_layout = Gtk.DrawingArea().create_pango_layout('') + layout.set_markup(''.join(markups)) + else: + self._comment_layout = None + + def draw(self, cr): + """ + Draw the signal block with label and inputs/outputs. + """ + border_color = colors.HIGHLIGHT_COLOR if self.highlighted else self._border_color + cr.translate(*self.coordinate) + + for port in self.active_ports(): # ports first + cr.save() + port.draw(cr) + cr.restore() + + cr.rectangle(*self._area) + cr.set_source_rgba(*self._bg_color) + cr.fill_preserve() + cr.set_source_rgba(*border_color) + cr.stroke() + + # title and params label + if self.is_vertical(): + cr.rotate(-math.pi / 2) + cr.translate(-self.width, 0) + cr.set_source_rgba(*self._font_color) + for layout, offset in zip(self._surface_layouts, self._surface_layouts_offsets): + cr.save() + cr.translate(*offset) + PangoCairo.update_layout(cr, layout) + PangoCairo.show_layout(cr, layout) + cr.restore() + + def what_is_selected(self, coor, coor_m=None): + """ + Get the element that is selected. + + Args: + coor: the (x,y) tuple + coor_m: the (x_m, y_m) tuple + + Returns: + this block, a port, or None + """ + for port in self.active_ports(): + port_selected = port.what_is_selected( + coor=[a - b for a, b in zip(coor, self.coordinate)], + coor_m=[a - b for a, b in zip(coor, self.coordinate)] if coor_m is not None else None + ) + if port_selected: + return port_selected + return Drawable.what_is_selected(self, coor, coor_m) + + def draw_comment(self, cr): + if not self._comment_layout: + return + x, y = self.coordinate + + if self.is_horizontal(): + y += self.height + BLOCK_LABEL_PADDING + else: + x += self.height + BLOCK_LABEL_PADDING + + cr.save() + cr.translate(x, y) + PangoCairo.update_layout(cr, self._comment_layout) + PangoCairo.show_layout(cr, self._comment_layout) + cr.restore() + + def get_extents(self): + extent = Drawable.get_extents(self) + x, y = self.coordinate + for port in self.active_ports(): + extent = (min_or_max(xy, offset + p_xy) for offset, min_or_max, xy, p_xy in zip( + (x, y, x, y), (min, min, max, max), extent, port.get_extents() + )) + return tuple(extent) + + ############################################## + # Controller Modify + ############################################## + def type_controller_modify(self, direction): + """ + Change the type controller. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + type_templates = ' '.join(p._type for p in self.params.values()) + type_templates += ' '.join(p.get_raw('dtype') for p in (self.sinks + self.sources)) + type_param = None + for key, param in six.iteritems(self.params): + if not param.is_enum(): + continue + # Priority to the type controller + if param.key in type_templates: + type_param = param + break + # Use param if type param is unset + if not type_param: + type_param = param + if not type_param: + return False + + # Try to increment the enum by direction + try: + values = list(type_param.options) + old_index = values.index(type_param.get_value()) + new_index = (old_index + direction + len(values)) % len(values) + type_param.set_value(values[new_index]) + return True + except: + return False + + def port_controller_modify(self, direction): + """ + Change the port controller. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + changed = False + # Concat the nports string from the private nports settings of all ports + nports_str = ' '.join(str(port.get_raw('multiplicity')) for port in self.ports()) + # Modify all params whose keys appear in the nports string + for key, param in six.iteritems(self.params): + if param.is_enum() or param.key not in nports_str: + continue + # Try to increment the port controller by direction + try: + value = param.get_evaluated() + direction + if value > 0: + param.set_value(value) + changed = True + except: + pass + return changed + diff --git a/grc/gui/canvas/colors.py b/grc/gui/canvas/colors.py new file mode 100644 index 0000000000..77d3203e78 --- /dev/null +++ b/grc/gui/canvas/colors.py @@ -0,0 +1,78 @@ +""" +Copyright 2008,2013 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import + +from gi.repository import Gtk, Gdk, cairo +# import pycairo + +from .. import Constants + + +def get_color(color_code): + color = Gdk.RGBA() + color.parse(color_code) + return color.red, color.green, color.blue, color.alpha + # chars_per_color = 2 if len(color_code) > 4 else 1 + # offsets = range(1, 3 * chars_per_color + 1, chars_per_color) + # return tuple(int(color_code[o:o + 2], 16) / 255.0 for o in offsets) + +################################################################################# +# fg colors +################################################################################# + +HIGHLIGHT_COLOR = get_color('#00FFFF') +BORDER_COLOR = get_color('#616161') +BORDER_COLOR_DISABLED = get_color('#888888') +FONT_COLOR = get_color('#000000') + +# Missing blocks stuff +MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2') +MISSING_BLOCK_BORDER_COLOR = get_color('#FF0000') + +# Flow graph color constants +FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF') +COMMENT_BACKGROUND_COLOR = get_color('#F3F3F3') +FLOWGRAPH_EDGE_COLOR = COMMENT_BACKGROUND_COLOR + +# Block color constants +BLOCK_ENABLED_COLOR = get_color('#F1ECFF') +BLOCK_DISABLED_COLOR = get_color('#CCCCCC') +BLOCK_BYPASSED_COLOR = get_color('#F4FF81') + +# Connection color constants +CONNECTION_ENABLED_COLOR = get_color('#000000') +CONNECTION_DISABLED_COLOR = get_color('#BBBBBB') +CONNECTION_ERROR_COLOR = get_color('#FF0000') + +DEFAULT_DOMAIN_COLOR = get_color('#777777') + + +################################################################################# +# port colors +################################################################################# + +PORT_TYPE_TO_COLOR = {key: get_color(color) for name, key, sizeof, color in Constants.CORE_TYPES} +PORT_TYPE_TO_COLOR.update((key, get_color(color)) for key, (_, color) in Constants.ALIAS_TYPES.items()) + + +################################################################################# +# param box colors +################################################################################# + diff --git a/grc/gui/canvas/connection.py b/grc/gui/canvas/connection.py new file mode 100644 index 0000000000..56dab45570 --- /dev/null +++ b/grc/gui/canvas/connection.py @@ -0,0 +1,253 @@ +""" +Copyright 2007, 2008, 2009 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import, division + +from argparse import Namespace +from math import pi + +from . import colors +from .drawable import Drawable +from .. import Utils +from ..Constants import ( + CONNECTOR_ARROW_BASE, + CONNECTOR_ARROW_HEIGHT, + GR_MESSAGE_DOMAIN, + LINE_SELECT_SENSITIVITY, +) +from ...core.Connection import Connection as CoreConnection +from ...core.utils.descriptors import nop_write + + +class Connection(CoreConnection, Drawable): + """ + A graphical connection for ports. + The connection has 2 parts, the arrow and the wire. + The coloring of the arrow and wire exposes the status of 3 states: + enabled/disabled, valid/invalid, highlighted/non-highlighted. + The wire coloring exposes the enabled and highlighted states. + The arrow coloring exposes the enabled and valid states. + """ + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + Drawable.__init__(self) + + self._line = [] + self._line_width_factor = 1.0 + self._color1 = self._color2 = None + + self._current_port_rotations = self._current_coordinates = None + + self._rel_points = None # connection coordinates relative to sink/source + self._arrow_rotation = 0.0 # rotation of the arrow in radians + self._current_cr = None # for what_is_selected() of curved line + self._line_path = None + + @nop_write + @property + def coordinate(self): + return self.source_port.connector_coordinate_absolute + + @nop_write + @property + def rotation(self): + """ + Get the 0 degree rotation. + Rotations are irrelevant in connection. + + Returns: + 0 + """ + return 0 + + def create_shapes(self): + """Pre-calculate relative coordinates.""" + source = self.source_port + sink = self.sink_port + rotate = Utils.get_rotated_coordinate + + # first two components relative to source connector, rest relative to sink connector + self._rel_points = [ + rotate((15, 0), source.rotation), # line from 0,0 to here, bezier curve start + rotate((50, 0), source.rotation), # bezier curve control point 1 + rotate((-50, 0), sink.rotation), # bezier curve control point 2 + rotate((-15, 0), sink.rotation), # bezier curve end + rotate((-CONNECTOR_ARROW_HEIGHT, 0), sink.rotation), # line to arrow head + ] + self._current_coordinates = None # triggers _make_path() + + def get_domain_color(domain_id): + domain = self.parent_platform.domains.get(domain_id, None) + return colors.get_color(domain.color) if domain else colors.DEFAULT_DOMAIN_COLOR + + if source.domain == GR_MESSAGE_DOMAIN: + self._line_width_factor = 1.0 + self._color1 = None + self._color2 = colors.CONNECTION_ENABLED_COLOR + else: + if source.domain != sink.domain: + self._line_width_factor = 2.0 + self._color1 = get_domain_color(source.domain) + self._color2 = get_domain_color(sink.domain) + + self._arrow_rotation = -sink.rotation / 180 * pi + + if not self._bounding_points: + self._make_path() # no cr set --> only sets bounding_points for extent + + def _make_path(self, cr=None): + x_pos, y_pos = self.coordinate # is source connector coordinate + # x_start, y_start = self.source_port.get_connector_coordinate() + x_end, y_end = self.sink_port.connector_coordinate_absolute + + # sink connector relative to sink connector + x_e, y_e = x_end - x_pos, y_end - y_pos + + # make rel_point all relative to source connector + p0 = 0, 0 # x_start - x_pos, y_start - y_pos + p1, p2, (dx_e1, dy_e1), (dx_e2, dy_e2), (dx_e3, dy_e3) = self._rel_points + p3 = x_e + dx_e1, y_e + dy_e1 + p4 = x_e + dx_e2, y_e + dy_e2 + p5 = x_e + dx_e3, y_e + dy_e3 + self._bounding_points = p0, p1, p4, p5 # ignores curved part =( + + if cr: + cr.move_to(*p0) + cr.line_to(*p1) + cr.curve_to(*(p2 + p3 + p4)) + cr.line_to(*p5) + self._line_path = cr.copy_path() + + def draw(self, cr): + """ + Draw the connection. + """ + self._current_cr = cr + sink = self.sink_port + source = self.source_port + + # check for changes + port_rotations = (source.rotation, sink.rotation) + if self._current_port_rotations != port_rotations: + self.create_shapes() # triggers _make_path() call below + self._current_port_rotations = port_rotations + + new_coordinates = (source.parent_block.coordinate, sink.parent_block.coordinate) + if self._current_coordinates != new_coordinates: + self._make_path(cr) + self._current_coordinates = new_coordinates + + color1, color2 = ( + None if color is None else + colors.HIGHLIGHT_COLOR if self.highlighted else + colors.CONNECTION_DISABLED_COLOR if not self.enabled else + colors.CONNECTION_ERROR_COLOR if not self.is_valid() else + color + for color in (self._color1, self._color2) + ) + + cr.translate(*self.coordinate) + cr.set_line_width(self._line_width_factor * cr.get_line_width()) + cr.new_path() + cr.append_path(self._line_path) + + arrow_pos = cr.get_current_point() + + if color1: # not a message connection + cr.set_source_rgba(*color1) + cr.stroke_preserve() + + if color1 != color2: + cr.save() + cr.set_dash([5.0, 5.0], 5.0 if color1 else 0.0) + cr.set_source_rgba(*color2) + cr.stroke() + cr.restore() + else: + cr.new_path() + + cr.move_to(*arrow_pos) + cr.set_source_rgba(*color2) + cr.rotate(self._arrow_rotation) + cr.rel_move_to(CONNECTOR_ARROW_HEIGHT, 0) + cr.rel_line_to(-CONNECTOR_ARROW_HEIGHT, -CONNECTOR_ARROW_BASE/2) + cr.rel_line_to(0, CONNECTOR_ARROW_BASE) + cr.close_path() + cr.fill() + + def what_is_selected(self, coor, coor_m=None): + """ + Returns: + self if one of the areas/lines encompasses coor, else None. + """ + if coor_m: + return Drawable.what_is_selected(self, coor, coor_m) + + x, y = [a - b for a, b in zip(coor, self.coordinate)] + + cr = self._current_cr + + if cr is None: + return + cr.save() + cr.new_path() + cr.append_path(self._line_path) + cr.set_line_width(cr.get_line_width() * LINE_SELECT_SENSITIVITY) + hit = cr.in_stroke(x, y) + cr.restore() + + if hit: + return self + + +class DummyCoreConnection(object): + def __init__(self, source_port, **kwargs): + self.parent_platform = source_port.parent_platform + self.source_port = source_port + self.sink_port = self._dummy_port = Namespace( + domain=source_port.domain, + rotation=0, + coordinate=(0, 0), + connector_coordinate_absolute=(0, 0), + connector_direction=0, + parent_block=Namespace(coordinate=(0, 0)), + ) + + self.enabled = True + self.highlighted = False, + self.is_valid = lambda: True + self.update(**kwargs) + + def update(self, coordinate=None, rotation=None, sink_port=None): + dp = self._dummy_port + self.sink_port = sink_port if sink_port else dp + if coordinate: + dp.coordinate = coordinate + dp.connector_coordinate_absolute = coordinate + dp.parent_block.coordinate = coordinate + if rotation is not None: + dp.rotation = rotation + dp.connector_direction = (180 + rotation) % 360 + + @property + def has_real_sink(self): + return self.sink_port is not self._dummy_port + +DummyConnection = Connection.make_cls_with_base(DummyCoreConnection) diff --git a/grc/gui/canvas/drawable.py b/grc/gui/canvas/drawable.py new file mode 100644 index 0000000000..d755d4418d --- /dev/null +++ b/grc/gui/canvas/drawable.py @@ -0,0 +1,183 @@ +""" +Copyright 2007, 2008, 2009, 2016 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import +from ..Constants import LINE_SELECT_SENSITIVITY + +from six.moves import zip + + +class Drawable(object): + """ + GraphicalElement is the base class for all graphical elements. + It contains an X,Y coordinate, a list of rectangular areas that the element occupies, + and methods to detect selection of those areas. + """ + + @classmethod + def make_cls_with_base(cls, super_cls): + name = super_cls.__name__ + bases = (super_cls,) + cls.__bases__[1:] + namespace = cls.__dict__.copy() + return type(name, bases, namespace) + + def __init__(self): + """ + Make a new list of rectangular areas and lines, and set the coordinate and the rotation. + """ + self.coordinate = (0, 0) + self.rotation = 0 + self.highlighted = False + + self._bounding_rects = [] + self._bounding_points = [] + + def is_horizontal(self, rotation=None): + """ + Is this element horizontal? + If rotation is None, use this element's rotation. + + Args: + rotation: the optional rotation + + Returns: + true if rotation is horizontal + """ + rotation = rotation or self.rotation + return rotation in (0, 180) + + def is_vertical(self, rotation=None): + """ + Is this element vertical? + If rotation is None, use this element's rotation. + + Args: + rotation: the optional rotation + + Returns: + true if rotation is vertical + """ + rotation = rotation or self.rotation + return rotation in (90, 270) + + def rotate(self, rotation): + """ + Rotate all of the areas by 90 degrees. + + Args: + rotation: multiple of 90 degrees + """ + self.rotation = (self.rotation + rotation) % 360 + + def move(self, delta_coor): + """ + Move the element by adding the delta_coor to the current coordinate. + + Args: + delta_coor: (delta_x,delta_y) tuple + """ + x, y = self.coordinate + dx, dy = delta_coor + self.coordinate = (x + dx, y + dy) + + def create_labels(self, cr=None): + """ + Create labels (if applicable) and call on all children. + Call this base method before creating labels in the element. + """ + + def create_shapes(self): + """ + Create shapes (if applicable) and call on all children. + Call this base method before creating shapes in the element. + """ + + def draw(self, cr): + raise NotImplementedError() + + def bounds_from_area(self, area): + x1, y1, w, h = area + x2 = x1 + w + y2 = y1 + h + self._bounding_rects = [(x1, y1, x2, y2)] + self._bounding_points = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)] + + def bounds_from_line(self, line): + self._bounding_rects = rects = [] + self._bounding_points = list(line) + last_point = line[0] + for x2, y2 in line[1:]: + (x1, y1), last_point = last_point, (x2, y2) + if x1 == x2: + x1, x2 = x1 - LINE_SELECT_SENSITIVITY, x2 + LINE_SELECT_SENSITIVITY + if y2 < y1: + y1, y2 = y2, y1 + elif y1 == y2: + y1, y2 = y1 - LINE_SELECT_SENSITIVITY, y2 + LINE_SELECT_SENSITIVITY + if x2 < x1: + x1, x2 = x2, x1 + + rects.append((x1, y1, x2, y2)) + + def what_is_selected(self, coor, coor_m=None): + """ + One coordinate specified: + Is this element selected at given coordinate? + ie: is the coordinate encompassed by one of the areas or lines? + Both coordinates specified: + Is this element within the rectangular region defined by both coordinates? + ie: do any area corners or line endpoints fall within the region? + + Args: + coor: the selection coordinate, tuple x, y + coor_m: an additional selection coordinate. + + Returns: + self if one of the areas/lines encompasses coor, else None. + """ + x, y = [a - b for a, b in zip(coor, self.coordinate)] + + if not coor_m: + for x1, y1, x2, y2 in self._bounding_rects: + if x1 <= x <= x2 and y1 <= y <= y2: + return self + else: + x_m, y_m = [a - b for a, b in zip(coor_m, self.coordinate)] + if y_m < y: + y, y_m = y_m, y + if x_m < x: + x, x_m = x_m, x + + for x1, y1 in self._bounding_points: + if x <= x1 <= x_m and y <= y1 <= y_m: + return self + + def get_extents(self): + x_min, y_min = x_max, y_max = self.coordinate + x_min += min(x for x, y in self._bounding_points) + y_min += min(y for x, y in self._bounding_points) + x_max += max(x for x, y in self._bounding_points) + y_max += max(y for x, y in self._bounding_points) + return x_min, y_min, x_max, y_max + + def mouse_over(self): + pass + + def mouse_out(self): + pass diff --git a/grc/gui/canvas/flowgraph.py b/grc/gui/canvas/flowgraph.py new file mode 100644 index 0000000000..3e0fd83dad --- /dev/null +++ b/grc/gui/canvas/flowgraph.py @@ -0,0 +1,772 @@ +""" +Copyright 2007-2011, 2016q Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import + +import ast +import functools +import random +from distutils.spawn import find_executable +from itertools import count + +import six +from gi.repository import GLib +from six.moves import filter + +from . import colors +from .drawable import Drawable +from .connection import DummyConnection +from .. import Actions, Constants, Utils, Bars, Dialogs +from ..external_editor import ExternalEditor +from ...core import Messages +from ...core.FlowGraph import FlowGraph as CoreFlowgraph + + +class FlowGraph(CoreFlowgraph, Drawable): + """ + FlowGraph is the data structure to store graphical signal blocks, + graphical inputs and outputs, + and the connections between inputs and outputs. + """ + + def __init__(self, parent, **kwargs): + """ + FlowGraph constructor. + Create a list for signal blocks and connections. Connect mouse handlers. + """ + super(self.__class__, self).__init__(parent, **kwargs) + Drawable.__init__(self) + self.drawing_area = None + # important vars dealing with mouse event tracking + self.element_moved = False + self.mouse_pressed = False + self.press_coor = (0, 0) + # selected + self.selected_elements = set() + self._old_selected_port = None + self._new_selected_port = None + # current mouse hover element + self.element_under_mouse = None + # context menu + self._context_menu = Bars.ContextMenu() + self.get_context_menu = lambda: self._context_menu + + self._new_connection = None + self._elements_to_draw = [] + self._external_updaters = {} + + def _get_unique_id(self, base_id=''): + """ + Get a unique id starting with the base id. + + Args: + base_id: the id starts with this and appends a count + + Returns: + a unique id + """ + block_ids = set(b.name for b in self.blocks) + for index in count(): + block_id = '{}_{}'.format(base_id, index) + if block_id not in block_ids: + break + return block_id + + def install_external_editor(self, param, parent=None): + target = (param.parent_block.name, param.key) + + if target in self._external_updaters: + editor = self._external_updaters[target] + else: + config = self.parent_platform.config + editor = (find_executable(config.editor) or + Dialogs.choose_editor(parent, config)) # todo: pass in parent + if not editor: + return + updater = functools.partial( + self.handle_external_editor_change, target=target) + editor = self._external_updaters[target] = ExternalEditor( + editor=editor, + name=target[0], value=param.get_value(), + callback=functools.partial(GLib.idle_add, updater) + ) + editor.start() + try: + editor.open_editor() + except Exception as e: + # Problem launching the editor. Need to select a new editor. + Messages.send('>>> Error opening an external editor. Please select a different editor.\n') + # Reset the editor to force the user to select a new one. + self.parent_platform.config.editor = '' + + def handle_external_editor_change(self, new_value, target): + try: + block_id, param_key = target + self.get_block(block_id).params[param_key].set_value(new_value) + + except (IndexError, ValueError): # block no longer exists + self._external_updaters[target].stop() + del self._external_updaters[target] + return + Actions.EXTERNAL_UPDATE() + + def add_new_block(self, key, coor=None): + """ + Add a block of the given key to this flow graph. + + Args: + key: the block key + coor: an optional coordinate or None for random + """ + id = self._get_unique_id(key) + scroll_pane = self.drawing_area.get_parent().get_parent() + # calculate the position coordinate + h_adj = scroll_pane.get_hadjustment() + v_adj = scroll_pane.get_vadjustment() + if coor is None: coor = ( + int(random.uniform(.25, .75)*h_adj.get_page_size() + h_adj.get_value()), + int(random.uniform(.25, .75)*v_adj.get_page_size() + v_adj.get_value()), + ) + # get the new block + block = self.new_block(key) + block.coordinate = coor + block.params['id'].set_value(id) + Actions.ELEMENT_CREATE() + return id + + def make_connection(self): + """this selection and the last were ports, try to connect them""" + if self._new_connection and self._new_connection.has_real_sink: + self._old_selected_port = self._new_connection.source_port + self._new_selected_port = self._new_connection.sink_port + if self._old_selected_port and self._new_selected_port: + try: + self.connect(self._old_selected_port, self._new_selected_port) + Actions.ELEMENT_CREATE() + except Exception as e: + Messages.send_fail_connection(e) + self._old_selected_port = None + self._new_selected_port = None + return True + return False + + def update(self): + """ + Call the top level rewrite and validate. + Call the top level create labels and shapes. + """ + self.rewrite() + self.validate() + self.update_elements_to_draw() + self.create_labels() + self.create_shapes() + + def reload(self): + """ + Reload flow-graph (with updated blocks) + + Args: + page: the page to reload (None means current) + Returns: + False if some error occurred during import + """ + success = False + data = self.export_data() + if data: + self.unselect() + success = self.import_data(data) + self.update() + return success + + ########################################################################### + # Copy Paste + ########################################################################### + def copy_to_clipboard(self): + """ + Copy the selected blocks and connections into the clipboard. + + Returns: + the clipboard + """ + #get selected blocks + blocks = list(self.selected_blocks()) + if not blocks: + return None + #calc x and y min + x_min, y_min = blocks[0].coordinate + for block in blocks: + x, y = block.coordinate + x_min = min(x, x_min) + y_min = min(y, y_min) + #get connections between selected blocks + connections = list(filter( + lambda c: c.source_block in blocks and c.sink_block in blocks, + self.connections, + )) + clipboard = ( + (x_min, y_min), + [block.export_data() for block in blocks], + [connection.export_data() for connection in connections], + ) + return clipboard + + def paste_from_clipboard(self, clipboard): + """ + Paste the blocks and connections from the clipboard. + + Args: + clipboard: the nested data of blocks, connections + """ + # todo: rewrite this... + selected = set() + (x_min, y_min), blocks_n, connections_n = clipboard + old_id2block = dict() + # recalc the position + scroll_pane = self.drawing_area.get_parent().get_parent() + h_adj = scroll_pane.get_hadjustment() + v_adj = scroll_pane.get_vadjustment() + x_off = h_adj.get_value() - x_min + h_adj.get_page_size() / 4 + y_off = v_adj.get_value() - y_min + v_adj.get_page_size() / 4 + + if len(self.get_elements()) <= 1: + x_off, y_off = 0, 0 + + # create blocks + for block_n in blocks_n: + block_key = block_n.get('id') + if block_key == 'options': + continue + block = self.new_block(block_key) + if not block: + continue # unknown block was pasted (e.g. dummy block) + + selected.add(block) + block.import_data(**block_n) + old_id2block[block.params['id'].value] = block + # move block to offset coordinate + block.move((x_off, y_off)) + + if block.params['id'].value in (blk.name for blk in self.blocks): + block.params['id'].value = self._get_unique_id(block_key) + + # update before creating connections + self.update() + # create connections + for connection_n in connections_n: + source = old_id2block[connection_n[0]].get_source(connection_n[1]) + sink = old_id2block[connection_n[2]].get_sink(connection_n[3]) + connection = self.connect(source, sink) + selected.add(connection) + self.selected_elements = selected + + ########################################################################### + # Modify Selected + ########################################################################### + def type_controller_modify_selected(self, direction): + """ + Change the registered type controller for the selected signal blocks. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + return any(sb.type_controller_modify(direction) for sb in self.selected_blocks()) + + def port_controller_modify_selected(self, direction): + """ + Change port controller for the selected signal blocks. + + Args: + direction: +1 or -1 + + Returns: + true for changed + """ + return any(sb.port_controller_modify(direction) for sb in self.selected_blocks()) + + def change_state_selected(self, new_state): + """ + Enable/disable the selected blocks. + + Args: + new_state: a block state + + Returns: + true if changed + """ + changed = False + for block in self.selected_blocks(): + changed |= block.state != new_state + block.state = new_state + return changed + + def move_selected(self, delta_coordinate): + """ + Move the element and by the change in coordinates. + + Args: + delta_coordinate: the change in coordinates + """ + for selected_block in self.selected_blocks(): + selected_block.move(delta_coordinate) + self.element_moved = True + + def align_selected(self, calling_action=None): + """ + Align the selected blocks. + + Args: + calling_action: the action initiating the alignment + + Returns: + True if changed, otherwise False + """ + blocks = list(self.selected_blocks()) + if calling_action is None or not blocks: + return False + + # compute common boundary of selected objects + min_x, min_y = max_x, max_y = blocks[0].coordinate + for selected_block in blocks: + x, y = selected_block.coordinate + min_x, min_y = min(min_x, x), min(min_y, y) + x += selected_block.width + y += selected_block.height + max_x, max_y = max(max_x, x), max(max_y, y) + ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 + + # align the blocks as requested + transform = { + Actions.BLOCK_VALIGN_TOP: lambda x, y, w, h: (x, min_y), + Actions.BLOCK_VALIGN_MIDDLE: lambda x, y, w, h: (x, ctr_y - h/2), + Actions.BLOCK_VALIGN_BOTTOM: lambda x, y, w, h: (x, max_y - h), + Actions.BLOCK_HALIGN_LEFT: lambda x, y, w, h: (min_x, y), + Actions.BLOCK_HALIGN_CENTER: lambda x, y, w, h: (ctr_x-w/2, y), + Actions.BLOCK_HALIGN_RIGHT: lambda x, y, w, h: (max_x - w, y), + }.get(calling_action, lambda *args: args) + + for selected_block in blocks: + x, y = selected_block.coordinate + w, h = selected_block.width, selected_block.height + selected_block.coordinate = transform(x, y, w, h) + + return True + + def rotate_selected(self, rotation): + """ + Rotate the selected blocks by multiples of 90 degrees. + + Args: + rotation: the rotation in degrees + + Returns: + true if changed, otherwise false. + """ + if not any(self.selected_blocks()): + return False + #initialize min and max coordinates + min_x, min_y = max_x, max_y = self.selected_block.coordinate + # rotate each selected block, and find min/max coordinate + for selected_block in self.selected_blocks(): + selected_block.rotate(rotation) + #update the min/max coordinate + x, y = selected_block.coordinate + min_x, min_y = min(min_x, x), min(min_y, y) + max_x, max_y = max(max_x, x), max(max_y, y) + #calculate center point of slected blocks + ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2 + #rotate the blocks around the center point + for selected_block in self.selected_blocks(): + x, y = selected_block.coordinate + x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation) + selected_block.coordinate = (x + ctr_x, y + ctr_y) + return True + + def remove_selected(self): + """ + Remove selected elements + + Returns: + true if changed. + """ + changed = False + for selected_element in self.selected_elements: + self.remove_element(selected_element) + changed = True + return changed + + def update_selected(self): + """ + Remove deleted elements from the selected elements list. + Update highlighting so only the selected are highlighted. + """ + selected_elements = self.selected_elements + elements = self.get_elements() + # remove deleted elements + for selected in list(selected_elements): + if selected in elements: + continue + selected_elements.remove(selected) + if self._old_selected_port and self._old_selected_port.parent not in elements: + self._old_selected_port = None + if self._new_selected_port and self._new_selected_port.parent not in elements: + self._new_selected_port = None + # update highlighting + for element in elements: + element.highlighted = element in selected_elements + + ########################################################################### + # Draw stuff + ########################################################################### + + def update_elements_to_draw(self): + hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active() + hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active() + + def draw_order(elem): + return elem.highlighted, elem.is_block, elem.enabled + + elements = sorted(self.get_elements(), key=draw_order) + del self._elements_to_draw[:] + + for element in elements: + if hide_disabled_blocks and not element.enabled: + continue # skip hidden disabled blocks and connections + if hide_variables and (element.is_variable or element.is_import): + continue # skip hidden disabled blocks and connections + self._elements_to_draw.append(element) + + def create_labels(self, cr=None): + for element in self._elements_to_draw: + element.create_labels(cr) + + def create_shapes(self): + for element in self._elements_to_draw: + element.create_shapes() + + def _drawables(self): + # todo: cache that + show_comments = Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active() + for element in self._elements_to_draw: + if element.is_block and show_comments and element.enabled: + yield element.draw_comment + if self._new_connection is not None: + yield self._new_connection.draw + for element in self._elements_to_draw: + yield element.draw + + def draw(self, cr): + """Draw blocks connections comment and select rectangle""" + for draw_element in self._drawables(): + cr.save() + draw_element(cr) + cr.restore() + + draw_multi_select_rectangle = ( + self.mouse_pressed and + (not self.selected_elements or self.drawing_area.ctrl_mask) and + not self._new_connection + ) + if draw_multi_select_rectangle: + x1, y1 = self.press_coor + x2, y2 = self.coordinate + x, y = int(min(x1, x2)), int(min(y1, y2)) + w, h = int(abs(x1 - x2)), int(abs(y1 - y2)) + cr.set_source_rgba( + colors.HIGHLIGHT_COLOR[0], + colors.HIGHLIGHT_COLOR[1], + colors.HIGHLIGHT_COLOR[2], + 0.5, + ) + cr.rectangle(x, y, w, h) + cr.fill() + cr.rectangle(x, y, w, h) + cr.stroke() + + ########################################################################## + # selection handling + ########################################################################## + def update_selected_elements(self): + """ + Update the selected elements. + The update behavior depends on the state of the mouse button. + When the mouse button pressed the selection will change when + the control mask is set or the new selection is not in the current group. + When the mouse button is released the selection will change when + the mouse has moved and the control mask is set or the current group is empty. + Attempt to make a new connection if the old and ports are filled. + If the control mask is set, merge with the current elements. + """ + selected_elements = None + if self.mouse_pressed: + new_selections = self.what_is_selected(self.coordinate) + # update the selections if the new selection is not in the current selections + # allows us to move entire selected groups of elements + if not new_selections: + selected_elements = set() + elif self.drawing_area.ctrl_mask or self.selected_elements.isdisjoint(new_selections): + selected_elements = new_selections + + if self._old_selected_port: + self._old_selected_port.force_show_label = False + self.create_shapes() + self.drawing_area.queue_draw() + elif self._new_selected_port: + self._new_selected_port.force_show_label = True + + else: # called from a mouse release + if not self.element_moved and (not self.selected_elements or self.drawing_area.ctrl_mask) and not self._new_connection: + selected_elements = self.what_is_selected(self.coordinate, self.press_coor) + + # this selection and the last were ports, try to connect them + if self.make_connection(): + return + + # update selected elements + if selected_elements is None: + return + + # if ctrl, set the selected elements to the union - intersection of old and new + if self.drawing_area.ctrl_mask: + self.selected_elements ^= selected_elements + else: + self.selected_elements.clear() + self.selected_elements.update(selected_elements) + Actions.ELEMENT_SELECT() + + def what_is_selected(self, coor, coor_m=None): + """ + What is selected? + At the given coordinate, return the elements found to be selected. + If coor_m is unspecified, return a list of only the first element found to be selected: + Iterate though the elements backwards since top elements are at the end of the list. + If an element is selected, place it at the end of the list so that is is drawn last, + and hence on top. Update the selected port information. + + Args: + coor: the coordinate of the mouse click + coor_m: the coordinate for multi select + + Returns: + the selected blocks and connections or an empty list + """ + selected_port = None + selected = set() + # check the elements + for element in reversed(self._elements_to_draw): + selected_element = element.what_is_selected(coor, coor_m) + if not selected_element: + continue + # update the selected port information + if selected_element.is_port: + if not coor_m: + selected_port = selected_element + selected_element = selected_element.parent_block + + selected.add(selected_element) + if not coor_m: + break + + if selected_port and selected_port.is_source: + selected.remove(selected_port.parent_block) + self._new_connection = DummyConnection(selected_port, coordinate=coor) + self.drawing_area.queue_draw() + # update selected ports + if selected_port is not self._new_selected_port: + self._old_selected_port = self._new_selected_port + self._new_selected_port = selected_port + return selected + + def unselect(self): + """ + Set selected elements to an empty set. + """ + self.selected_elements.clear() + + def select_all(self): + """Select all blocks in the flow graph""" + self.selected_elements.clear() + self.selected_elements.update(self._elements_to_draw) + + def selected_blocks(self): + """ + Get a group of selected blocks. + + Returns: + sub set of blocks in this flow graph + """ + return (e for e in self.selected_elements if e.is_block) + + @property + def selected_block(self): + """ + Get the selected block when a block or port is selected. + + Returns: + a block or None + """ + return next(self.selected_blocks(), None) + + def get_selected_elements(self): + """ + Get the group of selected elements. + + Returns: + sub set of elements in this flow graph + """ + return self.selected_elements + + def get_selected_element(self): + """ + Get the selected element. + + Returns: + a block, port, or connection or None + """ + return next(iter(self.selected_elements), None) + + ########################################################################## + # Event Handlers + ########################################################################## + def handle_mouse_context_press(self, coordinate, event): + """ + The context mouse button was pressed: + If no elements were selected, perform re-selection at this coordinate. + Then, show the context menu at the mouse click location. + """ + selections = self.what_is_selected(coordinate) + if not selections.intersection(self.selected_elements): + self.coordinate = coordinate + self.mouse_pressed = True + self.update_selected_elements() + self.mouse_pressed = False + if self._new_connection: + self._new_connection = None + self.drawing_area.queue_draw() + self._context_menu.popup(None, None, None, None, event.button, event.time) + + def handle_mouse_selector_press(self, double_click, coordinate): + """ + The selector mouse button was pressed: + Find the selected element. Attempt a new connection if possible. + Open the block params window on a double click. + Update the selection state of the flow graph. + """ + self.press_coor = coordinate + self.coordinate = coordinate + self.mouse_pressed = True + if double_click: + self.unselect() + self.update_selected_elements() + + if double_click and self.selected_block: + self.mouse_pressed = False + Actions.BLOCK_PARAM_MODIFY() + + def handle_mouse_selector_release(self, coordinate): + """ + The selector mouse button was released: + Update the state, handle motion (dragging). + And update the selected flowgraph elements. + """ + self.coordinate = coordinate + self.mouse_pressed = False + if self.element_moved: + Actions.BLOCK_MOVE() + self.element_moved = False + self.update_selected_elements() + if self._new_connection: + self._new_connection = None + self.drawing_area.queue_draw() + + def handle_mouse_motion(self, coordinate): + """ + The mouse has moved, respond to mouse dragging or notify elements + Move a selected element to the new coordinate. + Auto-scroll the scroll bars at the boundaries. + """ + # to perform a movement, the mouse must be pressed + # (no longer checking pending events via Gtk.events_pending() - always true in Windows) + redraw = False + if not self.mouse_pressed or self._new_connection: + redraw = self._handle_mouse_motion_move(coordinate) + if self.mouse_pressed: + redraw = redraw or self._handle_mouse_motion_drag(coordinate) + if redraw: + self.drawing_area.queue_draw() + + def _handle_mouse_motion_move(self, coordinate): + # only continue if mouse-over stuff is enabled (just the auto-hide port label stuff for now) + if not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active(): + return + redraw = False + for element in self._elements_to_draw: + over_element = element.what_is_selected(coordinate) + if not over_element: + continue + if over_element != self.element_under_mouse: # over sth new + if self.element_under_mouse: + redraw |= self.element_under_mouse.mouse_out() or False + self.element_under_mouse = over_element + redraw |= over_element.mouse_over() or False + break + else: + if self.element_under_mouse: + redraw |= self.element_under_mouse.mouse_out() or False + self.element_under_mouse = None + if redraw: + # self.create_labels() + self.create_shapes() + return redraw + + def _handle_mouse_motion_drag(self, coordinate): + redraw = False + # remove the connection if selected in drag event + if len(self.selected_elements) == 1 and self.get_selected_element().is_connection: + Actions.ELEMENT_DELETE() + redraw = True + + if self._new_connection: + e = self.element_under_mouse + if e and e.is_port and e.is_sink: + self._new_connection.update(sink_port=self.element_under_mouse) + else: + self._new_connection.update(coordinate=coordinate, rotation=0) + return True + # move the selected elements and record the new coordinate + x, y = coordinate + if not self.drawing_area.ctrl_mask: + X, Y = self.coordinate + dX, dY = int(x - X), int(y - Y) + active = Actions.TOGGLE_SNAP_TO_GRID.get_active() or self.drawing_area.mod1_mask + if not active or abs(dX) >= Constants.CANVAS_GRID_SIZE or abs(dY) >= Constants.CANVAS_GRID_SIZE: + self.move_selected((dX, dY)) + self.coordinate = (x, y) + redraw = True + return redraw + + def get_extents(self): + extent = 100000, 100000, 0, 0 + for element in self._elements_to_draw: + extent = (min_or_max(xy, e_xy) for min_or_max, xy, e_xy in zip( + (min, min, max, max), extent, element.get_extents() + )) + return tuple(extent) diff --git a/grc/gui/canvas/param.py b/grc/gui/canvas/param.py new file mode 100644 index 0000000000..5777423c68 --- /dev/null +++ b/grc/gui/canvas/param.py @@ -0,0 +1,162 @@ +# Copyright 2007-2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion 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 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +from __future__ import absolute_import + +import numbers + +from .drawable import Drawable +from .. import ParamWidgets, Utils, Constants +from ...core.params import Param as CoreParam + + +class Param(CoreParam): + """The graphical parameter.""" + + make_cls_with_base = classmethod(Drawable.make_cls_with_base.__func__) + + def get_input(self, *args, **kwargs): + """ + Get the graphical gtk class to represent this parameter. + An enum requires and combo parameter. + A non-enum with options gets a combined entry/combo parameter. + All others get a standard entry parameter. + + Returns: + gtk input class + """ + dtype = self.dtype + if dtype in ('file_open', 'file_save'): + input_widget_cls = ParamWidgets.FileParam + + elif dtype == 'enum': + input_widget_cls = ParamWidgets.EnumParam + + elif self.options: + input_widget_cls = ParamWidgets.EnumEntryParam + + elif dtype == '_multiline': + input_widget_cls = ParamWidgets.MultiLineEntryParam + + elif dtype == '_multiline_python_external': + input_widget_cls = ParamWidgets.PythonEditorParam + + else: + input_widget_cls = ParamWidgets.EntryParam + + return input_widget_cls(self, *args, **kwargs) + + def format_label_markup(self, have_pending_changes=False): + block = self.parent + # fixme: using non-public attribute here + has_callback = \ + hasattr(block, 'templates') and \ + any(self.key in callback for callback in block.templates.get('callbacks', '')) + + return '<span {underline} {foreground} font_desc="Sans 9">{label}</span>'.format( + underline='underline="low"' if has_callback else '', + foreground='foreground="blue"' if have_pending_changes else + 'foreground="red"' if not self.is_valid() else '', + label=Utils.encode(self.name) + ) + + def format_tooltip_text(self): + errors = self.get_error_messages() + tooltip_lines = ['Key: ' + self.key, 'Type: ' + self.dtype] + if self.is_valid(): + value = str(self.get_evaluated()) + if len(value) > 100: + value = '{}...{}'.format(value[:50], value[-50:]) + tooltip_lines.append('Value: ' + value) + elif len(errors) == 1: + tooltip_lines.append('Error: ' + errors[0]) + elif len(errors) > 1: + tooltip_lines.append('Error:') + tooltip_lines.extend(' * ' + msg for msg in errors) + return '\n'.join(tooltip_lines) + + def pretty_print(self): + """ + Get the repr (nice string format) for this param. + + Returns: + the string representation + """ + ################################################## + # Truncate helper method + ################################################## + def _truncate(string, style=0): + max_len = max(27 - len(self.name), 3) + if len(string) > max_len: + if style < 0: # Front truncate + string = '...' + string[3-max_len:] + elif style == 0: # Center truncate + string = string[:max_len//2 - 3] + '...' + string[-max_len//2:] + elif style > 0: # Rear truncate + string = string[:max_len-3] + '...' + return string + + ################################################## + # Simple conditions + ################################################## + value = self.get_value() + if not self.is_valid(): + return _truncate(value) + if value in self.options: + return self.options[value] # its name + + ################################################## + # Split up formatting by type + ################################################## + # Default center truncate + truncate = 0 + e = self.get_evaluated() + t = self.dtype + if isinstance(e, bool): + return str(e) + elif isinstance(e, numbers.Complex): + dt_str = Utils.num_to_str(e) + elif isinstance(e, Constants.VECTOR_TYPES): + # Vector types + if len(e) > 8: + # Large vectors use code + dt_str = self.get_value() + truncate = 1 + else: + # Small vectors use eval + dt_str = ', '.join(map(Utils.num_to_str, e)) + elif t in ('file_open', 'file_save'): + dt_str = self.get_value() + truncate = -1 + else: + # Other types + dt_str = str(e) + + # Done + return _truncate(dt_str, truncate) + + def format_block_surface_markup(self): + """ + Get the markup for this param. + + Returns: + a pango markup string + """ + return '<span {foreground} font_desc="{font}"><b>{label}:</b> {value}</span>'.format( + foreground='foreground="red"' if not self.is_valid() else '', font=Constants.PARAM_FONT, + label=Utils.encode(self.name), value=Utils.encode(self.pretty_print().replace('\n', ' ')) + ) diff --git a/grc/gui/canvas/port.py b/grc/gui/canvas/port.py new file mode 100644 index 0000000000..2ea35f3dd3 --- /dev/null +++ b/grc/gui/canvas/port.py @@ -0,0 +1,227 @@ +""" +Copyright 2007, 2008, 2009 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion 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 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from __future__ import absolute_import, division + +import math + +from gi.repository import Gtk, PangoCairo, Pango + +from . import colors +from .drawable import Drawable +from .. import Actions, Utils, Constants + +from ...core.utils.descriptors import nop_write +from ...core.ports import Port as CorePort + + +class Port(CorePort, Drawable): + """The graphical port.""" + + def __init__(self, parent, direction, **n): + """ + Port constructor. + Create list of connector coordinates. + """ + super(self.__class__, self).__init__(parent, direction, **n) + Drawable.__init__(self) + self._connector_coordinate = (0, 0) + self._hovering = False + self.force_show_label = False + + self._area = [] + self._bg_color = self._border_color = 0, 0, 0, 0 + self._font_color = list(colors.FONT_COLOR) + + self._line_width_factor = 1.0 + self._label_layout_offsets = 0, 0 + + self.width_with_label = self.height = 0 + + self.label_layout = None + + @property + def width(self): + return self.width_with_label if self._show_label else Constants.PORT_LABEL_HIDDEN_WIDTH + + @width.setter + def width(self, value): + self.width_with_label = value + self.label_layout.set_width(value * Pango.SCALE) + + def _update_colors(self): + """ + Get the color that represents this port's type. + Codes differ for ports where the vec length is 1 or greater than 1. + + Returns: + a hex color code. + """ + if not self.parent_block.enabled: + self._font_color[-1] = 0.4 + color = colors.BLOCK_DISABLED_COLOR + elif self.domain == Constants.GR_MESSAGE_DOMAIN: + color = colors.PORT_TYPE_TO_COLOR.get('message') + else: + self._font_color[-1] = 1.0 + color = colors.PORT_TYPE_TO_COLOR.get(self.dtype) or colors.PORT_TYPE_TO_COLOR.get('') + if self.vlen > 1: + dark = (0, 0, 30 / 255.0, 50 / 255.0, 70 / 255.0)[min(4, self.vlen)] + color = tuple(max(c - dark, 0) for c in color) + self._bg_color = color + self._border_color = tuple(max(c - 0.3, 0) for c in color) + + def create_shapes(self): + """Create new areas and labels for the port.""" + if self.is_horizontal(): + self._area = (0, 0, self.width, self.height) + elif self.is_vertical(): + self._area = (0, 0, self.height, self.width) + self.bounds_from_area(self._area) + + self._connector_coordinate = { + 0: (self.width, self.height / 2), + 90: (self.height / 2, 0), + 180: (0, self.height / 2), + 270: (self.height / 2, self.width) + }[self.connector_direction] + + def create_labels(self, cr=None): + """Create the labels for the socket.""" + self.label_layout = Gtk.DrawingArea().create_pango_layout('') + self.label_layout.set_alignment(Pango.Alignment.CENTER) + + if cr: + PangoCairo.update_layout(cr, self.label_layout) + + if self.domain in (Constants.GR_MESSAGE_DOMAIN, Constants.GR_STREAM_DOMAIN): + self._line_width_factor = 1.0 + else: + self._line_width_factor = 2.0 + + self._update_colors() + + layout = self.label_layout + layout.set_markup('<span font_desc="{font}">{name}</span>'.format( + name=Utils.encode(self.name), font=Constants.PORT_FONT + )) + label_width, label_height = self.label_layout.get_size() + + self.width = 2 * Constants.PORT_LABEL_PADDING + label_width / Pango.SCALE + self.height = 2 * Constants.PORT_LABEL_PADDING + label_height / Pango.SCALE + self._label_layout_offsets = [0, Constants.PORT_LABEL_PADDING] + # if self.dtype == 'bus': + # self.height += Constants.PORT_EXTRA_BUS_HEIGHT + # self._label_layout_offsets[1] += Constants.PORT_EXTRA_BUS_HEIGHT / 2 + self.height += self.height % 2 # uneven height + + def draw(self, cr): + """ + Draw the socket with a label. + """ + border_color = self._border_color + cr.set_line_width(self._line_width_factor * cr.get_line_width()) + cr.translate(*self.coordinate) + + cr.rectangle(*self._area) + cr.set_source_rgba(*self._bg_color) + cr.fill_preserve() + cr.set_source_rgba(*border_color) + cr.stroke() + + if not self._show_label: + return # this port is folded (no label) + + if self.is_vertical(): + cr.rotate(-math.pi / 2) + cr.translate(-self.width, 0) + cr.translate(*self._label_layout_offsets) + + cr.set_source_rgba(*self._font_color) + PangoCairo.update_layout(cr, self.label_layout) + PangoCairo.show_layout(cr, self.label_layout) + + @property + def connector_coordinate_absolute(self): + """the coordinate where connections may attach to""" + return [sum(c) for c in zip( + self._connector_coordinate, # relative to port + self.coordinate, # relative to block + self.parent_block.coordinate # abs + )] + + @property + def connector_direction(self): + """Get the direction that the socket points: 0,90,180,270.""" + if self.is_source: + return self.rotation + elif self.is_sink: + return (self.rotation + 180) % 360 + + @nop_write + @property + def rotation(self): + return self.parent_block.rotation + + def rotate(self, direction): + """ + Rotate the parent rather than self. + + Args: + direction: degrees to rotate + """ + self.parent_block.rotate(direction) + + def move(self, delta_coor): + """Move the parent rather than self.""" + self.parent_block.move(delta_coor) + + @property + def highlighted(self): + return self.parent_block.highlighted + + @highlighted.setter + def highlighted(self, value): + self.parent_block.highlighted = value + + @property + def _show_label(self): + """ + Figure out if the label should be hidden + + Returns: + true if the label should not be shown + """ + return self._hovering or self.force_show_label or not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() + + def mouse_over(self): + """ + Called from flow graph on mouse-over + """ + changed = not self._show_label + self._hovering = True + return changed + + def mouse_out(self): + """ + Called from flow graph on mouse-out + """ + label_was_shown = self._show_label + self._hovering = False + return label_was_shown != self._show_label diff --git a/grc/gui/external_editor.py b/grc/gui/external_editor.py index 010bd71d1a..155b0915c5 100644 --- a/grc/gui/external_editor.py +++ b/grc/gui/external_editor.py @@ -17,6 +17,8 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ +from __future__ import absolute_import, print_function + import os import sys import time @@ -67,7 +69,7 @@ class ExternalEditor(threading.Thread): time.sleep(1) except Exception as e: - print >> sys.stderr, "file monitor crashed:", str(e) + print("file monitor crashed:", str(e), file=sys.stderr) finally: try: os.remove(self.filename) @@ -76,10 +78,7 @@ class ExternalEditor(threading.Thread): if __name__ == '__main__': - def p(data): - print data - - e = ExternalEditor('/usr/bin/gedit', "test", "content", p) + e = ExternalEditor('/usr/bin/gedit', "test", "content", print) e.open_editor() e.start() time.sleep(15) |