def test_kwik_save(): """WARNING: this test should occur at the end of the module since it changes the mock data sets.""" # Open the mock data. dir = TEST_FOLDER xmlfile = os.path.join(dir, 'test.xml') l = KwikLoader(filename=xmlfile) clusters = l.get_clusters() cluster_colors = l.get_cluster_colors() cluster_groups = l.get_cluster_groups() group_colors = l.get_group_colors() group_names = l.get_group_names() # Set clusters. indices = get_indices(clusters) l.set_cluster(indices[::2], 2) l.set_cluster(indices[1::2], 3) # Set cluster info. cluster_indices = l.get_clusters_unique() l.set_cluster_colors(cluster_indices[::2], 10) l.set_cluster_colors(cluster_indices[1::2], 20) l.set_cluster_groups(cluster_indices[::2], 1) l.set_cluster_groups(cluster_indices[1::2], 0) # Save. l.remove_empty_clusters() l.save() clusters = l.get_clusters() cluster_colors = l.get_cluster_colors() cluster_groups = l.get_cluster_groups() group_colors = l.get_group_colors() group_names = l.get_group_names() assert np.all(clusters[::2] == 2) assert np.all(clusters[1::2] == 3) assert np.all(cluster_colors[::2] == 10) assert np.all(cluster_colors[1::2] == 20) print cluster_groups assert np.all(cluster_groups[::2] == 1) assert np.all(cluster_groups[1::2] == 0) l.close()
class KwikSkope(QtGui.QMainWindow): def __init__(self, parent=None, dolog=True, filename=None): super(KwikSkope, self).__init__(parent) # HACK: display the icon in Windows' taskbar. if os.name == 'nt': try: import ctypes myappid = 'klustateam.kwikskope' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except: pass self.dolog = dolog if self.dolog: create_file_logger() log.debug("Using {0:s}.".format(QT_BINDING)) # Main window options. self.move(50, 50) self.setWindowTitle('KwikSkope') # Focus options. self.setFocusPolicy(QtCore.Qt.WheelFocus) self.setMouseTracking(True) # Dock widgets options. self.setDockNestingEnabled(True) self.setAnimated(False) self.setWindowIcon(get_icon('logo')) # Initialize some variables. # self.statscache = None # self.loader = KlustersLoader() self.loader = KwikLoader() self.loader.progressReported.connect(self.open_progress_reported) self.loader.saveProgressReported.connect(self.save_progress_reported) self.wizard = Wizard() self.controller = None self.spikes_highlighted = [] self.spikes_selected = [] self._wizard = False self.is_file_open = False self.need_save = False self.busy_cursor = QtGui.QCursor(QtCore.Qt.BusyCursor) self.normal_cursor = QtGui.QCursor(QtCore.Qt.ArrowCursor) self.is_busy = False self.override_color = False self.computing_correlograms = False self.computing_matrix = False # Create the main window. self.create_views() self.create_file_actions() self.create_edit_actions() self.create_view_actions() self.create_help_actions() self.create_menu() self.create_toolbar() self.create_open_progress_dialog() self.create_save_progress_dialog() self.create_threads() # Update action enabled/disabled property. self.update_action_enabled() # Show the main window. self.set_styles() self.restore_geometry() # Automatically load a file upon startup if requested. if filename: filename = os.path.realpath(filename) self.open_task.open(self.loader, filename) self.show() def set_styles(self): # set stylesheet path = os.path.dirname(os.path.realpath(__file__)) path = os.path.join(path, "styles.css") with open(path, 'r') as f: stylesheet = f.read() stylesheet = stylesheet.replace('%ACCENT%', '#cdcdcd') stylesheet = stylesheet.replace('%ACCENT2%', '#a0a0a0') stylesheet = stylesheet.replace('%ACCENT3%', '#909090') stylesheet = stylesheet.replace('%ACCENT4%', '#cdcdcd') self.setStyleSheet(stylesheet) # Actions. # -------- def add_action(self, name, text, callback=None, shortcut=None, checkable=False, checked=False, icon=None): action = QtGui.QAction(text, self) if callback is None: callback = getattr(self, name + '_callback', None) if callback: action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) if icon: action.setIcon(get_icon(icon)) action.setCheckable(checkable) action.setChecked(checked) setattr(self, name + '_action', action) def create_file_actions(self): # Open actions. self.add_action('open', '&Open', shortcut='Ctrl+O', icon='open') # Open last file action path = SETTINGS['main_window.last_data_file'] if path: lastfile = os.path.basename(path) if len(lastfile) > 30: lastfile = '...' + lastfile[-30:] self.add_action('open_last', 'Open &last ({0:s})'.format( lastfile), shortcut='Ctrl+Alt+O') else: self.add_action('open_last', 'Open &last', shortcut='Ctrl+Alt+O') self.open_last_action.setEnabled(False) self.add_action('save', '&Save', shortcut='Ctrl+S', icon='save') self.add_action('close', '&Close file') # Quit action. self.add_action('quit', '&Quit', shortcut='Ctrl+Q') def create_edit_actions(self): self.add_action('undo', '&Undo', shortcut='Ctrl+Z', icon='undo') self.add_action('redo', '&Redo', shortcut='Ctrl+Y', icon='redo') def create_view_actions(self): self.add_action('add_ipython_view', 'Add &IPythonView') self.add_action('reset_views', '&Reset views') self.add_action('toggle_fullscreen', 'Toggle fullscreen', shortcut='F') self.add_action('override_color', 'Override channel &color', icon='override_color')#, shortcut='C') def create_help_actions(self): self.add_action('about', '&About') self.add_action('manual', 'Show &manual') self.add_action('shortcuts', 'Show &shortcuts') self.add_action('open_preferences', '&Open preferences') self.add_action('refresh_preferences', '&Refresh preferences', shortcut='CTRL+R') def create_menu(self): # File menu. file_menu = self.menuBar().addMenu("&File") file_menu.addAction(self.open_action) file_menu.addAction(self.open_last_action) file_menu.addSeparator() # file_menu.addSeparator() file_menu.addAction(self.save_action) file_menu.addSeparator() file_menu.addAction(self.quit_action) # Edit menu. # edit_menu = self.menuBar().addMenu("&Edit") # edit_menu.addAction(self.undo_action) # edit_menu.addAction(self.redo_action) # View menu. views_menu = self.menuBar().addMenu("&View") if vw.IPYTHON: views_menu.addAction(self.add_ipython_view_action) views_menu.addSeparator() # views_menu.addAction(self.override_color_action) # views_menu.addSeparator() views_menu.addAction(self.reset_views_action) views_menu.addAction(self.toggle_fullscreen_action) # Help menu. help_menu = self.menuBar().addMenu("&Help") help_menu.addAction(self.open_preferences_action) help_menu.addAction(self.refresh_preferences_action) help_menu.addSeparator() help_menu.addAction(self.shortcuts_action) help_menu.addAction(self.manual_action) help_menu.addAction(self.about_action) def create_toolbar(self): self.toolbar = self.addToolBar("KlustaViewaToolbar") self.toolbar.setObjectName("KlustaViewaToolbar") self.toolbar.addAction(self.open_action) self.toolbar.addAction(self.save_action) self.toolbar.addAction(self.undo_action) self.toolbar.addAction(self.redo_action) # self.toolbar.addSeparator() # self.toolbar.addAction(self.override_color_action) self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar) def create_open_progress_dialog(self): self.open_progress = QtGui.QProgressDialog("Converting to Kwik...", "Cancel", 0, 0, self, QtCore.Qt.Tool) self.open_progress.setWindowModality(QtCore.Qt.WindowModal) self.open_progress.setValue(0) self.open_progress.setWindowTitle('Loading') self.open_progress.setCancelButton(None) self.open_progress.setMinimumDuration(0) def create_save_progress_dialog(self): self.save_progress = QtGui.QProgressDialog("Saving...", "Cancel", 0, 0, self, QtCore.Qt.Tool) self.save_progress.setWindowModality(QtCore.Qt.WindowModal) self.save_progress.setValue(0) self.save_progress.setWindowTitle('Saving') self.save_progress.setCancelButton(None) self.save_progress.setMinimumDuration(0) # Action enabled. # --------------- def update_action_enabled(self): self.undo_action.setEnabled(self.can_undo()) self.redo_action.setEnabled(self.can_redo()) def can_undo(self): if self.controller is None: return False return self.controller.can_undo() def can_redo(self): if self.controller is None: return False return self.controller.can_redo() # View methods. # ------------- def create_view(self, view_class, position=None, closable=True, floatable=True, index=0, floating=None, title=None, **kwargs): """Add a widget to the main window.""" view = view_class(self, getfocus=False) view.set_data(**kwargs) # Create the dock widget. name = view_class.__name__ + '_' + str(index) dockwidget = ViewDockWidget(view_class.__name__) # dockwidget = ViewDockWidget(name) dockwidget.setObjectName(name) dockwidget.setWidget(view) dockwidget.closed.connect(self.dock_widget_closed) # Set dock widget options. options = QtGui.QDockWidget.DockWidgetMovable if closable: options = options | QtGui.QDockWidget.DockWidgetClosable if floatable: options = options | QtGui.QDockWidget.DockWidgetFloatable dockwidget.setFeatures(options) dockwidget.setAllowedAreas( QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea | QtCore.Qt.TopDockWidgetArea | QtCore.Qt.BottomDockWidgetArea) dockwidget.visibilityChanged.connect(partial( self.dock_visibility_changed_callback, view)) if position is not None: # Add the dock widget to the main window. self.addDockWidget(position, dockwidget) if floating is not None: dockwidget.setFloating(floating) if title is None: title = view_class.__name__ dockwidget.setTitleBarWidget(DockTitleBar(dockwidget, title)) # Return the view widget. return view def add_channel_view(self, do_update=None, floating=False): view = self.create_view(vw.ChannelView, position=QtCore.Qt.LeftDockWidgetArea, index=len(self.views['ChannelView']), closable=False, # floatable=False ) # Connect callback functions. # view.channelsSelected.connect(self.channels_selected_callback) # view.channelColorChanged.connect(self.channel_color_changed_callback) # view.groupColorChanged.connect(self.group_color_changed_callback) # view.groupRenamed.connect(self.group_renamed_callback) # view.channelsMoved.connect(self.channels_moved_callback) # view.groupAdded.connect(self.group_added_callback) # view.groupRemoved.connect(self.group_removed_callback) self.views['ChannelView'].append(view) if do_update: self.update_channel_view() def dock_visibility_changed_callback(self, view, visibility): # Register dock widget visibility. view.visibility = visibility def restore_last_view(self, name): """Return True if the last view was successfully restored, False if the view needs to be restored manually by creating a new view.""" # No existing view: need to create a new view. if not self.views[name]: return False view = self.views[name][-1] # A view exists and it is hidden: restore it. if getattr(view, 'visibility', None) is False: view.parent().toggleViewAction().activate(QtGui.QAction.Trigger) return True # A view exists but it is not hidden: just add a new view. else: return False def add_ipython_view(self, floating=None): view = self.create_view(vw.IPythonView, index=len(self.views['IPythonView']), position=QtCore.Qt.BottomDockWidgetArea, floating=True) # Create namespace for the interactive session. namespace = dict( window=self, select=self.get_view('ChannelView').select, loader=self.loader, # stats=self.statscache, wizard=self.wizard, ) view.set_data(**namespace) # Load all .py files in the code directory. paths = USERPREF['ipython_import_paths'] or [] if isinstance(paths, basestring): paths = [paths] for path in paths: path = os.path.realpath(os.path.expanduser(path)) if os.path.exists(path): files = [file for file in os.listdir(path) if file.endswith('.py')] for file in files: log.debug("Running {0:s}".format(file)) view.run_file(os.path.join(path, file)) self.views['IPythonView'].append(view) def add_trace_view(self, do_update=None, floating=False): if len(self.views['TraceView']) >= 1: return view = self.create_view(vw.TraceView, index=len(self.views['TraceView']), position=QtCore.Qt.RightDockWidgetArea, floating=floating) self.views['TraceView'].append(view) if do_update and self.is_file_open: self.update_trace_view() def update_trace_view(self): data = vd.get_traceview_data(self.loader) [view.set_data(**data) for view in self.get_views('TraceView')] def update_channel_view(self, channels=None): """Update the channel view using the data stored in the loader object.""" data = vd.get_channelview_data(self.loader, channels=channels) self.get_view('ChannelView').set_data(**data) if channels is not None: return def get_view(self, name, index=0): views = self.views[name] if not views: return None else: return views[index] def get_views(self, name): return self.views[name] def create_views(self): """Create all views at initialization.""" # Create the default layout. self.views = dict( ChannelView=[], IPythonView=[], TraceView=[], ) # count = SETTINGS['main_window.views'] count = None if count is None: self.create_default_views() else: self.create_custom_views(count) def create_default_views(self, do_update=None, floating=False): self.add_channel_view(do_update=do_update) self.add_trace_view(do_update=do_update) def create_custom_views(self, count): [self.add_channel_view() for _ in xrange(count['ChannelView'])] [self.add_ipython_view() for _ in xrange(count['IPythonView'])] [self.add_trace_view() for _ in xrange(count['TraceView'])] def dock_widget_closed(self, dock): for key in self.views.keys(): self.views[key] = [view for view in self.views[key] if view.parent() != dock] # Threads. # -------- def create_threads(self): # Create the external threads. self.open_task = inthread(OpenTask)() self.open_task.dataOpened.connect(self.open_done) self.open_task.dataSaved.connect(self.save_done) self.open_task.dataOpenFailed.connect(self.open_failed) def join_threads(self): self.open_task.join() # File menu callbacks. # -------------------- def open_callback(self, checked=None): # HACK: Force release of Ctrl key. self.force_key_release() folder = SETTINGS['main_window.last_data_dir'] path = QtGui.QFileDialog.getOpenFileName(self, "Open a file (.clu or other)", folder)[0] # If a file has been selected, open it. if path: # Launch the loading task in the background asynchronously. self.open_task.open(self.loader, path) # Save the folder. folder = os.path.dirname(path) SETTINGS['main_window.last_data_dir'] = folder SETTINGS['main_window.last_data_file'] = path def save_callback(self, checked=None): self.open_task.save(self.loader) def open_last_callback(self, checked=None): path = SETTINGS['main_window.last_data_file'] if path: self.open_task.open(self.loader, path) def close_callback(self, checked=None): self.is_file_open = False channels = self.get_view('ChannelView').selected_channels() if channels: self.get_view('ChannelView').unselect() time.sleep(.25) # Update the views. self.update_channel_view() self.update_trace_view() # Clear the ChannelView. self.get_view('ChannelView').clear() def quit_callback(self, checked=None): self.close() # Open callbacks. # -------------- def open_done(self): self.is_file_open = True # HACK: force release of Control key. self.force_key_release() channels = self.get_view('ChannelView').selected_channels() if channels: self.get_view('ChannelView').unselect() # Create the Controller. self.controller = Controller(self.loader) # Create the cache for the channel statistics that need to be # computed in the background. # self.statscache = StatsCache(self.loader.ncorrbins) # Update stats cache in IPython view. ipython = self.get_view('IPythonView') # if ipython: # ipython.set_data(stats=self.statscache) # Initialize the wizard. self.wizard = Wizard() # Update the views. self.update_channel_view() self.update_trace_view() def open_failed(self, message): self.open_progress.setValue(0) QtGui.QMessageBox.warning(self, "Error while opening the file", "An error occurred: {0:s}".format(message), QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok) def open_progress_reported(self, progress, progress_max): self.open_progress.setMaximum(progress_max) self.open_progress.setValue(progress) def save_progress_reported(self, progress, progress_max): self.save_progress.setMaximum(progress_max) self.save_progress.setValue(progress) def save_done(self): self.need_save = False # Selection methods. # ------------------ # def buffer_accepted_callback(self, (channels, wizard)): # self._wizard = wizard # # The wizard boolean specifies whether the autozoom is activated or not. # self.taskgraph.select(channels, wizard and # self.automatic_projection_action.isChecked()) def channels_selected_callback(self, channels, wizard=False): self.buffer.request((channels, wizard)) def channel_pair_selected_callback(self, channels): """Callback when the user clicks on a pair in the SimilarityMatrixView.""" self.get_view('ChannelView').select(channels) # Views menu callbacks. # --------------------- def add_trace_view_callback(self, checked=None): self.add_trace_view(do_update=True, floating=True) def add_ipython_view_callback(self, checked=None): self.add_ipython_view() def reset_views_callback(self, checked=None): # Delete all views. for key, views in self.views.iteritems(): for view in views: self.removeDockWidget(view.parent()) self.views[key] = [] # Re-create the default views. self.create_default_views(do_update=self.is_file_open, floating=False) def toggle_fullscreen_callback(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() # Override color callback. # ------------------------ # def override_color_callback(self, checked=None): # self.override_color = not self.override_color # self.taskgraph.override_color(self.override_color) # Actions callbacks. # ------------------ # # def undo_callback(self, checked=None): # if self.is_busy: # return # self.taskgraph.undo(self._wizard) # self.update_action_enabled() # # def redo_callback(self, checked=None): # if self.is_busy: # return # self.taskgraph.redo(self._wizard) # self.update_action_enabled() # # def channel_color_changed_callback(self, channel, color): # self.taskgraph.channel_color_changed(channel, color, self._wizard) # self.update_action_enabled() # # def group_color_changed_callback(self, group, color): # self.taskgraph.group_color_changed(group, color) # self.update_action_enabled() # # def group_renamed_callback(self, group, name): # self.taskgraph.group_renamed(group, name) # self.update_action_enabled() # # def channels_moved_callback(self, channels, group): # self.taskgraph.channels_moved(channels, group) # self.update_action_enabled() # # def group_removed_callback(self, group): # self.taskgraph.group_removed(group) # self.update_action_enabled() # # def group_added_callback(self, group, name, color): # self.taskgraph.group_added(group, name, color) # self.update_action_enabled() # # Help callbacks. # --------------- def manual_callback(self, checked=None): url = "https://github.com/klusta-team/klustaviewa/tree/master/docs/manual.md" webbrowser.open(url) def about_callback(self, checked=None): QtGui.QMessageBox.about(self, "KlustaViewa", ABOUT) def shortcuts_callback(self, checked=None): e = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_H, QtCore.Qt.NoModifier,) self.keyPressEvent(e) def open_preferences_callback(self, checked=None): url = USERPREF.filepath log.debug("Opening preferences file at '{0:s}'".format(url)) QtGui.QDesktopServices.openUrl(QtCore.QUrl('file:///' + url)) def refresh_preferences_callback(self, checked=None): log.debug("Refreshing user preferences.") USERPREF.refresh() # Geometry. # --------- def save_geometry(self): """Save the arrangement of the whole window.""" # SETTINGS['main_window.views'] = {name: len(self.get_views(name)) # for name in self.views.keys()} SETTINGS['main_window.geometry'] = encode_bytearray( self.saveGeometry()) SETTINGS['main_window.state'] = encode_bytearray(self.saveState()) def restore_geometry(self): """Restore the arrangement of the whole window.""" g = SETTINGS['main_window.geometry'] s = SETTINGS['main_window.state'] if s: self.restoreState(decode_bytearray(s)) if g: self.restoreGeometry(decode_bytearray(g)) # Event handlers. # --------------- def force_key_release(self): """HACK: force release of Ctrl, Shift and Alt when focus out.""" self.keyReleaseEvent(QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Control, QtCore.Qt.NoModifier)) self.keyReleaseEvent(QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Shift, QtCore.Qt.NoModifier)) self.keyReleaseEvent(QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Alt, QtCore.Qt.NoModifier)) def event(self, e): if e.type() == QtCore.QEvent.WindowActivate: pass elif e.type() == QtCore.QEvent.WindowDeactivate: self.force_key_release() return super(KwikSkope, self).event(e) def contextMenuEvent(self, e): """Disable the context menu in the main window.""" return def keyPressEvent(self, e): super(KwikSkope, self).keyPressEvent(e) for views in self.views.values(): [view.keyPressEvent(e) for view in views] def keyReleaseEvent(self, e): super(KwikSkope, self).keyReleaseEvent(e) for views in self.views.values(): [view.keyReleaseEvent(e) for view in views] def closeEvent(self, e): prompt_save_on_exit = USERPREF['prompt_save_on_exit'] if prompt_save_on_exit is None: prompt_save_on_exit = True if self.need_save and prompt_save_on_exit: reply = QtGui.QMessageBox.question(self, 'Save', "Do you want to save?", ( QtGui.QMessageBox.Save | QtGui.QMessageBox.Close | QtGui.QMessageBox.Cancel ), QtGui.QMessageBox.Save) if reply == QtGui.QMessageBox.Save: folder = SETTINGS.get('main_window.last_data_file') self.loader.save() elif reply == QtGui.QMessageBox.Cancel: e.ignore() return elif reply == QtGui.QMessageBox.Close: pass # Save the window geometry when closing the software. self.save_geometry() # End the threads. self.join_threads() # Close the loader. self.loader.close() # Close all views. for views in self.views.values(): for view in views: if hasattr(view, 'closeEvent'): view.closeEvent(e) # Close the logger file. if self.dolog: close_file_logger() # Close the main window. return super(KwikSkope, self).closeEvent(e) def sizeHint(self): return QtCore.QSize(1200, 800)
class KwikSkope(QtGui.QMainWindow): def __init__(self, parent=None, dolog=True, filename=None): super(KwikSkope, self).__init__(parent) # HACK: display the icon in Windows' taskbar. if os.name == 'nt': try: import ctypes myappid = 'klustateam.kwikskope' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( myappid) except: pass self.dolog = dolog if self.dolog: create_file_logger() log.debug("Using {0:s}.".format(QT_BINDING)) # Main window options. self.move(50, 50) self.setWindowTitle('KwikSkope') # Focus options. self.setFocusPolicy(QtCore.Qt.WheelFocus) self.setMouseTracking(True) # Dock widgets options. self.setDockNestingEnabled(True) self.setAnimated(False) self.setWindowIcon(get_icon('logo')) # Initialize some variables. # self.statscache = None # self.loader = KlustersLoader() self.loader = KwikLoader() self.loader.progressReported.connect(self.open_progress_reported) self.loader.saveProgressReported.connect(self.save_progress_reported) self.wizard = Wizard() self.controller = None self.spikes_highlighted = [] self.spikes_selected = [] self._wizard = False self.is_file_open = False self.need_save = False self.busy_cursor = QtGui.QCursor(QtCore.Qt.BusyCursor) self.normal_cursor = QtGui.QCursor(QtCore.Qt.ArrowCursor) self.is_busy = False self.override_color = False self.computing_correlograms = False self.computing_matrix = False # Create the main window. self.create_views() self.create_file_actions() self.create_edit_actions() self.create_view_actions() self.create_help_actions() self.create_menu() self.create_toolbar() self.create_open_progress_dialog() self.create_save_progress_dialog() self.create_threads() # Update action enabled/disabled property. self.update_action_enabled() # Show the main window. self.set_styles() self.restore_geometry() # Automatically load a file upon startup if requested. if filename: filename = os.path.realpath(filename) self.open_task.open(self.loader, filename) self.show() def set_styles(self): # set stylesheet path = os.path.dirname(os.path.realpath(__file__)) path = os.path.join(path, "styles.css") with open(path, 'r') as f: stylesheet = f.read() stylesheet = stylesheet.replace('%ACCENT%', '#cdcdcd') stylesheet = stylesheet.replace('%ACCENT2%', '#a0a0a0') stylesheet = stylesheet.replace('%ACCENT3%', '#909090') stylesheet = stylesheet.replace('%ACCENT4%', '#cdcdcd') self.setStyleSheet(stylesheet) # Actions. # -------- def add_action(self, name, text, callback=None, shortcut=None, checkable=False, checked=False, icon=None): action = QtGui.QAction(text, self) if callback is None: callback = getattr(self, name + '_callback', None) if callback: action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) if icon: action.setIcon(get_icon(icon)) action.setCheckable(checkable) action.setChecked(checked) setattr(self, name + '_action', action) def create_file_actions(self): # Open actions. self.add_action('open', '&Open', shortcut='Ctrl+O', icon='open') # Open last file action path = SETTINGS['main_window.last_data_file'] if path: lastfile = os.path.basename(path) if len(lastfile) > 30: lastfile = '...' + lastfile[-30:] self.add_action('open_last', 'Open &last ({0:s})'.format(lastfile), shortcut='Ctrl+Alt+O') else: self.add_action('open_last', 'Open &last', shortcut='Ctrl+Alt+O') self.open_last_action.setEnabled(False) self.add_action('save', '&Save', shortcut='Ctrl+S', icon='save') self.add_action('close', '&Close file') # Quit action. self.add_action('quit', '&Quit', shortcut='Ctrl+Q') def create_edit_actions(self): self.add_action('undo', '&Undo', shortcut='Ctrl+Z', icon='undo') self.add_action('redo', '&Redo', shortcut='Ctrl+Y', icon='redo') def create_view_actions(self): self.add_action('add_ipython_view', 'Add &IPythonView') self.add_action('reset_views', '&Reset views') self.add_action('toggle_fullscreen', 'Toggle fullscreen', shortcut='F') self.add_action('override_color', 'Override channel &color', icon='override_color') #, shortcut='C') def create_help_actions(self): self.add_action('about', '&About') self.add_action('manual', 'Show &manual') self.add_action('shortcuts', 'Show &shortcuts') self.add_action('open_preferences', '&Open preferences') self.add_action('refresh_preferences', '&Refresh preferences', shortcut='CTRL+R') def create_menu(self): # File menu. file_menu = self.menuBar().addMenu("&File") file_menu.addAction(self.open_action) file_menu.addAction(self.open_last_action) file_menu.addSeparator() # file_menu.addSeparator() file_menu.addAction(self.save_action) file_menu.addSeparator() file_menu.addAction(self.quit_action) # Edit menu. # edit_menu = self.menuBar().addMenu("&Edit") # edit_menu.addAction(self.undo_action) # edit_menu.addAction(self.redo_action) # View menu. views_menu = self.menuBar().addMenu("&View") if vw.IPYTHON: views_menu.addAction(self.add_ipython_view_action) views_menu.addSeparator() # views_menu.addAction(self.override_color_action) # views_menu.addSeparator() views_menu.addAction(self.reset_views_action) views_menu.addAction(self.toggle_fullscreen_action) # Help menu. help_menu = self.menuBar().addMenu("&Help") help_menu.addAction(self.open_preferences_action) help_menu.addAction(self.refresh_preferences_action) help_menu.addSeparator() help_menu.addAction(self.shortcuts_action) help_menu.addAction(self.manual_action) help_menu.addAction(self.about_action) def create_toolbar(self): self.toolbar = self.addToolBar("KlustaViewaToolbar") self.toolbar.setObjectName("KlustaViewaToolbar") self.toolbar.addAction(self.open_action) self.toolbar.addAction(self.save_action) self.toolbar.addAction(self.undo_action) self.toolbar.addAction(self.redo_action) # self.toolbar.addSeparator() # self.toolbar.addAction(self.override_color_action) self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar) def create_open_progress_dialog(self): self.open_progress = QtGui.QProgressDialog("Converting to Kwik...", "Cancel", 0, 0, self, QtCore.Qt.Tool) self.open_progress.setWindowModality(QtCore.Qt.WindowModal) self.open_progress.setValue(0) self.open_progress.setWindowTitle('Loading') self.open_progress.setCancelButton(None) self.open_progress.setMinimumDuration(0) def create_save_progress_dialog(self): self.save_progress = QtGui.QProgressDialog("Saving...", "Cancel", 0, 0, self, QtCore.Qt.Tool) self.save_progress.setWindowModality(QtCore.Qt.WindowModal) self.save_progress.setValue(0) self.save_progress.setWindowTitle('Saving') self.save_progress.setCancelButton(None) self.save_progress.setMinimumDuration(0) # Action enabled. # --------------- def update_action_enabled(self): self.undo_action.setEnabled(self.can_undo()) self.redo_action.setEnabled(self.can_redo()) def can_undo(self): if self.controller is None: return False return self.controller.can_undo() def can_redo(self): if self.controller is None: return False return self.controller.can_redo() # View methods. # ------------- def create_view(self, view_class, position=None, closable=True, floatable=True, index=0, floating=None, title=None, **kwargs): """Add a widget to the main window.""" view = view_class(self, getfocus=False) view.set_data(**kwargs) # Create the dock widget. name = view_class.__name__ + '_' + str(index) dockwidget = ViewDockWidget(view_class.__name__) # dockwidget = ViewDockWidget(name) dockwidget.setObjectName(name) dockwidget.setWidget(view) dockwidget.closed.connect(self.dock_widget_closed) # Set dock widget options. options = QtGui.QDockWidget.DockWidgetMovable if closable: options = options | QtGui.QDockWidget.DockWidgetClosable if floatable: options = options | QtGui.QDockWidget.DockWidgetFloatable dockwidget.setFeatures(options) dockwidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea | QtCore.Qt.TopDockWidgetArea | QtCore.Qt.BottomDockWidgetArea) dockwidget.visibilityChanged.connect( partial(self.dock_visibility_changed_callback, view)) if position is not None: # Add the dock widget to the main window. self.addDockWidget(position, dockwidget) if floating is not None: dockwidget.setFloating(floating) if title is None: title = view_class.__name__ dockwidget.setTitleBarWidget(DockTitleBar(dockwidget, title)) # Return the view widget. return view def add_channel_view(self, do_update=None, floating=False): view = self.create_view( vw.ChannelView, position=QtCore.Qt.LeftDockWidgetArea, index=len(self.views['ChannelView']), closable=False, # floatable=False ) # Connect callback functions. # view.channelsSelected.connect(self.channels_selected_callback) # view.channelColorChanged.connect(self.channel_color_changed_callback) # view.groupColorChanged.connect(self.group_color_changed_callback) # view.groupRenamed.connect(self.group_renamed_callback) # view.channelsMoved.connect(self.channels_moved_callback) # view.groupAdded.connect(self.group_added_callback) # view.groupRemoved.connect(self.group_removed_callback) self.views['ChannelView'].append(view) if do_update: self.update_channel_view() def dock_visibility_changed_callback(self, view, visibility): # Register dock widget visibility. view.visibility = visibility def restore_last_view(self, name): """Return True if the last view was successfully restored, False if the view needs to be restored manually by creating a new view.""" # No existing view: need to create a new view. if not self.views[name]: return False view = self.views[name][-1] # A view exists and it is hidden: restore it. if getattr(view, 'visibility', None) is False: view.parent().toggleViewAction().activate(QtGui.QAction.Trigger) return True # A view exists but it is not hidden: just add a new view. else: return False def add_ipython_view(self, floating=None): view = self.create_view(vw.IPythonView, index=len(self.views['IPythonView']), position=QtCore.Qt.BottomDockWidgetArea, floating=True) # Create namespace for the interactive session. namespace = dict( window=self, select=self.get_view('ChannelView').select, loader=self.loader, # stats=self.statscache, wizard=self.wizard, ) view.set_data(**namespace) # Load all .py files in the code directory. paths = USERPREF['ipython_import_paths'] or [] if isinstance(paths, basestring): paths = [paths] for path in paths: path = os.path.realpath(os.path.expanduser(path)) if os.path.exists(path): files = [ file for file in os.listdir(path) if file.endswith('.py') ] for file in files: log.debug("Running {0:s}".format(file)) view.run_file(os.path.join(path, file)) self.views['IPythonView'].append(view) def add_trace_view(self, do_update=None, floating=False): if len(self.views['TraceView']) >= 1: return view = self.create_view(vw.TraceView, index=len(self.views['TraceView']), position=QtCore.Qt.RightDockWidgetArea, floating=floating) self.views['TraceView'].append(view) if do_update and self.is_file_open: self.update_trace_view() def update_trace_view(self): data = vd.get_traceview_data(self.loader) [view.set_data(**data) for view in self.get_views('TraceView')] def update_channel_view(self, channels=None): """Update the channel view using the data stored in the loader object.""" data = vd.get_channelview_data(self.loader, channels=channels) self.get_view('ChannelView').set_data(**data) if channels is not None: return def get_view(self, name, index=0): views = self.views[name] if not views: return None else: return views[index] def get_views(self, name): return self.views[name] def create_views(self): """Create all views at initialization.""" # Create the default layout. self.views = dict( ChannelView=[], IPythonView=[], TraceView=[], ) # count = SETTINGS['main_window.views'] count = None if count is None: self.create_default_views() else: self.create_custom_views(count) def create_default_views(self, do_update=None, floating=False): self.add_channel_view(do_update=do_update) self.add_trace_view(do_update=do_update) def create_custom_views(self, count): [self.add_channel_view() for _ in xrange(count['ChannelView'])] [self.add_ipython_view() for _ in xrange(count['IPythonView'])] [self.add_trace_view() for _ in xrange(count['TraceView'])] def dock_widget_closed(self, dock): for key in self.views.keys(): self.views[key] = [ view for view in self.views[key] if view.parent() != dock ] # Threads. # -------- def create_threads(self): # Create the external threads. self.open_task = inthread(OpenTask)() self.open_task.dataOpened.connect(self.open_done) self.open_task.dataSaved.connect(self.save_done) self.open_task.dataOpenFailed.connect(self.open_failed) def join_threads(self): self.open_task.join() # File menu callbacks. # -------------------- def open_callback(self, checked=None): # HACK: Force release of Ctrl key. self.force_key_release() folder = SETTINGS['main_window.last_data_dir'] path = QtGui.QFileDialog.getOpenFileName( self, "Open a file (.clu or other)", folder)[0] # If a file has been selected, open it. if path: # Launch the loading task in the background asynchronously. self.open_task.open(self.loader, path) # Save the folder. folder = os.path.dirname(path) SETTINGS['main_window.last_data_dir'] = folder SETTINGS['main_window.last_data_file'] = path def save_callback(self, checked=None): self.open_task.save(self.loader) def open_last_callback(self, checked=None): path = SETTINGS['main_window.last_data_file'] if path: self.open_task.open(self.loader, path) def close_callback(self, checked=None): self.is_file_open = False channels = self.get_view('ChannelView').selected_channels() if channels: self.get_view('ChannelView').unselect() time.sleep(.25) # Update the views. self.update_channel_view() self.update_trace_view() # Clear the ChannelView. self.get_view('ChannelView').clear() def quit_callback(self, checked=None): self.close() # Open callbacks. # -------------- def open_done(self): self.is_file_open = True # HACK: force release of Control key. self.force_key_release() channels = self.get_view('ChannelView').selected_channels() if channels: self.get_view('ChannelView').unselect() # Create the Controller. self.controller = Controller(self.loader) # Create the cache for the channel statistics that need to be # computed in the background. # self.statscache = StatsCache(self.loader.ncorrbins) # Update stats cache in IPython view. ipython = self.get_view('IPythonView') # if ipython: # ipython.set_data(stats=self.statscache) # Initialize the wizard. self.wizard = Wizard() # Update the views. self.update_channel_view() self.update_trace_view() def open_failed(self, message): self.open_progress.setValue(0) QtGui.QMessageBox.warning(self, "Error while opening the file", "An error occurred: {0:s}".format(message), QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok) def open_progress_reported(self, progress, progress_max): self.open_progress.setMaximum(progress_max) self.open_progress.setValue(progress) def save_progress_reported(self, progress, progress_max): self.save_progress.setMaximum(progress_max) self.save_progress.setValue(progress) def save_done(self): self.need_save = False # Selection methods. # ------------------ # def buffer_accepted_callback(self, (channels, wizard)): # self._wizard = wizard # # The wizard boolean specifies whether the autozoom is activated or not. # self.taskgraph.select(channels, wizard and # self.automatic_projection_action.isChecked()) def channels_selected_callback(self, channels, wizard=False): self.buffer.request((channels, wizard)) def channel_pair_selected_callback(self, channels): """Callback when the user clicks on a pair in the SimilarityMatrixView.""" self.get_view('ChannelView').select(channels) # Views menu callbacks. # --------------------- def add_trace_view_callback(self, checked=None): self.add_trace_view(do_update=True, floating=True) def add_ipython_view_callback(self, checked=None): self.add_ipython_view() def reset_views_callback(self, checked=None): # Delete all views. for key, views in self.views.iteritems(): for view in views: self.removeDockWidget(view.parent()) self.views[key] = [] # Re-create the default views. self.create_default_views(do_update=self.is_file_open, floating=False) def toggle_fullscreen_callback(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() # Override color callback. # ------------------------ # def override_color_callback(self, checked=None): # self.override_color = not self.override_color # self.taskgraph.override_color(self.override_color) # Actions callbacks. # ------------------ # # def undo_callback(self, checked=None): # if self.is_busy: # return # self.taskgraph.undo(self._wizard) # self.update_action_enabled() # # def redo_callback(self, checked=None): # if self.is_busy: # return # self.taskgraph.redo(self._wizard) # self.update_action_enabled() # # def channel_color_changed_callback(self, channel, color): # self.taskgraph.channel_color_changed(channel, color, self._wizard) # self.update_action_enabled() # # def group_color_changed_callback(self, group, color): # self.taskgraph.group_color_changed(group, color) # self.update_action_enabled() # # def group_renamed_callback(self, group, name): # self.taskgraph.group_renamed(group, name) # self.update_action_enabled() # # def channels_moved_callback(self, channels, group): # self.taskgraph.channels_moved(channels, group) # self.update_action_enabled() # # def group_removed_callback(self, group): # self.taskgraph.group_removed(group) # self.update_action_enabled() # # def group_added_callback(self, group, name, color): # self.taskgraph.group_added(group, name, color) # self.update_action_enabled() # # Help callbacks. # --------------- def manual_callback(self, checked=None): url = "https://github.com/klusta-team/klustaviewa/tree/master/docs/manual.md" webbrowser.open(url) def about_callback(self, checked=None): QtGui.QMessageBox.about(self, "KlustaViewa", ABOUT) def shortcuts_callback(self, checked=None): e = QtGui.QKeyEvent( QtCore.QEvent.KeyPress, QtCore.Qt.Key_H, QtCore.Qt.NoModifier, ) self.keyPressEvent(e) def open_preferences_callback(self, checked=None): url = USERPREF.filepath log.debug("Opening preferences file at '{0:s}'".format(url)) QtGui.QDesktopServices.openUrl(QtCore.QUrl('file:///' + url)) def refresh_preferences_callback(self, checked=None): log.debug("Refreshing user preferences.") USERPREF.refresh() # Geometry. # --------- def save_geometry(self): """Save the arrangement of the whole window.""" # SETTINGS['main_window.views'] = {name: len(self.get_views(name)) # for name in self.views.keys()} SETTINGS['main_window.geometry'] = encode_bytearray( self.saveGeometry()) SETTINGS['main_window.state'] = encode_bytearray(self.saveState()) def restore_geometry(self): """Restore the arrangement of the whole window.""" g = SETTINGS['main_window.geometry'] s = SETTINGS['main_window.state'] if s: self.restoreState(decode_bytearray(s)) if g: self.restoreGeometry(decode_bytearray(g)) # Event handlers. # --------------- def force_key_release(self): """HACK: force release of Ctrl, Shift and Alt when focus out.""" self.keyReleaseEvent( QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Control, QtCore.Qt.NoModifier)) self.keyReleaseEvent( QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Shift, QtCore.Qt.NoModifier)) self.keyReleaseEvent( QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Alt, QtCore.Qt.NoModifier)) def event(self, e): if e.type() == QtCore.QEvent.WindowActivate: pass elif e.type() == QtCore.QEvent.WindowDeactivate: self.force_key_release() return super(KwikSkope, self).event(e) def contextMenuEvent(self, e): """Disable the context menu in the main window.""" return def keyPressEvent(self, e): super(KwikSkope, self).keyPressEvent(e) for views in self.views.values(): [view.keyPressEvent(e) for view in views] def keyReleaseEvent(self, e): super(KwikSkope, self).keyReleaseEvent(e) for views in self.views.values(): [view.keyReleaseEvent(e) for view in views] def closeEvent(self, e): prompt_save_on_exit = USERPREF['prompt_save_on_exit'] if prompt_save_on_exit is None: prompt_save_on_exit = True if self.need_save and prompt_save_on_exit: reply = QtGui.QMessageBox.question( self, 'Save', "Do you want to save?", (QtGui.QMessageBox.Save | QtGui.QMessageBox.Close | QtGui.QMessageBox.Cancel), QtGui.QMessageBox.Save) if reply == QtGui.QMessageBox.Save: folder = SETTINGS.get('main_window.last_data_file') self.loader.save() elif reply == QtGui.QMessageBox.Cancel: e.ignore() return elif reply == QtGui.QMessageBox.Close: pass # Save the window geometry when closing the software. self.save_geometry() # End the threads. self.join_threads() # Close the loader. self.loader.close() # Close all views. for views in self.views.values(): for view in views: if hasattr(view, 'closeEvent'): view.closeEvent(e) # Close the logger file. if self.dolog: close_file_logger() # Close the main window. return super(KwikSkope, self).closeEvent(e) def sizeHint(self): return QtCore.QSize(1200, 800)