summaryrefslogtreecommitdiff
path: root/grc/gui
diff options
context:
space:
mode:
authorAndrej Rode <mail@andrejro.de>2018-06-23 23:41:42 +0200
committerAndrej Rode <mail@andrejro.de>2018-06-24 00:03:35 +0200
commit167a6152bad060fc53dd29e0fa79ef83eff1be5b (patch)
treea01049672d9d7d1bf3d295ed96698a323941f8e8 /grc/gui
parent3c8e6008b092287246234001db7cf1a4038300da (diff)
parentfcd002b6ac82e1e0c1224e24506410ff0833e1aa (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')
-rw-r--r--grc/gui/Actions.py645
-rw-r--r--grc/gui/Application.py (renamed from grc/gui/ActionHandler.py)653
-rw-r--r--grc/gui/Bars.py529
-rw-r--r--grc/gui/Block.py350
-rw-r--r--grc/gui/BlockTreeWindow.py219
-rw-r--r--grc/gui/CMakeLists.txt32
-rw-r--r--grc/gui/Colors.py50
-rw-r--r--grc/gui/Config.py156
-rw-r--r--grc/gui/Connection.py181
-rw-r--r--grc/gui/Console.py57
-rw-r--r--grc/gui/Constants.py37
-rw-r--r--grc/gui/Dialogs.py458
-rw-r--r--grc/gui/DrawingArea.py253
-rw-r--r--grc/gui/Element.py278
-rw-r--r--grc/gui/Executor.py24
-rw-r--r--grc/gui/FileDialogs.py293
-rw-r--r--grc/gui/FlowGraph.py765
-rw-r--r--grc/gui/MainWindow.py321
-rw-r--r--grc/gui/Notebook.py187
-rw-r--r--grc/gui/NotebookPage.py244
-rw-r--r--grc/gui/Param.py442
-rw-r--r--grc/gui/ParamWidgets.py330
-rw-r--r--grc/gui/ParserErrorsDialog.py36
-rw-r--r--grc/gui/Platform.py48
-rw-r--r--grc/gui/Port.py277
-rw-r--r--grc/gui/Preferences.py173
-rw-r--r--grc/gui/PropsDialog.py260
-rw-r--r--grc/gui/StateCache.py9
-rw-r--r--grc/gui/Utils.py141
-rw-r--r--grc/gui/VariableEditor.py183
-rw-r--r--grc/gui/canvas/__init__.py22
-rw-r--r--grc/gui/canvas/block.py400
-rw-r--r--grc/gui/canvas/colors.py78
-rw-r--r--grc/gui/canvas/connection.py253
-rw-r--r--grc/gui/canvas/drawable.py183
-rw-r--r--grc/gui/canvas/flowgraph.py772
-rw-r--r--grc/gui/canvas/param.py162
-rw-r--r--grc/gui/canvas/port.py227
-rw-r--r--grc/gui/external_editor.py9
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)