class WorkerListStore(gtk.ListStore): implements(flavors.IStateListener) gsignal('changed') def __init__(self, whs): gtk.ListStore.__init__(self, str) for x in whs.get('names'): i = self.append() self.set_value(i, 0, x) whs.addListener(self, append=self.stateAppend, remove=self.stateRemove) def stateAppend(self, state, key, val): if key == 'names': i = self.append() self.set_value(i, 0, val) self.emit('changed') def stateRemove(self, state, key, val): if key == 'names': for r in self: if self.get_value(r.iter, 0) == val: self.remove(r.iter) self.emit('changed') return
class _SidebarSection(gtk.VBox): gsignal('step-chosen', str) def __init__(self, title, name): gtk.VBox.__init__(self) self.set_name(title) self._buttons = util.OrderedDict() self.title = _SidebarButton(title, 10) self.title.show() self.title.set_sensitive(False) self.pack_start(self.title, False, False) self.title.connect('clicked', lambda b: self.emit('step-chosen', name)) def __repr__(self): return '<SidebarSection object %s>' % self.name def set_active(self, active): if active: for button in self._buttons.values(): button.show() else: for button in self._buttons.values(): button.hide() def push_header(self): self.title.set_sensitive(True) def pop_header(self): self.title.set_sensitive(False) def push_step(self, step_name, step_title): if step_name in self._buttons: return def clicked_cb(b, name): self.emit('step-chosen', name) button = _SidebarButton(step_title, 20) button.connect('clicked', clicked_cb, step_name) self.pack_start(button, False, False) button.show() self._buttons[step_name] = button def pop_step(self): if not self._buttons: return None button = self._buttons.popitem()[1] if button: self.remove(button) return button
class Foo(gobject.GObject): gsignal('hcf', bool, str) gproperty(bool, 'burning', 'If the object is burning', False) def __init__(xself): gobject.GObject.__init__(xself) xself.connect('hcf', xself.on_hcf) xself.set_property('burning', False) def on_hcf(xself, again_self, x, y): self.assert_(isinstance(x, bool)) self.assert_(isinstance(y, str)) xself.set_property('burning', True)
class DebugMarkerDialog(GladeWindow): gladeFile = 'debug-marker.glade' gsignal('set-marker', str, int) def __init__(self): debugLevels = log.getLevelNames() GladeWindow.__init__(self) pos = 0 self._debugLevelCode = {} for level in debugLevels: if level == 'ERROR': continue self.level_selection.get_model().insert(pos, [level]) self._debugLevelCode[pos] = log.getLevelInt(level) pos = pos + 1 def _updateOkButtonSensitiveness(self): if (self.marker_entry.get_text() and self.level_selection.get_active() != -1): self.ok_button.set_sensitive(True) else: self.ok_button.set_sensitive(False) def _emitMarker(self): level = self._debugLevelCode[self.level_selection.get_active()] self.emit('set-marker', self.marker_entry.get_text(), level) # Callbacks def on_ok_button_clicked_cb(self, button): self._emitMarker() self.destroy() def on_cancel_button_clicked_cb(self, button): self.destroy() def on_marker_entry_changed_cb(self, entry): self._updateOkButtonSensitiveness() def on_level_selection_changed_cb(self, combo): self._updateOkButtonSensitiveness()
class ConnectionsDialog(GladeWindow): gladeFile = 'connection-dialog.glade' gsignal('have-connection', object) def on_connection_activated(self, widget, state): self.emit('have-connection', state) def on_cancel(self, button): self.destroy() def on_ok(self, button): self.emit('have-connection', self.widgets['connections'].get_selected()) def on_delete_event(self, dialog, event): self.destroy() def on_connections_cleared(self, widget): self.button_ok.set_sensitive(False)
class GladeWidget(gtk.VBox, GladeBacked): ''' Base class for composite widgets backed by glade interface definitions. The Window contents will be reparented to ourselves. All widgets inside the Window will be available as attributes on the object (dashes will be replaced with underscores). Example:: class MyWidget(GladeWidget): gladeFile = 'my_gladeFile.glade' ... gobject.type_register(MyWidget) Remember to chain up if you customize __init__(). ''' gsignal('validation-changed', bool) def __init__(self): GladeBacked.__init__(self) gtk.VBox.__init__(self) for name, widget in self.widgets.items(): # translate - to _ so we can access them as attributes if name.find('-') > -1: name = "_".join(name.split('-')) setattr(self, name, widget) # we reparent the contents of the window to ourselves w = self._window.get_child() self._window.remove(w) self.add(w) self._window.destroy() self._window = None
class View(gobject.GObject): gsignal('width-changed', int) gsignal('height-changed', int) gsignal('framerate-changed', float) gsignal('format-changed', int) gsignal('pattern-changed', int) latency = 100 # latency for timeouts on config changes def __init__(self): """ Construct a new videotest View. """ self.__gobject_init__() self._gladefile = os.path.join(configure.gladedir, 'videotest.glade') self._glade = gtk.glade.XML(self._gladefile, "videotest-widget") self._widget = self._glade.get_widget("videotest-widget") self._width_timeout = 0 self._height_timeout = 0 self._framerate_timeout = 0 self._format_timeout = 0 def prepare(self): # connect callbacks w = self._glade.get_widget("width-button") w.connect("value-changed", self.width_changed_cb) w = self._glade.get_widget("height-button") w.connect("value-changed", self.height_changed_cb) w = self._glade.get_widget("framerate-button") w.connect("value-changed", self.framerate_changed_cb) w = self._glade.get_widget("pattern-combo") self._pattern_combo = w self._pattern_model = gtk.ListStore(str) w.set_model(self._pattern_model) w.connect("changed", self.pattern_changed_cb) w = self._glade.get_widget("format-combo") self._format_combo = w self._format_model = gtk.ListStore(str) w.set_model(self._format_model) w.connect("changed", self.format_changed_cb) def get_widget(self): return self._widget def add_pattern(self, description): 'add a pattern description to the pattern combobox' # FIXME: for now we don't even store enum keys, which we might want # to do if they're added in a different order or not always start # at 0 self._pattern_model.append((description, )) def set_pattern(self, index): self._pattern_combo.set_active(index) def add_format(self, description): 'add a format description to the format combobox' # FIXME: for now we don't even store enum keys, which we might want # to do if they're added in a different order or not always start # at 0 self._format_model.append((description, )) def set_format(self, index): self._format_combo.set_active(index) ### timeouts def view_width_timeout(self, widget): width = widget.get_value() self.emit('width-changed', width) self._width_timeout = 0 return gtk.FALSE def view_height_timeout(self, widget): height = widget.get_value() self.emit('height-changed', height) self._height_timeout = 0 return gtk.FALSE def view_framerate_timeout(self, widget): framerate = widget.get_value() self.emit('framerate-changed', framerate) self._framerate_timeout = 0 return gtk.FALSE def view_format_timeout(self, widget): format = widget.get_active() self.emit('format-changed', format) self._format_timeout = 0 return gtk.FALSE ### callbacks def width_changed_cb(self, widget): if not self._width_timeout: id = gobject.timeout_add(View.latency, self.view_width_timeout, widget) self._width_timeout = id def height_changed_cb(self, widget): if not self._height_timeout: id = gobject.timeout_add(View.latency, self.view_height_timeout, widget) self._height_timeout = id def framerate_changed_cb(self, widget): if not self._framerate_timeout: id = gobject.timeout_add(View.latency, self.view_framerate_timeout, widget) self._framerate_timeout = id def format_changed_cb(self, widget): if not self._format_timeout: id = gobject.timeout_add(View.latency, self.view_format_timeout, widget) self._format_timeout = id def pattern_changed_cb(self, widget): index = widget.get_active() self.emit('pattern-changed', index)
class WorkerList(gtk.HBox): gsignal('worker-selected', str) _combobox = None _label = None def __init__(self): gtk.HBox.__init__(self) self._combobox = gtk.ComboBox() self._label = gtk.Label(_('Worker:')) self._label.show() self.pack_start(self._label, False, False, 0) vb = gtk.VBox() self.pack_start(vb, False, False, 10) vb.show() a = gtk.Alignment(0.5, 0.5) a.show() vb.pack_start(a, True, False, 0) cell = gtk.CellRendererText() cell.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE) cell.set_property('width', 100) self._combobox.pack_start(cell, True) self._combobox.add_attribute(cell, 'text', 0) def onChanged(cb): self.emit('worker-selected', self.getWorker()) self._combobox.connect('changed', onChanged) self._combobox.show() # GTK 2.4 try: self._combobox.set_property('focus-on-click', False) self._combobox.set_property('has-frame', False) except TypeError: pass a.add(self._combobox) def setWorkerHeavenState(self, whs): self._combobox.set_model(WorkerListStore(whs)) self.selectWorker(None) def onModelChanged(model): if not self.getWorker(): # need to select a new worker self.selectWorker(None) # will emit if worker selected if not self.getWorker(): # no workers present! self.emit('worker-selected', None) self._combobox.get_model().connect('changed', onModelChanged) def selectWorker(self, worker): # worker == none means select first worker for r in self._combobox.get_model(): if not worker or r.model.get_value(r.iter, 0) == worker: self._combobox.set_active_iter(r.iter) return if worker: # FIXME: let's not print, have correct logging print 'warning: worker %s not available' % worker def getWorker(self): i = self._combobox.get_active_iter() if i: return self._combobox.get_model().get_value(i, 0) return None def notifySelected(self): self.emit('worker-selected', self.getWorker())
class StartNew(WizardStep): name = 'start_new' title = _('Start a New Manager and Worker') text = _("""This will start a new manager and worker for you. The manager and worker will run under your user account. The manager will only accept connections from the local machine. This mode is only useful for testing Flumotion. """) start_worker_check = None next_pages = ['start_new_error', 'start_new_success'] gsignal('finished', str) _timeout_id = None # WizardStep def setup(self, state, available_pages): self.button_next.grab_focus() def on_next(self, state): self.label_starting.show() self.progressbar_starting.set_fraction(0.0) self.progressbar_starting.show() def pulse(): self.progressbar_starting.pulse() return True self._timeout_id = gobject.timeout_add(200, pulse) self._startManager(state) self.button_prev.set_sensitive(False) return '*signaled*' # Private def _startManager(self, state): port = tryPort() managerSpawner = LocalManagerSpawner(port) managerSpawner.connect('error', self._on_spawner_error, state) managerSpawner.connect('description-changed', self._on_spawner_description_changed) managerSpawner.connect('finished', self._on_spawner_finished, state) managerSpawner.start() def _on_spawner_error(self, spawner, failure, msg, args, state): # error should trigger going to next page with an overview state.update({ 'command': ' '.join(args), 'error': msg, 'failure': failure, }) self._finished('start_new_error') def _on_spawner_description_changed(self, spawner, description): self.label_starting.set_text(description) def _on_spawner_finished(self, spawner, state): # because of the ugly call-by-reference passing of state, # we have to update the existing dict, not re-bind with state = state['connectionInfo'] = parsePBConnectionInfo('localhost', username='******', password='******', port=spawner.getPort(), use_ssl=True) state.update( dict(confDir=spawner.getConfDir(), logDir=spawner.getLogDir(), runDir=spawner.getRunDir())) state['managerSpawner'] = spawner self._finished('start_new_success') def _finished(self, result): # result: start_new_error or start_new_success self.label_starting.hide() self.progressbar_starting.hide() gobject.source_remove(self._timeout_id) self.emit('finished', result)
class ComponentList(log.Loggable, gobject.GObject): """ I present a view on the list of components logged in to the manager. """ implements(flavors.IStateListener) logCategory = 'components' gsignal('selection-changed', object) # state-or-None gsignal('show-popup-menu', int, int) # button, click time gproperty(bool, 'can-start-any', 'True if any component can be started', False) gproperty(bool, 'can-stop-any', 'True if any component can be stopped', False) def __init__(self, treeView): """ @param treeView: the gtk.TreeView to put the view in. """ gobject.GObject.__init__(self) self.set_property('can-start-any', False) self.set_property('can-stop-any', False) self._iters = {} # componentState -> model iter self._lastStates = None self._model = None self._workers = [] self._view = None self._moodPixbufs = self._getMoodPixbufs() self._createUI(treeView) def _createUI(self, treeView): treeView.connect('button-press-event', self._view_button_press_event_cb) treeView.set_headers_visible(True) treeModel = gtk.ListStore( gtk.gdk.Pixbuf, # mood str, # name str, # worker str, # pid gtk.gdk.Pixbuf, # message level object, # state int, # mood-value str, # tooltip str, # color bool, # colorize ) treeView.set_model(treeModel) treeSelection = treeView.get_selection() treeSelection.set_mode(gtk.SELECTION_MULTIPLE) treeSelection.connect('changed', self._view_cursor_changed_cb) # put in all the columns col = gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), pixbuf=COL_MOOD) col.set_sort_column_id(COL_MOOD_VALUE) treeView.append_column(col) col = gtk.TreeViewColumn(_('Component'), gtk.CellRendererText(), text=COL_NAME, foreground=COL_FG, foreground_set=COL_SAD) col.set_sort_column_id(COL_NAME) treeView.append_column(col) col = gtk.TreeViewColumn(_('Worker'), gtk.CellRendererText(), markup=COL_WORKER, foreground=COL_FG, foreground_set=COL_SAD) col.set_sort_column_id(COL_WORKER) treeView.append_column(col) t = gtk.CellRendererText() col = gtk.TreeViewColumn(_('PID'), t, markup=COL_PID, foreground=COL_FG, foreground_set=COL_SAD) col.set_sort_column_id(COL_PID) treeView.append_column(col) col = gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), pixbuf=COL_MSG) treeView.append_column(col) if gtk.pygtk_version >= (2, 12): treeView.set_tooltip_column(COL_TOOLTIP) if hasattr(gtk.TreeView, 'set_rubber_banding'): treeView.set_rubber_banding(False) self._model = treeModel self._view = treeView def getSelectedNames(self): """ Get the names of the currently selected components, or None if none are selected. @rtype: list of str or None """ return self._getSelected(COL_NAME) def getSelectedStates(self): """ Get the states of the currently selected components, or None if none are selected. @rtype: list of L{flumotion.common.component.AdminComponentState} or None """ return self._getSelected(COL_STATE) def getComponentNames(self): """ Fetches a list of all component names. @returns: component names @rtype: list of str """ names = [] for row in self._model: names.append(row[COL_NAME]) return names def getComponentStates(self): """ Fetches a list of all component states @returns: component states @rtype: list of L{AdminComponentState} """ names = [] for row in self._model: names.append(row[COL_STATE]) return names def canDelete(self): """ Get whether the selected components can be deleted. Returns True if all components are sleeping. Also returns False if no components are selected. @rtype: bool """ states = self.getSelectedStates() if not states: return False canDelete = True for state in states: moodname = moods.get(state.get('mood')).name canDelete = canDelete and moodname == 'sleeping' return canDelete def canStart(self): """ Get whether the selected components can be started. Returns True if all components are sleeping and their worked has logged in. Also returns False if no components are selected. @rtype: bool """ # additionally to canDelete, the worker needs to be logged intoo if not self.canDelete(): return False canStart = True states = self.getSelectedStates() for state in states: workerName = state.get('workerRequested') canStart = canStart and workerName in self._workers return canStart def canStop(self): """ Get whether the selected components can be stopped. Returns True if none of the components are sleeping. Also returns False if no components are selected. @rtype: bool """ states = self.getSelectedStates() if not states: return False canStop = True for state in states: moodname = moods.get(state.get('mood')).name canStop = canStop and moodname != 'sleeping' return canStop def clearAndRebuild(self, components, componentNameToSelect=None): """ Update the components view by removing all old components and showing the new ones. @param components: dictionary of name -> L{flumotion.common.component.AdminComponentState} @param componentNameToSelect: name of the component to select or None """ # remove all Listeners self._model.foreach(self._removeListenerForeach) self.debug('updating components view') # clear and rebuild self._view.get_selection().unselect_all() self._model.clear() self._iters = {} components = sorted(components.values(), cmp=cmpComponentType, key=operator.itemgetter('type')) for component in components: self.appendComponent(component, componentNameToSelect) self.debug('updated components view') def appendComponent(self, component, componentNameToSelect): self.debug('adding component %r to listview' % component) component.addListener(self, set_=self.stateSet, append=self.stateSet, remove=self.stateSet) titer = self._model.append() self._iters[component] = titer mood = component.get('mood') self.debug('component has mood %r' % mood) messages = component.get('messages') self.debug('component has messages %r' % messages) self._setMsgLevel(titer, messages) if mood != None: self._setMoodValue(titer, mood) self._model.set(titer, COL_FG, SAD_COLOR) self._model.set(titer, COL_STATE, component) componentName = getComponentLabel(component) self._model.set(titer, COL_NAME, componentName) pid = component.get('pid') if not pid and component.hasKey('lastKnownPid'): if component.get('lastKnownPid'): pid = "<i>%d</i>" % component.get('lastKnownPid') self._model.set(titer, COL_PID, (pid and str(pid)) or '') self._updateWorker(titer, component) selection = self._view.get_selection() if (componentNameToSelect is not None and componentName == componentNameToSelect and not selection.get_selected_rows()[1]): selection.select_iter(titer) self._updateStartStop() def removeComponent(self, component): self.debug('removing component %r to listview' % component) titer = self._iters[component] self._model.remove(titer) del self._iters[component] self._updateStartStop() # IStateListener implementation def stateSet(self, state, key, value): if not isinstance(state, planet.AdminComponentState): self.warning('Got state change for unknown object %r' % state) return titer = self._iters[state] self.log('stateSet: state %r, key %s, value %r' % (state, key, value)) if key == 'mood': self.debug('stateSet: mood of %r changed to %r' % (state, value)) if value == moods.sleeping.value: self.debug('sleeping, removing local messages on %r' % state) for message in state.get('messages', []): state.observe_remove('messages', message) self._setMoodValue(titer, value) self._updateWorker(titer, state) elif key == 'name': if value: self._model.set(titer, COL_NAME, value) elif key == 'workerName': self._updateWorker(titer, state) elif key == 'pid': self._model.set(titer, COL_PID, (value and str(value) or '')) elif key == 'messages': self._setMsgLevel(titer, state.get('messages')) # Private def _setMsgLevel(self, titer, messages): icon = None if messages: messages = sorted(messages, cmp=lambda x, y: x.level - y.level) level = messages[0].level st = _stock_icons.get(level, gtk.STOCK_MISSING_IMAGE) w = gtk.Invisible() icon = w.render_icon(st, gtk.ICON_SIZE_MENU) self._model.set(titer, COL_MSG, icon) def _updateStartStop(self): oldstop = self.get_property('can-stop-any') oldstart = self.get_property('can-start-any') moodnames = [moods.get(x[COL_MOOD_VALUE]).name for x in self._model] canStop = bool([x for x in moodnames if (x != 'sleeping')]) canStart = bool([x for x in moodnames if (x == 'sleeping')]) if oldstop != canStop: self.set_property('can-stop-any', canStop) if oldstart != canStart: self.set_property('can-start-any', canStart) def workerAppend(self, name): self._workers.append(name) def workerRemove(self, name): self._workers.remove(name) for state, titer in self._iters.items(): self._updateWorker(titer, state) def _updateWorker(self, titer, componentState): # update the worker name: # - italic if workerName and workerRequested are not running # - normal if running workerName = componentState.get('workerName') workerRequested = componentState.get('workerRequested') if not workerName and not workerRequested: #FIXME: Should we raise an error here? # It's an impossible situation. workerName = _("[any worker]") markup = workerName or workerRequested if markup not in self._workers: self._model.set(titer, COL_TOOLTIP, _("<b>Worker %s is not connected</b>") % markup) markup = "<i>%s</i>" % markup self._model.set(titer, COL_WORKER, markup) def _removeListenerForeach(self, model, path, titer): # remove the listener for each state object state = model.get(titer, COL_STATE)[0] state.removeListener(self) def _setMoodValue(self, titer, value): """ Set the mood value on the given component name. @type value: int """ self._model.set(titer, COL_MOOD, self._moodPixbufs[value]) self._model.set(titer, COL_MOOD_VALUE, value) self._model.set(titer, COL_SAD, moods.sad.value == value) mood = moods.get(value) self._model.set( titer, COL_TOOLTIP, _("<b>Component is %s</b>") % (MOODS_INFO[mood].lower(), )) self._updateStartStop() def _getSelected(self, col_name): # returns None if no components are selected, a list otherwise selection = self._view.get_selection() if not selection: return None model, selected_tree_rows = selection.get_selected_rows() selected = [] for tree_row in selected_tree_rows: component_state = model[tree_row][col_name] selected.append(component_state) return selected def _getMoodPixbufs(self): # load all pixbufs for the moods pixbufs = {} for i in range(0, len(moods)): name = moods.get(i).name pixbufs[i] = gtk.gdk.pixbuf_new_from_file_at_size( os.path.join(configure.imagedir, 'mood-%s.png' % name), 24, 24) return pixbufs def _selectionChanged(self): states = self.getSelectedStates() if not states: self.debug( 'no component selected, emitting selection-changed None') # Emit this in an idle, since popups will not be shown # before this has completed, and it might possibly take a long # time to finish all the callbacks connected to selection-changed # This is not the proper fix, but makes the popups show up faster gobject.idle_add(self.emit, 'selection-changed', []) return if states == self._lastStates: self.debug('no new components selected, no emitting signal') return self.debug('components selected, emitting selection-changed') self.emit('selection-changed', states) self._lastStates = states def _showPopupMenu(self, event): selection = self._view.get_selection() retval = self._view.get_path_at_pos(int(event.x), int(event.y)) if retval is None: selection.unselect_all() return clicked_path = retval[0] selected_path = selection.get_selected_rows()[1] if clicked_path not in selected_path: selection.unselect_all() selection.select_path(clicked_path) self.emit('show-popup-menu', event.button, event.time) # Callbacks def _view_cursor_changed_cb(self, *args): self._selectionChanged() def _view_button_press_event_cb(self, treeview, event): if event.button == 3: self._showPopupMenu(event) return True return False
class ConfigurationAssistant(SectionWizard): """This is the main configuration assistant class, it is responsible for:: - executing tasks which will block the ui - showing a worker list in the UI - communicating with the manager, fetching bundles and registry information - running check defined by a step in a worker, for instance querying for hardware devices and their capabilities It extends SectionWizard which provides the basic user interface, such as sidebar, buttons, title bar and basic step navigation. """ gsignal('finished', str) def __init__(self, parent=None): SectionWizard.__init__(self, parent) self.connect('help-clicked', self._on_assistant__help_clicked) # Set the name of the toplevel window, this is used by the # ui unittest framework self.window1.set_name('ConfigurationAssistant') self.message_area.disableTimestamps() self._cursorWatch = gdk.Cursor(gdk.WATCH) self._tasks = [] self._adminModel = None self._workerHeavenState = None self._lastWorker = 0 # combo id last worker from step to step self._stepWorkers = {} self._scenario = None self._existingComponentNames = [] self._porters = [] self._mountPoints = [] self._consumers = {} self._workerList = WorkerList() self.top_vbox.pack_start(self._workerList, False, False) self._workerList.connect('worker-selected', self.on_combobox_worker_changed) # SectionWizard def completed(self): saver = AssistantSaver() saver.setFlowName('default') saver.setExistingComponentNames(self._existingComponentNames) self._scenario.save(self, saver) xml = saver.getXML() self.emit('finished', xml) def destroy(self): SectionWizard.destroy(self) self._adminModel = None def beforeShowStep(self, step): if isinstance(step, WorkerWizardStep): self._workerList.show() if step.worker: self._workerList.selectWorker(step.worker) self._workerList.notifySelected() else: self._workerList.hide() self._fetchDescription(step) self._setupWorker(step, self._workerList.getWorker()) def prepareNextStep(self, step): self._setupWorker(step, self._workerList.getWorker()) SectionWizard.prepareNextStep(self, step) def blockNext(self, block): # Do not block/unblock if we have tasks running if self._tasks: return SectionWizard.blockNext(self, block) # Public API # FIXME: Remove this and make fgc create a new scenario def addInitialSteps(self): """Add the step sections of the wizard, can be overridden in a subclass """ # These two steps are independent of the scenarios, they # should always be added. self.addStepSection(WelcomeStep) self.addStepSection(ScenarioStep) def setScenario(self, scenario): """Sets the current scenario of the assistant. Normally called by ScenarioStep to tell the assistant the current scenario just after creating it. @param scenario: the scenario of the assistant @type scenario: a L{flumotion.admin.assistant.scenarios.Scenario} subclass """ self._scenario = scenario def getScenario(self): """Fetches the currently set scenario of the assistant. @returns scenario: the scenario of the assistant @rtype: a L{flumotion.admin.assistant.scenarios.Scenario} subclass """ return self._scenario def setWorkerHeavenState(self, workerHeavenState): """ Sets the worker heaven state of the assistant @param workerHeavenState: the worker heaven state @type workerHeavenState: L{WorkerComponentUIState} """ self._workerHeavenState = workerHeavenState self._workerList.setWorkerHeavenState(workerHeavenState) def setAdminModel(self, adminModel): """ Sets the admin model of the assistant @param adminModel: the admin model @type adminModel: L{AdminModel} """ self._adminModel = adminModel self._adminModel.connect('connected', self.on_admin_connected_cb) self._adminModel.connect('disconnected', self.on_admin_disconnected_cb) def setHTTPPorters(self, porters): """ Sets the list of currently configured porters so we can reuse them for future streamers. @param porters: list of porters @type porters : list of L{flumotion.admin.assistant.models.Porter} """ self._porters = porters def getHTTPPorters(self): """ Obtains the list of the currently configured porters. @rtype : list of L{flumotion.admin.assistant.models.Porter} """ return self._porters def addMountPoint(self, worker, port, mount_point, consumer=None): """ Marks a mount point as used on the given worker and port. If a consumer name is provided it means we are changing the mount point for that consumer and that we should keep track of it for further modifications. @param worker : The worker where the mount_point is configured. @type worker : str @param port : The port where the streamer should be listening. @type port : int @param mount_point : The mount point where the data will be served. @type mount_point : str @param consumer : The consumer that is changing its mountpoint. @type consumer : str @returns : True if the mount point is not used and has been inserted correctly, False otherwise. @rtype : boolean """ if not worker or not port or not mount_point: return False if consumer in self._consumers: oldData = self._consumers[consumer] if oldData in self._mountPoints: self._mountPoints.remove(oldData) data = (worker, port, mount_point) if data in self._mountPoints: return False self._mountPoints.append(data) if consumer: self._consumers[consumer] = data return True def getAdminModel(self): """Gets the admin model of the assistant @returns adminModel: the admin model @rtype adminModel: L{AdminModel} """ return self._adminModel def waitForTask(self, taskName): """Instruct the assistant that we're waiting for a task to be finished. This changes the cursor and prevents the user from continuing moving forward. Each call to this method should have another call to taskFinished() when the task is actually done. @param taskName: name of the name @type taskName: string """ self.info("waiting for task %s" % (taskName, )) if not self._tasks: if self.window1.window is not None: self.window1.window.set_cursor(self._cursorWatch) self.blockNext(True) self._tasks.append(taskName) def taskFinished(self, blockNext=False): """Instruct the assistant that a task was finished. @param blockNext: if we should still next when done @type blockNext: boolean """ if not self._tasks: raise AssertionError( "Stray call to taskFinished(), forgot to call waitForTask()?") taskName = self._tasks.pop() self.info("task %s has now finished" % (taskName, )) if not self._tasks: self.window1.window.set_cursor(None) self.blockNext(blockNext) def pendingTask(self): """Returns true if there are any pending tasks @returns: if there are pending tasks @rtype: bool """ return bool(self._tasks) def checkElements(self, workerName, *elementNames): """Check if the given list of GStreamer elements exist on the given worker. @param workerName: name of the worker to check on @type workerName: string @param elementNames: names of the elements to check @type elementNames: list of strings @returns: a deferred returning a tuple of the missing elements @rtype: L{twisted.internet.defer.Deferred} """ if not self._adminModel: self.debug('No admin connected, not checking presence of elements') return asked = python.set(elementNames) def _checkElementsCallback(existing, workerName): existing = python.set(existing) self.taskFinished() return tuple(asked.difference(existing)) self.waitForTask('check elements %r' % (elementNames, )) d = self._adminModel.checkElements(workerName, elementNames) d.addCallback(_checkElementsCallback, workerName) return d def requireElements(self, workerName, *elementNames): """Require that the given list of GStreamer elements exists on the given worker. If the elements do not exist, an error message is posted and the next button remains blocked. @param workerName: name of the worker to check on @type workerName: string @param elementNames: names of the elements to check @type elementNames: list of strings @returns: element name @rtype: deferred -> list of strings """ if not self._adminModel: self.debug('No admin connected, not checking presence of elements') return self.debug('requiring elements %r' % (elementNames, )) f = ngettext( "Checking the existence of GStreamer element '%s' " "on %s worker.", "Checking the existence of GStreamer elements '%s' " "on %s worker.", len(elementNames)) msg = messages.Info(T_(f, "', '".join(elementNames), workerName), mid='require-elements') self.add_msg(msg) def gotMissingElements(elements, workerName): self.clear_msg('require-elements') if elements: self.warning('elements %r do not exist' % (elements, )) f = ngettext( "Worker '%s' is missing GStreamer element '%s'.", "Worker '%s' is missing GStreamer elements '%s'.", len(elements)) message = messages.Error( T_(f, workerName, "', '".join(elements))) message.add( T_( N_("\n" "Please install the necessary GStreamer plug-ins that " "provide these elements and restart the worker."))) message.add( T_( N_("\n\n" "You will not be able to go forward using this worker." ))) message.id = 'element' + '-'.join(elementNames) documentation.messageAddGStreamerInstall(message) self.add_msg(message) self.taskFinished(bool(elements)) return elements self.waitForTask('require elements %r' % (elementNames, )) d = self.checkElements(workerName, *elementNames) d.addCallback(gotMissingElements, workerName) return d def checkImport(self, workerName, moduleName): """Check if the given module can be imported. @param workerName: name of the worker to check on @type workerName: string @param moduleName: name of the module to import @type moduleName: string @returns: a deferred firing None or Failure. @rtype: L{twisted.internet.defer.Deferred} """ if not self._adminModel: self.debug('No admin connected, not checking presence of elements') return d = self._adminModel.checkImport(workerName, moduleName) return d def requireImport(self, workerName, moduleName, projectName=None, projectURL=None): """Require that the given module can be imported on the given worker. If the module cannot be imported, an error message is posted and the next button remains blocked. @param workerName: name of the worker to check on @type workerName: string @param moduleName: name of the module to import @type moduleName: string @param projectName: name of the module to import @type projectName: string @param projectURL: URL of the project @type projectURL: string @returns: a deferred firing None or Failure @rtype: L{twisted.internet.defer.Deferred} """ if not self._adminModel: self.debug('No admin connected, not checking presence of elements') return self.debug('requiring module %s' % moduleName) def _checkImportErrback(failure): self.warning('could not import %s', moduleName) message = messages.Error( T_(N_("Worker '%s' cannot import module '%s'."), workerName, moduleName)) if projectName: message.add( T_(N_("\n" "This module is part of '%s'."), projectName)) if projectURL: message.add( T_(N_("\n" "The project's homepage is %s"), projectURL)) message.add( T_( N_("\n\n" "You will not be able to go forward using this worker.") )) message.id = 'module-%s' % moduleName documentation.messageAddPythonInstall(message, moduleName) self.add_msg(message) self.taskFinished(blockNext=True) return False d = self.checkImport(workerName, moduleName) d.addErrback(_checkImportErrback) return d # FIXME: maybe add id here for return messages ? def runInWorker(self, workerName, moduleName, functionName, *args, **kwargs): """ Run the given function and arguments on the selected worker. The given function should return a L{messages.Result}. @param workerName: name of the worker to run the function in @type workerName: string @param moduleName: name of the module where the function is found @type moduleName: string @param functionName: name of the function to run @type functionName: string @returns: a deferred firing the Result's value. @rtype: L{twisted.internet.defer.Deferred} """ self.debug('runInWorker(moduleName=%r, functionName=%r)' % (moduleName, functionName)) admin = self._adminModel if not admin: self.warning('skipping runInWorker, no admin') return defer.fail(errors.FlumotionError('no admin')) if not workerName: self.warning('skipping runInWorker, no worker') return defer.fail(errors.FlumotionError('no worker')) def callback(result): self.debug('runInWorker callbacked a result') self.clear_msg(functionName) if not isinstance(result, messages.Result): msg = messages.Error(T_( N_("Internal error: could not run check code on worker.")), debug=( 'function %r returned a non-Result %r' % (functionName, result))) self.add_msg(msg) self.taskFinished(True) raise errors.RemoteRunError(functionName, 'Internal error.') for m in result.messages: self.debug('showing msg %r' % m) self.add_msg(m) if result.failed: self.debug('... that failed') self.taskFinished(True) raise errors.RemoteRunFailure(functionName, 'Result failed') self.debug('... that succeeded') self.taskFinished() return result.value def errback(failure): self.debug('runInWorker errbacked, showing error msg') if failure.check(errors.RemoteRunError): debug = failure.value else: debug = "Failure while running %s.%s:\n%s" % ( moduleName, functionName, failure.getTraceback()) msg = messages.Error(T_( N_("Internal error: could not run check code on worker.")), debug=debug) self.add_msg(msg) self.taskFinished(True) raise errors.RemoteRunError(functionName, 'Internal error.') self.waitForTask('run in worker: %s.%s(%r, %r)' % (moduleName, functionName, args, kwargs)) d = admin.workerRun(workerName, moduleName, functionName, *args, **kwargs) d.addErrback(errback) d.addCallback(callback) return d def getWizardEntry(self, componentType): """Fetches a assistant bundle from a specific kind of component @param componentType: the component type to get the assistant entry bundle from. @type componentType: string @returns: a deferred returning either:: - factory of the component - noBundle error: if the component lacks a assistant bundle @rtype: L{twisted.internet.defer.Deferred} """ self.waitForTask('get assistant entry %s' % (componentType, )) self.clear_msg('assistant-bundle') d = self._adminModel.callRemote('getEntryByType', componentType, 'wizard') d.addCallback(self._gotEntryPoint) return d def getWizardScenario(self, scenarioType): """ Fetches a scenario bundle from a specific kind of component. @param scenarioType: the scenario type to get the assistant entry bundle from. @type scenarioType: string @returns: a deferred returning either:: - factory of the component - noBundle error: if the component lacks a assistant bundle @rtype: L{twisted.internet.defer.Deferred} """ self.waitForTask('get assistant entry %s' % (scenarioType, )) self.clear_msg('assistant-bundle') d = self._adminModel.callRemote('getScenarioByType', scenarioType, 'wizard') d.addCallback(self._gotEntryPoint) return d def getWizardPlugEntry(self, plugType): """Fetches a assistant bundle from a specific kind of plug @param plugType: the plug type to get the assistant entry bundle from. @type plugType: string @returns: a deferred returning either:: - factory of the plug - noBundle error: if the plug lacks a assistant bundle @rtype: L{twisted.internet.defer.Deferred} """ self.waitForTask('get assistant plug %s' % (plugType, )) self.clear_msg('assistant-bundle') d = self._adminModel.callRemote('getPlugEntry', plugType, 'wizard') d.addCallback(self._gotEntryPoint) return d def getWizardEntries(self, wizardTypes=None, provides=None, accepts=None): """Queries the manager for a list of assistant entries matching the query. @param wizardTypes: list of component types to fetch, is usually something like ['video-producer'] or ['audio-encoder'] @type wizardTypes: list of str @param provides: formats provided, eg ['jpeg', 'speex'] @type provides: list of str @param accepts: formats accepted, eg ['theora'] @type accepts: list of str @returns: a deferred firing a list of L{flumotion.common.componentui.WizardEntryState} @rtype: L{twisted.internet.defer.Deferred} """ self.debug('querying wizard entries (wizardTypes=%r,provides=%r' ',accepts=%r)' % (wizardTypes, provides, accepts)) return self._adminModel.getWizardEntries(wizardTypes=wizardTypes, provides=provides, accepts=accepts) def setExistingComponentNames(self, componentNames): """Tells the assistant about the existing components available, so we can resolve naming conflicts when saving the configuration @param componentNames: existing component names @type componentNames: list of strings """ self._existingComponentNames = componentNames def workerChangedForStep(self, step, workerName): """Tell a step that its worker changed. @param step: step which worker changed for @type step: a L{WorkerWizardStep} subclass @param workerName: name of the worker @type workerName: string """ if self._stepWorkers.get(step) == workerName: return self.debug('calling %r.workerChanged' % step) step.workerChanged(workerName) self._stepWorkers[step] = workerName # Private def _gotEntryPoint(self, (filename, procname)): # The manager always returns / as a path separator, replace them with # the separator since the rest of our infrastructure depends on that. filename = filename.replace('/', os.path.sep) modname = pathToModuleName(filename) d = self._adminModel.getBundledFunction(modname, procname) self.clear_msg('assistant-bundle') self.taskFinished() return d
class _WizardSidebar(gtk.EventBox, log.Loggable): gsignal('step-chosen', str) logCategory = 'wizard' def __init__(self, wizard): # FIXME: Remove this reference self._wizard = wizard # FIXME: Join these three into one self._sections = [] self._sections2 = [] self._sectionsByName = {} self._active = -1 self._currentSection = 0 self._currentStep = None self._stack = _WalkableStack() self._steps = {} self._top = -1 gtk.EventBox.__init__(self) self.connect_after('realize', self.after_realize) self.set_size_request(160, -1) self.vbox = gtk.VBox() self.vbox.set_border_width(5) self.vbox.show() self.add(self.vbox) # Public API def appendSection(self, title, name): """Adds a new section to the sidebar @param title: title of the section @param name: name of the section """ def clicked_cb(b, name): self.emit('step-chosen', name) section = _SidebarSection(title, name) section.connect('step-chosen', clicked_cb) section.show() section.set_active(False) self.vbox.pack_start(section, False, False) self._sections.append(section) self._sectionsByName[name] = section def removeSection(self, name): """Removes a section by name @param name: name of the section """ section = self._sectionsByName.pop(name) self._sections.remove(section) self.vbox.remove(section) def jumpTo(self, section_name): for i, section in enumerate(self._sections): if section.name == section_name: self._set_active(i) return raise AssertionError() def push(self, section_name, step_name, step_title): active_section = self._sections[self._active] if active_section.name == section_name: # same section active_section.push_step(step_name, step_title) else: # new section self._set_active(self._active + 1) self._top += 1 self._sections[self._active].push_header() def pop(self): if self._top < 0 or self._top >= len(self._sections): return False top_section = self._sections[self._top] if not top_section.pop_step(): top_section.pop_header() self._top -= 1 if self._top < 0: return False if self._top < self._active: self._set_active(self._top) return True def cleanFutureSteps(self): oldSections = self._sections2[self._currentSection+1:][:] for i, oldSection in enumerate(oldSections): self.removeSection(oldSection.name) self._sections2.remove(oldSection) cleared = self._stack.clearToCurrent() for step in cleared: self._steps.pop(step.name) self._top = self._active def addStepSection(self, section): self.appendSection(section.section, section.name) self._sections2.append(section) def getStep(self, stepname): for step in self.getSteps(): if step.get_name() == stepname: return step else: raise KeyError(stepname) def getCurrentStep(self): return self._currentStep def getSteps(self): return self._steps.values() def hasStep(self, stepName): for step in self.getSteps(): if step.get_name() == stepName: return True return False def getVisitedSteps(self): for step in self.getSteps(): if step.visited: yield step def pushSteps(self): sectionClass = self._sections2[self._currentSection] if isinstance(sectionClass, (type, types.ClassType)): section = sectionClass(self._wizard) else: section = sectionClass self.push(section.section, None, section.section) self._stack.push(section) self._setStep(section) def canGoBack(self): return self._stack.pos != 0 def prepareNextStep(self, step): if hasattr(step, 'lastStep'): self._wizard.finish(completed=True) return stepNext = step.getNext() if isinstance(stepNext, WizardStep): nextStep = stepNext elif isinstance(stepNext, defer.Deferred): d = stepNext def getStep(step): if step is None: step = self._getNextStep() if step is None: return self._showNextStep(step) def manageBundleError(error): exceptionHandler(error.type, error.value, error.getTracebackObject()) self._wizard.taskFinished(False) d.addCallback(getStep) d.addErrback(manageBundleError) return elif stepNext is None: nextStep = self._getNextStep() if nextStep is None: return else: raise AssertionError(stepNext) self._showNextStep(nextStep) def jumpToStep(self, name): # If we're jumping to the same step don't do anything to # avoid unnecessary ui flashes if self.getStep(name) == self.getCurrentStep(): return self._stack.skipTo(lambda x: x.name == name) step = self._stack.current() self._wizard.sidebar.jumpTo(step.section) self._currentSection = self._getSectionByName(step.section) self._setStep(step) def showPreviousStep(self): step = self._stack.back() self._currentSection = self._getSectionByName(step.section) self._setStep(step) self._wizard.updateButtons(hasNext=True) self.jumpTo(step.section) hasNext = not hasattr(step, 'lastStep') self._wizard.updateButtons(hasNext) # Private def _set_active(self, i): if self._active >= 0: self._sections[self._active].set_active(False) self._active = i if self._active >= 0: self._sections[self._active].set_active(True) l = len(self._sections) for i, section in enumerate(self._sections): if i <= self._active: pos = i pack_type = gtk.PACK_START else: pos = l - i pack_type = gtk.PACK_END self.vbox.child_set_property(section, 'pack_type', pack_type) self.vbox.reorder_child(section, pos) def _showNextStep(self, step): while not self._stack.push(step): s = self._stack.pop() s.visited = False self.pop() hasNext = not hasattr(step, 'lastStep') if not step.visited and hasNext: self.push(step.section, step.name, step.sidebarName) else: self.jumpTo(step.section) step.visited = True self._setStep(step) self._wizard.updateButtons(hasNext) def _setStep(self, step): self._steps[step.name] = step self._currentStep = step self._wizard.blockNext(False) self._wizard.packStep(step) self._wizard.beforeShowStep(step) self.debug('showing step %r' % step) step.show() step.activated() def _getSectionByName(self, section_name): for sectionClass in self._sections2: if sectionClass.section == section_name: return self._sections2.index(sectionClass) def _getNextStep(self): if self._currentSection + 1 == len(self._sections2): self._wizard.finish(completed=True) return self._currentSection += 1 nextStepClass = self._sections2[self._currentSection] if isinstance(nextStepClass, WizardStep): return nextStepClass return nextStepClass(self._wizard) # Callbacks def after_realize(self, eventbox): # have to get the style from the theme, but it's not really # there until we're realized style = self.get_style() self.modify_bg(gtk.STATE_NORMAL, style.bg[gtk.STATE_SELECTED])
class Connections(GladeWidget): gladeFile = 'connections.glade' gsignal('have-connection', bool) gsignal('connection-activated', object) gsignal('connections-cleared') def __init__(self): GladeWidget.__init__(self) self.connections.set_columns([ Column("host", title=_("Hostname"), searchable=True, ellipsize=ELLIPSIZE_MIDDLE, expand=True, width=100), Column("manager", title=_("Manager"), searchable=True, ellipsize=ELLIPSIZE_END, expand=True, width=50), Column("timestamp", title=_("Last used"), sorted=True, order=gtk.SORT_DESCENDING, format_func=format_timestamp), ]) self.connections.add_list(getRecentConnections()) self.connections.get_treeview().set_search_equal_func( self._searchEqual) self.connections.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.connections.set_property('selection-mode', gtk.SELECTION_SINGLE) self.connections.set_size_request(-1, 160) self._updateButtons() def _updateButtons(self): canClear = hasRecentConnections() self.button_clear.set_sensitive(canClear) self.button_clear_all.set_sensitive(canClear) if not canClear: self.emit('connections-cleared') def _searchEqual(self, model, column, key, iter): connection = model.get(iter, 0)[0] if key in connection.manager: return False # True means doesn't match return True def _clear_all(self): for conn in self.connections: os.unlink(conn.filename) self.connections.clear() def _clear(self, conn): self.connections.remove(conn) os.unlink(conn.filename) # Public API def grab_focus(self): if len(self.connections): self.connections.select(self.connections[0]) self.connections.grab_focus() def get_selected(self): return self.connections.get_selected() def update(self, connection): os.utime(connection.filename, None) # Callbacks def on_button_clear_clicked(self, button): conn = self.connections.get_selected() if conn: self._clear(conn) self._updateButtons() def on_button_clear_all_clicked(self, button): self._clear_all() self._updateButtons() def _on_connections_row_activated(self, connections, connection): self.emit('connection-activated', connection) def _on_connections_selection_changed(self, connections, connection): self.emit('have-connection', bool(connection))
class SimpleWizard(GladeWindow): """ A simple generic wizard. This wizard runs its own GObject main loop. The wizard is run with the run() method. Example:: w = Wizard('foo', 'first-step', FirstStep) w.show() w.run() => {'bank-account': 'foo'} """ # should by filled by subclasses name = None steps = None # private # well really, if they're private, why not prefix them with _ gladeFile = 'admin-wizard.glade' gsignal('finished') def __init__(self, initial_page, parent=None): """ @param initial_page: name of the WizardStep to start on @type initial_page: str @param parent: parent window @type window: gtk.Window subclass or None """ GladeWindow.__init__(self, parent=parent) self.page = None self.page_stack = [] self.pages = {} self.state = {} # these should be filled by subclasses assert self.name assert self.steps # HACK warning, it would be better to pass in the wizard # to each step self.window1.wizard = self # instantiate steps for cls in self.steps: page = cls(self, self.name + '-') self.pages[cls.name] = page page.show() self._setup_ui() self.set_page(initial_page) def _setup_ui(self): w = self.widgets iconfile = os.path.join(configure.imagedir, 'flumotion.png') self.window.set_icon_from_file(iconfile) w['image_icon'].set_from_file(iconfile) # have to get the style from the theme, but it's not really there until # it's realized w['label_title'].realize() style = w['label_title'].get_style() title_bg = style.bg[gtk.STATE_SELECTED] title_fg = style.fg[gtk.STATE_SELECTED] w['eventbox_top'].modify_bg(gtk.STATE_NORMAL, title_bg) w['eventbox_top'].modify_bg(gtk.STATE_INSENSITIVE, title_bg) w['label_title'].modify_fg(gtk.STATE_NORMAL, title_fg) w['label_title'].modify_fg(gtk.STATE_INSENSITIVE, title_fg) normal_bg = style.bg[gtk.STATE_NORMAL] w['textview_text'].modify_base(gtk.STATE_INSENSITIVE, normal_bg) w['textview_text'].modify_bg(gtk.STATE_INSENSITIVE, normal_bg) w['eventbox_content'].modify_base(gtk.STATE_INSENSITIVE, normal_bg) w['eventbox_content'].modify_bg(gtk.STATE_INSENSITIVE, normal_bg) def set_page(self, name): try: page = self.pages[name] except KeyError: raise AssertionError('No page named %s in %r' % (name, self.pages)) w = self.widgets page.button_next = w['button_next'] page.button_prev = w['button_prev'] available_pages = [ p for p in page.next_pages if self.pages[p].is_available() ] w['button_prev'].set_sensitive(bool(self.page_stack)) self.page = page for x in w['page_bin'].get_children(): w['page_bin'].remove(x) w['page_bin'].add(page) w['label_title'].set_markup('<big><b>%s</b></big>' % page.title) w['textview_text'].get_buffer().set_text(page.text) w['button_next'].set_sensitive(True) page.setup(self.state, available_pages) def on_delete_event(self, *window): self.state = None self.emit('finished') def next(self): self.on_next(self.widgets['button_next']) def on_next(self, button): button.set_sensitive(False) next_page = self.page.on_next(self.state) if not next_page: # the input is incorrect pass elif next_page == '*finished*': button.set_sensitive(True) self.emit('finished') elif next_page == '*signaled*': # page wants to do more stuff and will signal us next_page # when done def on_finished(page, next_page): button.set_sensitive(True) self.page.disconnect(self._finished_id) if next_page == '*finished*': self.emit('finished') else: self.page_stack.append(self.page) self.set_page(next_page) self._finished_id = self.page.connect('finished', on_finished) else: # work around a gtk+ bug #56070 button.hide() button.show() button.set_sensitive(True) self.page_stack.append(self.page) self.set_page(next_page) def on_prev(self, button): page = self.page_stack.pop() self.button_next.set_label(gtk.STOCK_GO_FORWARD) self.set_page(page.name) def set_sensitive(self, is_sensitive): self.window.set_sensitive(is_sensitive) def run(self): """ Run in a recursive main loop. Will block until the user finishes or closes the wizard. """ loop = gobject.MainLoop() d = self.runAsync() def async_done(_): loop.quit() d.addCallbacks(async_done, async_done) loop.run() return self.state def stop(self): self.window.destroy() def runAsync(self): """ Show the wizard. Returns a deferred that fires when the user has closed the wizard, either via completing the process or has cancelled somehow. @returns: a deferred that will fire the state dict accumulated by the pages, or None on cancel """ assert self.window self.set_sensitive(True) self.show() from twisted.internet import defer d = defer.Deferred() def finished(x): self.disconnect(i) self.hide() if self.state: d.callback(self.state) else: d.errback(WizardCancelled()) i = self.connect('finished', finished) return d
class Controller(gobject.GObject): """ Controller for a video test producer, used to generate a video test feed. The controller's model produces a raw video test feed using videotestsrc. """ # FIXME: decide on a good name for prepared that says "you can do stuff with me # now" gsignal('prepared') def __init__(self): """ Create a new video test controller. The spyglass controller needs to be prepared. """ self.__gobject_init__() self.view = View() self.model = Model() ### public methods def prepare(self): """ Prepares the controller. Returns immediately. Emits 'prepared' signal when it is done preparing. """ # the view's prepare is synchronous for now self.view.prepare() # add possible patterns self.view.add_pattern("SMPTE") self.view.add_pattern("Snow") self.view.add_pattern("0% Black") self.view.set_pattern(1) # add possible formats self.view.add_format("RGB") self.view.add_format("YUY2") self.view.add_format("I420") self.view.set_format(0) self.view.connect('width-changed', self.view_width_changed_cb) self.view.connect('height-changed', self.view_height_changed_cb) self.view.connect('framerate-changed', self.view_framerate_changed_cb) self.view.connect('format-changed', self.view_format_changed_cb) self.view.connect('pattern-changed', self.view_pattern_changed_cb) # the model doesn't currently have a prepare ### callbacks def view_width_changed_cb(self, view, width): _debug("width changed to %d" % width) self.model.set_width(width) def view_height_changed_cb(self, view, height): _debug("height changed to %d" % height) self.model.set_height(height) def view_framerate_changed_cb(self, view, framerate): _debug("framerate changed to %f" % framerate) self.model.set_framerate(framerate) def view_format_changed_cb(self, view, format): _debug("format changed to %f" % format) self.model.set_format(format) def view_pattern_changed_cb(self, view, index): _debug("pattern changed to index %d" % index) self.model.set_pattern(index)
class SectionWizard(GladeWindow, log.Loggable): """I am a section wizard which consists of the following elements:: - header: showing step title and icon - sidebar should a list of step hierarchal sections - step area: step dependent area - footer: buttons; help, back, forward/finish. """ gsignal('destroy') gsignal('help-clicked', str, str, str) # section, anchor, version logCategory = 'wizard' gladeFile = 'sectionwizard.glade' def __init__(self, parent_window=None): self._useMain = True GladeWindow.__init__(self, parent_window) for k, v in self.widgets.items(): setattr(self, k, v) self.window.set_icon_from_file(os.path.join(configure.imagedir, 'flumotion.png')) self.window.connect_after('realize', self.on_window_realize) self.window.connect('destroy', self.on_window_destroy) self.sidebar = _WizardSidebar(self) self.sidebar.connect('step-chosen', self.on_sidebar_step_chosen) self.sidebar.set_size_request(160, -1) self.hbox_main.pack_start(self.sidebar, False, False) self.hbox_main.reorder_child(self.sidebar, 0) self.sidebar.show() def __nonzero__(self): return True def __len__(self): return len(self.getSteps()) # Override this in subclass def completed(self): pass def beforeShowStep(self, step): pass # Public API def cleanFutureSteps(self): """Removes all the steps in front of the current one""" self.sidebar.cleanFutureSteps() def addStepSection(self, section): """Adds a new step section @param section: section to add @type section: a WizardStep subclass """ self.sidebar.addStepSection(section) def getSteps(self): """Returns a sequence of all steps @returns: sequence of visited steps. @rtype: sequence of L{WizardStep} """ return self.sidebar.getSteps() def getStep(self, stepname): """Fetches a step. KeyError is raised when the step is not found. @param stepname: name of the step to fetch @type stepname: str @returns: a L{WizardStep} instance or raises KeyError """ return self.sidebar.getStep(stepname) def hasStep(self, stepName): """Find out if a step with name stepName exists @returns: if the stepName exists """ return self.sidebar.hasStep(stepName) def getVisitedSteps(self): """Returns a sequence of steps which has been visited. Visited means that the state of the step should be considered when finishing the wizard. @returns: sequence of visited steps. @rtype: sequence of L{WizardStep} """ return self.sidebar.getVisitedSteps() def getCurrentStep(self): """Get the current step @returns: the current step @rtype: L{WizardStep} """ return self.sidebar.getCurrentStep() def hide(self): self.window.hide() def clear_msg(self, id): self.message_area.clearMessage(id) def add_msg(self, msg): self.message_area.addMessage(msg) def clear_all_msg(self): self.message_area.clear() def goNext(self): """Show the next step, this is called when the next button is clicked """ self.sidebar.prepareNextStep(self.sidebar.getCurrentStep()) def setStepDescription(self, description): """Sets the steps description. This is usually a sentence describing a component. @param description: the description @type description: string """ self.label_description.set_markup( '<i>%s</i>' % _escape(description or '')) def blockNext(self, block): self.button_next.set_sensitive(not block) # work around a gtk+ bug #56070 if not block: self.button_next.hide() self.button_next.show() def run(self, main=True): self._useMain = main self.sidebar.pushSteps() self.window.present() self.window.grab_focus() if self._useMain: try: gtk.main() except KeyboardInterrupt: pass def packStep(self, step): # Remove previous step map(self.content_area.remove, self.content_area.get_children()) self.message_area.clear() # Add current self.content_area.pack_start(step, True, True, 0) self._setStepIcon(step.icon) self._setStepTitle(step.title) self.updateButtons(hasNext=True) def finish(self, main=True, completed=True): if completed: self.completed() if self._useMain: try: gtk.main_quit() except RuntimeError: pass def updateButtons(self, hasNext): # hasNext: whether or not there is a next step step = self.getCurrentStep() canGoBack = self.sidebar.canGoBack() hasHelp = bool(step.docSection) self.button_help.set_sensitive(hasHelp) self.button_prev.set_sensitive(canGoBack) if hasNext: self.button_next.set_label(gtk.STOCK_GO_FORWARD) else: self.button_next.set_label(_('_Finish')) self.button_next.grab_focus() # Private def _setStepIcon(self, icon): icon_filename = os.path.join(configure.imagedir, 'wizard', icon) assert os.path.exists(icon_filename) self.image_icon.set_from_file(icon_filename) def _setStepTitle(self, title): self.label_title.set_markup( '<span size="x-large">%s</span>' % _escape(title or '')) def _helpClicked(self): step = self.getCurrentStep() self.emit('help-clicked', step.docSection, step.docAnchor, step.docVersion) # Callbacks def on_window_realize(self, window): # have to get the style from the theme, but it's not really # there until it's attached style = self.eventbox_top.get_style() bg = style.bg[gtk.STATE_SELECTED] fg = style.fg[gtk.STATE_SELECTED] self.eventbox_top.modify_bg(gtk.STATE_NORMAL, bg) self.hbuttonbox2.modify_bg(gtk.STATE_NORMAL, bg) self.label_title.modify_fg(gtk.STATE_NORMAL, fg) self.label_description.modify_fg(gtk.STATE_NORMAL, fg) def on_window_destroy(self, window): self.emit('destroy') def on_window_delete_event(self, wizard, event): self.finish(self._useMain, completed=False) def on_button_help_clicked(self, button): self._helpClicked() def on_button_prev_clicked(self, button): self.sidebar.showPreviousStep() def on_button_next_clicked(self, button): self.goNext() def on_sidebar_step_chosen(self, sidebar, name): self.sidebar.jumpToStep(name)
class MessagesView(gtk.VBox): """ I am a widget that can show messages. """ # I am a vbox with first row the label and icons, # second row a separator # and third row a text view gsignal('resize-event', bool) def __init__(self): gtk.VBox.__init__(self) self._disableTimestamps = False self.active_button = None self._createUI() self.clear() self._translator = Translator() localedir = os.path.join(configure.localedatadir, 'locale') # FIXME: add locales as messages from domains come in self._translator.addLocaleDir(configure.PACKAGE, localedir) def _createUI(self): h1 = gtk.HBox() self.pack_start(h1, False, False, 0) self.hline = gtk.HSeparator() h1.pack_start(self.hline, True, True, 3) # button box holding the message icons at the top right h2 = gtk.HBox() h1.pack_end(h2, False, False, 0) self.buttonbox = h2 sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) self.pack_start(sw, True, True, 0) self.sw = sw # text view shows the messages, plus debug information # FIXME: needs to be hyperlinkable in the future tv = gtk.TextView() tv.set_wrap_mode(gtk.WRAP_WORD) tv.set_left_margin(6) tv.set_right_margin(6) tv.set_accepts_tab(False) tv.set_cursor_visible(False) tv.set_editable(False) #tv.set_sensitive(False) # connect signals to act on the hyperlink tv.connect('event-after', self._after_textview__event) tv.connect('motion-notify-event', self._on_textview___motion_notify_event) sw.add(tv) self.textview = tv self.show_all() def clear(self): """ Remove all messages and hide myself. """ for child in self.buttonbox.get_children(): self.clearMessage(child.message.id) self.hide() def addMessage(self, m): """ Add a message to me. @type m: L{flumotion.common.messages.Message} """ # clear all previously added messages with the same id. This allows # us to replace for example a "probing" message with the # result message self.clearMessage(m.id) # add a message button to show this message b = MessageButton(m) b.sigid = b.connect('toggled', self._on_message_button__toggled, m) b.show() self.buttonbox.pack_start(b, False, False, 0) firstButton = self._sortMessages() self.show() if not self.active_button: b.set_active(True) elif b == firstButton: b.set_active(True) def clearMessage(self, id): """ Clear all messages with the given id. Will bring the remaining most important message to the front, or hide the view completely if no messages are left. """ for button in self.buttonbox.get_children(): if button.message.id != id: continue self.buttonbox.remove(button) button.disconnect(button.sigid) button.sigid = 0 if not self.buttonbox.get_children(): self.active_button = None self.hide() elif self.active_button == button: self.active_button = self.buttonbox.get_children()[0] self.active_button.set_active(True) break def disableTimestamps(self): """Disable timestamps for this MessageView, it will make it easier to understand the error messages and make it suitable for end users. """ self._disableTimestamps = True # Private def _addMessageToBuffer(self, message): # FIXME: it would be good to have a "Debug" button when # applicable, instead of always showing the text text = self._translator.translate(message) textbuffer = gtk.TextBuffer() textbuffer.set_text(text) self.textview.set_buffer(textbuffer) # if we have help information, add it to the end of the text view description = message.getDescription() if description: textbuffer.insert(textbuffer.get_end_iter(), ' ') titer = textbuffer.get_end_iter() # we set the 'link' data field on tags to identify them translated = self._translator.translateTranslatable(description) tag = textbuffer.create_tag(translated) tag.set_property('underline', pango.UNDERLINE_SINGLE) tag.set_property('foreground', 'blue') tag.set_data('link', getMessageWebLink(message)) textbuffer.insert_with_tags_by_name(titer, translated, tag.get_property('name')) timestamp = message.getTimeStamp() if timestamp and not self._disableTimestamps: text = _("\nPosted on %s.\n") % time.strftime( "%c", time.localtime(timestamp)) textbuffer.insert(textbuffer.get_end_iter(), text) if message.debug: text = "\n\n" + _("Debug information:\n") + message.debug + '\n' textbuffer.insert(textbuffer.get_end_iter(), text) def _sortMessages(self): # Sort all messages first by (reverse of) level, then priority children = [(-w.message.level, w.message.priority, w) for w in self.buttonbox.get_children()] children.sort() children.reverse() children = [(i, children[i][2]) for i in range(len(children))] for child in children: self.buttonbox.reorder_child(child[1], child[0]) # the first button, e.g. highest priority return children[0][1] # Callbacks def _on_message_button__toggled(self, button, message): # on toggling the button, show the message if not button.get_active(): if self.active_button == button: self.sw.hide() self.hline.hide() button.set_active(False) self.active_button = None self.emit('resize-event', True) return old_active = self.active_button self.active_button = button if old_active and old_active != button: old_active.set_active(False) self._addMessageToBuffer(message) self.show_all() self.emit('resize-event', False) # when the mouse cursor moves, set the cursor image accordingly def _on_textview___motion_notify_event(self, textview, event): x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y)) tags = textview.get_iter_at_location(x, y).get_tags() # without this call, further motion notify events don't get # triggered textview.window.get_pointer() # if any of the tags is a link, show a hand cursor = None for tag in tags: if tag.get_data('link'): cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) break textview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor) return False def _after_textview__event(self, textview, event): if event.type != gtk.gdk.BUTTON_RELEASE: return False if event.button != 1: return False textbuffer = textview.get_buffer() # we shouldn't follow a link if the user has selected something bounds = textbuffer.get_selection_bounds() if bounds: [start, end] = bounds if start.get_offset() != end.get_offset(): return False x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y)) iter = textview.get_iter_at_location(x, y) for tag in iter.get_tags(): link = tag.get_data('link') if link: import webbrowser log.debug('messageview', 'opening %s' % link) webbrowser.open(link) break return False
class FluTrayIcon(log.Loggable, gobject.GObject): """ I represent a tray icon in GNOME's notification area for the Admin UI. If I cannot create a tray icon, I will still be usable but not do anything. """ implements(flavors.IStateListener) logCategory = 'trayui' gsignal("quit") def __init__(self, window): """ @type window: L{flumotion.admin.gtk.client.Window} """ gobject.GObject.__init__(self) self._components = None self._window = window self._tray_image = None self._tray_icon = self._create_trayicon() self._set_icon_from_filename(_DEFAULT_ICON) # Public methods def update(self, components): """ Update the components list @param components: dictionary of name -> L{flumotion.common.component.AdminComponentState} """ if not self._tray_icon: return self.debug('updating component in trayicon view') # get a dictionary of components self._components = components for component in components.values(): try: component.addListener(self, set_=self.stateSet) except KeyError: pass self._update_mood() def stateSet(self, state, key, value): if not isinstance(state, planet.AdminComponentState): self.warning('Got state change for unknown object %r' % state) return if key == 'mood': # one of the components has changed mood self._update_mood() elif key == 'message': # one of the components has sent a message self.debug("message: %s" % value) if self._tray_icon and hasattr(self._tray_icon, 'send_message'): self._tray_icon.send_message(1000, value) def set_tooltip(self, tooltip): """ @param tooltip: Text to show when the mouse is over the tray icon. @type tooltip: str """ if self._tray_icon: self._tray_icon.set_tooltip(tooltip) # Private methods def _create_trayicon(self): """ Create the icon """ icon = gtk.StatusIcon() icon.connect('popup-menu', self._on_trayicon__popup_menu) icon.connect('activate', self._on_trayicon__activated) return icon def _set_icon_from_filename(self, filename): if not self._tray_icon: return self._tray_icon.set_from_file(filename) # FIXME: looks like cutnpaste from a similar function, squash duplication def _update_mood(self): """ This method goes through all the components to determine and set the overall mood. """ # get a dictionary of components names = self._components.keys() if names: # get overall mood of components overallmood = moods.happy.value for compName in names: component = self._components[compName] mood = component.get('mood') self.debug("component %s has mood %d" % (compName, mood)) if mood > overallmood: overallmood = mood moodname = moods.get(overallmood).name self.debug("overall mood: %s %d" % (moodname, overallmood)) filename = os.path.join(configure.imagedir, 'mood-%s.png' % moodname) self._set_icon_from_filename(filename) else: # if no components, show fluendo logo self._set_icon_from_filename(_DEFAULT_ICON) # Callbacks def _on_trayicon__popup_menu(self, *p): """ Called when we click the tray icon with the second mouse's button. Shows a popup menu with the quit option. """ menu = gtk.Menu() quitButton = gtk.ImageMenuItem(gtk.STOCK_QUIT) quitButton.connect('activate', self._on_quit__activate) menu.add(quitButton) menu.popup(None, None, None, 3, gtk.get_current_event_time()) menu.show_all() def _on_trayicon__activated(self, *p): """ Called when we click the tray icon with the first mouse's button. Shows or hides the main window. """ if self._window.get_property('visible'): self._window.hide() else: self._window.show() def _on_quit__activate(self, menu): """ Called when we click the quit option on the popup menu. """ self.emit('quit')