diff options
author | jblum | 2008-09-07 21:38:12 +0000 |
---|---|---|
committer | jblum | 2008-09-07 21:38:12 +0000 |
commit | c86f6c23c6883f73d953d64c28ab42cedb77e4d7 (patch) | |
tree | 0193b2a649eb0f7f1065912862de340a02848e16 /grc/src/gui | |
parent | ddec4fc07744a6519086b1b111f29d551b7f19c6 (diff) | |
download | gnuradio-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.py | 445 | ||||
-rw-r--r-- | grc/src/gui/Actions.py | 103 | ||||
-rw-r--r-- | grc/src/gui/Bars.py | 139 | ||||
-rw-r--r-- | grc/src/gui/BlockTreeWindow.py | 150 | ||||
-rw-r--r-- | grc/src/gui/Constants.py | 82 | ||||
-rw-r--r-- | grc/src/gui/Dialogs.py | 146 | ||||
-rw-r--r-- | grc/src/gui/DrawingArea.py | 118 | ||||
-rw-r--r-- | grc/src/gui/FileDialogs.py | 154 | ||||
-rw-r--r-- | grc/src/gui/MainWindow.py | 321 | ||||
-rw-r--r-- | grc/src/gui/Makefile.am | 41 | ||||
-rw-r--r-- | grc/src/gui/Messages.py | 101 | ||||
-rw-r--r-- | grc/src/gui/NotebookPage.py | 172 | ||||
-rw-r--r-- | grc/src/gui/ParamsDialog.py | 131 | ||||
-rw-r--r-- | grc/src/gui/Preferences.py | 130 | ||||
-rw-r--r-- | grc/src/gui/StateCache.py | 92 | ||||
-rw-r--r-- | grc/src/gui/__init__.py | 1 |
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 @@ + |