summaryrefslogtreecommitdiff
path: root/grc/src/gui
diff options
context:
space:
mode:
authorjblum2008-09-07 21:38:12 +0000
committerjblum2008-09-07 21:38:12 +0000
commitc86f6c23c6883f73d953d64c28ab42cedb77e4d7 (patch)
tree0193b2a649eb0f7f1065912862de340a02848e16 /grc/src/gui
parentddec4fc07744a6519086b1b111f29d551b7f19c6 (diff)
downloadgnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.tar.gz
gnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.tar.bz2
gnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.zip
Merged r9481:9518 on jblum/grc_reorganize into trunk. Reorganized grc source under gnuradio.grc module. Trunk passes make distcheck.
git-svn-id: http://gnuradio.org/svn/gnuradio/trunk@9525 221aa14e-8319-0410-a670-987f0aec2ac5
Diffstat (limited to 'grc/src/gui')
-rw-r--r--grc/src/gui/ActionHandler.py445
-rw-r--r--grc/src/gui/Actions.py103
-rw-r--r--grc/src/gui/Bars.py139
-rw-r--r--grc/src/gui/BlockTreeWindow.py150
-rw-r--r--grc/src/gui/Constants.py82
-rw-r--r--grc/src/gui/Dialogs.py146
-rw-r--r--grc/src/gui/DrawingArea.py118
-rw-r--r--grc/src/gui/FileDialogs.py154
-rw-r--r--grc/src/gui/MainWindow.py321
-rw-r--r--grc/src/gui/Makefile.am41
-rw-r--r--grc/src/gui/Messages.py101
-rw-r--r--grc/src/gui/NotebookPage.py172
-rw-r--r--grc/src/gui/ParamsDialog.py131
-rw-r--r--grc/src/gui/Preferences.py130
-rw-r--r--grc/src/gui/StateCache.py92
-rw-r--r--grc/src/gui/__init__.py1
16 files changed, 2326 insertions, 0 deletions
diff --git a/grc/src/gui/ActionHandler.py b/grc/src/gui/ActionHandler.py
new file mode 100644
index 000000000..ab4d66502
--- /dev/null
+++ b/grc/src/gui/ActionHandler.py
@@ -0,0 +1,445 @@
+"""
+Copyright 2007 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 signal
+from .. platforms.base.Constants import PY_GTK_ICON, IMAGE_FILE_EXTENSION
+from Constants import DIR_LEFT, DIR_RIGHT
+import Actions
+import pygtk
+pygtk.require('2.0')
+import gtk
+import Preferences
+from threading import Thread
+import Messages
+from .. utils import ParseXML
+import random
+from .. platforms.gui.Platform import Platform
+from MainWindow import MainWindow
+from Dialogs import PreferencesDialog, AboutDialog, HotKeysDialog
+from FileDialogs import OpenFlowGraphFileDialog, SaveFlowGraphFileDialog, SaveImageFileDialog
+
+class ActionHandler:
+ """
+ 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):
+ """
+ ActionHandler 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.
+ @param file_paths a list of flow graph file passed from command line
+ @param platform platform module
+ """
+ self.clipboard = None
+ platform = Platform(platform)
+ if PY_GTK_ICON: gtk.window_set_default_icon_from_file(PY_GTK_ICON)
+ for action in Actions.ACTIONS_LIST: action.connect('activate', self._handle_actions)
+ #setup the main window
+ self.main_window = MainWindow(self.handle_states, 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_flow_graph = self.main_window.get_flow_graph
+ self.get_focus_flag = self.main_window.drawing_area.get_focus_flag
+ #setup the messages
+ Messages.register_messenger(self.main_window.add_report_line)
+ Messages.send_init()
+ #initialize
+ self.init_file_paths = file_paths
+ self.handle_states(Actions.APPLICATION_INITIALIZE)
+ #enter the mainloop
+ gtk.gdk.threads_init()
+ gtk.main()
+
+ def _handle_key_press(self, widget, event):
+ """
+ Handle key presses from the keyboard.
+ Translate key combos into actions.
+ Key combinations that do not include special keys, such as ctrl or Fcn*,
+ Also, require that the flow graph has mouse focus when choosing to handle keys.
+ @return true if the flow graph is in active use
+ """
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ ctrl = event.state & gtk.gdk.CONTROL_MASK
+ alt = event.state & gtk.gdk.MOD1_MASK
+ shift = event.state & gtk.gdk.SHIFT_MASK
+ #################### save/open/new/close ###############################
+ if ctrl and keyname == 's':
+ self.handle_states(Actions.FLOW_GRAPH_SAVE)
+ elif ctrl and keyname == 'o':
+ self.handle_states(Actions.FLOW_GRAPH_OPEN)
+ elif ctrl and keyname == 'n':
+ self.handle_states(Actions.FLOW_GRAPH_NEW)
+ elif ctrl and keyname == 'q':
+ self.handle_states(Actions.FLOW_GRAPH_CLOSE)
+ #################### Cut/Copy/Paste ###############################
+ elif self.get_focus_flag() and ctrl and keyname == 'x': #mouse focus
+ self.handle_states(Actions.BLOCK_CUT)
+ elif self.get_focus_flag() and ctrl and keyname == 'c': #mouse focus
+ self.handle_states(Actions.BLOCK_COPY)
+ elif self.get_focus_flag() and ctrl and keyname == 'v': #mouse focus
+ self.handle_states(Actions.BLOCK_PASTE)
+ #################### Undo/Redo ###############################
+ elif ctrl and keyname == 'z':
+ self.handle_states(Actions.FLOW_GRAPH_UNDO)
+ elif ctrl and keyname == 'y':
+ self.handle_states(Actions.FLOW_GRAPH_REDO)
+ #################### Delete ###############################
+ elif self.get_focus_flag() and keyname == 'Delete': #mouse focus
+ self.handle_states(Actions.ELEMENT_DELETE)
+ #################### Params ###############################
+ elif self.get_focus_flag() and keyname == 'Return': #mouse focus
+ self.handle_states(Actions.BLOCK_PARAM_MODIFY)
+ #################### Rotate ###############################
+ elif self.get_focus_flag() and keyname == 'Right': #mouse focus
+ self.handle_states(Actions.BLOCK_ROTATE_RIGHT)
+ elif self.get_focus_flag() and keyname == 'Left': #mouse focus
+ self.handle_states(Actions.BLOCK_ROTATE_LEFT)
+ #################### Enable/Disable ###############################
+ elif self.get_focus_flag() and keyname == 'e': #mouse focus
+ self.handle_states(Actions.BLOCK_ENABLE)
+ elif self.get_focus_flag() and keyname == 'd': #mouse focus
+ self.handle_states(Actions.BLOCK_DISABLE)
+ #################### Data Type ###############################
+ elif self.get_focus_flag() and keyname == 'Down': #mouse focus
+ self.handle_states(Actions.BLOCK_INC_TYPE)
+ elif self.get_focus_flag() and keyname == 'Up': #mouse focus
+ self.handle_states(Actions.BLOCK_DEC_TYPE)
+ #################### Port Controllers ###############################
+ elif self.get_focus_flag() and keyname in ('equal','plus', 'KP_Add'): #mouse focus
+ self.handle_states(Actions.PORT_CONTROLLER_INC)
+ elif self.get_focus_flag() and keyname in ('minus', 'KP_Subtract'): #mouse focus
+ self.handle_states(Actions.PORT_CONTROLLER_DEC)
+ #################### Gen/Exec/Stop/Print ###############################
+ elif keyname == 'F5':
+ self.handle_states(Actions.FLOW_GRAPH_GEN)
+ elif keyname == 'F6':
+ self.handle_states(Actions.FLOW_GRAPH_EXEC)
+ elif keyname == 'F7':
+ self.handle_states(Actions.FLOW_GRAPH_KILL)
+ elif keyname == 'Print':
+ self.handle_states(Actions.FLOW_GRAPH_SCREEN_CAPTURE)
+ #propagate this if the fg is not in focus or nothing is selected
+ return self.get_focus_flag() and self.get_flow_graph().is_selected()
+
+ def _quit(self, window, event):
+ """
+ Handle the delete event from the main window.
+ Generated by pressing X to close, alt+f4, or right click+close.
+ This method in turns calls the state handler to quit.
+ @return true
+ """
+ self.handle_states(Actions.APPLICATION_QUIT)
+ return True
+
+ def _handle_actions(self, event):
+ """
+ Handle all of the activate signals from the gtk actions.
+ The action signals derive from clicking on a toolbar or menu bar button.
+ Forward the action to the state handler.
+ """
+ self.handle_states(event.get_name())
+
+ def handle_states(self, state=''):
+ """
+ Handle the state changes in the GUI.
+ Handle all of the state changes that arise from the action handler or other gui and
+ inputs in the application. The state passed to the handle_states method is a string descriping
+ the change. A series of if/elif statements handle the state by greying out action buttons, causing
+ changes in the flow graph, saving/opening files... The handle_states method is passed to the
+ contructors of many of the classes used in this application enabling them to report any state change.
+ @param state a string describing the state change
+ """
+ #print state
+ ##################################################
+ # Initalize/Quit
+ ##################################################
+ if state == Actions.APPLICATION_INITIALIZE:
+ for action in Actions.ACTIONS_LIST: action.set_sensitive(False) #set all actions disabled
+ # enable a select few actions
+ for action in (
+ Actions.APPLICATION_QUIT, Actions.FLOW_GRAPH_NEW,
+ Actions.FLOW_GRAPH_OPEN, Actions.FLOW_GRAPH_SAVE_AS,
+ Actions.FLOW_GRAPH_CLOSE, Actions.ABOUT_WINDOW_DISPLAY,
+ Actions.HOTKEYS_WINDOW_DISPLAY, Actions.PREFS_WINDOW_DISPLAY,
+ Actions.FLOW_GRAPH_SCREEN_CAPTURE,
+ ): Actions.get_action_from_name(action).set_sensitive(True)
+ if not self.init_file_paths and Preferences.restore_files():
+ self.init_file_paths = Preferences.files_open()
+ if not self.init_file_paths: self.init_file_paths = ['']
+ for file_path in self.init_file_paths:
+ if file_path: self.main_window.new_page(file_path) #load pages from file paths
+ if Preferences.file_open() in self.init_file_paths:
+ self.main_window.new_page(Preferences.file_open(), show=True)
+ if not self.get_page(): self.main_window.new_page() #ensure that at least a blank page exists
+ elif state == Actions.APPLICATION_QUIT:
+ if self.main_window.close_pages():
+ gtk.main_quit()
+ exit(0)
+ ##################################################
+ # Selections
+ ##################################################
+ elif state == Actions.ELEMENT_SELECT:
+ self.get_flow_graph().update()
+ elif state == Actions.NOTHING_SELECT:
+ self.get_flow_graph().unselect()
+ self.get_flow_graph().update()
+ ##################################################
+ # Enable/Disable
+ ##################################################
+ elif state == Actions.BLOCK_ENABLE:
+ if self.get_flow_graph().enable_selected(True):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.BLOCK_DISABLE:
+ if self.get_flow_graph().enable_selected(False):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ ##################################################
+ # Cut/Copy/Paste
+ ##################################################
+ elif state == Actions.BLOCK_CUT:
+ self.handle_states(BLOCK_COPY)
+ self.handle_states(ELEMENT_DELETE)
+ elif state == Actions.BLOCK_COPY:
+ self.clipboard = self.get_flow_graph().copy_to_clipboard()
+ elif state == Actions.BLOCK_PASTE:
+ if self.clipboard:
+ self.get_flow_graph().paste_from_clipboard(self.clipboard)
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ ##################################################
+ # Move/Rotate/Delete/Create
+ ##################################################
+ elif state == Actions.BLOCK_MOVE:
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.BLOCK_ROTATE_LEFT:
+ if self.get_flow_graph().rotate_selected(DIR_LEFT):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.BLOCK_ROTATE_RIGHT:
+ if self.get_flow_graph().rotate_selected(DIR_RIGHT):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.ELEMENT_DELETE:
+ if self.get_flow_graph().remove_selected():
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.handle_states(Actions.NOTHING_SELECT)
+ self.get_page().set_saved(False)
+ elif state == Actions.ELEMENT_CREATE:
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.handle_states(Actions.NOTHING_SELECT)
+ self.get_page().set_saved(False)
+ elif state == Actions.BLOCK_INC_TYPE:
+ if self.get_flow_graph().type_controller_modify_selected(1):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.BLOCK_DEC_TYPE:
+ if self.get_flow_graph().type_controller_modify_selected(-1):
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.PORT_CONTROLLER_INC:
+ if self.get_flow_graph().port_controller_modify_selected(1):
+ self.get_flow_graph().update()
+ self.get_flow_graph().update() #2 times
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ elif state == Actions.PORT_CONTROLLER_DEC:
+ if self.get_flow_graph().port_controller_modify_selected(-1):
+ self.get_flow_graph().update()
+ self.get_flow_graph().update() #2 times
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ ##################################################
+ # Window stuff
+ ##################################################
+ elif state == Actions.PREFS_WINDOW_DISPLAY:
+ PreferencesDialog()
+ self.get_flow_graph().update()
+ elif state == Actions.ABOUT_WINDOW_DISPLAY:
+ AboutDialog()
+ elif state == Actions.HOTKEYS_WINDOW_DISPLAY:
+ HotKeysDialog()
+ ##################################################
+ # Param Modifications
+ ##################################################
+ elif state == Actions.BLOCK_PARAM_MODIFY:
+ if self.get_flow_graph().param_modify_selected():
+ self.get_flow_graph().update()
+ self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data())
+ self.get_page().set_saved(False)
+ ##################################################
+ # Undo/Redo
+ ##################################################
+ elif state == Actions.FLOW_GRAPH_UNDO:
+ n = self.get_page().get_state_cache().get_prev_state()
+ if n:
+ self.get_flow_graph().unselect()
+ self.get_flow_graph().import_data(n)
+ self.get_flow_graph().update()
+ self.get_page().set_saved(False)
+ elif state == Actions.FLOW_GRAPH_REDO:
+ n = self.get_page().get_state_cache().get_next_state()
+ if n:
+ self.get_flow_graph().unselect()
+ self.get_flow_graph().import_data(n)
+ self.get_flow_graph().update()
+ self.get_page().set_saved(False)
+ ##################################################
+ # New/Open/Save/Close
+ ##################################################
+ elif state == Actions.FLOW_GRAPH_NEW:
+ self.main_window.new_page()
+ elif state == Actions.FLOW_GRAPH_OPEN:
+ file_paths = OpenFlowGraphFileDialog(self.get_page().get_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):
+ self.main_window.new_page(file_path, show=(i==0))
+ elif state == Actions.FLOW_GRAPH_CLOSE:
+ self.main_window.close_page()
+ elif state == Actions.FLOW_GRAPH_SAVE:
+ if not self.get_page().get_file_path(): self.handle_states(Actions.FLOW_GRAPH_SAVE_AS)
+ else:
+ try:
+ ParseXML.to_file(self.get_flow_graph().export_data(), self.get_page().get_file_path())
+ self.get_page().set_saved(True)
+ except IOError:
+ Messages.send_fail_save(self.get_page().get_file_path())
+ self.get_page().set_saved(False)
+ elif state == Actions.FLOW_GRAPH_SAVE_AS:
+ file_path = SaveFlowGraphFileDialog(self.get_page().get_file_path()).run()
+ if file_path != None:
+ self.get_page().set_file_path(file_path)
+ self.handle_states(Actions.FLOW_GRAPH_SAVE)
+ elif state == Actions.FLOW_GRAPH_SCREEN_CAPTURE:
+ file_path = SaveImageFileDialog(self.get_page().get_file_path()).run()
+ if file_path != None:
+ pixmap = self.get_flow_graph().get_drawing_area().pixmap
+ width, height = pixmap.get_size()
+ pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, width, height)
+ pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), 0, 0, 0, 0, width, height)
+ pixbuf.save(file_path, IMAGE_FILE_EXTENSION[1:])
+ ##################################################
+ # Gen/Exec/Stop
+ ##################################################
+ elif state == Actions.FLOW_GRAPH_GEN:
+ if not self.get_page().get_pid():
+ if not self.get_page().get_saved() or not self.get_page().get_file_path():
+ self.handle_states(Actions.FLOW_GRAPH_SAVE) #only save if file path missing or not saved
+ if self.get_page().get_saved() and self.get_page().get_file_path():
+ generator = self.get_page().get_generator()
+ try:
+ Messages.send_start_gen(generator.get_file_path())
+ generator.write()
+ except Exception,e: Messages.send_fail_gen(e)
+ else: self.generator = None
+ elif state == Actions.FLOW_GRAPH_EXEC:
+ if not self.get_page().get_pid():
+ self.handle_states(Actions.FLOW_GRAPH_GEN)
+ if self.get_page().get_saved() and self.get_page().get_file_path():
+ ExecFlowGraphThread(self)
+ elif state == Actions.FLOW_GRAPH_KILL:
+ if self.get_page().get_pid():
+ try: os.kill(self.get_page().get_pid(), signal.SIGKILL)
+ except: print "could not kill pid: %s"%self.get_page().get_pid()
+ elif state == '': #pass and run the global actions
+ pass
+ else: print '!!! State "%s" not handled !!!'%state
+ ##################################################
+ # Global Actions for all States
+ ##################################################
+ #update general buttons
+ Actions.get_action_from_name(Actions.ELEMENT_DELETE).set_sensitive(bool(self.get_flow_graph().get_selected_elements()))
+ Actions.get_action_from_name(Actions.BLOCK_PARAM_MODIFY).set_sensitive(bool(self.get_flow_graph().get_selected_block()))
+ Actions.get_action_from_name(Actions.BLOCK_ROTATE_RIGHT).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ Actions.get_action_from_name(Actions.BLOCK_ROTATE_LEFT).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ #update cut/copy/paste
+ Actions.get_action_from_name(Actions.BLOCK_CUT).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ Actions.get_action_from_name(Actions.BLOCK_COPY).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ Actions.get_action_from_name(Actions.BLOCK_PASTE).set_sensitive(bool(self.clipboard))
+ #update enable/disable
+ Actions.get_action_from_name(Actions.BLOCK_ENABLE).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ Actions.get_action_from_name(Actions.BLOCK_DISABLE).set_sensitive(bool(self.get_flow_graph().get_selected_blocks()))
+ #set the exec and stop buttons
+ self.update_exec_stop()
+ #saved status
+ Actions.get_action_from_name(Actions.FLOW_GRAPH_SAVE).set_sensitive(not self.get_page().get_saved())
+ self.main_window.update()
+ #draw the flow graph
+ self.get_flow_graph().draw()
+
+ 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.get_flow_graph().is_valid() and not self.get_page().get_pid()
+ Actions.get_action_from_name(Actions.FLOW_GRAPH_GEN).set_sensitive(sensitive)
+ Actions.get_action_from_name(Actions.FLOW_GRAPH_EXEC).set_sensitive(sensitive)
+ Actions.get_action_from_name(Actions.FLOW_GRAPH_KILL).set_sensitive(self.get_page().get_pid() != None)
+
+class ExecFlowGraphThread(Thread):
+ """Execute the flow graph as a new process and wait on it to finish."""
+ def __init__ (self, action_handler):
+ """
+ ExecFlowGraphThread constructor.
+ @param action_handler an instance of an ActionHandler
+ """
+ Thread.__init__(self)
+ self.update_exec_stop = action_handler.update_exec_stop
+ self.flow_graph = action_handler.get_flow_graph()
+ #store page and dont use main window calls in run
+ self.page = action_handler.get_page()
+ Messages.send_start_exec(self.page.get_generator().get_file_path())
+ #get the popen
+ try:
+ self.p = self.page.get_generator().get_popen()
+ self.page.set_pid(self.p.pid)
+ #update
+ self.update_exec_stop()
+ self.start()
+ except Exception, e:
+ Messages.send_verbose_exec(str(e))
+ Messages.send_end_exec()
+
+ def run(self):
+ """Wait on the flow graph."""
+ #handle completion
+ r = "\n"
+ while(r):
+ gtk.gdk.threads_enter()
+ Messages.send_verbose_exec(r)
+ gtk.gdk.threads_leave()
+ r = os.read(self.p.stdout.fileno(), 1024)
+ gtk.gdk.threads_enter()
+ Messages.send_end_exec()
+ self.page.set_pid(None)
+ self.update_exec_stop()
+ gtk.gdk.threads_leave()
diff --git a/grc/src/gui/Actions.py b/grc/src/gui/Actions.py
new file mode 100644
index 000000000..ddd2573a8
--- /dev/null
+++ b/grc/src/gui/Actions.py
@@ -0,0 +1,103 @@
+"""
+Copyright 2007 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
+
+######################################################################################################
+# States
+######################################################################################################
+APPLICATION_INITIALIZE = 'app init'
+APPLICATION_QUIT = 'app quit'
+PARAM_MODIFY = 'param modify'
+BLOCK_MOVE = 'block move'
+BLOCK_ROTATE_LEFT = 'block rotate left'
+BLOCK_ROTATE_RIGHT = 'block rotate right'
+BLOCK_PARAM_MODIFY = 'block param modify'
+BLOCK_INC_TYPE = 'block increment type'
+BLOCK_DEC_TYPE = 'block decrement type'
+BLOCK_ENABLE = 'block enable'
+BLOCK_DISABLE = 'block disable'
+BLOCK_CUT = 'block cut'
+BLOCK_COPY = 'block copy'
+BLOCK_PASTE = 'block paste'
+PORT_CONTROLLER_INC = 'port controller increment'
+PORT_CONTROLLER_DEC = 'port controller decrement'
+ELEMENT_CREATE = 'element create'
+ELEMENT_DELETE = 'element delete'
+ELEMENT_SELECT = 'element select'
+NOTHING_SELECT = 'nothing select'
+FLOW_GRAPH_OPEN = 'flow graph open'
+FLOW_GRAPH_UNDO = 'flow graph undo'
+FLOW_GRAPH_REDO = 'flow graph redo'
+FLOW_GRAPH_SAVE = 'flow graph save'
+FLOW_GRAPH_SAVE_AS = 'flow graph save as'
+FLOW_GRAPH_CLOSE = 'flow graph close'
+FLOW_GRAPH_NEW = 'flow graph new'
+FLOW_GRAPH_GEN = 'flow graph gen'
+FLOW_GRAPH_EXEC = 'flow graph exec'
+FLOW_GRAPH_KILL = 'flow graph kill'
+FLOW_GRAPH_SCREEN_CAPTURE = 'flow graph screen capture'
+ABOUT_WINDOW_DISPLAY = 'about window display'
+HOTKEYS_WINDOW_DISPLAY = 'hotkeys window display'
+PREFS_WINDOW_DISPLAY = 'prefs window display'
+
+######################################################################################################
+# Actions
+######################################################################################################
+ACTIONS_LIST = (
+ gtk.Action(FLOW_GRAPH_NEW, '_New', 'Create a new flow graph', 'gtk-new'),
+ gtk.Action(FLOW_GRAPH_OPEN, '_Open', 'Open an existing flow graph', 'gtk-open'),
+ gtk.Action(FLOW_GRAPH_SAVE, '_Save', 'Save the current flow graph', 'gtk-save'),
+ gtk.Action(FLOW_GRAPH_SAVE_AS, 'Save _As', 'Save the current flow graph as...', 'gtk-save-as'),
+ gtk.Action(FLOW_GRAPH_CLOSE, '_Close', 'Close the current flow graph', 'gtk-close'),
+ gtk.Action(APPLICATION_QUIT, '_Quit', 'Quit program', 'gtk-quit'),
+ gtk.Action(FLOW_GRAPH_UNDO, '_Undo', 'Undo a change to the flow graph', 'gtk-undo'),
+ gtk.Action(FLOW_GRAPH_REDO, '_Redo', 'Redo a change to the flow graph', 'gtk-redo'),
+ gtk.Action(ELEMENT_DELETE, '_Delete', 'Delete the selected blocks', 'gtk-delete'),
+ gtk.Action(BLOCK_ROTATE_LEFT, 'Rotate _Left', 'Rotate the selected blocks 90 degrees', 'gtk-go-back'),
+ gtk.Action(BLOCK_ROTATE_RIGHT, 'Rotate _Right', 'Rotate the selected blocks -90 degrees', 'gtk-go-forward'),
+ gtk.Action(BLOCK_PARAM_MODIFY, '_Properties', 'Modify params for the selected block', 'gtk-properties'),
+ gtk.Action(BLOCK_ENABLE, 'E_nable', 'Enable the selected blocks', 'gtk-connect'),
+ gtk.Action(BLOCK_DISABLE, 'D_isable', 'Disable the selected blocks', 'gtk-disconnect'),
+ gtk.Action(BLOCK_CUT, 'Cu_t', 'Cut', 'gtk-cut'),
+ gtk.Action(BLOCK_COPY, '_Copy', 'Copy', 'gtk-copy'),
+ gtk.Action(BLOCK_PASTE, '_Paste', 'Paste', 'gtk-paste'),
+ gtk.Action(PREFS_WINDOW_DISPLAY, '_Preferences', 'Configure Preferences', 'gtk-preferences'),
+ gtk.Action(ABOUT_WINDOW_DISPLAY, '_About', 'About this program', 'gtk-about'),
+ gtk.Action(HOTKEYS_WINDOW_DISPLAY, '_HotKeys', 'Hot Keys', 'gtk-info'),
+ gtk.Action(FLOW_GRAPH_GEN, '_Generate', 'Generate the flow graph', 'gtk-convert'),
+ gtk.Action(FLOW_GRAPH_EXEC, '_Execute', 'Execute the flow graph', 'gtk-execute'),
+ gtk.Action(FLOW_GRAPH_KILL, '_Kill', 'Kill the flow graph', 'gtk-stop'),
+ gtk.Action(FLOW_GRAPH_SCREEN_CAPTURE, 'S_creen Capture', 'Create a screen capture of the flow graph', 'gtk-print'),
+)
+
+ACTIONS_DICT = dict((action.get_name(), action) for action in ACTIONS_LIST)
+
+def get_action_from_name(action_name):
+ """
+ Retrieve the action from the action list.
+ Search the list and find an action with said name.
+ @param action_name the action name(string)
+ @throw KeyError bad action name
+ @return a gtk action object
+ """
+ if ACTIONS_DICT.has_key(action_name): return ACTIONS_DICT[action_name]
+ raise KeyError('Action Name: "%s" does not exist'%action_name)
diff --git a/grc/src/gui/Bars.py b/grc/src/gui/Bars.py
new file mode 100644
index 000000000..915e5abec
--- /dev/null
+++ b/grc/src/gui/Bars.py
@@ -0,0 +1,139 @@
+"""
+Copyright 2007 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 Actions
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+##The list of actions for the toolbar.
+TOOLBAR_LIST = (
+ Actions.FLOW_GRAPH_NEW,
+ Actions.FLOW_GRAPH_OPEN,
+ Actions.FLOW_GRAPH_SAVE,
+ Actions.FLOW_GRAPH_CLOSE,
+ None,
+ 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.FLOW_GRAPH_GEN,
+ Actions.FLOW_GRAPH_EXEC,
+ Actions.FLOW_GRAPH_KILL,
+ None,
+ Actions.BLOCK_ROTATE_LEFT,
+ Actions.BLOCK_ROTATE_RIGHT,
+ None,
+ Actions.BLOCK_ENABLE,
+ Actions.BLOCK_DISABLE,
+)
+
+##The list of actions and categories for the menu bar.
+MENU_BAR_LIST = (
+ (gtk.Action('File', '_File', None, None), [
+ Actions.FLOW_GRAPH_NEW,
+ Actions.FLOW_GRAPH_OPEN,
+ None,
+ Actions.FLOW_GRAPH_SAVE,
+ Actions.FLOW_GRAPH_SAVE_AS,
+ 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,
+ None,
+ Actions.BLOCK_ROTATE_LEFT,
+ Actions.BLOCK_ROTATE_RIGHT,
+ None,
+ Actions.BLOCK_ENABLE,
+ Actions.BLOCK_DISABLE,
+ None,
+ Actions.BLOCK_PARAM_MODIFY,
+ ]),
+ (gtk.Action('Build', '_Build', None, None), [
+ Actions.FLOW_GRAPH_GEN,
+ Actions.FLOW_GRAPH_EXEC,
+ Actions.FLOW_GRAPH_KILL,
+ ]),
+ (gtk.Action('Options', '_Options', None, None), [
+ Actions.PREFS_WINDOW_DISPLAY,
+ ]),
+ (gtk.Action('Help', '_Help', None, None), [
+ Actions.ABOUT_WINDOW_DISPLAY,
+ Actions.HOTKEYS_WINDOW_DISPLAY,
+ ]),
+)
+
+class Toolbar(gtk.Toolbar):
+ """The gtk toolbar with actions added from the toolbar list."""
+
+ def __init__(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.
+ """
+ gtk.Toolbar.__init__(self)
+ self.set_style(gtk.TOOLBAR_ICONS)
+ for action_name in TOOLBAR_LIST:
+ if action_name: #add a tool item
+ action = Actions.get_action_from_name(action_name)
+ self.add(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'))
+ else: self.add(gtk.SeparatorToolItem())
+
+class MenuBar(gtk.MenuBar):
+ """The gtk menu bar with actions added from the menu bar list."""
+
+ def __init__(self):
+ """
+ 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)
+ for main_action,action_names in MENU_BAR_LIST:
+ #create the main menu item
+ main_menu_item = main_action.create_menu_item()
+ self.append(main_menu_item)
+ #create the menu
+ main_menu = gtk.Menu()
+ main_menu_item.set_submenu(main_menu)
+ for action_name in action_names:
+ if action_name: #append a menu item
+ action = Actions.get_action_from_name(action_name)
+ main_menu.append(action.create_menu_item())
+ else: main_menu.append(gtk.SeparatorMenuItem())
+ main_menu.show_all() #this show all is required for the separators to show
diff --git a/grc/src/gui/BlockTreeWindow.py b/grc/src/gui/BlockTreeWindow.py
new file mode 100644
index 000000000..291cc6f3b
--- /dev/null
+++ b/grc/src/gui/BlockTreeWindow.py
@@ -0,0 +1,150 @@
+"""
+Copyright 2007 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 BLOCK_SELECTION_WINDOW_WIDTH
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+
+NAME_INDEX = 0
+KEY_INDEX = 1
+
+class BlockTreeWindow(gtk.VBox):
+ """The block selection panel."""
+
+ def __init__(self, platform, get_flow_graph):
+ """
+ BlockTreeWindow constructor.
+ Create a tree view of the possible blocks in the platform.
+ The tree view nodes will be category names, the leaves will be block names.
+ A mouse double click or button press action will trigger the add block event.
+ @param platform the particular platform will all block prototypes
+ @param get_flow_graph get the selected flow graph
+ """
+ gtk.VBox.__init__(self)
+ self.platform = platform
+ self.get_flow_graph = get_flow_graph
+ #make the tree model for holding blocks
+ self.treestore = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+ self.treeview = gtk.TreeView(self.treestore)
+ self.treeview.set_enable_search(False) #disable pop up search box
+ self.treeview.add_events(gtk.gdk.BUTTON_PRESS_MASK)
+ self.treeview.connect('button_press_event', self._handle_mouse_button_press)
+ selection = self.treeview.get_selection()
+ selection.set_mode('single')
+ selection.connect('changed', self._handle_selection_change)
+ renderer = gtk.CellRendererText()
+ column = gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX)
+ self.treeview.append_column(column)
+ #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(BLOCK_SELECTION_WINDOW_WIDTH, -1)
+ self.pack_start(scrolled_window)
+ #add button
+ self.add_button = gtk.Button(None, 'gtk-add')
+ self.add_button.connect('clicked', self._handle_add_button)
+ self.pack_start(self.add_button, False)
+ #map categories to iters
+ self.categories = dict()
+ self.categories[tuple()] = None
+ #add blocks and categories
+ self.platform.load_block_tree(self)
+ #initialize
+ self._update_add_button()
+
+ ############################################################
+ ## Block Tree Methods
+ ############################################################
+ def add_block(self, category, block=None):
+ """
+ Add a block with category to this selection window.
+ Add only the category when block is None.
+ @param category the category string
+ @param block the block object or None
+ """
+ #rectify category
+ category = filter(lambda x: x, category.split('/'))
+ #add category and all sub categories
+ for i in range(len(category)):
+ sub_category = tuple(category[:i+1])
+ if sub_category not in self.categories.keys():
+ iter = self.treestore.insert_before(self.categories[tuple(category[:i])], None)
+ self.treestore.set_value(iter, NAME_INDEX, '[ %s ]'%category[i])
+ self.treestore.set_value(iter, KEY_INDEX, '')
+ self.categories[sub_category] = iter
+ #add block
+ if block is None: return
+ iter = self.treestore.insert_before(self.categories[tuple(category)], None)
+ self.treestore.set_value(iter, NAME_INDEX, block.get_name())
+ self.treestore.set_value(iter, KEY_INDEX, block.get_key())
+
+ ############################################################
+ ## Helper Methods
+ ############################################################
+ def _get_selected_block_key(self):
+ """
+ Get the currently selected block key.
+ @return the key of the selected block or a empty string
+ """
+ selection = self.treeview.get_selection()
+ treestore, iter = selection.get_selected()
+ return iter and treestore.get_value(iter, KEY_INDEX) or ''
+
+ def _update_add_button(self):
+ """
+ Update the add button's sensitivity.
+ The button should be active only if a block is selected.
+ """
+ key = self._get_selected_block_key()
+ self.add_button.set_sensitive(bool(key))
+
+ 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)
+
+ ############################################################
+ ## Event Handlers
+ ############################################################
+ 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()
+
+ def _handle_selection_change(self, selection):
+ """
+ Handle a selection change in the tree view.
+ If a selection changes, set the add button sensitive.
+ """
+ self._update_add_button()
+
+ def _handle_add_button(self, widget):
+ """
+ Handle the add button clicked signal.
+ Call add selected block.
+ """
+ self._add_selected_block()
diff --git a/grc/src/gui/Constants.py b/grc/src/gui/Constants.py
new file mode 100644
index 000000000..2f6af9a1f
--- /dev/null
+++ b/grc/src/gui/Constants.py
@@ -0,0 +1,82 @@
+"""
+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
+"""
+
+##################################################
+## Global Titles
+##################################################
+
+##The name to appear in the main window for a flow graph that has not been saved to file.
+NEW_FLOGRAPH_TITLE = 'untitled'
+
+##The prefix title on the main window.
+MAIN_WINDOW_PREFIX = "GRC"
+
+##################################################
+## Signal block rotations
+##################################################
+
+##direction of rotation left.
+DIR_LEFT = 'left'
+
+##direction of rotation right.
+DIR_RIGHT = 'right'
+
+##################################################
+## Dimension constraints for the various windows (in pixels)
+##################################################
+
+##main window constraints
+MIN_WINDOW_WIDTH = 600
+MIN_WINDOW_HEIGHT = 400
+
+##dialog constraints
+MIN_DIALOG_WIDTH = 500
+MIN_DIALOG_HEIGHT = 500
+
+##static height of reports window
+REPORTS_WINDOW_HEIGHT = 100
+
+##static width of block selection window
+BLOCK_SELECTION_WINDOW_WIDTH = 200
+
+##################################################
+## Dragging, scrolling, and redrawing constants for the flow graph window in pixels
+##################################################
+
+##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
+
+##When the window has to be scrolled, move it this distance in the required direction.
+SCROLL_DISTANCE = 15
+
+##The redrawing sensitivity, how many seconds must pass between motion events before a redraw?
+MOTION_DETECT_REDRAWING_SENSITIVITY = .02
+
+##How close the mouse click can be to a connection and register a connection select.
+CONNECTION_SELECT_SENSITIVITY = 5
+
+##################################################
+# A state is recorded for each change to the flow graph, the size dictates how many states we can record
+##################################################
+
+##The size of the state saving cache in the flow graph (for undo/redo functionality)
+STATE_CACHE_SIZE = 42
diff --git a/grc/src/gui/Dialogs.py b/grc/src/gui/Dialogs.py
new file mode 100644
index 000000000..f7ba2954b
--- /dev/null
+++ b/grc/src/gui/Dialogs.py
@@ -0,0 +1,146 @@
+"""
+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 pygtk
+pygtk.require('2.0')
+import gtk
+from Constants import \
+ MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT, \
+ MAIN_WINDOW_PREFIX
+from .. platforms.base.Constants import VERSION
+import Preferences
+
+class TextDisplay(gtk.TextView):
+ """A non editable gtk text view."""
+
+ def __init__(self, text=''):
+ """
+ TextDisplay constructor.
+ @param text the text to display (string)
+ """
+ text_buffer = gtk.TextBuffer()
+ text_buffer.set_text(text)
+ self.set_text = text_buffer.set_text
+ self.insert = lambda line: text_buffer.insert(text_buffer.get_end_iter(), line)
+ gtk.TextView.__init__(self, text_buffer)
+ self.set_editable(False)
+ self.set_cursor_visible(False)
+ self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+
+class PreferencesDialog(gtk.Dialog):
+ """A dialog box to display the preferences."""
+
+ def __init__(self):
+ """PreferencesDialog constructor."""
+ gtk.Dialog.__init__(self, buttons=('gtk-close', gtk.RESPONSE_CLOSE))
+ self.set_title("Preferences")
+ self.set_size_request(MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT)
+ notebook = gtk.Notebook()
+ for title,desc,params in Preferences.get_preferences():
+ vbox = gtk.VBox()
+ vbox.pack_start(gtk.Label(''), False) #blank label for spacing
+ for param in params: vbox.pack_start(param.get_input_object(), False)
+ desc = desc.strip('\n')
+ if desc: vbox.pack_start(TextDisplay(desc), False, padding=5)
+ notebook.append_page(vbox, gtk.Label(title))
+ self.vbox.pack_start(notebook, True)
+ self.show_all()
+ self.run()
+ self.destroy()
+
+def MessageDialogHelper(type, buttons, title=None, markup=None):
+ """
+ Create a modal message dialog and run it.
+ @param type the type of message: gtk.MESSAGE_INFO, gtk.MESSAGE_WARNING, gtk.MESSAGE_QUESTION or gtk.MESSAGE_ERROR
+ @param 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
+ @param tittle the title of the window (string)
+ @param markup the message text with pango markup
+ @return the gtk response from run()
+ """
+ message_dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, type, buttons)
+ if title != None: message_dialog.set_title(title)
+ if markup != None: message_dialog.set_markup(markup)
+ response = message_dialog.run()
+ message_dialog.destroy()
+ return response
+
+class AboutDialog(gtk.AboutDialog):
+ """A cute little about dialog."""
+
+ def __init__(self):
+ """AboutDialog constructor."""
+ gtk.AboutDialog.__init__(self)
+ self.set_version(VERSION)
+ self.set_name(MAIN_WINDOW_PREFIX)
+ self.set_license(__doc__)
+ self.set_copyright('Copyright 2008 Free Software Foundation, Inc.')
+ self.set_website('http://gnuradio.org/trac/wiki/GNURadioCompanion')
+ self.set_comments("""\
+Thank you to all those from the mailing list who tested GNU Radio Companion and offered advice.
+--
+Special Thanks:
+A. Brinton Cooper -> starting the project
+CER Technology Fellowship Grant -> initial funding
+William R. Kenan Jr. Fund -> usrp & computers
+Patrick Strasser -> the GRC icon
+Achilleas Anastasopoulos -> trellis support
+--""")
+ self.run()
+ self.destroy()
+
+class HotKeysDialog(gtk.Dialog):
+ """Display each action with the associated hotkey."""
+
+ def __init__(self):
+ """HotKeysDialog constructor."""
+ gtk.Dialog.__init__(self, buttons=('gtk-close', gtk.RESPONSE_CLOSE))
+ self.set_title('Hot Keys')
+ markup = ''
+ for action, hotkey in (
+ ('New Flow Graph', 'Ctrl + n'),
+ ('Open Flow Graph', 'Ctrl + o'),
+ ('Save Flow Graph', 'Ctrl + s'),
+ ('Close Flow Graph', 'Ctrl + q'),
+ ('Cut Block', 'Ctrl + x'),
+ ('Copy Block', 'Ctrl + c'),
+ ('Paste Block', 'Ctrl + v'),
+ ('Undo Change', 'Ctrl + z'),
+ ('Redo Change', 'Ctrl + y'),
+ ('Delete Block', 'Delete'),
+ ('Modify Parameters', 'Enter'),
+ ('Rotate Block', 'Right'),
+ ('Rotate Block', 'Left'),
+ ('Enable Block', 'e'),
+ ('Disable Block', 'd'),
+ ('Modify Data Type', 'Up'),
+ ('Modify Data Type', 'Down'),
+ ('Add a Port', '+'),
+ ('Remove a Port', '-'),
+ ('Flow Graph Generate', 'F5'),
+ ('Flow Graph Execute', 'F6'),
+ ('Flow Graph Kill', 'F7'),
+ ('Screen Shot', 'PrintScreen'),
+ ): markup = '%s\n<b>%s:</b>%s'%(markup, action, hotkey.rjust(25-len(action), ' '))
+ label = gtk.Label()
+ label.set_markup('<tt>%s</tt>\n'%markup) #append newline
+ self.vbox.pack_start(label, False)
+ self.show_all()
+ self.run()
+ self.destroy()
diff --git a/grc/src/gui/DrawingArea.py b/grc/src/gui/DrawingArea.py
new file mode 100644
index 000000000..3588e122d
--- /dev/null
+++ b/grc/src/gui/DrawingArea.py
@@ -0,0 +1,118 @@
+"""
+Copyright 2007 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 Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT
+
+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.
+ """
+
+ def __init__(self, main_window):
+ """
+ DrawingArea contructor.
+ Connect event handlers.
+ @param main_window the main_window containing all flow graphs
+ """
+ self.ctrl_mask = False
+ self._main_window = main_window
+ #inject drawing area into main_window
+ self._main_window.drawing_area = self
+ gtk.DrawingArea.__init__(self)
+ self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
+ self.connect('expose-event', self._handle_window_expose)
+ 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.set_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
+ )
+ #setup the focus flag
+ self._focus_flag = False
+ self.get_focus_flag = lambda: self._focus_flag
+ self.connect("leave-notify-event", self._handle_focus_event, False)
+ self.connect("enter-notify-event", self._handle_focus_event, True)
+ #pixmap for drawing
+ self.pixmap = None
+ self.gc = None
+
+ def draw(self):
+ """
+ Draw the pixmap onto this drawing area.
+ """
+ self.window.draw_drawable(self.gc, self.pixmap, 0, 0, 0, 0, -1, -1)
+
+ ##########################################################################
+ ## Handlers
+ ##########################################################################
+ def _handle_focus_event(self, widget, event, focus_flag):
+ """Record the focus state of the flow graph window."""
+ self._focus_flag = focus_flag
+
+ def _handle_mouse_button_press(self, widget, event):
+ """
+ Forward button click information to the flow graph.
+ """
+ self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK
+ self._main_window.get_flow_graph().handle_mouse_button_press(
+ left_click=(event.button == 1),
+ double_click=(event.type == gtk.gdk._2BUTTON_PRESS),
+ coordinate=(event.x, event.y),
+ )
+ return True
+
+ 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._main_window.get_flow_graph().handle_mouse_button_release(
+ left_click=(event.button == 1),
+ coordinate=(event.x, event.y),
+ )
+ return True
+
+ 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._main_window.get_flow_graph().handle_mouse_motion(
+ coordinate=(event.x, event.y),
+ )
+ return True
+
+ def _handle_window_expose(self, widget, event):
+ """
+ Called when the window initially appears or is resized: create a new pixmap, draw the flow graph.
+ """
+ self.gc = self.window.new_gc()
+ width, height = self.get_size_request()
+ if not self.pixmap or (width, height) != self.pixmap.get_size():
+ self.pixmap = gtk.gdk.Pixmap(self.window, width, height, -1)
+ self._main_window.get_flow_graph().draw()
+ return True
diff --git a/grc/src/gui/FileDialogs.py b/grc/src/gui/FileDialogs.py
new file mode 100644
index 000000000..320c1161c
--- /dev/null
+++ b/grc/src/gui/FileDialogs.py
@@ -0,0 +1,154 @@
+"""
+Copyright 2007 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 Dialogs import MessageDialogHelper
+from Constants import NEW_FLOGRAPH_TITLE
+from .. platforms.base.Constants import \
+ DEFAULT_FILE_PATH, FLOW_GRAPH_FILE_EXTENSION, \
+ IMAGE_FILE_EXTENSION
+from os import path
+
+OPEN_FLOW_GRAPH = 'open flow graph'
+SAVE_FLOW_GRAPH = 'save flow graph'
+SAVE_IMAGE = 'save image'
+
+##the filter for flow graph files
+FLOW_GRAPH_FILE_FILTER = gtk.FileFilter()
+FLOW_GRAPH_FILE_FILTER.set_name('GRC Files')
+FLOW_GRAPH_FILE_FILTER.add_pattern('*'+FLOW_GRAPH_FILE_EXTENSION)
+FLOW_GRAPH_FILE_FILTER.add_pattern('*.xml') #TEMP
+
+##the filter for image files
+IMAGE_FILE_FILTER = gtk.FileFilter()
+IMAGE_FILE_FILTER.set_name('Image Files')
+IMAGE_FILE_FILTER.add_pattern('*'+IMAGE_FILE_EXTENSION)
+
+##the filter for all files
+ALL_FILE_FILTER = gtk.FileFilter()
+ALL_FILE_FILTER.set_name('All Files')
+ALL_FILE_FILTER.add_pattern('*')
+
+class FileDialogHelper(gtk.FileChooserDialog):
+ """
+ A wrapper class for the gtk file chooser dialog.
+ Implement a file chooser dialog with only necessary parameters.
+ """
+
+ def __init__(self, action, title):
+ """
+ FileDialogHelper contructor.
+ Create a save or open dialog with cancel and ok buttons.
+ Use standard settings: no multiple selection, local files only, and the * filter.
+ @param action gtk.FILE_CHOOSER_ACTION_OPEN or gtk.FILE_CHOOSER_ACTION_SAVE
+ @param 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))
+ self.set_select_multiple(False)
+ self.set_local_only(True)
+ self.add_filter(ALL_FILE_FILTER)
+
+class FileDialog(FileDialogHelper):
+ """A dialog box to save or open flow graph files. This is a base class, do not use."""
+
+ def __init__(self, current_file_path=''):
+ """
+ FileDialog constructor.
+ @param 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 + FLOW_GRAPH_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(FLOW_GRAPH_FILE_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(FLOW_GRAPH_FILE_FILTER)
+ self.set_current_name(path.basename(current_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(IMAGE_FILE_FILTER)
+ current_file_path = current_file_path + IMAGE_FILE_EXTENSION
+ self.set_current_name(path.basename(current_file_path)) #show the current filename
+ 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.
+ @param filter a gtk file filter.
+ """
+ self.add_filter(filter)
+ self.set_filter(filter)
+
+ def get_rectified_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.
+ If the file name with the extension already exists, show a overwrite dialog.
+ If this is an open dialog, return a list of filenames.
+ @return 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_IMAGE):
+ filename = self.get_filename()
+ for extension, filter in (
+ (FLOW_GRAPH_FILE_EXTENSION, FLOW_GRAPH_FILE_FILTER),
+ (IMAGE_FILE_EXTENSION, IMAGE_FILE_FILTER),
+ ): #append the missing file extension if the filter matches
+ if filename[len(filename)-len(extension):] != extension \
+ and filter == self.get_filter(): 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!',
+ 'File <b>"%s"</b> Exists!\nWould you like to overwrite the existing file?'%filename,
+ ) == gtk.RESPONSE_NO: return self.get_rectified_filename()
+ return filename
+ #############################################
+ # Handle Open Dialogs
+ #############################################
+ elif self.type in (OPEN_FLOW_GRAPH,):
+ 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!',
+ 'File <b>"%s"</b> Does not Exist!'%filename,
+ )
+ return self.get_rectified_filename()
+ return filenames
+
+ def run(self):
+ """
+ Get the filename and destroy the dialog.
+ @return the filename or None if a close/cancel occured.
+ """
+ filename = self.get_rectified_filename()
+ self.destroy()
+ return filename
+
+class OpenFlowGraphFileDialog(FileDialog): type = OPEN_FLOW_GRAPH
+class SaveFlowGraphFileDialog(FileDialog): type = SAVE_FLOW_GRAPH
+class SaveImageFileDialog(FileDialog): type = SAVE_IMAGE
diff --git a/grc/src/gui/MainWindow.py b/grc/src/gui/MainWindow.py
new file mode 100644
index 000000000..e4c60fce5
--- /dev/null
+++ b/grc/src/gui/MainWindow.py
@@ -0,0 +1,321 @@
+"""
+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
+"""
+
+from Constants import \
+ MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, \
+ NEW_FLOGRAPH_TITLE, MAIN_WINDOW_PREFIX, \
+ REPORTS_WINDOW_HEIGHT
+from .. platforms.base.Constants import FLOW_GRAPH_FILE_EXTENSION
+from Actions import APPLICATION_QUIT, FLOW_GRAPH_KILL
+import pygtk
+pygtk.require('2.0')
+import gtk
+import Bars
+from BlockTreeWindow import BlockTreeWindow
+from Dialogs import TextDisplay, MessageDialogHelper
+from DrawingArea import DrawingArea
+from NotebookPage import NotebookPage
+import Preferences
+import Messages
+import os
+
+############################################################
+# Main window
+############################################################
+
+class MainWindow(gtk.Window):
+ """The topmost window with menus, the tool bar, and other major windows."""
+
+ def __init__(self, handle_states, platform):
+ """
+ MainWindow contructor.
+ @param handle_states the callback function
+ """
+ self._platform = platform
+ #setup window
+ self.handle_states = handle_states
+ gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+ vbox = gtk.VBox()
+ hbox = gtk.HBox()
+ self.add(vbox)
+ #create the menu bar and toolbar
+ vbox.pack_start(Bars.MenuBar(), False)
+ vbox.pack_start(Bars.Toolbar(), False)
+ #setup scrolled window
+ 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.drawing_area = DrawingArea(self)
+ self.scrolled_window.add_with_viewport(self.drawing_area)
+ #create the notebook
+ self.notebook = gtk.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)
+ fg_and_report_box = gtk.VBox(False, 0)
+ fg_and_report_box.pack_start(self.notebook, False, False, 0)
+ fg_and_report_box.pack_start(self.scrolled_window)
+ hbox.pack_start(fg_and_report_box)
+ vbox.pack_start(hbox)
+ #create the side windows
+ side_box = gtk.VBox()
+ hbox.pack_start(side_box, False)
+ side_box.pack_start(BlockTreeWindow(platform, self.get_flow_graph)) #allow resize, selection window can have more space
+ #create the reports window
+ self.text_display = TextDisplay()
+ #house the reports in a scrolled window
+ self.reports_scrolled_window = gtk.ScrolledWindow()
+ self.reports_scrolled_window.set_size_request(-1, REPORTS_WINDOW_HEIGHT)
+ self.reports_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ self.reports_scrolled_window.add_with_viewport(self.text_display)
+ fg_and_report_box.pack_end(self.reports_scrolled_window, False) #dont allow resize, fg should get all the space
+ #show all but the main window container and the reports window
+ vbox.show_all()
+ self.notebook.hide()
+ self._show_reports_window(False)
+ # load preferences and show the main window
+ Preferences.load(platform)
+ self.resize(*Preferences.window_size())
+ self.show()#show after resize in preferences
+
+ ############################################################
+ # Event Handlers
+ ############################################################
+
+ def _quit(self, window, event):
+ """
+ Handle the delete event from the main window.
+ Generated by pressing X to close, alt+f4, or right click+close.
+ This method in turns calls the state handler to quit.
+ @return true
+ """
+ self.handle_states(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.
+ @param notebook the notebook
+ @param page new page
+ @param page_num new page number
+ """
+ self.current_page = self.notebook.get_nth_page(page_num)
+ Messages.send_page_switch(self.current_page.get_file_path())
+ self.handle_states()
+
+ ############################################################
+ # Report Window
+ ############################################################
+
+ def add_report_line(self, line):
+ """
+ Place line at the end of the text buffer, then scroll its window all the way down.
+ @param line the new text
+ """
+ self.text_display.insert(line)
+ vadj = self.reports_scrolled_window.get_vadjustment()
+ vadj.set_value(vadj.upper)
+ vadj.emit('changed')
+
+ def _show_reports_window(self, show):
+ """
+ Show the reports window when show is True.
+ Hide the reports window when show is False.
+ @param show boolean flag
+ """
+ if show: self.reports_scrolled_window.show()
+ else: self.reports_scrolled_window.hide()
+
+ ############################################################
+ # Pages: create and close
+ ############################################################
+
+ def new_page(self, file_path='', show=False):
+ """
+ Create a new notebook page.
+ Set the tab to be selected.
+ @param file_path optional file to load into the flow graph
+ @param show true if the page should be shown after loading
+ """
+ #if the file is already open, show the open page and return
+ if file_path and file_path in self._get_files(): #already open
+ page = self.notebook.get_nth_page(self._get_files().index(file_path))
+ self._set_page(page)
+ return
+ try: #try to load from file
+ if file_path: Messages.send_start_load(file_path)
+ flow_graph = self._platform.get_new_flow_graph()
+ #inject drawing area and handle states into flow graph
+ flow_graph.drawing_area = self.drawing_area
+ flow_graph.handle_states = self.handle_states
+ page = NotebookPage(
+ self,
+ flow_graph=flow_graph,
+ file_path=file_path,
+ )
+ if file_path: Messages.send_end_load()
+ except Exception, e: #return on failure
+ Messages.send_fail_load(e)
+ 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)
+ #only show if blank or manual
+ if not file_path or show: self._set_page(page)
+
+ def close_pages(self):
+ """
+ Close all the pages in this notebook.
+ @return true if all closed
+ """
+ open_files = filter(lambda file: file, self._get_files()) #filter blank files
+ open_file = self.get_page().get_file_path()
+ #close each page
+ for page in self._get_pages():
+ self.page_to_be_closed = page
+ self.close_page(False)
+ if self.notebook.get_n_pages(): return False
+ #save state before closing
+ Preferences.files_open(open_files)
+ Preferences.file_open(open_file)
+ Preferences.window_size(self.get_size())
+ Preferences.save()
+ return True
+
+ def close_page(self, ensure=True):
+ """
+ Close the current page.
+ If the notebook becomes empty, and ensure is true,
+ call new page upon exit to ensure that at least one page exists.
+ @param ensure boolean
+ """
+ if not self.page_to_be_closed: self.page_to_be_closed = self.get_page()
+ #show the page if it has an executing flow graph or is unsaved
+ if self.page_to_be_closed.get_pid() or not self.page_to_be_closed.get_saved():
+ self._set_page(self.page_to_be_closed)
+ #unsaved? ask the user
+ if not self.page_to_be_closed.get_saved() and self._save_changes():
+ self.handle_states(FLOW_GRAPH_SAVE) #try to save
+ if not self.page_to_be_closed.get_saved(): #still unsaved?
+ self.page_to_be_closed = None #set the page to be closed back to None
+ return
+ #stop the flow graph if executing
+ if self.page_to_be_closed.get_pid(): self.handle_states(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
+ self.page_to_be_closed = None #set the page to be closed back to None
+
+ ############################################################
+ # Misc
+ ############################################################
+
+ def update(self):
+ """
+ Set the title of the main window.
+ Set the titles on the page tabs.
+ Show/hide the reports window.
+ @param title the window title
+ """
+ if self.get_page():
+ title = ''.join((
+ MAIN_WINDOW_PREFIX,
+ ' - Editing: ',
+ (self.get_page().get_file_path() or NEW_FLOGRAPH_TITLE),
+ (self.get_page().get_saved() and ' ' or '*'), #blank must be non empty
+ )
+ )
+ else: title = MAIN_WINDOW_PREFIX + ' - Editor '
+ gtk.Window.set_title(self, title)
+ #set tab titles
+ for page in self._get_pages():
+ title = os.path.basename(page.get_file_path())
+ #strip file extension #TEMP
+ if title.endswith('.xml'):
+ title = title[0:-len('.xml')]
+ #strip file extension
+ if title.endswith(FLOW_GRAPH_FILE_EXTENSION):
+ title = title[0:-len(FLOW_GRAPH_FILE_EXTENSION)]
+ page.set_text(''.join((
+ (title or NEW_FLOGRAPH_TITLE),
+ (page.get_saved() and ' ' or '*'), #blank must be non empty
+ )
+ )
+ )
+ #reports window
+ self._show_reports_window(Preferences.show_reports_window())
+ #show/hide notebook tabs
+ if len(self._get_pages()) > 1: self.notebook.show()
+ else: self.notebook.hide()
+
+ def get_page(self):
+ """
+ Get the selected page.
+ @return the selected page
+ """
+ return self.current_page
+
+ def get_flow_graph(self):
+ """
+ Get the selected flow graph.
+ @return the selected flow graph
+ """
+ return self.get_page().get_flow_graph()
+
+ ############################################################
+ # Helpers
+ ############################################################
+
+ def _set_page(self, page):
+ """
+ Set the current page.
+ @param page the page widget
+ """
+ self.current_page = page
+ self.notebook.set_current_page(self.notebook.page_num(self.current_page))
+
+ def _save_changes(self):
+ """
+ Save changes to flow graph?
+ @return true if yes
+ """
+ return MessageDialogHelper(
+ gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, 'Unsaved Changes!',
+ 'Would you like to save changes before closing?'
+ ) == gtk.RESPONSE_YES
+
+ def _get_files(self):
+ """
+ Get the file names for all the pages, in order.
+ @return list of file paths
+ """
+ return map(lambda page: page.get_file_path(), self._get_pages())
+
+ def _get_pages(self):
+ """
+ Get a list of all pages in the notebook.
+ @return list of pages
+ """
+ return [self.notebook.get_nth_page(page_num) for page_num in range(self.notebook.get_n_pages())]
diff --git a/grc/src/gui/Makefile.am b/grc/src/gui/Makefile.am
new file mode 100644
index 000000000..a6639af44
--- /dev/null
+++ b/grc/src/gui/Makefile.am
@@ -0,0 +1,41 @@
+#
+# Copyright 2008 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.
+#
+
+include $(top_srcdir)/grc/Makefile.inc
+
+ourpythondir = $(grc_src_prefix)/gui
+
+ourpython_PYTHON = \
+ ActionHandler.py \
+ Actions.py \
+ Bars.py \
+ BlockTreeWindow.py \
+ Constants.py \
+ Dialogs.py \
+ DrawingArea.py \
+ FileDialogs.py \
+ MainWindow.py \
+ Messages.py \
+ NotebookPage.py \
+ ParamsDialog.py \
+ Preferences.py \
+ StateCache.py \
+ __init__.py
diff --git a/grc/src/gui/Messages.py b/grc/src/gui/Messages.py
new file mode 100644
index 000000000..1d9a80211
--- /dev/null
+++ b/grc/src/gui/Messages.py
@@ -0,0 +1,101 @@
+"""
+Copyright 2007 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 .. platforms.base.Constants import VERSION
+import traceback
+import sys
+
+## A list of functions that can receive a message.
+MESSENGERS_LIST = list()
+
+def register_messenger(messenger):
+ """
+ Append the given messenger to the list of messengers.
+ @param messenger a method thats takes a string
+ """
+ MESSENGERS_LIST.append(messenger)
+
+def send(message):
+ """
+ Give the message to each of the messengers.
+ @param message a message string
+ """
+ for messenger in MESSENGERS_LIST: messenger(message)
+
+#register stdout by default
+register_messenger(sys.stdout.write)
+
+###########################################################################
+# Special functions for specific program functionalities
+###########################################################################
+def send_init():
+ send("""<<< Welcome to GRC %s >>>\n"""%VERSION)
+
+def send_page_switch(file_path):
+ send('\nShowing: "%s"\n'%file_path)
+
+################# functions for loading flow graphs ########################################
+def send_start_load(file_path):
+ send('\nLoading: "%s"'%file_path + '\n')
+
+def send_error_load(error):
+ send('>>> Error: %s\n'%error)
+ traceback.print_exc()
+
+def send_end_load():
+ send(">>> Done\n")
+
+def send_fail_load(error):
+ send('Parser Error: %s\n'%error)
+ send(">>> Failue\n")
+ traceback.print_exc()
+
+################# functions for generating flow graphs ########################################
+def send_start_gen(file_path):
+ send('\nGenerating: "%s"'%file_path + '\n')
+
+def send_fail_gen(error):
+ send('Generate Error: %s\n'%error)
+ send(">>> Failue\n")
+ traceback.print_exc()
+
+################# functions for executing flow graphs ########################################
+def send_start_exec(file_path):
+ send('\nExecuting: "%s"'%file_path + '\n')
+
+def send_verbose_exec(verbose):
+ send(verbose)
+
+def send_end_exec():
+ send("\n>>> Done\n")
+
+################# functions for saving flow graphs ########################################
+def send_fail_save(file_path):
+ send('>>> Error: Cannot save: %s\n'%file_path)
+
+################# functions for connections ########################################
+def send_fail_connection():
+ send('>>> Warning: A connection can only be created between a source and an unconnected sink.\n')
+
+################# functions for preferences ########################################
+def send_fail_load_preferences(prefs_file_path):
+ send('>>> Error: Cannot load preferences file: "%s"\n'%prefs_file_path)
+
+def send_fail_save_preferences(prefs_file_path):
+ send('>>> Error: Cannot save preferences file: "%s"\n'%prefs_file_path)
diff --git a/grc/src/gui/NotebookPage.py b/grc/src/gui/NotebookPage.py
new file mode 100644
index 000000000..a25243b18
--- /dev/null
+++ b/grc/src/gui/NotebookPage.py
@@ -0,0 +1,172 @@
+"""
+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
+"""
+
+from Actions import FLOW_GRAPH_CLOSE
+import pygtk
+pygtk.require('2.0')
+import gtk
+from .. utils import ParseXML
+from StateCache import StateCache
+from .. platforms.base.Constants import FLOW_GRAPH_DTD
+import os
+
+############################################################
+## Notebook Page
+############################################################
+
+class NotebookPage(gtk.HBox):
+ """A page in the notebook."""
+
+ def __init__(self, main_window, flow_graph, file_path=''):
+ """
+ Page constructor.
+ @param main_window main window
+ @param file_path path to a flow graph file
+ """
+ self._flow_graph = flow_graph
+ self.set_pid(None)
+ #import the file
+ self.main_window = main_window
+ self.set_file_path(file_path)
+ file_path = file_path or flow_graph.get_parent().get_default_flow_graph()
+ ############################################################
+ from .. utils import converter
+ converter.convert(file_path, flow_graph.get_parent())
+ ############################################################
+ ParseXML.validate_dtd(file_path, FLOW_GRAPH_DTD)
+ initial_state = ParseXML.from_file(file_path)
+ self.state_cache = StateCache(initial_state)
+ self.set_saved(True)
+ #import the data to the flow graph
+ self.get_flow_graph().import_data(initial_state)
+ self.get_flow_graph().update()
+ #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()
+
+ def get_generator(self):
+ """
+ Get the generator object for this flow graph.
+ @return generator
+ """
+ return self.get_flow_graph().get_parent().get_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.
+ @param the button
+ """
+ self.main_window.page_to_be_closed = self
+ self.main_window.handle_states(FLOW_GRAPH_CLOSE)
+
+ def set_text(self, text):
+ """
+ Set the text in this label.
+ @param text the new text
+ """
+ self.label.set_text(text)
+
+ def get_tab(self):
+ """
+ Get the gtk widget for this page's tab.
+ @return gtk widget
+ """
+ return self.tab
+
+ def get_pid(self):
+ """
+ Get the pid for the flow graph.
+ @return the pid number
+ """
+ return self.pid
+
+ def set_pid(self, pid):
+ """
+ Set the pid number.
+ @param pid the new pid number
+ """
+ self.pid = pid
+
+ def get_flow_graph(self):
+ """
+ Get the flow graph.
+ @return the flow graph
+ """
+ return self._flow_graph
+
+ def get_file_path(self):
+ """
+ Get the file path for the flow graph.
+ @return the file path or ''
+ """
+ return self.file_path
+
+ def set_file_path(self, file_path=''):
+ """
+ Set the file path, '' for no file path.
+ @param file_path file path string
+ """
+ if file_path: self.file_path = os.path.abspath(file_path)
+ else: self.file_path = ''
+
+ def get_saved(self):
+ """
+ Get the saved status for the flow graph.
+ @return true if saved
+ """
+ return self.saved
+
+ def set_saved(self, saved=True):
+ """
+ Set the saved status.
+ @param saved boolean status
+ """
+ self.saved = saved
+
+ def get_state_cache(self):
+ """
+ Get the state cache for the flow graph.
+ @return the state cache
+ """
+ return self.state_cache
diff --git a/grc/src/gui/ParamsDialog.py b/grc/src/gui/ParamsDialog.py
new file mode 100644
index 000000000..46940db2e
--- /dev/null
+++ b/grc/src/gui/ParamsDialog.py
@@ -0,0 +1,131 @@
+"""
+Copyright 2007 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 Dialogs import TextDisplay
+from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT
+
+def get_title_label(title):
+ """
+ Get a title label for the params window.
+ The title will be bold, underlined, and left justified.
+ @param title the text of the title
+ @return 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 ParamsDialog(gtk.Dialog):
+ """A dialog box to set block parameters."""
+
+ def __init__(self, block):
+ """
+ SignalBlockParamsDialog contructor.
+ @param block the signal block
+ """
+ gtk.Dialog.__init__(self, buttons=('gtk-close', gtk.RESPONSE_CLOSE))
+ self.block = block
+ self.set_title('Properties: %s'%block.get_name())
+ self.set_size_request(MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT)
+ vbox = gtk.VBox()
+ #Add the title label
+ vbox.pack_start(get_title_label('Parameters'), False)
+ #Create the scrolled window to hold all the parameters
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ scrolled_window.add_with_viewport(vbox)
+ self.vbox.pack_start(scrolled_window, True)
+ #Error Messages for the block
+ self._error_messages_box = err_box = gtk.VBox()
+ self._error_messages_text_display = TextDisplay('')
+ err_box.pack_start(gtk.Label(''), False, False, 7) #spacing
+ err_box.pack_start(get_title_label('Error Messages'), False)
+ err_box.pack_start(self._error_messages_text_display, False)
+ #Add all the parameters
+ for param in self.block.get_params():
+ vbox.pack_start(param.get_input_object(self._handle_changed), False)
+ vbox.pack_start(err_box, False)
+ #Done adding parameters
+ if self.block.get_doc():
+ vbox.pack_start(gtk.Label(''), False, False, 7) #spacing
+ vbox.pack_start(get_title_label('Documentation'), False)
+ #Create the text box to display notes about the block
+ vbox.pack_start(TextDisplay(self.block.get_doc()), False)
+ self.connect('key_press_event', self._handle_key_press)
+ self.show_all()
+ #initial update
+ for param in self.block.get_params(): param.update()
+ self._update_error_messages()
+
+ def _update_error_messages(self):
+ """
+ Update the error messages in the error messages box.
+ Hide the box if there are no errors.
+ """
+ if self.block.is_valid(): self._error_messages_box.hide()
+ else: self._error_messages_box.show()
+ messages = '\n'.join(self.block.get_error_messages())
+ self._error_messages_text_display.set_text(messages)
+
+ def _handle_key_press(self, widget, event):
+ """
+ Handle key presses from the keyboard.
+ Call the ok response when enter is pressed.
+ @return false to forward the keypress
+ """
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Return': self.response(gtk.RESPONSE_OK)
+ return False #forward the keypress
+
+ def _handle_changed(self, param):
+ """
+ A change occured, update any dependent parameters:
+ The enum inside the variable type may have changed and,
+ the variable param will need an external update.
+ @param param the graphical parameter that initiated the callback
+ """
+ self._update_error_messages()
+ #update dependent params
+ if param.is_enum():
+ for other_param in param.get_parent().get_params():
+ if param.get_key() is not other_param.get_key() and (
+ param.get_key() in other_param._type or \
+ param.get_key() in other_param._hide): other_param.update()
+ return True
+
+ def run(self):
+ """
+ Call run().
+ @return true if a change occured.
+ """
+ original_data = list()
+ for param in self.block.get_params():
+ original_data.append(param.get_value())
+ gtk.Dialog.run(self)
+ self.destroy()
+ new_data = list()
+ for param in self.block.get_params():
+ new_data.append(param.get_value())
+ return original_data != new_data
diff --git a/grc/src/gui/Preferences.py b/grc/src/gui/Preferences.py
new file mode 100644
index 000000000..a5591500d
--- /dev/null
+++ b/grc/src/gui/Preferences.py
@@ -0,0 +1,130 @@
+"""
+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
+"""
+
+from .. platforms.base.Constants import HOME_DIR, FLOW_GRAPH_DTD
+from .. utils import ParseXML
+import Messages
+import os
+
+##Access the loaded preferences in this module
+_prefs = list()
+def _set_prefs(prefs): _prefs.append(prefs)
+def _get_prefs(): return _prefs[0]
+def load(platform): _Preferences(platform)
+def save(): _get_prefs().save()
+def get_preferences(): return _get_prefs().get_preferences()
+
+class _Preferences(object):
+
+ def __init__(self, platform):
+ #make self available to module
+ _set_prefs(self)
+ #get prefs block
+ self._prefs_block = platform.get_prefs_block()
+ #prefs file path
+ self._prefs_file_path = os.path.join(HOME_DIR, self._prefs_block.get_param('prefs_file').get_value())
+ #load
+ try:
+ ParseXML.validate_dtd(self._prefs_file_path, FLOW_GRAPH_DTD)
+ n = ParseXML.from_file(self._prefs_file_path)
+ self._prefs_block.import_data(n['block'])
+ except: Messages.send_fail_load_preferences(self._prefs_file_path)
+ ##all params
+ self.snap_to_grid_param = self._prefs_block.get_param('snap_to_grid')
+ self.grid_size_param = self._prefs_block.get_param('grid_size')
+ self.show_grid_param = self._prefs_block.get_param('show_grid')
+ self.show_reports_param = self._prefs_block.get_param('show_reports')
+ self.restore_files_param = self._prefs_block.get_param('restore_files')
+ self.window_size_param = self._prefs_block.get_param('window_size')
+ self.file_open_param = self._prefs_block.get_param('file_open')
+ self.files_open_param = self._prefs_block.get_param('files_open')
+ self.show_params_param = self._prefs_block.get_param('show_params')
+ self.show_id_param = self._prefs_block.get_param('show_id')
+
+ def save(self):
+ try: ParseXML.to_file({'block': self._prefs_block.export_data()}, self._prefs_file_path)
+ except IOError: Messages.send_fail_save_preferences(self._prefs_file_path)
+
+ def get_preferences(self):
+ ##Preferences: title, notes, params
+ return [
+ (
+ 'Grid',
+ '''
+Show grid will draw a square grid onto the flow graph with grid points separated by grid size pixels. \
+Snap to Grid forces the upper right corner of the signal block to align with a grid point.
+''',
+ [self.snap_to_grid_param, self.grid_size_param, self.show_grid_param],
+ ),
+ (
+ 'Appearance',
+ '''
+Show or hide the reports window at the bottom of the main window.
+Show or hide all paramater labels in the signal blocks.
+Show or hide the ID label in the signal blocks.
+''',
+ [self.show_reports_param, self.show_params_param, self.show_id_param],
+ ),
+ (
+ 'Misc',
+ '''
+Restore previously opened files on start-up.
+''',
+ [self.restore_files_param],
+ ),
+ ]
+
+###########################################################################
+# Special methods for specific program functionalities
+###########################################################################
+
+def window_size(size=None):
+ if size: _get_prefs().window_size_param.set_value(size)
+ else:
+ try: return eval(_get_prefs().window_size_param.get_value())
+ except: return (-1, -1)
+
+def restore_files():
+ return _get_prefs().restore_files_param.get_value() == 'yes'
+
+def file_open(file=None):
+ if file is not None: _get_prefs().file_open_param.set_value(file)
+ else: return _get_prefs().file_open_param.get_value()
+
+def files_open(files=None):
+ if files is not None: _get_prefs().files_open_param.set_value('\n'.join(files))
+ else: return _get_prefs().files_open_param.get_value().split('\n')
+
+def show_reports_window():
+ return _get_prefs().show_reports_param.get_value() == 'show'
+
+def get_grid_size():
+ return int(_get_prefs().grid_size_param.get_value())
+
+def snap_to_grid():
+ return _get_prefs().snap_to_grid_param.get_value() == 'on'
+
+def show_grid():
+ return _get_prefs().show_grid_param.get_value() == 'show'
+
+def show_params():
+ return _get_prefs().show_params_param.get_value() == 'show'
+
+def show_id():
+ return _get_prefs().show_id_param.get_value() == 'show'
diff --git a/grc/src/gui/StateCache.py b/grc/src/gui/StateCache.py
new file mode 100644
index 000000000..04b18b18a
--- /dev/null
+++ b/grc/src/gui/StateCache.py
@@ -0,0 +1,92 @@
+"""
+Copyright 2007 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 Actions import FLOW_GRAPH_UNDO, FLOW_GRAPH_REDO, get_action_from_name
+from Constants import STATE_CACHE_SIZE
+
+class StateCache(object):
+ """
+ The state cache is an interface to a list to record data/states and to revert to previous states.
+ States are recorded into the list in a circular fassion by using an index for the current state,
+ and counters for the range where states are stored.
+ """
+
+ def __init__(self, initial_state):
+ """
+ StateCache constructor.
+ @param initial_state the intial state (nested data)
+ """
+ self.states = [None] * STATE_CACHE_SIZE #fill states
+ self.current_state_index = 0
+ self.num_prev_states = 0
+ self.num_next_states = 0
+ self.states[0] = initial_state
+ self.update_actions()
+
+ def save_new_state(self, state):
+ """
+ Save a new state.
+ Place the new state at the next index and add one to the number of previous states.
+ @param state the new state
+ """
+ self.current_state_index = (self.current_state_index + 1)%STATE_CACHE_SIZE
+ self.states[self.current_state_index] = state
+ self.num_prev_states = self.num_prev_states + 1
+ if self.num_prev_states == STATE_CACHE_SIZE: self.num_prev_states = STATE_CACHE_SIZE - 1
+ self.num_next_states = 0
+ self.update_actions()
+
+ def get_current_state(self):
+ """
+ Get the state at the current index.
+ @return the current state (nested data)
+ """
+ self.update_actions()
+ return self.states[self.current_state_index]
+
+ def get_prev_state(self):
+ """
+ Get the previous state and decrement the current index.
+ @return the previous state or None
+ """
+ if self.num_prev_states > 0:
+ self.current_state_index = (self.current_state_index + STATE_CACHE_SIZE -1)%STATE_CACHE_SIZE
+ self.num_next_states = self.num_next_states + 1
+ self.num_prev_states = self.num_prev_states - 1
+ return self.get_current_state()
+ return None
+
+ def get_next_state(self):
+ """
+ Get the nest state and increment the current index.
+ @return the next state or None
+ """
+ if self.num_next_states > 0:
+ self.current_state_index = (self.current_state_index + 1)%STATE_CACHE_SIZE
+ self.num_next_states = self.num_next_states - 1
+ self.num_prev_states = self.num_prev_states + 1
+ return self.get_current_state()
+ return None
+
+ def update_actions(self):
+ """
+ Update the undo and redo actions based on the number of next and prev states.
+ """
+ get_action_from_name(FLOW_GRAPH_REDO).set_sensitive(self.num_next_states != 0)
+ get_action_from_name(FLOW_GRAPH_UNDO).set_sensitive(self.num_prev_states != 0)
diff --git a/grc/src/gui/__init__.py b/grc/src/gui/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/gui/__init__.py
@@ -0,0 +1 @@
+