class SliderControl(QObject): """This class implements a slider control for a colormap""" def __init__(self, name, value, minval, maxval, step, format="%s: %.1f"): QObject.__init__(self) self.name, self.value, self.minval, self.maxval, self.step, self.format = \ name, value, minval, maxval, step, format self._default = value self._wlabel = None def makeControlWidgets(self, parent, gridlayout, row, column): toprow = QWidget(parent) gridlayout.addWidget(toprow, row * 2, column) top_lo = QHBoxLayout(toprow) top_lo.setContentsMargins(0, 0, 0, 0) self._wlabel = QLabel(self.format % (self.name, self.value), toprow) top_lo.addWidget(self._wlabel) self._wreset = QToolButton(toprow) self._wreset.setText("reset") self._wreset.setToolButtonStyle(Qt.ToolButtonTextOnly) self._wreset.setAutoRaise(True) self._wreset.setEnabled(self.value != self._default) QObject.connect(self._wreset, SIGNAL("clicked()"), self._resetValue) top_lo.addWidget(self._wreset) self._wslider = QwtSlider(parent) # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above self._wslider_timer = QTimer(parent) self._wslider_timer.setSingleShot(True) self._wslider_timer.setInterval(500) QObject.connect(self._wslider_timer, SIGNAL("timeout()"), self.setValue) gridlayout.addWidget(self._wslider, row * 2 + 1, column) self._wslider.setRange(self.minval, self.maxval) self._wslider.setStep(self.step) self._wslider.setValue(self.value) self._wslider.setTracking(False) QObject.connect(self._wslider, SIGNAL("valueChanged(double)"), self.setValue) QObject.connect(self._wslider, SIGNAL("sliderMoved(double)"), self._previewValue) def _resetValue(self): self._wslider.setValue(self._default) self.setValue(self._default) def setValue(self, value=None, notify=True): # only update widgets if already created self.value = value if self._wlabel is not None: if value is None: self.value = value = self._wslider.value() self._wreset.setEnabled(value != self._default) self._wlabel.setText(self.format % (self.name, self.value)) # stop timer if being called to finalize the change in value if notify: self._wslider_timer.stop() self.emit(SIGNAL("valueChanged"), self.value) def _previewValue(self, value): self.setValue(notify=False) self._wslider_timer.start(500) self.emit(SIGNAL("valueMoved"), self.value)
class AboutWindow(QDialog): def __init__(self, html, width=600, timeout=None): QDialog.__init__(self) self.setMinimumWidth(width) self.setObjectName("About NanoEngineer-1") TextEditLayout = QVBoxLayout(self) TextEditLayout.setSpacing(0) TextEditLayout.setMargin(0) self.text_edit = QTextEdit(self) self.text_edit.setHtml(html) self.text_edit.setReadOnly(True) self.text_edit.setWordWrapMode(QTextOption.WordWrap) TextEditLayout.addWidget(self.text_edit) self.quit_button = QPushButton("OK") TextEditLayout.addWidget(self.quit_button) self.connect(self.quit_button, SIGNAL("clicked()"), self.close) if timeout is not None: self.qt = QTimer() self.qt.setInterval(1000 * timeout) self.qt.start() self.connect(self.qt, SIGNAL("timeout()"), self.close) self.show() self.exec_()
class AboutWindow(QDialog): def __init__(self, html, width=600, timeout=None): QDialog.__init__(self) self.setMinimumWidth(width) self.setObjectName("About NanoEngineer-1") TextEditLayout = QVBoxLayout(self) TextEditLayout.setSpacing(0) TextEditLayout.setMargin(0) self.text_edit = QTextEdit(self) self.text_edit.setHtml(html) self.text_edit.setReadOnly(True) self.text_edit.setWordWrapMode(QTextOption.WordWrap) TextEditLayout.addWidget(self.text_edit) self.quit_button = QPushButton("OK") TextEditLayout.addWidget(self.quit_button) self.connect(self.quit_button, SIGNAL("clicked()"), self.close) if timeout is not None: self.qt = QTimer() self.qt.setInterval(1000*timeout) self.qt.start() self.connect(self.qt, SIGNAL("timeout()"), self.close) self.show() self.exec_()
def setupWidget(self, context): super(MkzGui, self).__init__(context) # Give QObjects reasonable names self.setObjectName(self.gui_object_name) # Create QWidget self._widget = QWidget() # Get path to UI file which should be in the "resource" folder of this package ui_file = os.path.join(rospkg.RosPack().get_path(self.package_name), 'resource', self.ui_filename) # Extend the widget with all attributes and children from UI file loadUi(ui_file, self._widget) # Give QObjects reasonable names self._widget.setObjectName(self.gui_object_name) # Add widget to the user interface context.add_widget(self._widget) # Set up a timer to update the GUI update_timer = QTimer(self._widget) update_timer.setInterval(self.update_period) update_timer.setSingleShot(False) update_timer.timeout.connect(lambda: self.updateGuiCallback()) update_timer.start()
class ImageControlDialog(QDialog): def __init__(self, parent, rc, imgman): """An ImageControlDialog is initialized with a parent widget, a RenderControl object, and an ImageManager object""" QDialog.__init__(self, parent) image = rc.image self.setWindowTitle("%s: Colour Controls" % image.name) self.setWindowIcon(pixmaps.colours.icon()) self.setModal(False) self.image = image self._rc = rc self._imgman = imgman self._currier = PersistentCurrier() # init internal state self._prev_range = self._display_range = None, None self._hist = None self._geometry = None # create layouts lo0 = QVBoxLayout(self) # lo0.setContentsMargins(0,0,0,0) # histogram plot whide = self.makeButton("Hide", self.hide, width=128) whide.setShortcut(Qt.Key_F9) lo0.addWidget(Separator(self, "Histogram and ITF", extra_widgets=[whide])) lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) self._histplot = QwtPlot(self) self._histplot.setAutoDelete(False) lo1.addWidget(self._histplot, 1) lo2 = QHBoxLayout() lo2.setContentsMargins(0, 0, 0, 0) lo2.setSpacing(2) lo0.addLayout(lo2) lo0.addLayout(lo1) self._wautozoom = QCheckBox("autozoom", self) self._wautozoom.setChecked(True) self._wautozoom.setToolTip("""<P>If checked, then the histrogram plot will zoom in automatically when you narrow the current intensity range.</P>""") self._wlogy = QCheckBox("log Y", self) self._wlogy.setChecked(True) self._ylogscale = True self._wlogy.setToolTip( """<P>If checked, a log-scale Y axis is used for the histogram plot instead of a linear one.""") QObject.connect(self._wlogy, SIGNAL("toggled(bool)"), self._setHistLogScale) self._whistunzoom = self.makeButton("", self._unzoomHistogram, icon=pixmaps.full_range.icon()) self._whistzoomout = self.makeButton("-", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(.1))) self._whistzoomin = self.makeButton("+", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(10))) self._whistzoomin.setToolTip("""<P>Click to zoom into the histogram plot by one step. This does not change the current intensity range.</P>""") self._whistzoomout.setToolTip("""<P>Click to zoom out of the histogram plot by one step. This does not change the current intensity range.</P>""") self._whistunzoom.setToolTip("""<P>Click to reset the histogram plot back to its full extent. This does not change the current intensity range.</P>""") self._whistzoom = QwtWheel(self) self._whistzoom.setOrientation(Qt.Horizontal) self._whistzoom.setMaximumWidth(80) self._whistzoom.setRange(10, 0) self._whistzoom.setStep(0.1) self._whistzoom.setTickCnt(30) self._whistzoom.setTracking(False) QObject.connect(self._whistzoom, SIGNAL("valueChanged(double)"), self._zoomHistogramFinalize) QObject.connect(self._whistzoom, SIGNAL("sliderMoved(double)"), self._zoomHistogramPreview) self._whistzoom.setToolTip("""<P>Use this wheel control to zoom in/out of the histogram plot. This does not change the current intensity range. Note that the zoom wheel should also respond to your mouse wheel, if you have one.</P>""") # This works around a stupid bug in QwtSliders -- when using the mousewheel, only sliderMoved() signals are emitted, # with no final valueChanged(). If we want to do a fast preview of something on sliderMoved(), and a "slow" final # step on valueChanged(), we're in trouble. So we start a timer on sliderMoved(), and if the timer expires without # anything else happening, do a valueChanged(). # Here we use a timer to call zoomHistogramFinalize() w/o an argument. self._whistzoom_timer = QTimer(self) self._whistzoom_timer.setSingleShot(True) self._whistzoom_timer.setInterval(500) QObject.connect(self._whistzoom_timer, SIGNAL("timeout()"), self._zoomHistogramFinalize) # set same size for all buttons and controls width = 24 for w in self._whistunzoom, self._whistzoomin, self._whistzoomout: w.setMinimumSize(width, width) w.setMaximumSize(width, width) self._whistzoom.setMinimumSize(80, width) self._wlab_histpos_text = "(hover here for help)" self._wlab_histpos = QLabel(self._wlab_histpos_text, self) self._wlab_histpos.setToolTip(""" <P>The plot shows a histogram of either the full image or its selected subset (as per the "Data subset" section below).</P> <P>The current intensity range is indicated by the grey box in the plot.</P> <P>Use the left mouse button to change the low intensity limit, and the right button (on Macs, use Ctrl-click) to change the high limit.</P> <P>Use Shift with the left mouse button to zoom into an area of the histogram, or else use the "zoom wheel" control or the plus/minus toolbuttons above the histogram to zoom in or out. To zoom back out to the full extent of the histogram, click on the rightmost button above the histogram.</P> """) lo2.addWidget(self._wlab_histpos, 1) lo2.addWidget(self._wautozoom) lo2.addWidget(self._wlogy, 0) lo2.addWidget(self._whistzoomin, 0) lo2.addWidget(self._whistzoom, 0) lo2.addWidget(self._whistzoomout, 0) lo2.addWidget(self._whistunzoom, 0) self._zooming_histogram = False sliced_axes = rc.slicedAxes() dprint(1, "sliced axes are", sliced_axes) self._stokes_axis = None # subset indication lo0.addWidget(Separator(self, "Data subset")) # sliced axis selectors self._wslicers = [] if sliced_axes: lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1) lo1.addWidget(QLabel("Current slice: ", self)) for i, (iextra, name, labels) in enumerate(sliced_axes): lo1.addWidget(QLabel("%s:" % name, self)) if name == "STOKES": self._stokes_axis = iextra # add controls wslicer = QComboBox(self) self._wslicers.append(wslicer) wslicer.addItems(labels) wslicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % name) wslicer.setCurrentIndex(self._rc.currentSlice()[iextra]) QObject.connect(wslicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra)) lo2 = QVBoxLayout() lo1.addLayout(lo2) lo2.setContentsMargins(0, 0, 0, 0) lo2.setSpacing(0) wminus = QToolButton(self) wminus.setArrowType(Qt.UpArrow) QObject.connect(wminus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, 1)) if i == 0: wminus.setShortcut(Qt.SHIFT + Qt.Key_F7) elif i == 1: wminus.setShortcut(Qt.SHIFT + Qt.Key_F8) wplus = QToolButton(self) wplus.setArrowType(Qt.DownArrow) QObject.connect(wplus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, -1)) if i == 0: wplus.setShortcut(Qt.Key_F7) elif i == 1: wplus.setShortcut(Qt.Key_F8) wminus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) wplus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sz = QSize(12, 8) wminus.setMinimumSize(sz) wplus.setMinimumSize(sz) wminus.resize(sz) wplus.resize(sz) lo2.addWidget(wminus) lo2.addWidget(wplus) lo1.addWidget(wslicer) lo1.addSpacing(5) lo1.addStretch(1) # subset indicator lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1) self._wlab_subset = QLabel("Subset: xxx", self) self._wlab_subset.setToolTip("""<P>This indicates the current data subset to which the histogram and the stats given here apply. Use the "Reset to" control on the right to change the current subset and recompute the histogram and stats.</P>""") lo1.addWidget(self._wlab_subset, 1) self._wreset_full = self.makeButton("\u2192 full", self._rc.setFullSubset) lo1.addWidget(self._wreset_full) if sliced_axes: # if self._stokes_axis is not None and len(sliced_axes)>1: # self._wreset_stokes = self.makeButton(u"\u21920Stokes",self._rc.setFullSubset) self._wreset_slice = self.makeButton("\u2192 slice", self._rc.setSliceSubset) lo1.addWidget(self._wreset_slice) else: self._wreset_slice = None # min/max controls lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) self._wlab_stats = QLabel(self) lo1.addWidget(self._wlab_stats, 0) self._wmore_stats = self.makeButton("more...", self._showMeanStd) self._wlab_stats.setMinimumHeight(self._wmore_stats.height()) lo1.addWidget(self._wmore_stats, 0) lo1.addStretch(1) # intensity controls lo0.addWidget(Separator(self, "Intensity mapping")) lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1, 0) self._range_validator = FloatValidator(self) self._wrange = QLineEdit(self), QLineEdit(self) self._wrange[0].setToolTip("""<P>This is the low end of the intensity range.</P>""") self._wrange[1].setToolTip("""<P>This is the high end of the intensity range.</P>""") for w in self._wrange: w.setValidator(self._range_validator) QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange) lo1.addWidget(QLabel("low:", self), 0) lo1.addWidget(self._wrange[0], 1) self._wrangeleft0 = self.makeButton("\u21920", self._setZeroLeftLimit, width=32) self._wrangeleft0.setToolTip("""<P>Click this to set the low end of the intensity range to 0.</P>""") lo1.addWidget(self._wrangeleft0, 0) lo1.addSpacing(8) lo1.addWidget(QLabel("high:", self), 0) lo1.addWidget(self._wrange[1], 1) lo1.addSpacing(8) self._wrange_full = self.makeButton(None, self._setHistDisplayRange, icon=pixmaps.intensity_graph.icon()) lo1.addWidget(self._wrange_full) self._wrange_full.setToolTip( """<P>Click this to reset the intensity range to the current extent of the histogram plot.</P>""") # add menu for display range range_menu = QMenu(self) wrange_menu = QToolButton(self) wrange_menu.setText("Reset to") wrange_menu.setToolTip("""<P>Use this to reset the intensity range to various pre-defined settings.</P>""") lo1.addWidget(wrange_menu) self._qa_range_full = range_menu.addAction(pixmaps.full_range.icon(), "Full subset", self._rc.resetSubsetDisplayRange) self._qa_range_hist = range_menu.addAction(pixmaps.intensity_graph.icon(), "Current histogram limits", self._setHistDisplayRange) for percent in (99.99, 99.9, 99.5, 99, 98, 95): range_menu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent)) wrange_menu.setMenu(range_menu) wrange_menu.setPopupMode(QToolButton.InstantPopup) lo1 = QGridLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) self._wimap = QComboBox(self) lo1.addWidget(QLabel("Intensity policy:", self), 0, 0) lo1.addWidget(self._wimap, 1, 0) self._wimap.addItems(rc.getIntensityMapNames()) QObject.connect(self._wimap, SIGNAL("currentIndexChanged(int)"), self._rc.setIntensityMapNumber) self._wimap.setToolTip("""<P>Use this to change the type of the intensity transfer function (ITF).</P>""") # log cycles control lo1.setColumnStretch(1, 1) self._wlogcycles_label = QLabel("Log cycles: ", self) lo1.addWidget(self._wlogcycles_label, 0, 1) # self._wlogcycles = QwtWheel(self) # self._wlogcycles.setTotalAngle(360) self._wlogcycles = QwtSlider(self) self._wlogcycles.setToolTip( """<P>Use this to change the log-base for the logarithmic intensity transfer function (ITF).</P>""") # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above self._wlogcycles_timer = QTimer(self) self._wlogcycles_timer.setSingleShot(True) self._wlogcycles_timer.setInterval(500) QObject.connect(self._wlogcycles_timer, SIGNAL("timeout()"), self._setIntensityLogCycles) lo1.addWidget(self._wlogcycles, 1, 1) self._wlogcycles.setRange(1., 10) self._wlogcycles.setStep(0.1) self._wlogcycles.setTracking(False) QObject.connect(self._wlogcycles, SIGNAL("valueChanged(double)"), self._setIntensityLogCycles) QObject.connect(self._wlogcycles, SIGNAL("sliderMoved(double)"), self._previewIntensityLogCycles) self._updating_imap = False # lock intensity map lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) # lo1.addWidget(QLabel("Lock range accross",self)) wlock = QCheckBox("Lock display range", self) wlock.setToolTip("""<P>If checked, then the intensity range will be locked. The ranges of all locked images change simultaneously.</P>""") lo1.addWidget(wlock) wlockall = QToolButton(self) wlockall.setIcon(pixmaps.locked.icon()) wlockall.setText("Lock all to this") wlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) wlockall.setAutoRaise(True) wlockall.setToolTip("""<P>Click this to lock together the intensity ranges of all images.</P>""") lo1.addWidget(wlockall) wunlockall = QToolButton(self) wunlockall.setIcon(pixmaps.unlocked.icon()) wunlockall.setText("Unlock all") wunlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) wunlockall.setAutoRaise(True) wunlockall.setToolTip("""<P>Click this to unlock the intensity ranges of all images.</P>""") lo1.addWidget(wunlockall) wlock.setChecked(self._rc.isDisplayRangeLocked()) QObject.connect(wlock, SIGNAL("clicked(bool)"), self._rc.lockDisplayRange) QObject.connect(wlockall, SIGNAL("clicked()"), self._currier.curry(self._imgman.lockAllDisplayRanges, self._rc)) QObject.connect(wunlockall, SIGNAL("clicked()"), self._imgman.unlockAllDisplayRanges) QObject.connect(self._rc, SIGNAL("displayRangeLocked"), wlock.setChecked) # self._wlock_imap_axis = [ QCheckBox(name,self) for iaxis,name,labels in sliced_axes ] # for iw,w in enumerate(self._wlock_imap_axis): # QObject.connect(w,SIGNAL("toggled(bool)"),self._currier.curry(self._rc.lockDisplayRangeForAxis,iw)) # lo1.addWidget(w,0) lo1.addStretch(1) # lo0.addWidget(Separator(self,"Colourmap")) # color bar self._colorbar = QwtPlot(self) lo0.addWidget(self._colorbar) self._colorbar.setAutoDelete(False) self._colorbar.setMinimumHeight(32) self._colorbar.enableAxis(QwtPlot.yLeft, False) self._colorbar.enableAxis(QwtPlot.xBottom, False) # color plot self._colorplot = QwtPlot(self) lo0.addWidget(self._colorplot) self._colorplot.setAutoDelete(False) self._colorplot.setMinimumHeight(64) self._colorplot.enableAxis(QwtPlot.yLeft, False) self._colorplot.enableAxis(QwtPlot.xBottom, False) # self._colorplot.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Preferred) self._colorbar.hide() self._colorplot.hide() # color controls lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 1) lo1.addWidget(QLabel("Colourmap:", self)) # colormap list ### NB: use setIconSize() and icons in QComboBox!!! self._wcolmaps = QComboBox(self) self._wcolmaps.setIconSize(QSize(128, 16)) self._wcolmaps.setToolTip("""<P>Use this to select a different colourmap.</P>""") for cmap in self._rc.getColormapList(): self._wcolmaps.addItem(QIcon(cmap.makeQPixmap(128, 16)), cmap.name) lo1.addWidget(self._wcolmaps) QObject.connect(self._wcolmaps, SIGNAL("activated(int)"), self._rc.setColorMapNumber) # add widgetstack for colormap controls self._wcolmap_control_stack = QStackedWidget(self) self._wcolmap_control_blank = QWidget(self._wcolmap_control_stack) self._wcolmap_control_stack.addWidget(self._wcolmap_control_blank) lo0.addWidget(self._wcolmap_control_stack) self._colmap_controls = [] # add controls to stack for index, cmap in enumerate(self._rc.getColormapList()): if isinstance(cmap, Colormaps.ColormapWithControls): controls = cmap.makeControlWidgets(self._wcolmap_control_stack) self._wcolmap_control_stack.addWidget(controls) QObject.connect(cmap, SIGNAL("colormapChanged"), self._currier.curry(self._previewColormapParameters, index, cmap)) QObject.connect(cmap, SIGNAL("colormapPreviewed"), self._currier.curry(self._previewColormapParameters, index, cmap)) self._colmap_controls.append(controls) else: self._colmap_controls.append(self._wcolmap_control_blank) # connect updates from renderControl and image self.image.connect(SIGNAL("slice"), self._updateImageSlice) QObject.connect(self._rc, SIGNAL("intensityMapChanged"), self._updateIntensityMap) QObject.connect(self._rc, SIGNAL("colorMapChanged"), self._updateColorMap) QObject.connect(self._rc, SIGNAL("dataSubsetChanged"), self._updateDataSubset) QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange) # update widgets self._setupHistogramPlot() self._updateDataSubset(*self._rc.currentSubset()) self._updateColorMap(image.colorMap()) self._updateIntensityMap(rc.currentIntensityMap(), rc.currentIntensityMapNumber()) self._updateDisplayRange(*self._rc.displayRange()) def makeButton(self, label, callback=None, width=None, icon=None): btn = QToolButton(self) # btn.setAutoRaise(True) label and btn.setText(label) icon and btn.setIcon(icon) # btn = QPushButton(label,self) # btn.setFlat(True) if width: btn.setMinimumWidth(width) btn.setMaximumWidth(width) if icon: btn.setIcon(icon) if callback: QObject.connect(btn, SIGNAL("clicked()"), callback) return btn # def closeEvent (self,ev): # ev.ignore() # self.hide() def hide(self): self._geometry = self.geometry() QDialog.hide(self) def show(self): dprint(4, "show entrypoint") if self._geometry: dprint(4, "setting geometry") self.setGeometry(self._geometry) if self._hist is None: busy = BusyIndicator() dprint(4, "updating histogram") self._updateHistogram() dprint(4, "updating stats") self._updateStats(self._subset, self._subset_range) busy = None dprint(4, "calling QDialog.show") QDialog.show(self) # number of bins used to compute intensity transfer function NumItfBins = 1000 # number of bins used for displaying histograms NumHistBins = 500 # number of bins used for high-res histograms NumHistBinsHi = 10000 # colorbar height, as fraction of plot area ColorBarHeight = 0.1 class HistLimitPicker(QwtPlotPicker): """Auguments QwtPlotPicker with functions for selecting hist min/max values""" def __init__(self, plot, label, color="green", mode=QwtPicker.PointSelection, rubber_band=QwtPicker.VLineRubberBand, tracker_mode=QwtPicker.ActiveOnly, track=None): QwtPlotPicker.__init__(self, QwtPlot.xBottom, QwtPlot.yRight, mode, rubber_band, tracker_mode, plot.canvas()) self.plot = plot self.label = label self.track = track self.color = QColor(color) self.setRubberBandPen(QPen(self.color)) def trackerText(self, pos): x, y = self.plot.invTransform(QwtPlot.xBottom, pos.x()), self.plot.invTransform(QwtPlot.yLeft, pos.y()) if self.track: text = self.track(x, y) if text is not None: return text if self.label: text = QwtText(self.label % dict(x=x, y=y)) text.setColor(self.color) return text return QwtText() def widgetLeaveEvent(self, ev): if self.track: self.track(None, None) QwtPlotPicker.widgetLeaveEvent(self, ev) class ColorBarPlotItem(QwtPlotItem): def __init__(self, y0, y1, *args): QwtPlotItem.__init__(self, *args) self._y0 = y1 self._dy = y1 - y0 def setIntensityMap(self, imap): self.imap = imap def setColorMap(self, cmap): self.cmap = cmap def draw(self, painter, xmap, ymap, rect): """Implements QwtPlotItem.draw(), to render the colorbar on the given painter.""" xp1, xp2, xdp, xs1, xs2, xds = xinfo = xmap.p1(), xmap.p2(), xmap.pDist(), xmap.s1(), xmap.s2(), xmap.sDist() yp1, yp2, ydp, ys1, ys2, yds = yinfo = ymap.p1(), ymap.p2(), ymap.pDist(), ymap.s1(), ymap.s2(), ymap.sDist() # xp: coordinates of pixels xp1...xp2 in data units xp = xs1 + (xds / xdp) * (0.5 + numpy.arange(int(xdp))) # convert y0 and y1 into pixel coordinates y0 = yp1 - (self._y0 - ys1) * (ydp / yds) dy = self._dy * (ydp / yds) # remap into an Nx1 image qimg = self.cmap.colorize(self.imap.remap(xp.reshape((len(xp), 1)))) # plot image painter.drawImage(QRect(xp1, y0, xdp, dy), qimg) class HistogramLineMarker(object): """Helper class implementing a line marker for a histogram plot""" def __init__(self, plot, color="black", linestyle=Qt.DotLine, align=Qt.AlignBottom | Qt.AlignRight, z=90, label="", zlabel=None, linewidth=1, spacing=2, yaxis=QwtPlot.yRight): self.line = TiggerPlotCurve() self.color = color = color if isinstance(color, QColor) else QColor(color) self.line.setPen(QPen(color, linewidth, linestyle)) self.marker = TiggerPlotMarker() self.marker.setLabelAlignment(align) try: self.marker.setSpacing(spacing) except AttributeError: pass self.setText(label) self.line.setZ(z) self.marker.setZ(zlabel if zlabel is not None else z) # set axes -- using yRight, since that is the "markup" z-axis self.line.setAxis(QwtPlot.xBottom, yaxis) self.marker.setAxis(QwtPlot.xBottom, yaxis) # attach to plot self.line.attach(plot) self.marker.attach(plot) def show(self): self.line.show() self.marker.show() def hide(self): self.line.hide() self.marker.hide() def setText(self, text): label = QwtText(text) label.setColor(self.color) self.marker.setLabel(label) def _setupHistogramPlot(self): self._histplot.setCanvasBackground(QColor("lightgray")) self._histplot.setAxisFont(QwtPlot.yLeft, QApplication.font()) self._histplot.setAxisFont(QwtPlot.xBottom, QApplication.font()) # add histogram curves self._histcurve1 = TiggerPlotCurve() self._histcurve2 = TiggerPlotCurve() self._histcurve1.setStyle(QwtPlotCurve.Steps) self._histcurve2.setStyle(QwtPlotCurve.Steps) self._histcurve1.setPen(QPen(Qt.NoPen)) self._histcurve1.setBrush(QBrush(QColor("slategrey"))) pen = QPen(QColor("red")) pen.setWidth(1) self._histcurve2.setPen(pen) self._histcurve1.setZ(0) self._histcurve2.setZ(100) # self._histcurve1.attach(self._histplot) self._histcurve2.attach(self._histplot) # add maxbin and half-max curves self._line_0 = self.HistogramLineMarker(self._histplot, color="grey50", linestyle=Qt.SolidLine, align=Qt.AlignTop | Qt.AlignLeft, z=90) self._line_mean = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine, align=Qt.AlignBottom | Qt.AlignRight, z=91, label="mean", zlabel=151) self._line_std = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine, align=Qt.AlignTop | Qt.AlignRight, z=91, label="std", zlabel=151) sym = QwtSymbol() sym.setStyle(QwtSymbol.VLine) sym.setSize(8) self._line_std.line.setSymbol(sym) self._line_maxbin = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine, align=Qt.AlignTop | Qt.AlignRight, z=92, label="max bin", zlabel=150) self._line_halfmax = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine, align=Qt.AlignBottom | Qt.AlignRight, z=90, label="half-max", yaxis=QwtPlot.yLeft) # add current range self._rangebox = TiggerPlotCurve() self._rangebox.setStyle(QwtPlotCurve.Steps) self._rangebox.setYAxis(QwtPlot.yRight) self._rangebox.setPen(QPen(Qt.NoPen)) self._rangebox.setBrush(QBrush(QColor("darkgray"))) self._rangebox.setZ(50) self._rangebox.attach(self._histplot) self._rangebox2 = TiggerPlotCurve() self._rangebox2.setStyle(QwtPlotCurve.Sticks) self._rangebox2.setYAxis(QwtPlot.yRight) self._rangebox2.setZ(60) # self._rangebox2.attach(self._histplot) # add intensity transfer function self._itfcurve = TiggerPlotCurve() self._itfcurve.setStyle(QwtPlotCurve.Lines) self._itfcurve.setPen(QPen(QColor("blue"))) self._itfcurve.setYAxis(QwtPlot.yRight) self._itfcurve.setZ(120) self._itfcurve.attach(self._histplot) self._itfmarker = TiggerPlotMarker() label = QwtText("ITF") label.setColor(QColor("blue")) self._itfmarker.setLabel(label) try: self._itfmarker.setSpacing(0) except AttributeError: pass self._itfmarker.setLabelAlignment(Qt.AlignTop | Qt.AlignRight) self._itfmarker.setZ(120) self._itfmarker.attach(self._histplot) # add colorbar self._cb_item = self.ColorBarPlotItem(1, 1 + self.ColorBarHeight) self._cb_item.setYAxis(QwtPlot.yRight) self._cb_item.attach(self._histplot) # add pickers self._hist_minpicker = self.HistLimitPicker(self._histplot, "low: %(x).4g") self._hist_minpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton) QObject.connect(self._hist_minpicker, SIGNAL("selected(const QwtDoublePoint &)"), self._selectLowLimit) self._hist_maxpicker = self.HistLimitPicker(self._histplot, "high: %(x).4g") self._hist_maxpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.RightButton) QObject.connect(self._hist_maxpicker, SIGNAL("selected(const QwtDoublePoint &)"), self._selectHighLimit) self._hist_maxpicker1 = self.HistLimitPicker(self._histplot, "high: %(x).4g") self._hist_maxpicker1.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.CTRL) QObject.connect(self._hist_maxpicker1, SIGNAL("selected(const QwtDoublePoint &)"), self._selectHighLimit) self._hist_zoompicker = self.HistLimitPicker(self._histplot, label="zoom", tracker_mode=QwtPicker.AlwaysOn, track=self._trackHistCoordinates, color="black", mode=QwtPicker.RectSelection, rubber_band=QwtPicker.RectRubberBand) self._hist_zoompicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.SHIFT) QObject.connect(self._hist_zoompicker, SIGNAL("selected(const QwtDoubleRect &)"), self._zoomHistogramIntoRect) def _trackHistCoordinates(self, x, y): self._wlab_histpos.setText((DataValueFormat + " %d") % (x, y) if x is not None else self._wlab_histpos_text) return QwtText() def _updateITF(self): """Updates current ITF array.""" # do nothing if no histogram -- means we're not visible if self._hist is not None: xdata = self._itf_bins ydata = self.image.intensityMap().remap(xdata) self._rangebox.setData(self._rc.displayRange(), [1, 1]) self._rangebox2.setData(self._rc.displayRange(), [1, 1]) self._itfcurve.setData(xdata, ydata) self._itfmarker.setValue(xdata[0], 1) def _updateHistogram(self, hmin=None, hmax=None): """Recomputes histogram. If no arguments, computes full histogram for data subset. If hmin/hmax is specified, computes zoomed-in histogram.""" busy = BusyIndicator() self._prev_range = self._display_range dmin, dmax = self._subset_range hmin0, hmax0 = dmin, dmax if hmin0 >= hmax0: hmax0 = hmin0 + 1 subset, mask = self.image.optimalRavel(self._subset) # compute full-subset hi-res histogram, if we don't have one (for percentile stats) if self._hist_hires is None: dprint(1, "computing histogram for full subset range", hmin0, hmax0) self._hist_hires = measurements.histogram(subset, hmin0, hmax0, self.NumHistBinsHi, labels=mask, index=None if mask is None else False) self._hist_bins_hires = hmin0 + (hmax0 - hmin0) * (numpy.arange(self.NumHistBinsHi) + 0.5) / float( self.NumHistBinsHi) self._hist_binsize_hires = (hmax0 - hmin0) / self.NumHistBins # if hist limits not specified, then compute lo-res histogram based on the hi-res one if hmin is None: hmin, hmax = hmin0, hmax0 # downsample to low-res histogram self._hist = self._hist_hires.reshape((self.NumHistBins, self.NumHistBinsHi / self.NumHistBins)).sum(1) else: # zoomed-in low-res histogram # bracket limits at subset range hmin, hmax = max(hmin, dmin), min(hmax, dmax) if hmin >= hmax: hmax = hmin + 1 dprint(1, "computing histogram for", self._subset.shape, self._subset.dtype, hmin, hmax) self._hist = measurements.histogram(subset, hmin, hmax, self.NumHistBins, labels=mask, index=None if mask is None else False) dprint(1, "histogram computed") # compute bins self._itf_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumItfBins)) / (float(self.NumItfBins) - 1) self._hist_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumHistBins) + 0.5) / float(self.NumHistBins) # histogram range and position of peak self._hist_range = hmin, hmax self._hist_min, self._hist_max, self._hist_imin, self._hist_imax = measurements.extrema(self._hist) self._hist_peak = self._hist_bins[self._hist_imax] # set controls accordingly if dmin >= dmax: dmax = dmin + 1 zoom = math.log10((dmax - dmin) / (hmax - hmin)) self._whistzoom.setValue(zoom) self._whistunzoom.setEnabled(zoom > 0) self._whistzoomout.setEnabled(zoom > 0) # reset scales self._histplot.setAxisScale(QwtPlot.xBottom, hmin, hmax) self._histplot.setAxisScale(QwtPlot.yRight, 0, 1 + self.ColorBarHeight) # update curves # call _setHistLogScale() (with current setting) to update axis scales and set data self._setHistLogScale(self._ylogscale, replot=False) # set plot lines self._line_0.line.setData([0, 0], [0, 1]) self._line_0.marker.setValue(0, 0) self._line_maxbin.line.setData([self._hist_peak, self._hist_peak], [0, 1]) self._line_maxbin.marker.setValue(self._hist_peak, 0) self._line_maxbin.setText(("max bin:" + DataValueFormat) % self._hist_peak) # set half-max line self._line_halfmax.line.setData(self._hist_range, [self._hist_max / 2, self._hist_max / 2]) self._line_halfmax.marker.setValue(hmin, self._hist_max / 2) # update ITF self._updateITF() def _updateStats(self, subset, minmax): """Recomputes subset statistics.""" if subset.size <= (2048 * 2048): self._showMeanStd(busy=False) else: self._wlab_stats.setText( ("min: %s max: %s np: %d" % (DataValueFormat, DataValueFormat, self._subset.size)) % minmax) self._wmore_stats.show() def _updateDataSubset(self, subset, minmax, desc, subset_type): """Called when the displayed data subset is changed. Updates the histogram.""" self._subset = subset self._subset_range = minmax self._wlab_subset.setText("Subset: %s" % desc) self._hist = self._hist_hires = None self._wreset_full.setVisible(subset_type is not RenderControl.SUBSET_FULL) self._wreset_slice and self._wreset_slice.setVisible(subset_type is not RenderControl.SUBSET_SLICE) # hide the mean/std markers, they will only be shown when _showMeanStd() is called self._line_mean.hide() self._line_std.hide() # if we're visibile, recompute histograms and stats if self.isVisible(): # if subset is sufficiently small, compute extended stats on-the-fly. Else show the "more" button to compute them later self._updateHistogram() self._updateStats(subset, minmax) self._histplot.replot() def _showMeanStd(self, busy=True): if busy: busy = BusyIndicator() dmin, dmax = self._subset_range subset, mask = self.image.optimalRavel(self._subset) dprint(5, "computing mean") mean = measurements.mean(subset, labels=mask, index=None if mask is None else False) dprint(5, "computing std") std = measurements.standard_deviation(subset, labels=mask, index=None if mask is None else False) dprint(5, "done") text = " ".join([("%s: " + DataValueFormat) % (name, value) for name, value in ("min", dmin), ("max", dmax), ("mean", mean), ("std", std)] + ["np: %d" % self._subset.size]) self._wlab_stats.setText(text) self._wmore_stats.hide() # update markers ypos = 0.3 self._line_mean.line.setData([mean, mean], [0, 1]) self._line_mean.marker.setValue(mean, ypos) self._line_mean.setText(("\u03BC=" + DataValueFormat) % mean) self._line_mean.show() self._line_std.line.setData([mean - std, mean + std], [ypos, ypos]) self._line_std.marker.setValue(mean, ypos) self._line_std.setText(("\u03C3=" + DataValueFormat) % std) self._line_std.show() self._histplot.replot() def _setIntensityLogCyclesLabel(self, value): self._wlogcycles_label.setText("Log cycles: %4.1f" % value) def _previewIntensityLogCycles(self, value): self._setIntensityLogCycles(value, notify_image=False, write_config=False) self._wlogcycles_timer.start(500) def _setIntensityLogCycles(self, value=None, notify_image=True, write_config=True): if value is None: value = self._wlogcycles.value() # stop timer if being called to finalize the change in value if notify_image: self._wlogcycles_timer.stop() if not self._updating_imap: self._setIntensityLogCyclesLabel(value) self._rc.setIntensityMapLogCycles(value, notify_image=notify_image, write_config=write_config) self._updateITF() self._histplot.replot() def _updateDisplayRange(self, dmin, dmax): self._rangebox.setData([dmin, dmax], [.9, .9]) self._wrange[0].setText(DataValueFormat % dmin) self._wrange[1].setText(DataValueFormat % dmax) self._wrangeleft0.setEnabled(dmin != 0) self._display_range = dmin, dmax # if auto-zoom is on, zoom the histogram # try to be a little clever about this. Zoom only if (a) both limits have changed (so that adjusting one end of the range # does not cause endless rezooms), or (b) display range is < 1/10 of the histogram range if self._wautozoom.isChecked() and self._hist is not None: if (dmax - dmin) / (self._hist_range[1] - self._hist_range[0]) < .1 or ( dmin != self._prev_range[0] and dmax != self._prev_range[1]): margin = (dmax - dmin) / 8 self._updateHistogram(dmin - margin, dmax + margin) self._updateITF() self._histplot.replot() def _updateIntensityMap(self, imap, index): self._updating_imap = True try: self._cb_item.setIntensityMap(imap) self._updateITF() self._histplot.replot() self._wimap.setCurrentIndex(index) if isinstance(imap, Colormaps.LogIntensityMap): self._wlogcycles.setValue(imap.log_cycles) self._setIntensityLogCyclesLabel(imap.log_cycles) self._wlogcycles.show() self._wlogcycles_label.show() else: self._wlogcycles.hide() self._wlogcycles_label.hide() finally: self._updating_imap = False def _updateColorMap(self, cmap): self._cb_item.setColorMap(cmap) self._histplot.replot() try: index = self._rc.getColormapList().index(cmap) except: return self._setCurrentColormapNumber(index, cmap) def _previewColormapParameters(self, index, cmap): """Called to preview a new colormap parameter value""" self._histplot.replot() self._wcolmaps.setItemIcon(index, QIcon(cmap.makeQPixmap(128, 16))) def _setCurrentColormapNumber(self, index, cmap): self._wcolmaps.setCurrentIndex(index) # show controls for colormap self._wcolmap_control_stack.setCurrentWidget(self._colmap_controls[index]) def _changeDisplayRange(self): """Gets display range from widgets and updates the image with it.""" try: newrange = [float(str(w.text())) for w in self._wrange] except ValueError: return self._rc.setDisplayRange(*newrange) def _setHistDisplayRange(self): self._rc.setDisplayRange(*self._hist_range) def _updateImageSlice(self, slice): for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()): self._wslicers[i].setCurrentIndex(slice[iextra]) def _changeDisplayRangeToPercent(self, percent): busy = BusyIndicator() if self._hist is None: self._updateHistogram() self._updateStats(self._subset, self._subset_range) # delta: we need the [delta,100-delta] interval of the total distribution delta = self._subset.size * ((100. - percent) / 200.) # get F(x): cumulative sum cumsum = numpy.zeros(len(self._hist_hires) + 1, dtype=int) cumsum[1:] = numpy.cumsum(self._hist_hires) bins = numpy.zeros(len(self._hist_hires) + 1, dtype=float) bins[0] = self._subset_range[0] bins[1:] = self._hist_bins_hires + self._hist_binsize_hires / 2 # use interpolation to find value interval corresponding to [delta,100-delta] of the distribution dprint(2, self._subset.size, delta, self._subset.size - delta) dprint(2, cumsum, self._hist_bins_hires) # if first bin is already > delta, then set colour range to first bin x0, x1 = numpy.interp([delta, self._subset.size - delta], cumsum, bins) # and change the display range (this will also cause a histplot.replot() via _updateDisplayRange above) self._rc.setDisplayRange(x0, x1) def _setZeroLeftLimit(self): self._rc.setDisplayRange(0., self._rc.displayRange()[1]) def _selectLowLimit(self, pos): self._rc.setDisplayRange(pos.x(), self._rc.displayRange()[1]) def _selectHighLimit(self, pos): self._rc.setDisplayRange(self._rc.displayRange()[0], pos.x()) def _unzoomHistogram(self): self._updateHistogram() self._histplot.replot() def _zoomHistogramByFactor(self, factor): """Changes histogram limits by specified factor""" # get max distance of plot limit from peak dprint(1, "zooming histogram by", factor) halfdist = (self._hist_range[1] - self._hist_range[0]) / (factor * 2) self._updateHistogram(self._hist_peak - halfdist, self._hist_peak + halfdist) self._histplot.replot() def _zoomHistogramIntoRect(self, rect): hmin, hmax = rect.bottomLeft().x(), rect.bottomRight().x() if hmax > hmin: self._updateHistogram(rect.bottomLeft().x(), rect.bottomRight().x()) self._histplot.replot() def _zoomHistogramPreview(self, value): dprint(2, "wheel moved to", value) self._zoomHistogramFinalize(value, preview=True) self._whistzoom_timer.start() def _zoomHistogramFinalize(self, value=None, preview=False): if self._zooming_histogram: return self._zooming_histogram = True try: if value is not None: dmin, dmax = self._subset_range dist = max(dmax - self._hist_peak, self._hist_peak - dmin) / 10 ** value self._preview_hist_range = max(self._hist_peak - dist, dmin), min(self._hist_peak + dist, dmax) if preview: self._histplot.setAxisScale(QwtPlot.xBottom, *self._preview_hist_range) else: dprint(2, "wheel finalized at", value) self._whistzoom_timer.stop() self._updateHistogram(*self._preview_hist_range) self._histplot.replot() finally: self._zooming_histogram = False def _setHistLogScale(self, logscale, replot=True): self._ylogscale = logscale if logscale: self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLog10ScaleEngine()) ymax = max(1, self._hist_max) self._histplot.setAxisScale(QwtPlot.yLeft, 1, 10 ** (math.log10(ymax) * (1 + self.ColorBarHeight))) y = self._hist.copy() y[y == 0] = 1 self._histcurve1.setData(self._hist_bins, y) self._histcurve2.setData(self._hist_bins, y) else: self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLinearScaleEngine()) self._histplot.setAxisScale(QwtPlot.yLeft, 0, self._hist_max * (1 + self.ColorBarHeight)) self._histcurve1.setData(self._hist_bins, self._hist) self._histcurve2.setData(self._hist_bins, self._hist) if replot: self._histplot.replot()
class ScudCloud(QtGui.QMainWindow): plugins = True debug = False forceClose = False messages = 0 speller = Speller() def __init__(self, parent = None, settings_path = ""): super(ScudCloud, self).__init__(parent) self.setWindowTitle('ScudCloud') self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.cfg', QSettings.IniFormat) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id("scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: self.startURL = self.domain() self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(ScudCloud.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') # Starting unread msgs counter self.setupTimer() def addWrapper(self, url): webView = Wrapper(self) webView.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) def setupTimer(self): self.timer = QTimer(self) self.timer.timeout.connect(self.count) self.timer.setInterval(2000) self.timer.start() def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # Required by Youtube videos (HTML5 video support only on Qt5) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, self.plugins) # We don't want Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # We don't need History QWebSettings.globalSettings().setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.settings_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute(QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, self.debug) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "False") else: self.settings.setValue("Menu", "True") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addMenu(self): self.menus = { "file": { "preferences": self.createAction("Preferences", lambda : self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda : self.switchTo(Resources.SIGNIN_URL)), "signout": self.createAction("Signout", lambda : self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": { }, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda : self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda : self.current().helpCenter()), "about": self.createAction("About", lambda : self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) self.editMenu = menu.addMenu("&Edit") self.updateEditMenu() viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) if Unity is None: viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def updateEditMenu(self): self.editMenu.clear() self.menus["edit"] = { "undo": self.current().pageAction(QtWebKit.QWebPage.Undo), "redo": self.current().pageAction(QtWebKit.QWebPage.Redo), "cut": self.current().pageAction(QtWebKit.QWebPage.Cut), "copy": self.current().pageAction(QtWebKit.QWebPage.Copy), "paste": self.current().pageAction(QtWebKit.QWebPage.Paste), "back": self.current().pageAction(QtWebKit.QWebPage.Back), "forward": self.current().pageAction(QtWebKit.QWebPage.Forward), "reload": self.current().pageAction(QtWebKit.QWebPage.Reload) } self.editMenu.addAction(self.menus["edit"]["undo"]) self.editMenu.addAction(self.menus["edit"]["redo"]) self.editMenu.addSeparator() self.editMenu.addAction(self.menus["edit"]["cut"]) self.editMenu.addAction(self.menus["edit"]["copy"]) self.editMenu.addAction(self.menus["edit"]["paste"]) self.editMenu.addSeparator() self.editMenu.addAction(self.menus["edit"]["back"]) self.editMenu.addAction(self.menus["edit"]["forward"]) self.editMenu.addAction(self.menus["edit"]["reload"]) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtGui.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def domain(self): if self.identifier.endswith(".slack.com"): return self.identifier else: return "https://"+self.identifier+".slack.com" def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): for t in teams: # If team_icon is not present, it's because team is already connected if 'team_icon' in t: self.leftPane.addTeam(t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() exists = True break if not exists: self.addWrapper(url) self.enableMenus(self.current().isConnected()) self.updateEditMenu() def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtGui.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtGui.QMainWindow.eventFilter(self, obj, event); def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() def titleChanged(self): self.setWindowTitle(self.current().title()) def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() else: self.cookiesjar.save() self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) # Let's save the first team registered as default qUrl = self.stackedWidget.widget(0).url() if self.identifier is None and Resources.MESSAGES_URL_RE.match(qUrl.toString()): self.settings.setValue("Domain", 'https://'+qUrl.host()) def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): self.forceClose = True self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if c['is_member']: item = Dbusmenu.Menuitem.new () item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, "#"+c['name']) item.property_set ("id", c['name']) item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon=None): self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() def count(self): total = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) messages = widget.count() if messages == 0: self.leftPane.stopAlert(widget.team()) else: self.leftPane.alert(widget.team()) if messages is not None: total+=messages if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.messages = total
def setupTimer(self): timer = QTimer(self) timer.timeout.connect(self.count) timer.setInterval(2000) timer.start()
class ScudCloud(QtGui.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug = False, parent = None, minimized = None, settings_path = ""): super(ScudCloud, self).__init__(parent) self.debug = debug self.minimized = minimized self.setWindowTitle(self.title) self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.cfg', QSettings.IniFormat) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id("scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: if isinstance(self.identifier, str): self.domains = self.identifier.split(",") else: self.domains = self.identifier self.startURL = self.normalize(self.domains[0]) else: self.domains = [] self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(self.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') self.tickler = QTimer(self) self.tickler.setInterval(1800000) # Watch for ScreenLock events if DBusQtMainLoop is not None: DBusQtMainLoop(set_as_default=True) sessionBus = dbus.SessionBus() # Ubuntu 12.04 and other distros sessionBus.add_match_string("type='signal',interface='org.gnome.ScreenSaver'") # Ubuntu 14.04 and above sessionBus.add_match_string("type='signal',interface='com.ubuntu.Upstart0_6'") sessionBus.add_message_filter(self.screenListener) self.tickler.timeout.connect(self.sendTickle) # If dbus is not present, tickler timer will act like a blocker to not send tickle too often else: self.tickler.setSingleShot(True) self.tickler.start() def screenListener(self, bus, message): event = message.get_member() # "ActiveChanged" for Ubuntu 12.04 and other distros. "EventEmitted" for Ubuntu 14.04 and above if event == "ActiveChanged" or event == "EventEmitted": arg = message.get_args_list()[0] # True for Ubuntu 12.04 and other distros. "desktop-lock" for Ubuntu 14.04 and above if (arg == True or arg == "desktop-lock") and self.tickler.isActive(): self.tickler.stop() elif (arg == False or arg == "desktop-unlock") and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def sendTickle(self): for i in range(0, self.stackedWidget.count()): self.stackedWidget.widget(i).sendTickle() def addWrapper(self, url): webView = Wrapper(self) webView.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # We don't want Flash (it causes a lot of trouble in some distros) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, False) # We don't need Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # Enabling Local Storage (now required by Slack) QWebSettings.globalSettings().setAttribute(QWebSettings.LocalStorageEnabled, True) # We need browsing history (required to not limit LocalStorage) QWebSettings.globalSettings().setAttribute(QWebSettings.PrivateBrowsingEnabled, False) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.settings_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute(QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, self.debug) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "False") else: self.settings.setValue("Menu", "True") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addMenu(self): # We'll register the webpage shorcuts with the window too (Fixes #338) undo = self.current().pageAction(QtWebKit.QWebPage.Undo) redo = self.current().pageAction(QtWebKit.QWebPage.Redo) cut = self.current().pageAction(QtWebKit.QWebPage.Cut) copy = self.current().pageAction(QtWebKit.QWebPage.Copy) paste = self.current().pageAction(QtWebKit.QWebPage.Paste) back = self.current().pageAction(QtWebKit.QWebPage.Back) forward = self.current().pageAction(QtWebKit.QWebPage.Forward) reload = self.current().pageAction(QtWebKit.QWebPage.Reload) self.menus = { "file": { "preferences": self.createAction("Preferences", lambda : self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda : self.switchTo(Resources.SIGNIN_URL)), "signout": self.createAction("Signout", lambda : self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": { "undo": self.createAction(undo.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Undo), undo.shortcut()), "redo": self.createAction(redo.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Redo), redo.shortcut()), "cut": self.createAction(cut.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Cut), cut.shortcut()), "copy": self.createAction(copy.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Copy), copy.shortcut()), "paste": self.createAction(paste.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Paste), paste.shortcut()), "back": self.createAction(back.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Back), back.shortcut()), "forward": self.createAction(forward.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Forward), forward.shortcut()), "reload": self.createAction(reload.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Reload), reload.shortcut()), }, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda : self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda : self.current().helpCenter()), "about": self.createAction("About", lambda : self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) editMenu = menu.addMenu("&Edit") editMenu.addAction(self.menus["edit"]["undo"]) editMenu.addAction(self.menus["edit"]["redo"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["cut"]) editMenu.addAction(self.menus["edit"]["copy"]) editMenu.addAction(self.menus["edit"]["paste"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["back"]) editMenu.addAction(self.menus["edit"]["forward"]) editMenu.addAction(self.menus["edit"]["reload"]) viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtGui.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def normalize(self, url): if url.endswith(".slack.com"): url+= "/" elif not url.endswith(".slack.com/"): url = "https://"+url+".slack.com/" return url def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): if len(self.domains) == 0: self.domains.append(teams[0]['team_url']) team_list = [t['team_url'] for t in teams] for t in teams: for i in range(0, len(self.domains)): self.domains[i] = self.normalize(self.domains[i]) # When team_icon is missing, the team already exists (Fixes #381, #391) if 'team_icon' in t: if self.domains[i] in team_list: add = next(item for item in teams if item['team_url'] == self.domains[i]) if 'team_icon' in add: self.leftPane.addTeam(add['id'], add['team_name'], add['team_url'], add['team_icon']['image_44'], add == teams[0]) # Adding new teams and saving loading positions if t['team_url'] not in self.domains: self.leftPane.addTeam(t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) self.domains.append(t['team_url']) self.settings.setValue("Domain", self.domains) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() self.leftPane.click(i) exists = True break if not exists: self.addWrapper(url) def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtGui.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtGui.QMainWindow.eventFilter(self, obj, event); def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() # Let's tickle all teams on window focus, but only if tickle was not fired in last 30 minutes if DBusQtMainLoop is None and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def titleChanged(self): self.setWindowTitle(self.current().title()) def setForceClose(self): self.forceClose = True def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() else: self.cookiesjar.save() self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.forceClose = False def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): self.setForceClose() self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if c['is_member']: item = Dbusmenu.Menuitem.new () item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, "#"+c['name']) item.property_set ("id", c['name']) item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon): if self.debug: print("Notification: title [{}] message [{}] icon [{}]".format(title, message, icon)) self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() def count(self): total = 0 unreads = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) highlights = widget.highlights unreads+= widget.unreads total+=highlights if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) if unreads > 0: self.setWindowTitle("*{}".format(self.title)) else: self.setWindowTitle(self.title) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.setWindowTitle("[{}]{}".format(str(total), self.title)) self.messages = total
class Splitter(QSplitter): state_changed = pyqtSignal(object) def __init__(self, name, label, icon, initial_show=True, initial_side_size=120, connect_button=True, orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None): QSplitter.__init__(self, parent) self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.desired_side_size = initial_side_size self.desired_show = initial_show self.resize_timer.setInterval(5) self.resize_timer.timeout.connect(self.do_resize) self.setOrientation(orientation) self.side_index = side_index self._name = name self.label = label self.initial_side_size = initial_side_size self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection) self.button = LayoutButton(icon, label, self, shortcut=shortcut) if connect_button: self.button.clicked.connect(self.double_clicked) if shortcut is not None: self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self) self.action_toggle.triggered.connect(self.toggle_triggered) if parent is not None: parent.addAction(self.action_toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut('splitter %s %s'%(name, label), unicode(self.action_toggle.text()), default_keys=(shortcut,), action=self.action_toggle) else: self.action_toggle.setShortcut(shortcut) else: self.action_toggle.setShortcut(shortcut) def toggle_triggered(self, *args): self.toggle_side_pane() def createHandle(self): return SplitterHandle(self.orientation(), self) def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: h.splitter_moved() self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) @property def is_side_index_hidden(self): sizes = list(self.sizes()) try: return sizes[self.side_index] == 0 except IndexError: return True @property def save_name(self): ori = 'horizontal' if self.orientation() == Qt.Horizontal \ else 'vertical' return self._name + '_' + ori def print_sizes(self): if self.count() > 1: print self.save_name, 'side:', self.side_index_size, 'other:', print list(self.sizes())[self.other_index] @dynamic_property def side_index_size(self): def fget(self): if self.count() < 2: return 0 return self.sizes()[self.side_index] def fset(self, val): if self.count() < 2: return if val == 0 and not self.is_side_index_hidden: self.save_state() sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) total = sum(self.sizes()) sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else total-val self.setSizes(sizes) self.initialize() return property(fget=fget, fset=fset) def do_resize(self, *args): orig = self.desired_side_size QSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: self.apply_state(self.get_state(), save_desired=False) c += 1 def resizeEvent(self, ev): if self.resize_timer.isActive(): self.resize_timer.stop() self._resize_ev = ev self.resize_timer.start() def get_state(self): if self.count() < 2: return (False, 200) return (self.desired_show, self.desired_side_size) def apply_state(self, state, save_desired=True): if state[0]: self.side_index_size = state[1] if save_desired: self.desired_side_size = self.side_index_size else: self.side_index_size = 0 self.desired_show = state[0] def default_state(self): return (self.initial_show, self.initial_side_size) # Public API {{{ def update_desired_state(self): self.desired_show = not self.is_side_index_hidden def save_state(self): if self.count() > 1: gprefs[self.save_name+'_state'] = self.get_state() @property def other_index(self): return (self.side_index+1)%2 def restore_state(self): if self.count() > 1: state = gprefs.get(self.save_name+'_state', self.default_state()) self.apply_state(state, save_desired=False) self.desired_side_size = state[1] def toggle_side_pane(self, hide=None): if hide is None: action = 'show' if self.is_side_index_hidden else 'hide' else: action = 'hide' if hide else 'show' getattr(self, action+'_side_pane')() def show_side_pane(self): if self.count() < 2 or not self.is_side_index_hidden: return if self.desired_side_size == 0: self.desired_side_size = self.initial_side_size self.apply_state((True, self.desired_side_size)) def hide_side_pane(self): if self.count() < 2 or self.is_side_index_hidden: return self.apply_state((False, self.desired_side_size)) def double_clicked(self, *args): self.toggle_side_pane()
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel('<h3>' + _( 'No style information found') + '</h3><p>' + _( 'Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == (None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode(mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % ( json.dumps(sourceline), json.dumps(tags))).toString()) result = json.loads(result) if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get(p.name, (0,0,0,0,0,0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith('file://'): href = href[len('file://'):] if iswindows and href.startswith('/'): href = href[1:] if href: rule['href'] = current_container().abspath_to_name(href, root=self.preview.current_root) @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self, name): self.start_update_timer() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag() if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])
def setupTimer(self): timer = QTimer(self) timer.timeout.connect(self.overrideNotifications) # Hope each 10 minutes will not be produce high CPU usage timer.setInterval(600000) timer.start()
class DiagnosticGui(Plugin): last_suppress_time = Time() last_twist_time = Time() gui_update_timer = QTimer() # Raw joystick data joystick_data = [] joystick_table_vals = [] joystick_channel_text = ['CHAN_ENABLE: ', 'CHAN_ROTATE: ', 'CHAN_FORWARD: ', 'CHAN_LATERAL: ', 'CHAN_MODE: ', 'CHAN_EXTRA: '] joystick_bind_status = False joystick_bind_dot_counter = 0 # Mode current_mode = -1 # Topic pub timeouts JOYSTICK_SUPPRESS_PERIOD = 0.2 # 5 Hz CMD_VEL_TIMEOUT_PERIOD = 0.2 # 5 Hz joystick_suppressed = False command_received = False current_cmd = Twist() battery_percent = 0 battery_voltage = 0 # Checklist icons bad_icon = QPixmap() good_icon = QPixmap() none_icon = QPixmap() # Checklist status GOOD = 0 BAD = 1 NONE = 2 # Checklist variables checklist_status = [] BATT_MAN = 0 ESTOP_MAN = 1 DISABLE_MAN = 2 JOYSTICK_MAN = 3 SUPPRESS_MAN = 4 BATT_COMP = 5 ESTOP_COMP = 6 DISABLE_COMP = 7 JOYSTICK_COMP = 8 SUPPRESS_COMP = 9 CMD_COMP = 10 # Bumpers bumper_front_left = 0 bumper_front_right = 0 bumper_rear_left = 0 bumper_rear_right = 0 # Gyro cal gyro_cal_status = False gyro_x = 0.0 gyro_y = 0.0 gyro_z = 0.0 cal_enabled = True cal_time = rospy.Time(0) # Max speed config max_speed_known = False max_speed_dirty = True max_linear_actual = 0.0 max_angular_actual = 0.0 max_linear_setting = 0.0 max_angular_setting = 0.0 # Wake time current_wake_time = rospy.Time(0) rel_wake_days = 0 rel_wake_hours = 0 rel_wake_minutes = 0 rel_wake_secs = 0 # Switching between tabs and full GUI is_currently_tab = False widget_count = 0 current_tab_idx = -1 raw_data_tab_idx = 5 # Check connection to base base_connected = False last_joystick_time = rospy.Time(0) # Constants stick_ind_lox = 80 stick_ind_loy = 136 stick_ind_rox = 286 stick_ind_roy = 135 stick_ind_range_pix = 88.0 stick_ind_range_max = JoystickRaw.MAX stick_ind_range_min = JoystickRaw.MIN stick_ind_range_mid = JoystickRaw.CENTER stick_ind_range_factor = stick_ind_range_pix / (stick_ind_range_max - stick_ind_range_min) stick_ind_radius = 7 mode_ind_x1 = 52 mode_ind_y1 = 37 mode_ind_x2 = 44 mode_ind_y2 = 13 power_ind_x1 = 160 power_ind_x2 = 206 power_ind_y = 213 bumper_fl_x = 70 bumper_fl_y = 60 bumper_fr_x = 293 bumper_fr_y = 60 bumper_rl_x = 70 bumper_rl_y = 282 bumper_rr_x = 293 bumper_rr_y = 282 bumper_dx = 62 bumper_dy = 54 joystick_table_left_edge = 440 joystick_table_top_edge = 525 twist_table_left_edge = 700 twist_table_top_edge = 580 battery_table_left_edge = 700 battery_table_top_edge = 730 def __init__(self, context): # Qt setup self.context_ = context self.initGui('full') # ROS setup topic_timeout_timer = rospy.Timer(rospy.Duration(0.02), self.topicTimeoutCallback) self.subscribeTopics() self.advertiseTopics() def initGui(self, gui_type): if gui_type == 'full': self.spawnFullGui() self.initTables(self._widget, self.joystick_table_left_edge, self.joystick_table_top_edge) else: self.spawnTabGui() self.initTables(self._widget.gui_tabs.widget(self.raw_data_tab_idx), 20, 20) self._widget.gui_tabs.setCurrentIndex(self.current_tab_idx) self.resetGuiTimer() self.initJoystickGraphics() self.initBumperGraphics() self.initChecklists() self.bindCallbacks() self.refreshMaxSpeed() # Initialize absolute wake time setter to current time datetime_now = QDateTime(QDate.currentDate(), QTime.currentTime()) self._widget.absolute_wake_time_obj.setDateTime(datetime_now) temp_time = self._widget.absolute_wake_time_obj.time() temp_time = QTime(temp_time.hour(), temp_time.minute()) self._widget.absolute_wake_time_obj.setTime(temp_time) # Set connection label text if self.base_connected: self._widget.disconnected_lbl.setVisible(False) else: self._widget.disconnected_lbl.setVisible(True) self._widget.disconnected_lbl.setText('<font color=#FF0000>NOT CONNECTED</font>') def topicTimeoutCallback(self, event): # Joystick suppression if (event.current_real - self.last_suppress_time).to_sec() < self.JOYSTICK_SUPPRESS_PERIOD and self.suppress_dt.to_sec() <= 1.0/9.0: self.joystick_suppressed = True else: self.joystick_suppressed = False # Command message if (event.current_real - self.last_twist_time).to_sec() < self.CMD_VEL_TIMEOUT_PERIOD and self.twist_dt.to_sec() <= 1.0/6.0: self.command_received = True else: self.command_received = False def initChecklists(self): self.bad_icon.load(os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'images', 'bad.png')) self.good_icon.load(os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'images', 'good.png')) self.none_icon.load(os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'images', 'none.png')) self.checklist_status=[0 for i in range(self.CMD_COMP + 1)] self.checklist_status[self.BATT_MAN] = self.NONE self.checklist_status[self.ESTOP_MAN] = self.NONE self.checklist_status[self.DISABLE_MAN] = self.NONE self.checklist_status[self.JOYSTICK_MAN] = self.NONE self.checklist_status[self.SUPPRESS_MAN] = self.NONE self.checklist_status[self.BATT_COMP] = self.NONE self.checklist_status[self.ESTOP_COMP] = self.NONE self.checklist_status[self.DISABLE_COMP] = self.NONE self.checklist_status[self.JOYSTICK_COMP] = self.NONE self.checklist_status[self.SUPPRESS_COMP] = self.NONE self.checklist_status[self.CMD_COMP] = self.NONE def initTabTables(self): self.joystick_table_vals = [QLabel(self._widget.gui_tabs.widget(self.raw_data_tab_idx)) for i in range(ChannelIndex.CHAN_EXTRA+1)] self.joystick_table_labels = [QLabel(self._widget.gui_tabs.widget(self.raw_data_tab_idx)) for i in range(ChannelIndex.CHAN_EXTRA+1)] self.joystick_table_heading = QLabel(self._widget.gui_tabs.widget(self.raw_data_tab_idx)) def initTables(self, widget, left, top): # Joystick data table self.joystick_data = [0 for i in range(ChannelIndex.CHAN_EXTRA+1)] self.joystick_table_vals = [QLabel(widget) for i in range(ChannelIndex.CHAN_EXTRA+1)] self.joystick_table_labels = [QLabel(widget) for i in range(ChannelIndex.CHAN_EXTRA+1)] self.joystick_table_heading = QLabel(widget) self.joystick_table_heading.setText('Raw Joystick Data') self.joystick_table_heading.setFont(QFont('Ubuntu', 11, QFont.Bold)) self.joystick_table_heading.move(left, top) for i in range(len(self.joystick_table_vals)): self.joystick_table_vals[i].move(left + 150, top + 30 * (i+1)) self.joystick_table_vals[i].setText('0000') self.joystick_table_vals[i].setFixedWidth(200) self.joystick_table_labels[i].move(left, top + 30 * (i+1)) self.joystick_table_labels[i].setText(self.joystick_channel_text[i]) # Twist data table self.twist_table_heading = QLabel(widget) self.twist_table_heading.setText('Current Twist Command') self.twist_table_heading.setFont(QFont('Ubuntu', 11, QFont.Bold)) self.twist_table_heading.move(left + 260, top) self.twist_table_labels = [QLabel(widget) for i in range(0, 3)] self.twist_table_vals = [QLabel(widget) for i in range(0, 3)] for i in range(len(self.twist_table_vals)): self.twist_table_vals[i].move(left + 260 + 150, top + 30 * (i+1)) self.twist_table_vals[i].setText('Not Published') self.twist_table_vals[i].setFixedWidth(200) self.twist_table_labels[i].move(left + 260, top + 30 * (i+1)) self.twist_table_labels[0].setText('Forward (m/s):') self.twist_table_labels[1].setText('Lateral (m/s):') self.twist_table_labels[2].setText('Rotation (rad/s):') # Battery percentage self.battery_heading = QLabel(widget) self.battery_heading.setText('Current Battery State') self.battery_heading.setFont(QFont('Ubuntu', 11, QFont.Bold)) self.battery_heading.move(left + 260, top + 150) self.battery_label = QLabel(widget) self.battery_label.move(left + 260, top + 150 +30) self.battery_label.setText('000 %') self.battery_voltage_label = QLabel(widget) self.battery_voltage_label.move(left + 260, top + 150 +60) self.battery_voltage_label.setText('00.00 V') self.battery_voltage_label.setFixedWidth(200) # Mode self.mode_heading = QLabel(widget) self.mode_heading.setFont(QFont('Ubuntu', 11, QFont.Bold)) self.mode_heading.move(left, top + 225) self.mode_heading.setText('Current Mode') self.mode_label = QLabel(widget) self.mode_label.move(left + 120, top + 225) self.mode_label.setText('XXXXXXXXXXXXXXXXXXXXXX') def bindCallbacks(self): self._widget.start_bind_btn.clicked.connect(self.startBind) self._widget.stop_bind_btn.clicked.connect(self.stopBind) self._widget.gyro_cal_btn.clicked.connect(self.calGyro) self._widget.clear_cal_btn.clicked.connect(self.clearCal) self._widget.max_linear_txt.editingFinished.connect(self.maxLinearChanged) self._widget.max_angular_txt.editingFinished.connect(self.maxAngularChanged) self._widget.set_speed_btn.clicked.connect(self.setMaxSpeed) self._widget.clear_speed_btn.clicked.connect(self.clearMaxSpeed) self._widget.wake_time_days_txt.editingFinished.connect(self.wakeDaysChanged) self._widget.wake_time_hours_txt.editingFinished.connect(self.wakeHoursChanged) self._widget.wake_time_minutes_txt.editingFinished.connect(self.wakeMinutesChanged) self._widget.wake_time_secs_txt.editingFinished.connect(self.wakeSecsChanged) self._widget.set_relative_wake_time_btn.clicked.connect(self.setRelativeWakeTime) self._widget.set_absolute_wake_time_btn.clicked.connect(self.setAbsoluteWakeTime) self._widget.clear_wake_time_btn.clicked.connect(self.clearWakeTime) if self.is_currently_tab: self._widget.gui_tabs.currentChanged.connect(self.setCurrentTab) def setCurrentTab(self, idx): self.current_tab_idx = self._widget.gui_tabs.currentIndex() def startBind(self): self.pub_start_bind.publish(std_msgs.msg.Empty()) def stopBind(self): self.pub_stop_bind.publish(std_msgs.msg.Empty()) def calGyro(self): gyro_cal_srv = rospy.ServiceProxy('/mobility_base/imu/calibrate', std_srvs.srv.Empty) try: gyro_cal_srv() self.cal_enabled = False self.cal_time = rospy.Time.now() except: pass def clearCal(self): clear_cal_srv = rospy.ServiceProxy('/mobility_base/imu/clear_cal', std_srvs.srv.Empty) try: clear_cal_srv() except: pass def maxLinearChanged(self): try: float_val = float(self._widget.max_linear_txt.text()) self.max_linear_setting = float_val; except ValueError: if self.max_linear_actual <= 0 or not isfinite(self.max_linear_actual): self.max_linear_setting = 0.0 else: self.max_linear_setting = self.max_linear_actual self.max_speed_dirty = True def maxAngularChanged(self): try: float_val = float(self._widget.max_angular_txt.text()) self.max_angular_setting = float_val; except ValueError: if self.max_angular_actual <= 0 or not isfinite(self.max_angular_actual): self.max_angular_setting = 0.0 else: self.max_angular_setting = self.max_angular_actual self.max_speed_dirty = True def setMaxSpeed(self): set_max_speed_srv = rospy.ServiceProxy('/mobility_base/set_max_speed', SetMaxSpeed) req = SetMaxSpeedRequest() req.linear = self.max_linear_setting req.angular = self.max_angular_setting try: set_max_speed_srv(req) self.max_speed_known = False except: pass def clearMaxSpeed(self): set_max_speed_srv = rospy.ServiceProxy('/mobility_base/set_max_speed', SetMaxSpeed) req = SetMaxSpeedRequest() req.linear = float('nan') req.angular = float('nan') try: set_max_speed_srv(req) self.max_speed_known = False except: pass def wakeDaysChanged(self): try: self.rel_wake_days = float(self._widget.wake_time_days_txt.text()) except: self._widget.wake_time_days_txt.setText(str(self.rel_wake_days)) def wakeHoursChanged(self): try: self.rel_wake_hours = float(self._widget.wake_time_hours_txt.text()) except: self._widget.wake_time_hours_txt.setText(str(self.rel_wake_hours)) def wakeMinutesChanged(self): try: self.rel_wake_minutes = float(self._widget.wake_time_minutes_txt.text()) except: self._widget.wake_time_minutes_txt.setText(str(self.rel_wake_minutes)) def wakeSecsChanged(self): try: self.rel_wake_secs = float(self._widget.wake_time_secs_txt.text()) except: self._widget.wake_time_secs_txt.setText(str(self.rel_wake_secs)) def setRelativeWakeTime(self): new_wake_time = std_msgs.msg.Time() rel_wake_time = 86400 * self.rel_wake_days + 3600 * self.rel_wake_hours + 60 * self.rel_wake_minutes + self.rel_wake_secs new_wake_time.data = rospy.Time.now() + rospy.Duration(rel_wake_time) self.pub_set_wake_time.publish(new_wake_time) def setAbsoluteWakeTime(self): self.pub_set_wake_time.publish(rospy.Time(self._widget.absolute_wake_time_obj.dateTime().toTime_t())) def clearWakeTime(self): self.pub_set_wake_time.publish(rospy.Time(0)) def refreshMaxSpeed(self): self.max_speed_dirty = True self.max_speed_known = False def updateGuiCallback(self): # Switch between tabs and full GUI if self.is_currently_tab: if self._widget.height() > 830 and self._widget.width() > 1205: self.is_currently_tab = False self.context_.remove_widget(self._widget) self.initGui('full') else: if self._widget.height() < 810 or self._widget.width() < 1185: self.is_currently_tab = True self.context_.remove_widget(self._widget) self.initGui('tab') # Check connection to base if self.base_connected and (rospy.Time.now() - self.last_joystick_time).to_sec() > 1.0: self.base_connected = False self._widget.disconnected_lbl.setText('<font color=#FF0000>NOT CONNECTED</font>') self._widget.disconnected_lbl.setVisible(True) if not self.base_connected and (rospy.Time.now() - self.last_joystick_time).to_sec() < 1.0: self.base_connected = True # self._widget.disconnected_lbl.setText('') self._widget.disconnected_lbl.setVisible(False) # Update checklists self.updateChecklist(); # Manage 5 second disable of gyro calibration button if not self.cal_enabled: if (rospy.Time.now() - self.cal_time).to_sec() > 5.0: self._widget.gyro_cal_btn.setEnabled(True) self._widget.gyro_cal_btn.setText('Calibrate') self.cal_enabled = True else: self._widget.gyro_cal_btn.setEnabled(False) self._widget.gyro_cal_btn.setText(str(5 - 0.1*floor(10*(rospy.Time.now() - self.cal_time).to_sec()))) # Update joystick graphics if not self.checkJoystickValid(): self.updateCheckStatus(self._widget.joystick_bind_chk, self.NONE) if not self.joystick_bind_status: self._widget.joystick_bind_lbl.setText('') for l in self.joystick_power_ind: l.setVisible(True) self.updateRightStickIndicator(self.stick_ind_range_mid, self.stick_ind_range_mid) self.updateLeftStickIndicator(self.stick_ind_range_mid, self.stick_ind_range_mid) else: self.updateCheckStatus(self._widget.joystick_bind_chk, self.GOOD) self._widget.joystick_bind_lbl.setText('Joystick bound') for l in self.joystick_power_ind: l.setVisible(False) self.updateRightStickIndicator(self.joystick_data[ChannelIndex.CHAN_LATERAL], self.joystick_data[ChannelIndex.CHAN_FORWARD]) self.updateLeftStickIndicator(self.joystick_data[ChannelIndex.CHAN_ROTATE], self.joystick_data[ChannelIndex.CHAN_ENABLE]) if self.joystick_data[ChannelIndex.CHAN_MODE] < self.stick_ind_range_mid: self.mode_ind.setPen(self.cyan_pen) self._widget.modeLabel.setText("0 (Computer)") else: self.mode_ind.setPen(self.magenta_pen) self._widget.modeLabel.setText("1 (Manual)") # Update joystick data table for i in range(len(self.joystick_table_vals)): self.joystick_table_vals[i].setText(str(self.joystick_data[i])) # Update twist data table if self.command_received: self.twist_table_vals[0].setText(str(0.01 * floor(100 * self.current_cmd.linear.x))) self.twist_table_vals[1].setText(str(0.01 * floor(100 * self.current_cmd.linear.y))) self.twist_table_vals[2].setText(str(0.01 * floor(100 * self.current_cmd.angular.z))) else: self.twist_table_vals[0].setText('Not Published') self.twist_table_vals[1].setText('Not Published') self.twist_table_vals[2].setText('Not Published') # Update battery percentage self.battery_label.setText(str(self.battery_percent) + ' %') self.battery_voltage_label.setText(str(0.01 * floor(100 * self.battery_voltage)) + ' V') # Update mode self.mode_label.setText(self.getModeString(self.current_mode)) # Update bumper graphics self.bumperVisibleSwitch(BumperIndex.BUMPER_1F, self.bumper_front_left) self.bumperVisibleSwitch(BumperIndex.BUMPER_2F, self.bumper_front_right) self.bumperVisibleSwitch(BumperIndex.BUMPER_3F, self.bumper_rear_left) self.bumperVisibleSwitch(BumperIndex.BUMPER_4F, self.bumper_rear_right) self.bumper_state_labels[0].setPlainText(str(self.bumper_front_left)) self.bumper_state_labels[1].setPlainText(str(self.bumper_front_right)) self.bumper_state_labels[2].setPlainText(str(self.bumper_rear_left)) self.bumper_state_labels[3].setPlainText(str(self.bumper_rear_right)) # Update gyro cal graphics if self.gyro_cal_status: self.updateCheckStatus(self._widget.gyro_cal_chk, self.GOOD) self._widget.gyro_cal_lbl.setText('Gyro calibrated') else: self.updateCheckStatus(self._widget.gyro_cal_chk, self.BAD) self._widget.gyro_cal_lbl.setText('Gyro NOT calibrated') self._widget.gyro_x_lbl.setText('x: ' + str(1e-5*floor(1e5*self.gyro_x))) self._widget.gyro_y_lbl.setText('y: ' + str(1e-5*floor(1e5*self.gyro_y))) self._widget.gyro_z_lbl.setText('z: ' + str(1e-5*floor(1e5*self.gyro_z))) # Update max speed configuration if not self.max_speed_known: service_name = '/mobility_base/get_max_speed' get_max_speed_srv = rospy.ServiceProxy(service_name, GetMaxSpeed) try: resp = get_max_speed_srv() self.max_speed_known = True self.max_linear_actual = resp.linear self.max_angular_actual = resp.angular if self.max_linear_actual <= 0 or not isfinite(self.max_linear_actual): self._widget.max_linear_lbl.setText('Unlimited') else: self._widget.max_linear_lbl.setText(str(1e-2*round(1e2*self.max_linear_actual))) if self.max_angular_actual <= 0 or not isfinite(self.max_angular_actual): self._widget.max_angular_lbl.setText('Unlimited') else: self._widget.max_angular_lbl.setText(str(1e-2*round(1e2*self.max_angular_actual))) except: pass # print service_name + " doesn't exist" if self.max_speed_dirty: self._widget.max_linear_txt.setText(str(self.max_linear_setting)) self._widget.max_angular_txt.setText(str(self.max_angular_setting)) self.max_speed_dirty = False # Wake time if self.current_wake_time == rospy.Time(0): self._widget.wake_time_lbl.setText('Not Set') else: self._widget.wake_time_lbl.setText(time.strftime('%m/%d/%Y, %H:%M:%S', time.localtime(self.current_wake_time.to_sec()))) def checkJoystickValid(self): return self.joystick_data[ChannelIndex.CHAN_FORWARD] > 0 or self.joystick_data[ChannelIndex.CHAN_LATERAL] > 0 or self.joystick_data[ChannelIndex.CHAN_ROTATE] > 0 or self.joystick_data[ChannelIndex.CHAN_MODE] > 0 def getModeString(self, mode_num): if mode_num == Mode.MODE_ESTOP: return 'MODE_ESTOP' elif mode_num == Mode.MODE_DISABLED: return 'MODE_DISABLED' elif mode_num == Mode.MODE_BATTERY_LIMP_HOME: return 'MODE_BATTERY_LIMP_HOME' elif mode_num == Mode.MODE_BATTERY_CRITICAL: return 'MODE_BATTERY_CRITICAL' elif mode_num == Mode.MODE_WIRELESS: return 'MODE_WIRELESS' elif mode_num == Mode.MODE_TIMEOUT: return 'MODE_TIMEOUT' elif mode_num == Mode.MODE_VELOCITY: return 'MODE_VELOCITY' elif mode_num == Mode.MODE_VELOCITY_RAW: return 'MODE_VELOCITY_RAW' else: return '' def updateChecklist(self): if self.current_mode >= Mode.MODE_TIMEOUT: self.checklist_status[self.BATT_MAN] = self.GOOD self.checklist_status[self.ESTOP_MAN] = self.GOOD self.checklist_status[self.DISABLE_MAN] = self.GOOD self.checklist_status[self.JOYSTICK_MAN] = self.BAD self.checklist_status[self.BATT_COMP] = self.GOOD self.checklist_status[self.ESTOP_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.GOOD self.checklist_status[self.JOYSTICK_COMP] = self.GOOD elif self.current_mode == Mode.MODE_WIRELESS: self.checklist_status[self.BATT_MAN] = self.GOOD self.checklist_status[self.ESTOP_MAN] = self.GOOD self.checklist_status[self.DISABLE_MAN] = self.GOOD self.checklist_status[self.JOYSTICK_MAN] = self.GOOD self.checklist_status[self.BATT_COMP] = self.GOOD self.checklist_status[self.ESTOP_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.GOOD self.checklist_status[self.JOYSTICK_COMP] = self.BAD elif self.current_mode == Mode.MODE_DISABLED: self.checklist_status[self.BATT_MAN] = self.NONE self.checklist_status[self.ESTOP_MAN] = self.GOOD self.checklist_status[self.DISABLE_MAN] = self.BAD self.checklist_status[self.JOYSTICK_MAN] = self.NONE self.checklist_status[self.BATT_COMP] = self.NONE self.checklist_status[self.ESTOP_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.BAD self.checklist_status[self.JOYSTICK_COMP] = self.NONE elif self.current_mode == Mode.MODE_ESTOP: self.checklist_status[self.BATT_MAN] = self.NONE self.checklist_status[self.ESTOP_MAN] = self.BAD self.checklist_status[self.DISABLE_MAN] = self.NONE self.checklist_status[self.JOYSTICK_MAN] = self.NONE self.checklist_status[self.BATT_COMP] = self.NONE self.checklist_status[self.ESTOP_COMP] = self.BAD self.checklist_status[self.DISABLE_COMP] = self.NONE self.checklist_status[self.JOYSTICK_COMP] = self.NONE elif self.current_mode == Mode.MODE_BATTERY_CRITICAL: self.checklist_status[self.BATT_MAN] = self.BAD self.checklist_status[self.ESTOP_MAN] = self.GOOD self.checklist_status[self.DISABLE_MAN] = self.GOOD self.checklist_status[self.JOYSTICK_MAN] = self.NONE self.checklist_status[self.BATT_COMP] = self.BAD self.checklist_status[self.ESTOP_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.GOOD self.checklist_status[self.JOYSTICK_COMP] = self.NONE elif self.current_mode == Mode.MODE_BATTERY_LIMP_HOME: self.checklist_status[self.BATT_MAN] = self.GOOD self.checklist_status[self.ESTOP_MAN] = self.GOOD self.checklist_status[self.DISABLE_MAN] = self.GOOD self.checklist_status[self.JOYSTICK_MAN] = self.NONE self.checklist_status[self.BATT_COMP] = self.BAD self.checklist_status[self.ESTOP_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.GOOD self.checklist_status[self.JOYSTICK_COMP] = self.NONE # Check if joystick is suppressed by topic if self.joystick_suppressed: self.checklist_status[self.SUPPRESS_COMP] = self.GOOD self.checklist_status[self.DISABLE_COMP] = self.NONE self.checklist_status[self.JOYSTICK_COMP] = self.NONE else: self.checklist_status[self.SUPPRESS_COMP] = self.NONE if (rospy.Time.now() - self.last_suppress_time).to_sec() > self.JOYSTICK_SUPPRESS_PERIOD: self.checklist_status[self.SUPPRESS_MAN] = self.GOOD else: self.checklist_status[self.SUPPRESS_MAN] = self.BAD # Command message received if self.command_received: self.checklist_status[self.CMD_COMP] = self.GOOD else: self.checklist_status[self.CMD_COMP] = self.BAD # Override twist checkbox if mode is in TIMEOUT if self.current_mode == Mode.MODE_TIMEOUT: self.checklist_status[self.CMD_COMP] = self.BAD # Update checklist graphics self.updateCheckStatus(self._widget.batt_man_chk, self.checklist_status[self.BATT_MAN]) self.updateCheckStatus(self._widget.estop_man_chk, self.checklist_status[self.ESTOP_MAN]) self.updateCheckStatus(self._widget.disable_man_chk, self.checklist_status[self.DISABLE_MAN]) self.updateCheckStatus(self._widget.joystick_man_chk, self.checklist_status[self.JOYSTICK_MAN]) self.updateCheckStatus(self._widget.suppress_man_chk, self.checklist_status[self.SUPPRESS_MAN]) self.updateCheckStatus(self._widget.batt_comp_chk, self.checklist_status[self.BATT_COMP]) self.updateCheckStatus(self._widget.estop_comp_chk, self.checklist_status[self.ESTOP_COMP]) self.updateCheckStatus(self._widget.disable_comp_chk, self.checklist_status[self.DISABLE_COMP]) self.updateCheckStatus(self._widget.joystick_comp_chk, self.checklist_status[self.JOYSTICK_COMP]) self.updateCheckStatus(self._widget.suppress_comp_chk, self.checklist_status[self.SUPPRESS_COMP]) self.updateCheckStatus(self._widget.cmd_comp_chk, self.checklist_status[self.CMD_COMP]) self.updateCheckStatus(self._widget.joystick_bind_chk, self.NONE) def updateCheckStatus(self, obj, icon_type): if icon_type == self.GOOD: obj.setPixmap(self.good_icon) elif icon_type == self.BAD: obj.setPixmap(self.bad_icon) else: obj.setPixmap(self.none_icon) def updateTable(self): for i in range(0, len(self.joystick_table_vals)): self.joystick_table_vals[i].setText(55) def recvMode(self, mode_msg): self.current_mode = mode_msg.mode def recvSuppress(self, suppress_msg): self.suppress_dt = rospy.Time.now() - self.last_suppress_time self.last_suppress_time = rospy.Time.now() def recvTwist(self, twist_msg): self.twist_dt = rospy.Time.now() - self.last_twist_time self.last_twist_time = rospy.Time.now() self.current_cmd = twist_msg def recvJoystick(self, joystick_msg): self.joystick_data = joystick_msg.channels; self.last_joystick_time = rospy.Time.now() def recvBumpers(self, bumper_msg): self.bumper_front_left = bumper_msg.front_left self.bumper_front_right = bumper_msg.front_right self.bumper_rear_left = bumper_msg.rear_left self.bumper_rear_right = bumper_msg.rear_right def recvBattery(self, battery_msg): self.battery_percent = battery_msg.percent self.battery_voltage = battery_msg.voltage def recvBindStatus(self, bind_msg): self.joystick_bind_status = bind_msg.data if bind_msg.data: if self.joystick_bind_dot_counter == 0: self._widget.joystick_bind_lbl.setText('Binding.') elif self.joystick_bind_dot_counter == 1: self._widget.joystick_bind_lbl.setText('Binding..') elif self.joystick_bind_dot_counter == 2: self._widget.joystick_bind_lbl.setText('Binding...') self.joystick_bind_dot_counter = (self.joystick_bind_dot_counter + 1) % 3 def recvGyroCalibrated(self, cal_msg): self.gyro_cal_status = cal_msg.data def recvImu(self, imu_msg): self.gyro_x = imu_msg.angular_velocity.x self.gyro_y = imu_msg.angular_velocity.y self.gyro_z = imu_msg.angular_velocity.z def recvWakeTime(self, time_msg): self.current_wake_time = time_msg.data def subscribeTopics(self): sub_joystick = rospy.Subscriber('/mobility_base/joystick_raw', JoystickRaw, self.recvJoystick) sub_suppress = rospy.Subscriber('/mobility_base/suppress_wireless', std_msgs.msg.Empty, self.recvSuppress) sub_twist = rospy.Subscriber('/mobility_base/cmd_vel', Twist, self.recvTwist) sub_mode = rospy.Subscriber('/mobility_base/mode', Mode, self.recvMode) sub_bumpers = rospy.Subscriber('/mobility_base/bumper_states', BumperState, self.recvBumpers) sub_battery = rospy.Subscriber('/mobility_base/battery', BatteryState, self.recvBattery) sub_bind = rospy.Subscriber('/mobility_base/bind_status', std_msgs.msg.Bool, self.recvBindStatus) sub_gyro_calibrated = rospy.Subscriber('/mobility_base/imu/is_calibrated', std_msgs.msg.Bool, self.recvGyroCalibrated) sub_imu = rospy.Subscriber('/mobility_base/imu/data_raw', Imu, self.recvImu) sub_wake_time = rospy.Subscriber('/mobility_base/wake_time', std_msgs.msg.Time, self.recvWakeTime) def advertiseTopics(self): self.pub_start_bind = rospy.Publisher('/mobility_base/bind_start', std_msgs.msg.Empty, queue_size=1) self.pub_stop_bind = rospy.Publisher('/mobility_base/bind_stop', std_msgs.msg.Empty, queue_size=1) self.pub_set_wake_time = rospy.Publisher('/mobility_base/set_wake_time', std_msgs.msg.Time, queue_size=1) def initJoystickGraphics(self): # Pens self.cyan_pen = QPen(QColor(0, 255, 255)) self.magenta_pen = QPen(QColor(255, 0, 255)) self.red_pen = QPen(QColor(255, 0, 0)) self.cyan_pen.setWidth(3) self.magenta_pen.setWidth(3) self.red_pen.setWidth(3) self.stick_ind_l = QGraphicsEllipseItem() self.stick_ind_r = QGraphicsEllipseItem() self.stick_line_l = QGraphicsLineItem() self.stick_line_r = QGraphicsLineItem() self.mode_ind = QGraphicsLineItem() # Left joystick indicator circle px_l = self.stick_ind_lox - self.stick_ind_radius py_l = self.stick_ind_loy - self.stick_ind_radius self.stick_ind_l.setRect(px_l, py_l, 2 * self.stick_ind_radius, 2 * self.stick_ind_radius) self.stick_ind_l.setBrush(QBrush(QColor(255, 0, 0))) self.stick_ind_l.setPen(QPen(QColor(0, 0, 0))) # Right joystick indicator circle px_r = self.stick_ind_rox - self.stick_ind_radius py_r = self.stick_ind_roy - self.stick_ind_radius self.stick_ind_r.setRect(px_r, py_r, 2 * self.stick_ind_radius, 2 * self.stick_ind_radius) self.stick_ind_r.setBrush(QBrush(QColor(255, 0, 0))) self.stick_ind_r.setPen(QPen(QColor(0, 0, 0))) # Left joystick indicator line line_pen = QPen(QColor(255,0,0)) line_pen.setWidth(4) self.stick_line_l.setLine(self.stick_ind_lox, self.stick_ind_loy, self.stick_ind_lox, self.stick_ind_loy) self.stick_line_l.setPen(line_pen) # Right joystick indicator line self.stick_line_r.setLine(self.stick_ind_rox, self.stick_ind_roy, self.stick_ind_rox, self.stick_ind_roy) self.stick_line_r.setPen(line_pen) # Mode indicator line self.mode_ind.setLine(self.mode_ind_x1, self.mode_ind_y1, self.mode_ind_x2, self.mode_ind_y2) self.mode_ind.setPen(self.cyan_pen) # Joystick power indicator self.joystick_power_ind = [] self.joystick_power_ind.append(QGraphicsLineItem(self.power_ind_x1, self.power_ind_y + 20, self.power_ind_x1, self.power_ind_y - 20)) self.joystick_power_ind.append(QGraphicsLineItem(self.power_ind_x1, self.power_ind_y - 20, self.power_ind_x1 + 50, self.power_ind_y - 20)) self.joystick_power_ind.append(QGraphicsLineItem(self.power_ind_x1+50, self.power_ind_y - 20, self.power_ind_x1+50, self.power_ind_y + 20)) self.joystick_power_ind.append(QGraphicsLineItem(self.power_ind_x1+50, self.power_ind_y + 20, self.power_ind_x1, self.power_ind_y + 20)) # Populate scene graphics_scene = QGraphicsScene() graphics_scene.addItem(self.stick_ind_l) graphics_scene.addItem(self.stick_ind_r) graphics_scene.addItem(self.stick_line_l) graphics_scene.addItem(self.stick_line_r) graphics_scene.addItem(self.mode_ind) for l in self.joystick_power_ind: l.setPen(self.red_pen) graphics_scene.addItem(l) graphics_scene.setSceneRect(0, 0, self._widget.joystickGraphicsView.width() - 4, self._widget.joystickGraphicsView.height() - 4) self._widget.joystickGraphicsView.setScene(graphics_scene) self._widget.joystickGraphicsView.setBackgroundBrush(QBrush(QImage(os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'images', 'dx6ilabels.jpg')))) self._widget.joystickGraphicsView.show() def initBumperGraphics(self): # Pens self.blue_pen = QPen(QColor(0,0,255)) self.blue_pen.setWidth(10) self.bumper_lines = [] # Text state labels self.bumper_state_labels = [QGraphicsTextItem() for i in range(0,4)] for i in range(len(self.bumper_state_labels)): self.bumper_state_labels[i].setFont(QFont('Ubuntu', 14, QFont.Bold)) self.bumper_state_labels[i].setPlainText('00') self.bumper_state_labels[0].setPos(self.bumper_fl_x-10, self.bumper_fl_y + 55) self.bumper_state_labels[1].setPos(self.bumper_fr_x-10, self.bumper_fr_y + 55) self.bumper_state_labels[2].setPos(self.bumper_rl_x-10, self.bumper_rl_y - 80) self.bumper_state_labels[3].setPos(self.bumper_rr_x-10, self.bumper_rr_y - 80) # Bumper indicator lines self.bumperLine(self.bumper_fl_x - 20, self.bumper_fl_y - self.bumper_dy, True) self.bumperLine(self.bumper_fl_x - self.bumper_dx, self.bumper_fl_y - 20, False) self.bumperLine(self.bumper_fl_x + self.bumper_dx, self.bumper_fl_y - 20, False) self.bumperLine(self.bumper_fl_x - 20, self.bumper_fl_y + self.bumper_dy, True) self.bumperLine(self.bumper_fr_x - 20, self.bumper_fr_y - self.bumper_dy, True) self.bumperLine(self.bumper_fr_x - self.bumper_dx, self.bumper_fr_y - 20, False) self.bumperLine(self.bumper_fr_x + self.bumper_dx, self.bumper_fr_y - 20, False) self.bumperLine(self.bumper_fr_x - 20, self.bumper_fr_y + self.bumper_dy, True) self.bumperLine(self.bumper_rl_x - 20, self.bumper_rl_y - self.bumper_dy, True) self.bumperLine(self.bumper_rl_x - self.bumper_dx, self.bumper_rl_y - 20, False) self.bumperLine(self.bumper_rl_x + self.bumper_dx, self.bumper_rl_y - 20, False) self.bumperLine(self.bumper_rl_x - 20, self.bumper_rl_y + self.bumper_dy, True) self.bumperLine(self.bumper_rr_x - 20, self.bumper_rr_y - self.bumper_dy, True) self.bumperLine(self.bumper_rr_x - self.bumper_dx, self.bumper_rr_y - 20, False) self.bumperLine(self.bumper_rr_x + self.bumper_dx, self.bumper_rr_y - 20, False) self.bumperLine(self.bumper_rr_x - 20, self.bumper_rr_y + self.bumper_dy, True) # Populate scene graphics_scene = QGraphicsScene() for bumper in self.bumper_lines: graphics_scene.addItem(bumper) for label in self.bumper_state_labels: graphics_scene.addItem(label) graphics_scene.setSceneRect(0, 0, self._widget.bumperGraphicsView.width() - 4, self._widget.bumperGraphicsView.height() - 4) self._widget.bumperGraphicsView.setScene(graphics_scene) self._widget.bumperGraphicsView.setBackgroundBrush(QBrush(QImage(os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'images', 'mb_top.png')))) self._widget.bumperGraphicsView.show() def bumperLine(self, x, y, front): new_line = QGraphicsLineItem() if front: new_line.setLine(x, y, x+40, y) else: new_line.setLine(x, y, x, y+40) new_line.setPen(self.blue_pen) new_line.setVisible(False) self.bumper_lines.append(new_line) def bumperVisibleSwitch(self, idx, bumper_state): if (bumper_state & BumperState.BUMPER_FRONT): self.bumper_lines[idx].setVisible(True) else: self.bumper_lines[idx].setVisible(False) if (bumper_state & BumperState.BUMPER_LEFT): self.bumper_lines[idx+1].setVisible(True) else: self.bumper_lines[idx+1].setVisible(False) if (bumper_state & BumperState.BUMPER_RIGHT): self.bumper_lines[idx+2].setVisible(True) else: self.bumper_lines[idx+2].setVisible(False) if (bumper_state & BumperState.BUMPER_REAR): self.bumper_lines[idx+3].setVisible(True) else: self.bumper_lines[idx+3].setVisible(False) def resetGuiTimer(self): # del self.gui_update_timer self.gui_update_timer = QTimer(self._widget) self.gui_update_timer.setInterval(100) self.gui_update_timer.setSingleShot(False) self.gui_update_timer.timeout.connect(lambda:self.updateGuiCallback()) self.gui_update_timer.start() def spawnFullGui(self): super(DiagnosticGui, self).__init__(self.context_) # Give QObjects reasonable names self.setObjectName('DiagnosticGui') # Create QWidget self._widget = QWidget() # Get path to UI file which should be in the "resource" folder of this package ui_file = os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'resource', 'DiagnosticGui.ui') # Extend the widget with all attributes and children from UI file loadUi(ui_file, self._widget) # Give QObjects reasonable names self._widget.setObjectName('DiagnosticGui' + str(self.widget_count)) self.widget_count += 1 # Add widget to the user interface self.context_.add_widget(self._widget) def spawnTabGui(self): super(DiagnosticGui, self).__init__(self.context_) # Give QObjects reasonable names self.setObjectName('DiagnosticGui') # Create QWidget self._widget = QWidget() # Get path to UI file which should be in the "resource" folder of this package ui_file = os.path.join(rospkg.RosPack().get_path('mobility_base_tools'), 'resource', 'DiagnosticGuiTabs.ui') # Extend the widget with all attributes and children from UI file loadUi(ui_file, self._widget) # Give QObjects reasonable names self._widget.setObjectName('DiagnosticGui' + str(self.widget_count)) self.widget_count += 1 # Add widget to the user interface self.context_.add_widget(self._widget) def updateRightStickIndicator(self, lat_val, forward_val): horiz_val = -self.stick_ind_range_factor * (lat_val - self.stick_ind_range_mid) vert_val = -self.stick_ind_range_factor * (forward_val - self.stick_ind_range_mid) r = sqrt(horiz_val * horiz_val + vert_val * vert_val) if r > self.stick_ind_range_pix / 2: r = self.stick_ind_range_pix / 2 ang = atan2(vert_val, horiz_val) px = r * cos(ang) py = r * sin(ang) self.stick_ind_r.setPos(QPoint(px, py)) self.stick_line_r.setLine(self.stick_ind_rox, self.stick_ind_roy, self.stick_ind_rox + px, self.stick_ind_roy + py) def updateLeftStickIndicator(self, yaw_val, enable_val): horiz_val = -self.stick_ind_range_factor * (yaw_val - self.stick_ind_range_mid) vert_val = -self.stick_ind_range_factor * (enable_val - self.stick_ind_range_mid) r = sqrt(horiz_val * horiz_val + vert_val * vert_val) if r > self.stick_ind_range_pix / 2: r = self.stick_ind_range_pix / 2 ang = atan2(vert_val, horiz_val) px = r * cos(ang) py = r * sin(ang) self.stick_ind_l.setPos(QPoint(px, py)) self.stick_line_l.setLine(self.stick_ind_lox, self.stick_ind_loy, self.stick_ind_lox + px, self.stick_ind_loy + py) def shutdown_plugin(self): pass
class Updater: def __init__(self, app): if not hasattr(app, 'preferences'): print("update: need 'preferences' from app.") exit(1) if not hasattr(app, 'accounts'): print("update: need 'accounts' from app.") exit(1) self.app = app self.update = {} self.timers = [] self.updates = drug() self.timer = QTimer(app) self.settings = QSettings("blain", "timers") def connect(self): win = self.app.window.ui win.actionDoUpdates.triggered.connect(self.do) win.actionUpdate_now.triggered.connect(self.all) self.timer.timeout.connect(self.timer_step) self.app.window.ui.actionDoUpdates.setChecked( self.app.preferences.settings.value("timer/active",False).toBool()) def setup(self): app, st, pref = self.app, self.settings, self.app.preferences.settings account_id = {} # thread starting functions self.update['user'] = app.updateUser.emit self.update['group'] = app.updateGroup.emit self.update['groups'] = lambda *args: \ app.updateGroups.emit(*(args[:2]+(False,)+args[3:])) self.update['friends'] = lambda *args: \ app.updateFriends.emit(*(args[:2]+(False,)+args[3:])) # read existing friends and groups friends, friends_list, groups, groups_list = {}, {}, {}, {} for account in self.app.accounts.get(): if account.service not in friends: friends[account.service] = {} if account.service not in friends_list: friends_list[account.service] = [] friends_list[account.service].append(account.name) friends[account.service][account.name] = \ list(map(unicode, account.friends.allKeys())) + [account.name] if account.groups is not None: if account.service not in groups: groups[account.service] = {} if account.service not in groups_list: groups_list[account.service] = [] groups_list[account.service].append(account.name) groups[account.service][account.name] = \ list(map(unicode, account.groups.allKeys())) # read existing timer events # format: (timestamp, func, service, account, user, *args) timers = [ unicode(st.value(str(i)).toString()) for i in range(st.value("count",0).toInt()[0]) ] #find new timer events user_leveled = {'user': friends, 'group': groups} account_leveled = {'friends': friends_list, 'groups': groups_list} for timer in map(lambda t: unicode(t).split(","), timers): if timer[1] == 'user' or timer[1] == 'group': # choose current data service_level = user_leveled[timer[1]] # dive data levels if timer[2] in service_level: account_level = service_level[timer[2]] if timer[3] in account_level: user_level = account_level[timer[3]] if timer[4] in user_level: # event found, remove it user_level.remove(timer[4]) elif timer[1] == 'friends' or timer[1] == 'groups': # choose current data service_level = account_leveled[timer[1]] # dive data levels if timer[2] in service_level: account_level = service_level[timer[2]] if timer[3] in account_level: # event found, remove it account_level.remove(timer[3]) # save left overs t = time() # add new group lists timers.extend([ u"{0},groups,{1},{2},".format(t, service, account) for service in groups_list for account in groups_list[service] ]) # add new friend lists timers.extend([ u"{0},friends,{1},{2},".format(t, service, account) for service in friends_list for account in friends_list[service] ]) # add new groups timers.extend([ u"{0},group,{1},{2},{3}".format(t,service,account,group) for service in groups for account in groups[service] for group in groups[service][account] ]) # add new friends timers.extend([ u"{0},user,{1},{2},{3}".format(t,service,account,user) for service in friends for account in friends[service] for user in friends[service][account] ]) # add some random to the order so twitter # wont get called to often in a row hopfully if len(timers) != st.value("count", 0).toInt()[0]: shuffle(timers) # inplace # save new timers st.setValue('count',len(timers)) for i, timer in enumerate(timers): st.setValue(str(i), timer) # more python readable format timers = [ unicode(t).split(",") for t in timers ] timers = [ [float(t[0])] + t[1:] for t in timers ] self.timers = timers # start timers self.updates.user = self.user self.updates.group = self.group self.updates.groups = self.groups self.updates.friends = self.friends self.timer.setInterval( pref.value("timer/interval",1e4).toInt()[0]) # 10 sec if pref.value("timer/active", True).toBool(): self.timer.start() def add_timer(self, func, service, account, user, *args): timer = ",".join(map(unicode, [time(), func, service, account, user])) if args: timer += "," + ",".join(map(unicode, args)) self.settings.setValue(str(len(self.timers)), timer) timer = timer.split(",") self.timers.append([float(timer[0])] + timer[1:]) self.settings.setValue("count", len(self.timers)) def remove_timer(self, func, service, account, user): found, cur = [], ",".join(map(unicode, [func, service, account, user])) for i, timer in enumerate(self.timers): if cur in ",".join(map(unicode, timer)): found.append(i) if not found: return for i in reversed(found): self.timers.pop(i) self.settings.setValue('count',len(self.timers)) for i, timer in enumerate(self.timers[found[0]:]): self.settings.setValue(str(i+found[0]),",".join(map(unicode,timer))) def new_updates(self, account, new_time, break_): # new_updates count cur, n, service, accid = None, -1, account.service, account.name for i, timer in enumerate(self.timers): if service == timer[2] and accid == timer[3] and break_(timer): n, cur = i, timer break if cur is None: return cur[0] = new_time self.settings.setValue(str(n), ",".join(map(unicode, cur))) def user(self, account, user, count, ok): # new_updates count if count: self.app.notifier.notify_by_mode( amount = count, user = user) self.new_updates(account, time() - (not ok) * 5 - count / len(self.timers), lambda t: t[1] == "user" and t[4] == user) def group(self, account, group, count, ok): # new_updates count if count: self.app.notifier.notify_by_mode( amount = count, user = "******" + group) self.new_updates(account, time() - (not ok) * 5 - count / len(self.timers), lambda t: t[1] == "group" and t[4] == group) def groups(self, account): # new_updates count self.new_updates(account, time(), lambda t: t[1] == "groups") def friends(self, account): # new_updates count self.new_updates(account, time(), lambda t: t[1] == "friends") def timer_step(self): cur = self.timers[0] for timer in self.timers: if timer[0] < cur[0]: cur = timer print "* timer update", cur self.update[cur[1]](*cur[2:]) def account(self, account, start = True): acc = account.service, account.name ids = [u"{0}{1}friends".format(*acc)] self.app.threads.updateFriends(*acc) if account.groups is not None: ids.append(u"{0}{1}groups".format(*acc)) self.app.threads.updateGroups(*acc) if start: self.app.threads.start(*ids) return [] return ids def do(self, checked): if checked: self.timer.start() else: self.timer.stop() self.app.preferences.settings.setValue("timer/active", checked) def all(self): self.app.threads.start( *sum([ self.account(account,False) for account in self.app.accounts.get() ],[]))
class ScudCloud(QtGui.QMainWindow): plugins = True debug = False forceClose = False messages = 0 speller = Speller() def __init__(self, parent=None, settings_path=""): super(ScudCloud, self).__init__(parent) self.setWindowTitle('ScudCloud') self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.cfg', QSettings.IniFormat) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id( "scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: self.startURL = self.domain() self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(ScudCloud.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') # Starting unread msgs counter self.setupTimer() def addWrapper(self, url): webView = Wrapper(self) webView.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) def setupTimer(self): self.timer = QTimer(self) self.timer.timeout.connect(self.count) self.timer.setInterval(2000) self.timer.start() def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # Required by Youtube videos (HTML5 video support only on Qt5) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, self.plugins) # We don't want Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # We don't need History QWebSettings.globalSettings().setAttribute( QWebSettings.PrivateBrowsingEnabled, True) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.settings_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute( QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute( QWebSettings.DeveloperExtrasEnabled, self.debug) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "False") else: self.settings.setValue("Menu", "True") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addMenu(self): self.menus = { "file": { "preferences": self.createAction("Preferences", lambda: self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda: self.switchTo(Resources.SIGNIN_URL)), "signout": self.createAction("Signout", lambda: self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": {}, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda: self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda: self.current().helpCenter()), "about": self.createAction("About", lambda: self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) self.editMenu = menu.addMenu("&Edit") self.updateEditMenu() viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) if Unity is None: viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def updateEditMenu(self): self.editMenu.clear() self.menus["edit"] = { "undo": self.current().pageAction(QtWebKit.QWebPage.Undo), "redo": self.current().pageAction(QtWebKit.QWebPage.Redo), "cut": self.current().pageAction(QtWebKit.QWebPage.Cut), "copy": self.current().pageAction(QtWebKit.QWebPage.Copy), "paste": self.current().pageAction(QtWebKit.QWebPage.Paste), "back": self.current().pageAction(QtWebKit.QWebPage.Back), "forward": self.current().pageAction(QtWebKit.QWebPage.Forward), "reload": self.current().pageAction(QtWebKit.QWebPage.Reload) } self.editMenu.addAction(self.menus["edit"]["undo"]) self.editMenu.addAction(self.menus["edit"]["redo"]) self.editMenu.addSeparator() self.editMenu.addAction(self.menus["edit"]["cut"]) self.editMenu.addAction(self.menus["edit"]["copy"]) self.editMenu.addAction(self.menus["edit"]["paste"]) self.editMenu.addSeparator() self.editMenu.addAction(self.menus["edit"]["back"]) self.editMenu.addAction(self.menus["edit"]["forward"]) self.editMenu.addAction(self.menus["edit"]["reload"]) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtGui.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def domain(self): if self.identifier.endswith(".slack.com"): return self.identifier else: return "https://" + self.identifier + ".slack.com" def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): for t in teams: # If team_icon is not present, it's because team is already connected if 'team_icon' in t: self.leftPane.addTeam(t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() exists = True break if not exists: self.addWrapper(url) self.enableMenus(self.current().isConnected()) self.updateEditMenu() def eventFilter(self, obj, event): if event.type( ) == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtGui.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and ( modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and ( modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtGui.QMainWindow.eventFilter(self, obj, event) def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() def titleChanged(self): self.setWindowTitle(self.current().title()) def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() else: self.cookiesjar.save() self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) # Let's save the first team registered as default qUrl = self.stackedWidget.widget(0).url() if self.identifier is None and Resources.MESSAGES_URL_RE.match( qUrl.toString()): self.settings.setValue("Domain", 'https://' + qUrl.host()) def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): self.forceClose = True self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if c['is_member']: item = Dbusmenu.Menuitem.new() item.property_set(Dbusmenu.MENUITEM_PROP_LABEL, "#" + c['name']) item.property_set("id", c['name']) item.property_set_bool(Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon=None): self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() def count(self): total = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) messages = widget.count() if messages == 0: self.leftPane.stopAlert(widget.team()) else: self.leftPane.alert(widget.team()) if messages is not None: total += messages if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.messages = total
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect(self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.padding_left = 0 self.set_color() self.ignore_render_requests = Event() self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(self.delegate.cover_size.width(), self.delegate.cover_size.height())) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) @property def first_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.top(), (self.spacing()*2) + geom.top(), 5): for x in xrange(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in xrange(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width() + 2*self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in xrange(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def double_clicked(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() self.gui.iactions['View'].view_triggered(index) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) dark = (r + g + b)/3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height ) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() self.delegate.cover_cache.set_limit(gprefs['cover_grid_cache_size']) if size_changed: self.thumbnail_cache.set_thumbnail_size(self.delegate.cover_size.width(), self.delegate.cover_size.height()) cs = gprefs['cover_grid_disk_cache_size'] if (cs*(1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) def shown(self): if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id,)) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image(width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height()) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id,)) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id,)) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id,)) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if not hasattr(newdb, 'new_api'): return if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print ('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x): group = list(map(operator.itemgetter(1), g)) sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect)
def setupTimer(self): timer = QTimer(self) timer.timeout.connect(self.loadFinished) # Hope each 10 minutes will not be produce high CPU usage timer.setInterval(600000) timer.start()
class ScudCloud(QtGui.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug=False, parent=None, minimized=None, urgent_hint=None, settings_path=""): super(ScudCloud, self).__init__(parent) self.debug = debug self.minimized = minimized self.urgent_hint = urgent_hint self.setWindowTitle(self.title) self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.cfg', QSettings.IniFormat) self.notifier.enabled = self.settings.value('Notifications', defaultValue=True, type=bool) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id( "scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: if isinstance(self.identifier, str): self.domains = self.identifier.split(",") else: self.domains = self.identifier self.startURL = self.normalize(self.domains[0]) else: self.domains = [] self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(self.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') self.tickler = QTimer(self) self.tickler.setInterval(1800000) # Watch for ScreenLock events if DBusQtMainLoop is not None: DBusQtMainLoop(set_as_default=True) sessionBus = dbus.SessionBus() # Ubuntu 12.04 and other distros sessionBus.add_match_string( "type='signal',interface='org.gnome.ScreenSaver'") # Ubuntu 14.04 and above sessionBus.add_match_string( "type='signal',interface='com.ubuntu.Upstart0_6'") sessionBus.add_message_filter(self.screenListener) self.tickler.timeout.connect(self.sendTickle) # If dbus is not present, tickler timer will act like a blocker to not send tickle too often else: self.tickler.setSingleShot(True) self.tickler.start() def screenListener(self, bus, message): event = message.get_member() # "ActiveChanged" for Ubuntu 12.04 and other distros. "EventEmitted" for Ubuntu 14.04 and above if event == "ActiveChanged" or event == "EventEmitted": arg = message.get_args_list()[0] # True for Ubuntu 12.04 and other distros. "desktop-lock" for Ubuntu 14.04 and above if (arg == True or arg == "desktop-lock") and self.tickler.isActive(): self.tickler.stop() elif (arg == False or arg == "desktop-unlock") and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def sendTickle(self): for i in range(0, self.stackedWidget.count()): self.stackedWidget.widget(i).sendTickle() def addWrapper(self, url): webView = Wrapper(self) webView.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # We don't want Flash (it causes a lot of trouble in some distros) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, False) # We don't need Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # Enabling Local Storage (now required by Slack) QWebSettings.globalSettings().setAttribute( QWebSettings.LocalStorageEnabled, True) # We need browsing history (required to not limit LocalStorage) QWebSettings.globalSettings().setAttribute( QWebSettings.PrivateBrowsingEnabled, False) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.settings_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute( QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute( QWebSettings.DeveloperExtrasEnabled, self.debug) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "False") else: self.settings.setValue("Menu", "True") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addTeam(self): self.switchTo(Resources.SIGNIN_URL) def addMenu(self): # We'll register the webpage shorcuts with the window too (Fixes #338) undo = self.current().pageAction(QtWebKit.QWebPage.Undo) redo = self.current().pageAction(QtWebKit.QWebPage.Redo) cut = self.current().pageAction(QtWebKit.QWebPage.Cut) copy = self.current().pageAction(QtWebKit.QWebPage.Copy) paste = self.current().pageAction(QtWebKit.QWebPage.Paste) back = self.current().pageAction(QtWebKit.QWebPage.Back) forward = self.current().pageAction(QtWebKit.QWebPage.Forward) reload = self.current().pageAction(QtWebKit.QWebPage.Reload) self.menus = { "file": { "preferences": self.createAction("Preferences", lambda: self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda: self.addTeam()), "signout": self.createAction("Signout", lambda: self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": { "undo": self.createAction( undo.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Undo), undo.shortcut()), "redo": self.createAction( redo.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Redo), redo.shortcut()), "cut": self.createAction( cut.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Cut), cut.shortcut()), "copy": self.createAction( copy.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Copy), copy.shortcut()), "paste": self.createAction( paste.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Paste), paste.shortcut()), "back": self.createAction( back.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Back), back.shortcut()), "forward": self.createAction( forward.text(), lambda: self.current().page() .triggerAction(QtWebKit.QWebPage.Forward), forward.shortcut()), "reload": self.createAction( reload.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Reload), reload.shortcut()), }, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda: self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda: self.current().helpCenter()), "about": self.createAction("About", lambda: self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) editMenu = menu.addMenu("&Edit") editMenu.addAction(self.menus["edit"]["undo"]) editMenu.addAction(self.menus["edit"]["redo"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["cut"]) editMenu.addAction(self.menus["edit"]["copy"]) editMenu.addAction(self.menus["edit"]["paste"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["back"]) editMenu.addAction(self.menus["edit"]["forward"]) editMenu.addAction(self.menus["edit"]["reload"]) viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtGui.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def normalize(self, url): if url.endswith(".slack.com"): url += "/" elif not url.endswith(".slack.com/"): url = "https://" + url + ".slack.com/" return url def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): if len(self.domains) == 0: self.domains.append(teams[0]['team_url']) team_list = [t['team_url'] for t in teams] for t in teams: for i in range(0, len(self.domains)): self.domains[i] = self.normalize(self.domains[i]) # When team_icon is missing, the team already exists (Fixes #381, #391) if 'team_icon' in t: if self.domains[i] in team_list: add = next(item for item in teams if item['team_url'] == self.domains[i]) if 'team_icon' in add: self.leftPane.addTeam(add['id'], add['team_name'], add['team_url'], add['team_icon']['image_44'], add == teams[0]) # Adding new teams and saving loading positions if t['team_url'] not in self.domains: self.leftPane.addTeam( t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) self.domains.append(t['team_url']) self.settings.setValue("Domain", self.domains) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() self.leftPane.click(i) exists = True break if not exists: self.addWrapper(url) def eventFilter(self, obj, event): if event.type( ) == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtGui.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and ( modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and ( modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtGui.QMainWindow.eventFilter(self, obj, event) def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() # Let's tickle all teams on window focus, but only if tickle was not fired in last 30 minutes if DBusQtMainLoop is None and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def titleChanged(self): self.setWindowTitle(self.current().title()) def setForceClose(self): self.forceClose = True def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() else: self.cookiesjar.save() self.settings.setValue("Domain", self.domains) self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.forceClose = False def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): self.setForceClose() self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if hasattr(c, '__getitem__') and c['is_member']: item = Dbusmenu.Menuitem.new() item.property_set(Dbusmenu.MENUITEM_PROP_LABEL, "#" + c['name']) item.property_set("id", c['name']) item.property_set_bool(Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon): if self.debug: print("Notification: title [{}] message [{}] icon [{}]".format( title, message, icon)) self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() if self.urgent_hint is True: QApplication.alert(self) def count(self): total = 0 unreads = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) highlights = widget.highlights unreads += widget.unreads total += highlights if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) if unreads > 0: self.setWindowTitle("*{}".format(self.title)) else: self.setWindowTitle(self.title) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.setWindowTitle("[{}]{}".format(str(total), self.title)) self.messages = total
class Updater: def __init__(self, app): if not hasattr(app, 'preferences'): print("update: need 'preferences' from app.") exit(1) self.app = app self.update = {} self.timers = [] self.updates = drug() self.timer = QTimer(app) self.settings = QSettings("blain", "timers") def connect(self): win = self.app.window.ui win.actionDoUpdates.triggered.connect(self.do) win.actionUpdate_now.triggered.connect(self.all) self.timer.timeout.connect(self.timer_step) self.app.window.ui.actionDoUpdates.setChecked( self.app.preferences.settings.value("timer/active",True).toBool()) def setup(self): app, st, pref = self.app, self.settings, self.app.preferences.settings self.update['user'] = app.updateUser.emit self.update['friends'] = lambda *args: \ app.updateMicroblogging.emit(args[0], app.preferences.settings.value(\ "account/"+args[0]+"/id").toString(),\ False, *args[2:]) friends = drug(twitter = [], identica = []) if pref.contains("account/twitter/id"): friends.twitter = map(unicode, QSettings("blain", "%s-twitter-friends" % pref.value("account/twitter/id").\ toString()).allKeys()) if pref.contains("account/identica/id"): friends.identica = map(unicode, QSettings("blain", "%s-identica-friends" % pref.value("account/identica/id").\ toString()).allKeys()) # format: (timestamp, func, service, user, *args) self.timers = timers = [ unicode(st.value(str(i)).toString()) for i in range(st.value("count",0).toInt()[0]) ] # add timer entries new_friends = ['twitter', 'identica'] new_friend = {'twitter':friends.twitter , 'identica':friends.identica} for timer in map(lambda t: unicode(t).split(","), timers): if timer[1] == 'user': if timer[3] in new_friend[timer[2]]: new_friend[timer[2]].remove(timer[3]) elif timer[1] == 'friends': if timer[2] in new_friends: new_friends.remove(timer[2]) for service in new_friends: timers.append("{0},friends,{1},".format(time(),service)) for service in new_friend: for i, user in enumerate(new_friend[service]): new_friend[service][i] = "{0},user,{1},{2}".\ format(time(),service,user) if new_friend['twitter'] or new_friend['identica']: timers.extend(list(sum(zip(new_friend['identica'], new_friend['twitter']), ()))) if len(new_friend['twitter']) > len(new_friend['identica']): timers.extend(new_friend['twitter'][len(new_friend['identica']):]) else: timers.extend(new_friend['identica'][len(new_friend['twitter']):]) st.setValue('count',len(timers)) for i in range(len(timers)): st.setValue(str(i), timers[i]) self.timers = list(map(lambda t: [float(t[0])] + t[1:], map(lambda t: unicode(t).split(","), timers))) self.updates.user = self.user self.updates.friends = self.friends self.timer.setInterval( pref.value("timer/interval",1e4).toInt()[0]) # 10 sec if pref.value("timer/active", True).toBool(): self.timer.start() def user(self, service, user, count , ok): # new_updates count service, user = unicode(service), unicode(user) cur, n = None, -1 for i, timer in enumerate(self.timers): if timer[1] == "user" and timer[2] == service and timer[3] == user: n, cur = i, timer break if cur is None: return cur[0] = time() - (not ok) * 5 - count / len(self.timers) self.settings.setValue(str(n), ",".join(map(unicode, cur))) def friends(self, service, user): # new_updates count service, user = unicode(service), unicode(user) cur, n = None, -1 for i, timer in enumerate(self.timers): if timer[1] == "friends" and timer[2] == service: n, cur = i, timer break if cur is None: return cur[0] = time() self.settings.setValue(str(n), ",".join(map(unicode, cur))) def timer_step(self): print "* timer update" cur = self.timers[0] for timer in self.timers: if timer[0] < cur[0]: cur = timer print cur self.update[cur[1]](*cur[2:]) def twitter(self, start = True): self.app.threads.updateMicroblogging('twitter', self.app.preferences.ui.twitteridEdit.text()) if start: self.app.threads.start('twitter') def identica(self, start = True): self.app.threads.updateMicroblogging('identica', self.app.preferences.ui.identicaidEdit.text()) if start: self.app.threads.start('identica') def do(self, checked): if checked: self.timer.start() else: self.timer.stop() self.app.preferences.settings.setValue("timer/active", checked) def all(self): self.identica(False) self.twitter(False) self.app.threads.start('identica', 'twitter')
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect( self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.padding_left = 0 self.set_color() self.ignore_render_requests = Event() self.thumbnail_cache = ThumbnailCache( max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(self.delegate.cover_size.width(), self.delegate.cover_size.height())) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) @property def first_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.top(), (self.spacing() * 2) + geom.top(), 5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width( ) + 2 * self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in xrange(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def double_clicked(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in { 'edit_metadata', 'edit_cell' }: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) tex = gprefs['cover_grid_texture'] if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) pal.setBrush(pal.Base, QBrush(pm)) dark = (r + g + b) / 3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() self.delegate.cover_cache.set_limit(gprefs['cover_grid_cache_size']) if size_changed: self.thumbnail_cache.set_thumbnail_size( self.delegate.cover_size.width(), self.delegate.cover_size.height()) cs = gprefs['cover_grid_disk_cache_size'] if (cs * (1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) def shown(self): if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height()) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class Updater: def __init__(self, app): if not hasattr(app, 'preferences'): print("update: need 'preferences' from app.") exit(1) self.app = app self.update = {} self.timers = [] self.updates = drug() self.timer = QTimer(app) self.settings = QSettings("blain", "timers") def connect(self): win = self.app.window.ui win.actionDoUpdates.triggered.connect(self.do) win.actionUpdate_now.triggered.connect(self.all) self.timer.timeout.connect(self.timer_step) self.app.window.ui.actionDoUpdates.setChecked( self.app.preferences.settings.value("timer/active",True).toBool()) def setup(self): app, st, pref = self.app, self.settings, self.app.preferences.settings account_id = {} # thread starting functions self.update['user'] = app.updateUser.emit self.update['group'] = lambda *args: app.updateGroup.emit(*args[1:]) self.update['groups'] = lambda *args: \ app.updateGroups.emit(args[1], False, *args[2:]) self.update['friends'] = lambda *args: \ app.updateMicroblogging.emit(args[0], account_id[service], False, *args[2:]) # read existing friends friends = {} for service in ["twitter", "identica"]: friends[service] = [] if pref.contains("account/%s/id" % service): account_id[service] = pref.value( "account/%s/id" % service).toString() friends[service] = map(unicode, QSettings("blain", "%s-%s-friends" % (account_id[service], service)).allKeys()) friends = drug(**friends) # read existing groups groups = [] if pref.contains("account/identica/id"): groups = map(unicode, QSettings("blain", "%s-groups" % account_id['identica']).allKeys()) # format: (timestamp, func, service, user, *args) timers = [ unicode(st.value(str(i)).toString()) for i in range(st.value("count",0).toInt()[0]) ] # find new timer entries new_friend = {'twitter':friends.twitter , 'identica':friends.identica} new_friends = new_friend.keys() new_group = groups new_groups = True for timer in map(lambda t: unicode(t).split(","), timers): if timer[1] == 'user': if timer[3] in new_friend[timer[2]]: new_friend[timer[2]].remove(timer[3]) elif timer[1] == 'friends': if timer[2] in new_friends: new_friends.remove(timer[2]) elif timer[1] == 'groups': new_groups = False elif timer[1] == 'group': if timer[3] in new_group: new_group.remove(timer[3]) # add new friend lists for service in new_friends: timers.append("{0},friends,{1},".format(time(), service)) # add groups update timer if new_groups and 'identica' in account_id: timers.append("{0},groups,,{1}".\ format(time(), account_id['identica'])) # add new groups for group in new_group: timers.append("{0},group,,{1}".format(time(), group)) # add new friends for service in new_friend: for i, user in enumerate(new_friend[service]): new_friend[service][i] = "{0},user,{1},{2}".\ format(time(),service,user) # zip list(friends) of both services together if new_friend['twitter'] or new_friend['identica']: timers.extend(list(sum(zip(new_friend['identica'], new_friend['twitter']), ()))) if len(new_friend['twitter']) > len(new_friend['identica']): timers.extend(new_friend['twitter'][len(new_friend['identica']):]) else: timers.extend(new_friend['identica'][len(new_friend['twitter']):]) # save new timers st.setValue('count',len(timers)) for i in range(len(timers)): st.setValue(str(i), timers[i]) # more python readable format timers = [ unicode(t).split(",") for t in timers ] timers = [ [float(t[0])] + t[1:] for t in timers ] self.timers = timers # start timers self.updates.user = self.user self.updates.group = self.group self.updates.groups = self.groups self.updates.friends = self.friends self.timer.setInterval( pref.value("timer/interval",1e4).toInt()[0]) # 10 sec if pref.value("timer/active", True).toBool(): self.timer.start() def new_updates(self, service, user, new_time, break_): # new_updates count cur, n = None, -1 for i, timer in enumerate(self.timers): if break_(timer): n, cur = i, timer break if cur is None: return cur[0] = new_time self.settings.setValue(str(n), ",".join(map(unicode, cur))) def user(self, service, user, count , ok): # new_updates count if count: self.app.notifier.notify_by_mode( amount = count, user = user) service, user = unicode(service), unicode(user) self.new_updates(service, user, time() - (not ok) * 5 - count / len(self.timers), lambda t: t[1] == "user" and t[2] == service and t[3] == user) def group(self, _, group, count , ok): # new_updates count if count: self.app.notifier.notify_by_mode( amount = count, user = "******" + group) user = unicode(group) self.new_updates("identica", group, time() - (not ok) * 5 - count / len(self.timers), lambda t: t[1] == "group" and t[3] == group) def groups(self, user): # new_updates count user = unicode(user) self.new_updates('identica', user, time(), lambda t: t[1] == "groups") def friends(self, service, user): # new_updates count service, user = unicode(service), unicode(user) self.new_updates(service, user, time(), lambda t: t[1] == "friends" and t[2] == service) def timer_step(self): print "* timer update" cur = self.timers[0] for timer in self.timers: if timer[0] < cur[0]: cur = timer print cur self.update[cur[1]](*cur[2:]) def twitter(self, start = True): self.app.threads.updateMicroblogging('twitter', self.app.preferences.ui.twitteridEdit.text()) ids = ['__twitter__'] if start: self.app.threads.start(*ids) return [] return ids def identica(self, start = True): user = self.app.preferences.ui.identicaidEdit.text() self.app.threads.updateMicroblogging('identica', user) self.app.threads.updateGroups(user) ids = ['__identica__', '%s groups' % user] if start: self.app.threads.start(*ids) return [] return ids def do(self, checked): if checked: self.timer.start() else: self.timer.stop() self.app.preferences.settings.setValue("timer/active", checked) def all(self): self.app.threads.start(*(self.identica(False) + self.twitter(False)))