Ejemplo n.º 1
0
class External():
    def __init__(self):
        self.watchTimer = QTimer()
        self.watchTimer.setInterval(800)
        self.watchTimer.timerEvent = self.onWatch
        self.files_changed = queue.Queue()
        pass

    def onWatch(self,e):
        try:
            file_type, filename, action = self.files_changed.get_nowait()
            print(file_type, filename, action)
        except queue.Empty:
            pass
        pass

    def onFileChanged(self, path=None):
        print('file changed', path)
        pass

    def load(self, path):
        print('load', path)
        Watcher(path, self.files_changed)
        if not self.watchTimer.isActive():
            self.watchTimer.start()
            pass
        pass

    def update(self, path):
        pass

    def create(self, tmppath=None):
        if not tmppath:
            tmppath = 'tmp'
        if not os.path.exists(tmppath):
            os.mkdir(tmppath)
            pass
        pass
Ejemplo n.º 2
0
class ScudCloud(QtWidgets.QMainWindow):

    forceClose = False
    messages = 0
    speller = Speller()
    title = 'ScudCloud'

    def __init__(self, debug = False, minimized = None, urgent_hint = None, settings_path = '', cache_path = ''):
        super(ScudCloud, self).__init__(None)
        self.debug = debug
        self.minimized = minimized
        self.urgent_hint = urgent_hint
        self.setWindowTitle(self.title)
        self.settings_path = settings_path
        self.cache_path = cache_path
        self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png'))
        self.settings = QSettings(self.settings_path + '/scudcloud_qt5.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.snippetsSettings()
        self.leftPane = LeftPane(self)
        self.stackedWidget = QtWidgets.QStackedWidget()
        centralWidget = QtWidgets.QWidget(self)
        layout = QtWidgets.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
            sessionBus.add_match_string("type='signal',interface='com.ubuntu.Upstart0_6'")
            # Ubuntu 16.04 and KDE
            sessionBus.add_match_string("type='signal',interface='org.freedesktop.ScreenSaver'")
            # Cinnamon
            sessionBus.add_match_string("type='signal',interface='org.cinnamon.ScreenSaver'")
            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.load(QtCore.QUrl(url))
        webView.show()
        webView.setZoomFactor(self.zoom)
        self.stackedWidget.addWidget(webView)
        self.stackedWidget.setCurrentWidget(webView)
        self.clearMemory()

    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.cache_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)
        # Sharing the same networkAccessManager
        self.networkAccessManager = QNetworkAccessManager(self)
        self.networkAccessManager.setCookieJar(self.cookiesjar)
        self.networkAccessManager.setCache(self.diskCache)

    def snippetsSettings(self):
        self.disable_snippets = self.settings.value("Snippets")
        if self.disable_snippets is not None:
            self.disable_snippets = self.disable_snippets == "False"
        else:
            self.disable_snippets = False
        if self.disable_snippets:
            disable_snippets_css = ''
            with open(Resources.get_path('disable_snippets.css'), 'r') as f:
                disable_snippets_css = f.read()
            with open(os.path.join(self.cache_path, 'resources.css'), 'a') as f:
                f.write(disable_snippets_css)

    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", "True")
        else:
            self.settings.setValue("Menu", "False")

    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(QWebPage.Undo)
        redo = self.current().pageAction(QWebPage.Redo)
        cut = self.current().pageAction(QWebPage.Cut)
        copy = self.current().pageAction(QWebPage.Copy)
        paste = self.current().pageAction(QWebPage.Paste)
        back = self.current().pageAction(QWebPage.Back)
        forward = self.current().pageAction(QWebPage.Forward)
        reload = self.current().pageAction(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(QWebPage.Undo), undo.shortcut()),
                "redo":        self.createAction(redo.text(), lambda : self.current().page().triggerAction(QWebPage.Redo), redo.shortcut()),
                "cut":         self.createAction(cut.text(), lambda : self.current().page().triggerAction(QWebPage.Cut), cut.shortcut()),
                "copy":        self.createAction(copy.text(), lambda : self.current().page().triggerAction(QWebPage.Copy), copy.shortcut()),
                "paste":       self.createAction(paste.text(), lambda : self.current().page().triggerAction(QWebPage.Paste), paste.shortcut()),
                "back":        self.createAction(back.text(), lambda : self.current().page().triggerAction(QWebPage.Back), back.shortcut()),
                "forward":     self.createAction(forward.text(), lambda : self.current().page().triggerAction(QWebPage.Forward), forward.shortcut()),
                "reload":      self.createAction(reload.text(), lambda : self.current().page().triggerAction(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 = QtWidgets.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)
                self.clearMemory()
                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 = QtWidgets.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 QtWidgets.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()
        elif self.forceClose:
            self.cookiesjar.save()
            self.settings.setValue("Domain", self.domains)
            self.settings.setValue("geometry", self.saveGeometry())
            self.settings.setValue("windowState", self.saveState())
            self.settings.setValue("Domain", self.domains)

    def show(self):
        self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
        self.activateWindow()
        self.setVisible(True)

    def exit(self):
        # Make sure tray is not visible (Fixes #513)
        self.tray.setVisible(False)
        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 type(c) is dict and 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

    def clearMemory(self):
        QWebSettings.globalSettings().clearMemoryCaches()
        QWebSettings.globalSettings().clearIconDatabase()
Ejemplo n.º 3
0
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.Link, theme_color(theme, 'Link', 'fg'))
        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_type(mf.evaluateJavaScript(
            'window.calibre_preview_integration.live_css(%s, %s)' % (
                json.dumps(sourceline), json.dumps(tags))) or '')
        try:
            result = json.loads(result)
        except ValueError:
            result = None
        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, SelectorError):
                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('%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)):
            qurl = QUrl(href)
            name = qurl.path()[1:]
            if name:
                rule['href'] = name

    @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):
        self.update_data()

    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(for_position_sync=False)
            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'])
Ejemplo n.º 4
0
class HostQWidget(QWidget):
    """
        Class who create QWidget to display host data
    """

    def __init__(self, parent=None):
        super(HostQWidget, self).__init__(parent)
        # Fields
        self.actions_widget = ActionsQWidget()
        self.host_item = None
        self.service_items = None
        self.labels = {
            'host_icon': QLabel(),
            'host_name': QLabel(),
            'state_icon': QLabel(),
            'ls_last_check': QLabel(),
            'ls_output': QLabel(),
            'realm': QLabel(),
            'address': QLabel(),
            'business_impact': QLabel(),
            'notes': QLabel()
        }
        self.activecheck_btn = ToggleQWidgetButton()
        self.passivecheck_btn = ToggleQWidgetButton()
        self.history_btn = QPushButton()
        self.history_widget = HistoryQWidget()
        self.host_history = None
        self.customs_widget = CustomsQWidget()
        self.customs_btn = QPushButton()
        self.spy_btn = QPushButton()
        self.refresh_timer = QTimer()
        self.refresh_counter = 0

    def initialize(self):
        """
        Initialize QWidget

        """

        layout = QGridLayout()
        self.setLayout(layout)

        # Add Qwidgets
        layout.addWidget(self.get_host_icon_widget(), 0, 0, 2, 1)

        layout.addWidget(self.get_last_check_widget(), 0, 1, 1, 1)

        layout.addWidget(self.get_variables_widget(), 0, 2, 1, 1)

        layout.addWidget(self.get_notes_output_widget(), 1, 1, 1, 2)

        layout.addWidget(self.get_actions_widget(), 0, 3, 2, 1)

        update_host = int(settings.get_config('Alignak-app', 'update_host')) * 1000
        self.refresh_timer.setInterval(update_host)
        self.refresh_timer.start()
        self.refresh_timer.timeout.connect(self.update_host)

    def set_data(self, host_item):
        """
        Set data of host and service

        :param host_item: the Host item
        :type host_item: alignak_app.items.host.Host
        """

        # Query services of host
        self.host_item = host_item

        # Get problems
        host_and_services = data_manager.get_host_with_services(host_item.item_id)
        self.host_item = host_and_services['host']
        self.service_items = host_and_services['services']

    def get_host_icon_widget(self):
        """
        Return QWidget with overall icon state and host name

        :return: widget with host name and icon
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)

        # Host Icon
        layout.addWidget(self.labels['host_icon'])
        layout.setAlignment(self.labels['host_icon'], Qt.AlignCenter)

        # Host Name
        self.labels['host_name'].setObjectName('itemname')
        self.labels['host_name'].setWordWrap(True)
        layout.addWidget(self.labels['host_name'])
        layout.setAlignment(self.labels['host_name'], Qt.AlignCenter)

        # Customs button
        customs_lbl = QLabel(_('Configuration:'))
        customs_lbl.setObjectName('subtitle')
        layout.addWidget(customs_lbl)
        layout.setAlignment(customs_lbl, Qt.AlignBottom)
        self.customs_btn.setIcon(QIcon(settings.get_image('settings')))
        self.customs_btn.setFixedSize(80, 20)
        self.customs_btn.clicked.connect(self.show_customs)
        layout.addWidget(self.customs_btn)
        layout.setAlignment(self.customs_btn, Qt.AlignCenter)

        # Initialize Customs QWidget
        self.customs_widget.initialize()

        return widget

    def get_actions_widget(self):
        """
        Return QWidget with actions buttons

        :return: widget with buttons
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)

        # Actions
        action_title = QLabel(_('Host actions'))
        action_title.setObjectName('itemtitle')
        action_title.setFixedHeight(25)
        layout.addWidget(action_title)

        ack_down_lbl = QLabel(_('Acknowledge / Downtime:'))
        ack_down_lbl.setObjectName('subtitle')
        layout.addWidget(ack_down_lbl)

        self.actions_widget.initialize(self.host_item)
        layout.addWidget(self.actions_widget)

        # Active Checks
        activecheck_lbl = QLabel(_('Active checks:'))
        activecheck_lbl.setObjectName('subtitle')
        layout.addWidget(activecheck_lbl)
        self.activecheck_btn.initialize()
        self.activecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks(
            'active_checks_enabled', self.activecheck_btn.is_checked()
        ))
        layout.addWidget(self.activecheck_btn)

        # Passive Checks
        passivecheck_lbl = QLabel(_('Passive checks:'))
        passivecheck_lbl.setObjectName('subtitle')
        layout.addWidget(passivecheck_lbl)
        self.passivecheck_btn.initialize()
        self.passivecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks(
            'passive_checks_enabled', self.passivecheck_btn.is_checked()
        ))
        layout.addWidget(self.passivecheck_btn)

        # History
        hist_lbl = QLabel(_('Timeline:'))
        hist_lbl.setObjectName('subtitle')
        layout.addWidget(hist_lbl)
        self.history_btn.setIcon(QIcon(settings.get_image('time')))
        self.history_btn.setFixedSize(80, 20)
        self.history_btn.clicked.connect(self.show_history)
        self.history_btn.setToolTip(_('See history of host'))
        layout.addWidget(self.history_btn)
        layout.setAlignment(self.history_btn, Qt.AlignCenter)

        self.history_widget.initialize()

        # Spy Button
        spy_lbl = QLabel(_('Spy Host:'))
        spy_lbl.setObjectName('subtitle')
        layout.addWidget(spy_lbl)
        self.spy_btn.setIcon(QIcon(settings.get_image('spy')))
        self.spy_btn.setFixedSize(80, 20)
        self.spy_btn.setToolTip(_('Spy current host'))
        layout.addWidget(self.spy_btn)
        layout.setAlignment(self.spy_btn, Qt.AlignCenter)

        layout.setAlignment(Qt.AlignCenter | Qt.AlignTop)

        return widget

    def get_last_check_widget(self):
        """
        Return QWidget with last check data

        :return: widget with last check data
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)

        # Title
        check_title = QLabel(_('My last check'))
        check_title.setObjectName('itemtitle')
        check_title.setFixedHeight(25)
        layout.addWidget(check_title, 0, 0, 1, 2)

        # State
        state_title = QLabel(_("State:"))
        state_title.setObjectName('subtitle')
        layout.addWidget(state_title, 1, 0, 1, 1)

        layout.addWidget(self.labels['state_icon'], 1, 1, 1, 1)

        # When last check
        when_title = QLabel(_("When:"))
        when_title.setObjectName('subtitle')
        layout.addWidget(when_title, 2, 0, 1, 1)

        layout.addWidget(self.labels['ls_last_check'], 2, 1, 1, 1)

        return widget

    def get_variables_widget(self):
        """
        Return QWidget with host variables

        :return: widget with host variables
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)

        # Title
        check_title = QLabel(_('My variables'))
        check_title.setObjectName('itemtitle')
        check_title.setFixedHeight(25)
        layout.addWidget(check_title, 0, 0, 1, 2)

        # Realm
        realm_title = QLabel(_("Realm:"))
        realm_title.setObjectName('subtitle')
        layout.addWidget(realm_title, 1, 0, 1, 1)

        layout.addWidget(self.labels['realm'], 1, 1, 1, 1)

        # Address
        address_title = QLabel(_("Host address:"))
        address_title.setObjectName('subtitle')
        layout.addWidget(address_title, 2, 0, 1, 1)

        layout.addWidget(self.labels['address'], 2, 1, 1, 1)

        # Business impact
        business_title = QLabel(_("Business impact:"))
        business_title.setObjectName('subtitle')
        layout.addWidget(business_title, 3, 0, 1, 1)

        layout.addWidget(self.labels['business_impact'], 3, 1, 1, 1)

        return widget

    def get_notes_output_widget(self):
        """
        Return QWidget with output and notes data

        :return: widget with host output and notes
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)

        # Output
        output_title = QLabel(_("Output"))
        output_title.setObjectName('title')
        layout.addWidget(output_title, 0, 0, 1, 1)

        self.labels['ls_output'].setWordWrap(True)
        self.labels['ls_output'].setTextInteractionFlags(Qt.TextSelectableByMouse)
        output_scrollarea = QScrollArea()
        output_scrollarea.setWidget(self.labels['ls_output'])
        output_scrollarea.setWidgetResizable(True)
        output_scrollarea.setObjectName('output')
        layout.addWidget(output_scrollarea, 1, 0, 1, 2)

        # Notes
        notes_title = QLabel(_("Notes:"))
        notes_title.setObjectName('title')
        layout.addWidget(notes_title, 0, 2, 1, 1)

        notes_btn = QPushButton()
        notes_btn.setIcon(QIcon(settings.get_image('edit')))
        notes_btn.setToolTip(_("Edit host notes."))
        notes_btn.setFixedSize(32, 32)
        notes_btn.clicked.connect(self.patch_data)
        layout.addWidget(notes_btn, 0, 3, 1, 1)

        self.labels['notes'].setWordWrap(True)
        self.labels['notes'].setTextInteractionFlags(Qt.TextSelectableByMouse)
        notes_scrollarea = QScrollArea()
        notes_scrollarea.setWidget(self.labels['notes'])
        notes_scrollarea.setWidgetResizable(True)
        notes_scrollarea.setObjectName('notes')
        layout.addWidget(notes_scrollarea, 1, 2, 1, 2)

        return widget

    def show_history(self):
        """
        Update and show HistoryQWidget

        """

        self.history_widget.update_history_data(self.host_item.name, self.host_history)
        self.history_widget.show()

    def show_customs(self):
        """
        Update and show CustomsQWidget

        """

        self.customs_widget.update_customs(self.host_item)
        self.customs_widget.show()

    def patch_host_checks(self, check_type, state):  # pragma: no cover
        """
        Patch the host check: 'active_checks_enabled' | 'passive_checks_enabled'

        :param check_type: type of check: 'active_checks_enabled' | 'passive_checks_enabled'
        :type check_type: str
        :param state: state of Toggle button
        :type state: bool
        """

        data = {check_type: state}
        headers = {'If-Match': self.host_item.data['_etag']}
        endpoint = '/'.join([self.host_item.item_type, self.host_item.item_id])

        patched = app_backend.patch(endpoint, data, headers)

        if patched:
            self.host_item.data[check_type] = state
            data_manager.update_item_data(
                self.host_item.item_type, self.host_item.item_id, self.host_item.data
            )
            enabled = _('enabled') if state else _('disabled')
            event_type = 'OK' if state else 'WARN'
            message = _(
                _('%s %s for %s' %
                  (Item.get_check_text(check_type), enabled, self.host_item.get_display_name()))
            )
            send_event(event_type, message, timer=True)
        else:
            send_event(
                'ERROR',
                _("Backend PATCH failed, please check your logs !")
            )

    def patch_data(self):  # pragma: no cover
        """
        Display QDialog for patch

        """

        notes_dialog = EditQDialog()
        notes_dialog.initialize(
            _('Edit Host Notes'),
            self.host_item.data['notes']
        )

        if notes_dialog.exec_() == EditQDialog.Accepted:
            data = {'notes': str(notes_dialog.text_edit.toPlainText())}
            headers = {'If-Match': self.host_item.data['_etag']}
            endpoint = '/'.join(['host', self.host_item.item_id])

            patched = app_backend.patch(endpoint, data, headers)

            if patched:
                data_manager.update_item_data(
                    self.host_item.item_type,
                    self.host_item.item_id,
                    {'notes': notes_dialog.text_edit.toPlainText()}
                )
                self.labels['notes'].setText(notes_dialog.text_edit.toPlainText())
                message = _(
                    _("Host notes have been edited.")
                )
                send_event('INFO', message)
            else:
                send_event(
                    'ERROR',
                    _("Backend PATCH failed, please check your logs !")
                )

    def update_host(self, host_item=None):
        """
        Update HostQWidget data and QLabels

        :param host_item: the Host item
        :type host_item: alignak_app.items.host.Host
        """

        if self.host_item and not host_item:
            self.set_data(self.host_item)
        if host_item:
            self.set_data(host_item)

        if self.host_item or host_item:
            # Update host services
            self.refresh_counter += 1
            if self.refresh_counter > 10:
                thread_manager.add_high_priority_thread('service', self.host_item.item_id)
                self.refresh_counter = 0

            # Update host
            icon_name = get_overall_state_icon(
                self.service_items,
                self.host_item.data['_overall_state_id']
            )
            icon_pixmap = QPixmap(settings.get_image(icon_name))

            self.labels['host_icon'].setPixmap(QPixmap(icon_pixmap))
            self.labels['host_icon'].setToolTip(
                self.host_item.get_overall_tooltip(self.service_items)
            )
            self.labels['host_name'].setText('%s' % self.host_item.get_display_name())

            monitored = self.host_item.data[
                'passive_checks_enabled'] + self.host_item.data['active_checks_enabled']
            icon_name = get_icon_name(
                'host',
                self.host_item.data['ls_state'],
                self.host_item.data['ls_acknowledged'],
                self.host_item.data['ls_downtimed'],
                monitored
            )
            pixmap_icon = QPixmap(settings.get_image(icon_name))
            final_icon = pixmap_icon.scaled(32, 32, Qt.KeepAspectRatio)
            self.labels['state_icon'].setPixmap(final_icon)
            self.labels['state_icon'].setToolTip(self.host_item.get_tooltip())

            since_last_check = get_diff_since_last_timestamp(
                self.host_item.data['ls_last_check']
            )
            last_check_tooltip = get_date_fromtimestamp(self.host_item.data['ls_last_check'])

            self.labels['ls_last_check'].setText(since_last_check)
            self.labels['ls_last_check'].setToolTip(last_check_tooltip)
            self.labels['ls_output'].setText(self.host_item.data['ls_output'])

            self.labels['realm'].setText(
                data_manager.get_realm_name(self.host_item.data['_realm'])
            )
            self.labels['address'].setText(self.host_item.data['address'])
            self.labels['business_impact'].setText(str(self.host_item.data['business_impact']))
            self.labels['notes'].setText(self.host_item.data['notes'])

            self.actions_widget.item = self.host_item
            self.actions_widget.update_widget()

            self.activecheck_btn.update_btn_state(self.host_item.data['active_checks_enabled'])
            self.passivecheck_btn.update_btn_state(self.host_item.data['passive_checks_enabled'])
            self.customs_btn.setEnabled(bool(self.host_item.data['customs']))

            # Update host history
            self.host_history = data_manager.get_item('history', self.host_item.item_id)
            if self.host_history:
                self.history_btn.setEnabled(True)
                self.history_btn.setToolTip(_('History is available'))
            else:
                self.history_btn.setToolTip(_('History is not available, please wait...'))
                self.history_btn.setEnabled(False)

                if app_backend.connected:
                    thread_manager.add_high_priority_thread(
                        'history',
                        {'hostname': self.host_item.name, 'host_id': self.host_item.item_id}
                    )
                else:
                    thread_manager.stop_threads()
Ejemplo n.º 5
0
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, hide_handle_on_single_panel=True):
        QSplitter.__init__(self, parent)
        if hide_handle_on_single_panel:
            self.state_changed.connect(self.update_handle_width)
        self.original_handle_width = self.handleWidth()
        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.changed.connect(self.update_shortcut)
            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_type(self.action_toggle.text()),
                        default_keys=(shortcut,), action=self.action_toggle)
                else:
                    self.action_toggle.setShortcut(shortcut)
            else:
                self.action_toggle.setShortcut(shortcut)

    def update_shortcut(self):
        self.button.update_shortcut(self.action_toggle)

    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)

    def update_handle_width(self, not_one_panel):
        self.setHandleWidth(self.original_handle_width if not_one_panel else 0)

    @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:', end=' ')
            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()
Ejemplo n.º 6
0
class MaterialManager(QObject):

    materialsUpdated = pyqtSignal()  # Emitted whenever the material lookup tables are updated.

    def __init__(self, container_registry, parent = None):
        super().__init__(parent)
        self._application = Application.getInstance()
        self._container_registry = container_registry  # type: ContainerRegistry

        self._fallback_materials_map = dict()  # material_type -> generic material metadata
        self._material_group_map = dict()  # root_material_id -> MaterialGroup
        self._diameter_machine_variant_material_map = dict()  # approximate diameter str -> dict(machine_definition_id -> MaterialNode)

        # We're using these two maps to convert between the specific diameter material id and the generic material id
        # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
        # i.e. generic_pla -> generic_pla_175
        self._material_diameter_map = defaultdict(dict)  # root_material_id -> approximate diameter str -> root_material_id for that diameter
        self._diameter_material_map = dict()  # material id including diameter (generic_pla_175) -> material root id (generic_pla)

        # This is used in Legacy UM3 send material function and the material management page.
        self._guid_material_groups_map = defaultdict(list)  # GUID -> a list of material_groups

        # The machine definition ID for the non-machine-specific materials.
        # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
        self._default_machine_definition_id = "fdmprinter"
        self._default_approximate_diameter_for_quality_search = "3"

        # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
        # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
        # react too many time.
        self._update_timer = QTimer(self)
        self._update_timer.setInterval(300)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._updateMaps)

        self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
        self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
        self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)

    def initialize(self):
        # Find all materials and put them in a matrix for quick search.
        material_metadatas = {metadata["id"]: metadata for metadata in
                              self._container_registry.findContainersMetadata(type = "material") if
                              metadata.get("GUID")}

        self._material_group_map = dict()
                
        # Map #1
        #    root_material_id -> MaterialGroup
        for material_id, material_metadata in material_metadatas.items():
            # We don't store empty material in the lookup tables
            if material_id == "empty_material":
                continue

            root_material_id = material_metadata.get("base_file")
            if root_material_id not in self._material_group_map:
                self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
                self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
            group = self._material_group_map[root_material_id]

            # Store this material in the group of the appropriate root material.
            if material_id != root_material_id:
                new_node = MaterialNode(material_metadata)
                group.derived_material_node_list.append(new_node)

        # Order this map alphabetically so it's easier to navigate in a debugger
        self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))

        # Map #1.5
        #    GUID -> material group list
        self._guid_material_groups_map = defaultdict(list)
        for root_material_id, material_group in self._material_group_map.items():
            guid = material_group.root_material_node.metadata["GUID"]
            self._guid_material_groups_map[guid].append(material_group)

        # Map #2
        # Lookup table for material type -> fallback material metadata, only for read-only materials
        grouped_by_type_dict = dict()
        material_types_without_fallback = set()
        for root_material_id, material_node in self._material_group_map.items():
            material_type = material_node.root_material_node.metadata["material"]
            if material_type not in grouped_by_type_dict:
                grouped_by_type_dict[material_type] = {"generic": None,
                                                       "others": []}
                material_types_without_fallback.add(material_type)
            brand = material_node.root_material_node.metadata["brand"]
            if brand.lower() == "generic":
                to_add = True
                if material_type in grouped_by_type_dict:
                    diameter = material_node.root_material_node.metadata.get("approximate_diameter")
                    if diameter != self._default_approximate_diameter_for_quality_search:
                        to_add = False  # don't add if it's not the default diameter

                if to_add:
                    # Checking this first allow us to differentiate between not read only materials:
                    #  - if it's in the list, it means that is a new material without fallback
                    #  - if it is not, then it is a custom material with a fallback material (parent)
                    if material_type in material_types_without_fallback:
                        grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
                        material_types_without_fallback.remove(material_type)

        # Remove the materials that have no fallback materials
        for material_type in material_types_without_fallback:
            del grouped_by_type_dict[material_type]
        self._fallback_materials_map = grouped_by_type_dict

        # Map #3
        # There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
        # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
        # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
        # for quality search.
        self._material_diameter_map = defaultdict(dict)
        self._diameter_material_map = dict()

        # Group the material IDs by the same name, material, brand, and color but with different diameters.
        material_group_dict = dict()
        keys_to_fetch = ("name", "material", "brand", "color")
        for root_material_id, machine_node in self._material_group_map.items():
            root_material_metadata = machine_node.root_material_node.metadata

            key_data = []
            for key in keys_to_fetch:
                key_data.append(root_material_metadata.get(key))
            key_data = tuple(key_data)

            # If the key_data doesn't exist, it doesn't matter if the material is read only...
            if key_data not in material_group_dict:
                material_group_dict[key_data] = dict()
            else:
                # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
                if not machine_node.is_read_only:
                    continue
            approximate_diameter = root_material_metadata.get("approximate_diameter")
            material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]

        # Map [root_material_id][diameter] -> root_material_id for this diameter
        for data_dict in material_group_dict.values():
            for root_material_id1 in data_dict.values():
                if root_material_id1 in self._material_diameter_map:
                    continue
                diameter_map = data_dict
                for root_material_id2 in data_dict.values():
                    self._material_diameter_map[root_material_id2] = diameter_map

            default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
            if default_root_material_id is None:
                default_root_material_id = list(data_dict.values())[0]  # no default diameter present, just take "the" only one
            for root_material_id in data_dict.values():
                self._diameter_material_map[root_material_id] = default_root_material_id

        # Map #4
        #    "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
        # Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
        self._diameter_machine_variant_material_map = dict()
        for material_metadata in material_metadatas.values():
            # We don't store empty material in the lookup tables
            if material_metadata["id"] == "empty_material":
                continue

            root_material_id = material_metadata["base_file"]
            definition = material_metadata["definition"]
            approximate_diameter = material_metadata["approximate_diameter"]

            if approximate_diameter not in self._diameter_machine_variant_material_map:
                self._diameter_machine_variant_material_map[approximate_diameter] = {}

            machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
            if definition not in machine_variant_material_map:
                machine_variant_material_map[definition] = MaterialNode()

            machine_node = machine_variant_material_map[definition]
            variant_name = material_metadata.get("variant_name")
            if not variant_name:
                # if there is no variant, this material is for the machine, so put its metadata in the machine node.
                machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
            else:
                # this material is variant-specific, so we save it in a variant-specific node under the
                # machine-specific node

                # Check first if the variant exist in the manager
                existing_variant = self._application.getVariantManager().getVariantNode(definition, variant_name)
                if existing_variant is not None:
                    if variant_name not in machine_node.children_map:
                        machine_node.children_map[variant_name] = MaterialNode()

                    variant_node = machine_node.children_map[variant_name]
                    if root_material_id in variant_node.material_map:  # We shouldn't have duplicated variant-specific materials for the same machine.
                        ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
                        continue
                    variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
                else:
                    # Add this container id to the wrong containers list in the registry
                    Logger.log("w", "Not adding {id} to the material manager because the variant does not exist.".format(id = material_metadata["id"]))
                    self._container_registry.addWrongContainerId(material_metadata["id"])

        self.materialsUpdated.emit()

    def _updateMaps(self):
        Logger.log("i", "Updating material lookup data ...")
        self.initialize()

    def _onContainerMetadataChanged(self, container):
        self._onContainerChanged(container)

    def _onContainerChanged(self, container):
        container_type = container.getMetaDataEntry("type")
        if container_type != "material":
            return

        # update the maps
        self._update_timer.start()

    def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
        return self._material_group_map.get(root_material_id)

    def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
        return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)

    def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
        return self._diameter_material_map.get(root_material_id)

    def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
        return self._guid_material_groups_map.get(guid)

    #
    # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
    #
    def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str],
                              diameter: float) -> dict:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_variant_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
            return dict()

        machine_definition_id = machine_definition.getId()

        # If there are variant materials, get the variant material
        machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
        machine_node = machine_variant_material_map.get(machine_definition_id)
        default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
        variant_node = None
        if extruder_variant_name is not None and machine_node is not None:
            variant_node = machine_node.getChildNode(extruder_variant_name)

        nodes_to_check = [variant_node, machine_node, default_machine_node]

        # Fallback mechanism of finding materials:
        #  1. variant-specific material
        #  2. machine-specific material
        #  3. generic material (for fdmprinter)
        machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])

        material_id_metadata_dict = dict()
        for node in nodes_to_check:
            if node is not None:
                # Only exclude the materials that are explicitly specified in the "exclude_materials" field.
                # Do not exclude other materials that are of the same type.
                for material_id, node in node.material_map.items():
                    if material_id in machine_exclude_materials:
                        Logger.log("d", "Exclude material [%s] for machine [%s]",
                                   material_id, machine_definition.getId())
                        continue

                    if material_id not in material_id_metadata_dict:
                        material_id_metadata_dict[material_id] = node

        return material_id_metadata_dict

    #
    # A convenience function to get available materials for the given machine with the extruder position.
    #
    def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
                                                extruder_stack: "ExtruderStack") -> Optional[dict]:
        variant_name = None
        if extruder_stack.variant.getId() != "empty_variant":
            variant_name = extruder_stack.variant.getName()
        diameter = extruder_stack.approximateMaterialDiameter

        # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
        return self.getAvailableMaterials(machine.definition, variant_name, diameter)

    #
    # Gets MaterialNode for the given extruder and machine with the given material name.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str],
                        diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_variant_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
                       diameter, rounded_diameter, root_material_id)
            return None

        # If there are variant materials, get the variant material
        machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
        machine_node = machine_variant_material_map.get(machine_definition_id)
        variant_node = None

        # Fallback for "fdmprinter" if the machine-specific materials cannot be found
        if machine_node is None:
            machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
        if machine_node is not None and extruder_variant_name is not None:
            variant_node = machine_node.getChildNode(extruder_variant_name)

        # Fallback mechanism of finding materials:
        #  1. variant-specific material
        #  2. machine-specific material
        #  3. generic material (for fdmprinter)
        nodes_to_check = [variant_node, machine_node,
                          machine_variant_material_map.get(self._default_machine_definition_id)]

        material_node = None
        for node in nodes_to_check:
            if node is not None:
                material_node = node.material_map.get(root_material_id)
                if material_node:
                    break

        return material_node

    #
    # Gets MaterialNode for the given extruder and machine with the given material type.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]:
        node = None
        machine_definition = global_stack.definition
        if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
            material_diameter = machine_definition.getProperty("material_diameter", "value")
            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)

            # Look at the guid to material dictionary
            root_material_id = None
            for material_group in self._guid_material_groups_map[material_guid]:
                if material_group.is_read_only:
                    root_material_id = material_group.root_material_node.metadata["id"]
                    break

            if not root_material_id:
                Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
                return None

            node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
                                        material_diameter, root_material_id)
        return node

    #
    # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
    # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
    # the generic material IDs to search for qualities.
    #
    # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
    # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
    # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
    # be "generic_pla". This function is intended to get a generic fallback material for the given material type.
    #
    # This function returns the generic root material ID for the given material type, where material types are "PLA",
    # "ABS", etc.
    #
    def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
        # For safety
        if material_type not in self._fallback_materials_map:
            Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
            return None
        fallback_material = self._fallback_materials_map[material_type]
        if fallback_material:
            return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
        else:
            return None

    def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: Optional[str]) -> Optional["MaterialNode"]:
        node = None
        machine_definition = global_stack.definition
        if parseBool(global_stack.getMetaDataEntry("has_materials", False)):
            material_diameter = machine_definition.getProperty("material_diameter", "value")
            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)
            approximate_material_diameter = str(round(material_diameter))
            root_material_id = machine_definition.getMetaDataEntry("preferred_material")
            root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
            node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
                                        material_diameter, root_material_id)
        return node

    def removeMaterialByRootId(self, root_material_id: str):
        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
            return

        nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
        for node in nodes_to_remove:
            self._container_registry.removeContainer(node.metadata["id"])

    #
    # Methods for GUI
    #

    #
    # Sets the new name for the given material.
    #
    @pyqtSlot("QVariant", str)
    def setMaterialName(self, material_node: "MaterialNode", name: str):
        root_material_id = material_node.metadata["base_file"]
        if self._container_registry.isReadOnly(root_material_id):
            Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
            return

        material_group = self.getMaterialGroup(root_material_id)
        if material_group:
            material_group.root_material_node.getContainer().setName(name)

    #
    # Removes the given material.
    #
    @pyqtSlot("QVariant")
    def removeMaterial(self, material_node: "MaterialNode"):
        root_material_id = material_node.metadata["base_file"]
        self.removeMaterialByRootId(root_material_id)

    #
    # Creates a duplicate of a material, which has the same GUID and base_file metadata.
    # Returns the root material ID of the duplicated material if successful.
    #
    @pyqtSlot("QVariant", result = str)
    def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]:
        root_material_id = material_node.metadata["base_file"]

        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
            return None

        base_container = material_group.root_material_node.getContainer()
        if not base_container:
            return None

        # Ensure all settings are saved.
        self._application.saveSettings()

        # Create a new ID & container to hold the data.
        new_containers = []
        if new_base_id is None:
            new_base_id = self._container_registry.uniqueName(base_container.getId())
        new_base_container = copy.deepcopy(base_container)
        new_base_container.getMetaData()["id"] = new_base_id
        new_base_container.getMetaData()["base_file"] = new_base_id
        if new_metadata is not None:
            for key, value in new_metadata.items():
                new_base_container.getMetaData()[key] = value
        new_containers.append(new_base_container)

        # Clone all of them.
        for node in material_group.derived_material_node_list:
            container_to_copy = node.getContainer()
            if not container_to_copy:
                continue
            # Create unique IDs for every clone.
            new_id = new_base_id
            if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
                new_id += "_" + container_to_copy.getMetaDataEntry("definition")
                if container_to_copy.getMetaDataEntry("variant_name"):
                    variant_name = container_to_copy.getMetaDataEntry("variant_name")
                    new_id += "_" + variant_name.replace(" ", "_")

            new_container = copy.deepcopy(container_to_copy)
            new_container.getMetaData()["id"] = new_id
            new_container.getMetaData()["base_file"] = new_base_id
            if new_metadata is not None:
                for key, value in new_metadata.items():
                    new_container.getMetaData()[key] = value

            new_containers.append(new_container)

        for container_to_add in new_containers:
            container_to_add.setDirty(True)
            self._container_registry.addContainer(container_to_add)
        return new_base_id

    #
    # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
    #
    @pyqtSlot(result = str)
    def createMaterial(self) -> str:
        from UM.i18n import i18nCatalog
        catalog = i18nCatalog("cura")
        # Ensure all settings are saved.
        self._application.saveSettings()

        machine_manager = self._application.getMachineManager()
        extruder_stack = machine_manager.activeStack

        approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
        root_material_id = "generic_pla"
        root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
        material_group = self.getMaterialGroup(root_material_id)

        # Create a new ID & container to hold the data.
        new_id = self._container_registry.uniqueName("custom_material")
        new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
                        "brand": catalog.i18nc("@label", "Custom"),
                        "GUID": str(uuid.uuid4()),
                        }

        self.duplicateMaterial(material_group.root_material_node,
                               new_base_id = new_id,
                               new_metadata = new_metadata)
        return new_id
class EventsQWidget(QWidget):
    """
        Class who create QWidget for events
    """

    def __init__(self):
        super(EventsQWidget, self).__init__()
        self.setObjectName('events')
        # Fields
        self.events_list = QListWidget()
        self.timer = QTimer()

    def initialize(self):
        """
        Intialize QWidget

        """

        self.timer.setInterval(30000)
        self.timer.start()
        self.timer.timeout.connect(self.send_datamanager_events)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

        self.events_list.setDragDropMode(QAbstractItemView.DragOnly)
        self.events_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.events_list.setDropIndicatorShown(True)
        self.events_list.doubleClicked.connect(self.remove_event)
        self.events_list.setWordWrap(True)
        self.events_list.setIconSize(QSize(16, 16))

        self.add_event(
            'OK',
            _('Welcome %s, you are connected to Alignak Backend') %
            data_manager.database['user'].name,
            timer=True
        )

        layout.addWidget(self.events_list)

    def send_datamanager_events(self):
        """
        Add events stored in DataManager

        """

        events = data_manager.get_events()

        if events:
            for event in events:
                self.add_event(
                    event['event_type'],
                    event['message'],
                    timer=False,
                    host=event['host']
                )

    def add_event(self, event_type, msg, timer=False, host=None):
        """
        Add event to events list

        :param event_type: the type of event: OK, DOWN, ACK, ...
        :type event_type: str
        :param msg: message of event
        :type msg: str
        :param timer: timer to hide event at end of time
        :type timer: bool
        :param host: data of a host to set ``Qt.UserRole``
        :type host: None | str
        """

        if not self.event_exist(msg):
            logger.debug(
                'Add Event: msg: %s, timer: %s, host: %s', msg, timer, host
            )
            event = EventItem()
            event.initialize(event_type, msg, timer=timer, host=host)

            self.events_list.insertItem(0, event)
            if timer:
                event_duration = int(
                    settings.get_config('Alignak-app', 'notification_duration')
                ) * 1000
                QTimer.singleShot(
                    event_duration,
                    lambda: self.remove_timer_event(event)
                )
        else:
            logger.debug(
                'Event with msg: %s already exist.', msg
            )

    def event_exist(self, msg):
        """
        Check if event already displayed, move it to top and update tooltip.
        Only for EventItem who have a ``Qt.UserRole``

        :param msg: message of event
        :type msg: str
        :return: if message exist or not in events QWidgetList
        :rtype: bool
        """

        for i in range(0, self.events_list.count()):
            if self.events_list.item(i).data(Qt.DisplayRole) == msg:
                item = self.events_list.takeItem(i)
                msg_to_send = '%s. (Send at %s)' % (msg, get_current_time())
                item.setToolTip(msg_to_send)
                self.events_list.insertItem(0, item)

                return True

        return False

    def remove_timer_event(self, event):
        """
        Remove EventItem with timer

        :param event: EventItem with timer
        :type event: EventItem
        """

        logger.debug('Remove Timer Event: %s', event.text())
        self.events_list.takeItem(self.events_list.row(event))

    def remove_event(self, item=None):
        """
        Remove item when user double click on an item

        :param item: item to remove, else remove the current row
        :type item: EventItem
        """

        if isinstance(item, EventItem):
            row = self.events_list.row(item)
            self.events_list.takeItem(row)
        else:
            item = self.events_list.takeItem(self.events_list.currentRow())

        logger.debug('Remove Event: %s', item.text())
class AlignakQWidget(QWidget):
    """
        Class who display daemons status, backend connection and user informations
    """

    def __init__(self, parent=None):
        super(AlignakQWidget, self).__init__(parent)
        # Fields
        self.backend_connected = QLabel('pending...')
        self.status_dialog = StatusQDialog()
        self.status_btn = QPushButton()
        self.ws_connected = QLabel()
        self.profile_widget = ProfileQWidget()
        self.profile_btn = QPushButton()
        self.refresh_timer = QTimer()

    def initialize(self):
        """
        Initialize QWidget

        """

        self.update_status()

        layout = QGridLayout()
        self.setLayout(layout)

        # Backend state
        connected_title = QLabel(_('Backend API'))
        connected_title.setObjectName('subtitle')
        layout.addWidget(connected_title, 0, 0, 1, 1)

        self.backend_connected.setFixedSize(16, 16)
        self.backend_connected.setScaledContents(True)
        self.backend_connected.setToolTip(_('Status of the backend API connection'))
        layout.addWidget(self.backend_connected, 0, 1, 1, 1)
        layout.setAlignment(self.backend_connected, Qt.AlignCenter)

        # WS state
        ws_title = QLabel(_('Web Service'))
        ws_title.setObjectName('subtitle')
        layout.addWidget(ws_title, 0, 2, 1, 1)

        self.ws_connected.setFixedSize(16, 16)
        self.ws_connected.setScaledContents(True)
        self.ws_connected.setToolTip(_('Status of the Web Service connection'))
        layout.addWidget(self.ws_connected, 0, 3, 1, 1)
        layout.setAlignment(self.ws_connected, Qt.AlignCenter)

        # Daemons state
        daemons_title = QLabel(_('Alignak'))
        daemons_title.setObjectName('subtitle')
        layout.addWidget(daemons_title, 1, 0, 1, 1)

        self.status_dialog.initialize()
        self.update_status_btn(self.status_dialog.update_dialog())
        self.status_btn.setFixedSize(32, 32)
        self.status_btn.clicked.connect(self.show_status_dialog)
        self.status_btn.setToolTip(_('Status of daemons connection'))
        layout.addWidget(self.status_btn, 1, 1, 1, 1)
        layout.setAlignment(self.status_btn, Qt.AlignCenter)

        # User
        user_lbl = QLabel(_('User'))
        user_lbl.setObjectName('subtitle')
        layout.addWidget(user_lbl, 1, 2, 1, 1)

        self.profile_widget.initialize()
        self.profile_btn.setIcon(QIcon(settings.get_image('user')))
        self.profile_btn.setFixedSize(32, 32)
        self.profile_btn.clicked.connect(self.show_user_widget)
        self.profile_btn.setToolTip(_('View current user'))
        layout.addWidget(self.profile_btn, 1, 3, 1, 1)

        # Refresh timer
        update_status = int(settings.get_config('Alignak-app', 'update_status')) * 1000
        self.refresh_timer.setInterval(update_status)
        self.refresh_timer.start()
        self.refresh_timer.timeout.connect(self.update_status)

    def show_status_dialog(self):  # pragma: no cover
        """
        Update and show StatusQDialog

        """

        self.update_status_btn(self.status_dialog.update_dialog())
        self.status_dialog.show()

    def show_user_widget(self):  # pragma: no cover
        """
        Update and show ProfileQWidget

        """

        self.profile_widget.update_widget()
        self.profile_widget.show()

    def update_status_btn(self, status):
        """
        Update status button, depending if "status_ok" is False or True

        :param status: current status of alignak daemons
        :type status: bool
        """

        self.status_btn.setIcon(
            QIcon(get_icon_pixmap(status, ['connected', 'disconnected']))
        )

    def update_status(self):
        """
        Update daemons and backend status

        """

        self.backend_connected.setPixmap(
            QPixmap(settings.get_image(app_backend.get_backend_status_icon()))
        )
        self.ws_connected.setPixmap(
            QPixmap(settings.get_image(app_backend.get_ws_status_icon()))
        )
        self.status_btn.setEnabled(bool(data_manager.database['alignakdaemon']))

        if self.status_dialog.labels:
            self.update_status_btn(self.status_dialog.update_dialog())
Ejemplo n.º 9
0
class ToolButton(QToolButton):
    _MultiIconOption = 1
    _ShowMenuInsideOption = 2
    _ToolBarLookOption = 4
    _ShowMenuOnRightClick = 8
    def __init__(self, parent=None):
        super(ToolButton, self).__init__(parent)
        self._multiIcon = QImage()
        self._themeIcon = ''
        self._pressTimer = QTimer()
        self._menu = None  # QMenu
        self._options = 0

        self.setMinimumWidth(16)
        opt = QStyleOptionToolButton()
        self.initStyleOption(opt)

        self._pressTimer.setSingleShot(True)
        self._pressTimer.setInterval(QApplication.style().styleHint(
            QStyle.SH_ToolButton_PopupDelay, opt, self
        ))
        self._pressTimer.timeout.connect(self._showMenu)

    def size(self):
        return super().size()

    def setFixedSize(self, size):
        return super().setFixedSize(size)

    fixedsize = pyqtProperty(QSize, size, setFixedSize)

    def width(self):
        return super().width()

    def setFixedWidth(self, width):
        return super().setFixedWidth(width)

    fixedwidth = pyqtProperty(int, width, setFixedWidth)

    def height(self):
        return super().height()

    def setFixedHeight(self, height):
        return super().setFixedHeight(height)

    fixedheight = pyqtProperty(int, height, setFixedHeight)

    def multiIcon(self):
        '''
        @brief: MultiIcon - Image containing pixmaps for all button states
        @return: QImage
        '''
        return self._multiIcon

    def setMultiIcon(self, image):
        '''
        @param: image QImage
        '''
        self._options |= self._MultiIconOption
        self._multiIcon = image
        self.setFixedSize(self._multiIcon.width(), self._multiIcon.height())

        self.update()

    multiIcon = pyqtProperty(QImage, multiIcon, setMultiIcon)

    def icon(self):
        '''
        @brief: Icon - Standard QToolButton with icon
        @return: QIcon
        '''
        return super().icon()

    def setIcon(self, icon):
        '''
        @param: QIcon
        '''
        if self._options & self._MultiIconOption:
            self.setFixedSize(self.sizeHint())

        self._options &= ~self._MultiIconOption
        if not isinstance(icon, QIcon):
            icon = QIcon(icon)
        super().setIcon(icon)

    icon = pyqtProperty(QIcon, icon, setIcon)

    def themeIcon(self):
        '''
        @brief: ThemeIcon - Standard QToolButton with theme icon
        @return: QString
        '''
        return self._themeIcon

    def setThemeIcon(self, icon):
        '''
        @param: icon QString
        '''
        # QIcon ic
        ic = QIcon.fromTheme(icon)
        if not ic.isNull():
            self._themeIcon = icon
            self.setIcon(QIcon.fromTheme(self._themeIcon))

    themeIcon = pyqtProperty(str, themeIcon, setThemeIcon)

    def fallbackIcon(self):
        '''
        @brief: FallbackIcon - In case theme doesn't contain ThemeIcon
        @return: QIcon
        '''
        return self.icon

    def setFallbackIcon(self, fallbackIcon):
        '''
        @param: fallbackIcon QIcon
        '''
        if self.icon.isNull():
            self.setIcon(fallbackIcon)

    fallbackIcon = pyqtProperty(QIcon, fallbackIcon, setFallbackIcon)

    def menu(self):
        '''
        @note: Menu - Menu is handled in ToolButton and is not passed to QToolButton
            There won't be menu indicator shown in the button
            QToolButton::MenuButtonPopup is not supported
        @return: QMenu
        '''
        return self._menu

    def setMenu(self, menu):
        '''
        @param: menu QMenu
        '''
        assert(menu)

        if self._menu:
            self._menu.aboutToHide.disconnect(self._menuAboutToHide)

        self._menu = menu
        self._menu.aboutToHide.connect(self._menuAboutToHide)

    def showMenuInside(self):
        '''
        @brief: Align the right corner of menu to the right corner of button
        '''
        return self._options & self._ShowMenuInsideOption

    def setShowMenuInside(self, enable):
        if enable:
            self._options |= self._ShowMenuInsideOption
        else:
            self._options &= ~self._ShowMenuInsideOption

    def showMenuOnRightClick(self):
        '''
        @brief: Show button menu on right click
        '''
        return self._options & self._ShowMenuOnRightClick

    def setShowMenuOnRightClick(self, enable):
        if enable:
            self._options |= self._ShowMenuOnRightClick
        else:
            self._options &= ~self._ShowMenuOnRightClick

    def toolbarButtonLook(self):
        '''
        @brief: Set the button to look as it was in toolbar
            (it now only sets the correct icon size)
        @return: bool
        '''
        return self._options & self._ToolBarLookOption

    def setToolbarButtonLook(self, enable):
        if enable:
            self._options |= self._ToolBarLookOption

            opt = QStyleOption()
            opt.initFrom(self)
            size = self.style().pixelMetric(QStyle.PM_ToolBarIconSize, opt, self)
            self.setIconSize(QSize(size, size))
        else:
            self._options &= ~self._ToolBarLookOption

        self.setProperty('toolbar-look', enable)
        self.style().unpolish(self)
        self.style().polish(self)

    # Q_SIGNALS
    middleMouseClicked = pyqtSignal()
    controlClicked = pyqtSignal()
    doubleClicked = pyqtSignal()

    # It is needed to use these signals with ShowMenuInside
    aboutToShowMenu = pyqtSignal()
    aboutToHideMenu = pyqtSignal()

    # private Q_SLOTS
    def _menuAboutToHide(self):
        self.setDown(False)
        self.aboutToHideMenu.emit()

    def _showMenu(self):
        if not self._menu or self._menu.isVisible():
            return

        self.aboutToShowMenu.emit()

        pos = QPoint()

        if self._options & self._ShowMenuInsideOption:
            pos = self.mapToGlobal(self.rect().bottomRight())
            if QApplication.layoutDirection() == Qt.RightToLeft:
                pos.setX(pos.x() - self.rect().width())
            else:
                pos.setX(pos.x() - self._menu.sizeHint().width())
        else:
            pos = self.mapToGlobal(self.rect().bottomLeft())

        self._menu.popup(pos)

    # protected:
    # override
    def mousePressEvent(self, event):
        '''
        @param: event QMouseEvent
        '''
        buttons = event.buttons()
        if buttons == Qt.LeftButton and self.popupMode() == QToolButton.DelayedPopup:
            self._pressTimer.start()

        if buttons == Qt.LeftButton and self.menu() and self.popupMode() == QToolButton.InstantPopup:
            self.setDown(True)
            self._showMenu()
        elif buttons == Qt.RightButton and self.menu() and self._options & self.showMenuOnRightClick:
            self.setDown(True)
            self._showMenu()
        else:
            super().mousePressEvent(event)

    # override
    def mouseReleaseEvent(self, event):
        '''
        @param: event QMouseEvent
        '''
        self._pressTimer.stop()

        button = event.button()
        if button == Qt.MiddleButton and self.rect().contains(event.pos()):
            self.middleMouseClicked.emit()
            self.setDown(False)
        elif button == Qt.LeftButton and self.rect().contains(event.pos()) and \
                event.modifiers() == Qt.ControlModifier:
            self.controlClicked.emit()
            self.setDown(False)
        else:
            super().mouseReleaseEvent(event)

    # override
    def mouseDoubleClickEvent(self, event):
        '''
        @param: event QMouseEvent
        '''
        super().mouseDoubleClickEvent(event)

        self._pressTimer.stop()

        if event.buttons() == Qt.LeftButton:
            self.doubleClicked.emit()

    # override
    def contextMenuEvent(self, event):
        '''
        @param: event QContextMenuEvent
        '''
        # Block to prevent showing both context menu and button menu
        if self.menu() and self._options & self._ShowMenuOnRightClick:
            return

        super().contextMenuEvent(event)

    # override
    def paintEvent(self, event):
        '''
        @param: event QPaintEvent
        '''
        if not (self._options & self._MultiIconOption):
            super().paintEvent(event)
            return

        p = QPainter(self)

        w = self._multiIcon.width()
        h4 = self._multiIcon.height() / 4

        if not self.isEnabled():
            p.drawImage(0, 0, self._multiIcon, 0, h4 * 3, w, h4)
        elif self.isDown():
            p.drawImage(0, 0, self._multiIcon, 0, h4 * 2, w, h4)
        elif self.underMouse():
            p.drawImage(0, 0, self._multiIcon, 0, h4 * 1, w, h4)
        else:
            p.drawImage(0, 0, self._multiIcon, 0, h4 * 0, w, h4)
Ejemplo n.º 10
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_dropped = pyqtSignal(object)
    books_dropped = pyqtSignal(object)

    def __init__(self, parent):
        QListView.__init__(self, parent)
        self._ncols = None
        self.gesture_manager = GestureManager(self)
        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.set_color()
        self.ignore_render_requests = Event()
        dpr = self.device_pixel_ratio
        self.thumbnail_cache = ThumbnailCache(
            max_size=gprefs['cover_grid_disk_cache_size'],
            thumbnail_size=(int(dpr * self.delegate.cover_size.width()),
                            int(dpr * self.delegate.cover_size.height())))
        self.render_thread = None
        self.update_item.connect(self.re_render,
                                 type=Qt.ConnectionType.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.CursorShape.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)
        self.resize_timer = t = QTimer(self)
        t.setInterval(200), t.setSingleShot(True)
        t.timeout.connect(self.update_memory_cover_cache_size)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return QListView.viewportEvent(self, ev)

    @property
    def device_pixel_ratio(self):
        try:
            return self.devicePixelRatioF()
        except AttributeError:
            return self.devicePixelRatio()

    @property
    def first_visible_row(self):
        geom = self.viewport().geometry()
        for y in range(geom.top(), (self.spacing() * 2) + geom.top(), 5):
            for x in range(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 range(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5):
            for x in range(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 range(self.first_visible_row or 0, self.last_visible_row
                       or (m.count() - 1)):
            self.update(m.index(r, 0))

    def start_view_animation(self, index):
        d = self.delegate
        if d.animating is None and not config['disable_animations']:
            d.animating = index
            d.animation.start()

    def double_clicked(self, index):
        self.start_view_animation(index)
        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']
        tex = gprefs['cover_grid_texture']
        pal = self.palette()
        pal.setColor(pal.Base, QColor(r, g, b))
        self.setPalette(pal)
        ss = ''
        if tex:
            from calibre.gui2.preferences.texture_chooser import texture_path
            path = texture_path(tex)
            if path:
                path = os.path.abspath(path).replace(os.sep, '/')
                ss += 'background-image: url({});'.format(path)
                ss += 'background-attachment: fixed;'
                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)
        dark = max(r, g, b) < 115
        col = '#eee' if dark else '#111'
        ss += 'color: {};'.format(col)
        self.delegate.highlight_color = QColor(col)
        self.setStyleSheet('QListView {{ {} }}'.format(ss))

    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 or
                gprefs['show_emblems'] != self.delegate.original_show_emblems
                or gprefs['emblem_size'] != self.delegate.orginal_emblem_size
                or gprefs['emblem_position'] !=
                self.delegate.orginal_emblem_position):
            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.set_thumbnail_cache_image_size()
        cs = gprefs['cover_grid_disk_cache_size']
        if (cs * (1024**2)) != self.thumbnail_cache.max_size:
            self.thumbnail_cache.set_size(cs)
        self.update_memory_cover_cache_size()

    def set_thumbnail_cache_image_size(self):
        dpr = self.device_pixel_ratio
        self.thumbnail_cache.set_thumbnail_size(
            int(dpr * self.delegate.cover_size.width()),
            int(dpr * self.delegate.cover_size.height()))

    def resizeEvent(self, ev):
        self._ncols = None
        self.resize_timer.start()
        return QListView.resizeEvent(self, ev)

    def update_memory_cover_cache_size(self):
        try:
            sz = self.delegate.item_size
        except AttributeError:
            return
        rows, cols = self.width() // sz.width(), self.height() // sz.height()
        num = (rows + 1) * (cols + 1)
        limit = max(100,
                    num * max(2, gprefs['cover_grid_cache_size_multiple']))
        if limit != self.delegate.cover_cache.limit:
            self.delegate.cover_cache.set_limit(limit)

    def shown(self):
        self.update_memory_cover_cache_size()
        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
        dpr = self.device_pixel_ratio
        page_width = int(dpr * self.delegate.cover_size.width())
        page_height = int(dpr * self.delegate.cover_size.height())
        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')
            p.setDevicePixelRatio(dpr)
            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, page_width, page_height)
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(
                            nwidth, nheight,
                            Qt.AspectRatioMode.IgnoreAspectRatio,
                            Qt.TransformationMode.SmoothTransformation)
                        p.setDevicePixelRatio(dpr)
                    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[0] - i_x[1]):
            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 selectAll(self):
        # We re-implement this to ensure that only indexes from column 0 are
        # selected. The base class implementation selects all columns. This
        # causes problems with selection syncing, see
        # https://bugs.launchpad.net/bugs/1236348
        m = self.model()
        sm = self.selectionModel()
        sel = QItemSelection(m.index(0, 0),
                             m.index(m.rowCount(QModelIndex()) - 1, 0))
        sm.select(sel, sm.ClearAndSelect)

    def set_current_row(self, row):
        sm = self.selectionModel()
        sm.setCurrentIndex(self.model().index(row, 0), sm.NoUpdate)

    def set_context_menu(self, menu):
        self.context_menu = menu

    def contextMenuEvent(self, event):
        if self.context_menu is None:
            return
        from calibre.gui2.main_window import clone_menu
        m = clone_menu(self.context_menu) if islinux else self.context_menu
        m.popup(event.globalPos())
        event.accept()

    def get_selected_ids(self):
        m = self.model()
        return [m.id(i) for i in self.selectionModel().selectedIndexes()]

    def restore_vpos(self, vpos):
        self.verticalScrollBar().setValue(vpos)

    def restore_hpos(self, hpos):
        pass

    def handle_mouse_press_event(self, ev):
        if QApplication.keyboardModifiers(
        ) & Qt.KeyboardModifier.ShiftModifier:
            # Shift-Click in QListView is broken. It selects extra items in
            # various circumstances, for example, click on some item in the
            # middle of a row then click on an item in the next row, all items
            # in the first row will be selected instead of only items after the
            # middle item.
            index = self.indexAt(ev.pos())
            if not index.isValid():
                return
            ci = self.currentIndex()
            sm = self.selectionModel()
            sm.setCurrentIndex(index, sm.NoUpdate)
            if not ci.isValid():
                return
            if not sm.hasSelection():
                sm.select(index, sm.ClearAndSelect)
                return
            cr = ci.row()
            tgt = index.row()
            top = self.model().index(min(cr, tgt), 0)
            bottom = self.model().index(max(cr, tgt), 0)
            sm.select(QItemSelection(top, bottom), sm.Select)
        else:
            return QListView.mousePressEvent(self, ev)

    def indices_for_merge(self, resolved=True):
        return self.selectionModel().selectedIndexes()

    def number_of_columns(self):
        # Number of columns currently visible in the grid
        if self._ncols is None:
            dpr = self.device_pixel_ratio
            width = int(dpr * self.delegate.cover_size.width())
            height = int(dpr * self.delegate.cover_size.height())
            step = max(10, self.spacing())
            for y in range(step, 2 * height, step):
                for x in range(step, 2 * width, step):
                    i = self.indexAt(QPoint(x, y))
                    if i.isValid():
                        for x in range(self.viewport().width() - step,
                                       self.viewport().width() - width, -step):
                            j = self.indexAt(QPoint(x, y))
                            if j.isValid():
                                self._ncols = j.row() - i.row() + 1
                                return self._ncols
        return self._ncols

    def keyPressEvent(self, ev):
        if handle_enter_press(self, ev, self.start_view_animation, False):
            return
        k = ev.key()
        if ev.modifiers() & Qt.KeyboardModifier.ShiftModifier and k in (
                Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up,
                Qt.Key.Key_Down):
            ci = self.currentIndex()
            if not ci.isValid():
                return
            c = ci.row()
            ncols = self.number_of_columns() or 1
            delta = {
                Qt.Key.Key_Left: -1,
                Qt.Key.Key_Right: 1,
                Qt.Key.Key_Up: -ncols,
                Qt.Key.Key_Down: ncols
            }[k]
            n = max(0, min(c + delta, self.model().rowCount(None) - 1))
            if n == c:
                return
            sm = self.selectionModel()
            rows = {i.row() for i in sm.selectedIndexes()}
            if rows:
                mi, ma = min(rows), max(rows)
                end = mi if c == ma else ma if c == mi else c
            else:
                end = c
            top = self.model().index(min(n, end), 0)
            bottom = self.model().index(max(n, end), 0)
            sm.select(QItemSelection(top, bottom), sm.ClearAndSelect)
            sm.setCurrentIndex(self.model().index(n, 0), sm.NoUpdate)
        else:
            return QListView.keyPressEvent(self, ev)

    @property
    def current_book(self):
        ci = self.currentIndex()
        if ci.isValid():
            try:
                return self.model().db.data.index_to_id(ci.row())
            except (IndexError, ValueError, KeyError, TypeError,
                    AttributeError):
                pass

    def current_book_state(self):
        return self.current_book

    def restore_current_book_state(self, state):
        book_id = state
        self.setFocus(Qt.FocusReason.OtherFocusReason)
        try:
            row = self.model().db.data.id_to_index(book_id)
        except (IndexError, ValueError, KeyError, TypeError, AttributeError):
            return
        self.set_current_row(row)
        self.select_rows((row, ))
        self.scrollTo(self.model().index(row, 0), self.PositionAtCenter)

    def marked_changed(self, old_marked, current_marked):
        changed = old_marked | current_marked
        m = self.model()
        for book_id in changed:
            try:
                self.update(m.index(m.db.data.id_to_index(book_id), 0))
            except ValueError:
                pass

    def moveCursor(self, action, modifiers):
        index = QListView.moveCursor(self, action, modifiers)
        if action in (QListView.MoveLeft,
                      QListView.MoveRight) and index.isValid():
            ci = self.currentIndex()
            if ci.isValid() and index.row() == ci.row():
                nr = index.row() + (1 if action == QListView.MoveRight else -1)
                if 0 <= nr < self.model().rowCount(QModelIndex()):
                    index = self.model().index(nr, 0)
        return index

    def selectionCommand(self, index, event):
        if event and event.type() == event.KeyPress and event.key() in (
                Qt.Key.Key_Home,
                Qt.Key.Key_End) and event.modifiers() & Qt.Modifier.CTRL:
            return QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows
        return super(GridView, self).selectionCommand(index, event)

    def wheelEvent(self, ev):
        if ev.phase() not in (Qt.ScrollPhase.ScrollUpdate, 0,
                              Qt.ScrollPhase.ScrollMomentum):
            return
        number_of_pixels = ev.pixelDelta()
        number_of_degrees = ev.angleDelta() / 8.0
        b = self.verticalScrollBar()
        if number_of_pixels.isNull() or islinux:
            # pixelDelta() is broken on linux with wheel mice
            dy = number_of_degrees.y() / 15.0
            # Scroll by approximately half a row
            dy = int(math.ceil((dy) * b.singleStep() / 2.0))
        else:
            dy = number_of_pixels.y()
        if abs(dy) > 0:
            b.setValue(b.value() - dy)

    def paintEvent(self, ev):
        dpr = self.device_pixel_ratio
        page_width = int(dpr * self.delegate.cover_size.width())
        page_height = int(dpr * self.delegate.cover_size.height())
        size_changed = self.thumbnail_cache.set_thumbnail_size(
            page_width, page_height)
        if size_changed:
            self.delegate.cover_cache.clear()

        return super(GridView, self).paintEvent(ev)
Ejemplo n.º 11
0
class LocationCompleterView(QWidget):
    def __init__(self):
        super().__init__(None)
        self._view = None  # QListView
        self._delegate = None  # LocationCompleterDelegate
        self._searchEnginesLayout = None  # QHBoxLayout
        self._resizeHeight = -1
        self._resizeTimer = None  # QTimer
        self._forceResize = True

        self.setAttribute(Qt.WA_ShowWithoutActivating)
        self.setAttribute(Qt.WA_X11NetWmWindowTypeCombo)

        if gVar.app.platformName() == 'xcb':
            self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint
                                | Qt.BypassWindowManagerHint)
        else:
            self.setWindowFlags(Qt.Popup)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._view = QListView(self)
        layout.addWidget(self._view)

        self._view.setUniformItemSizes(True)
        self._view.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self._view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self._view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._view.setSelectionBehavior(QAbstractItemView.SelectRows)
        self._view.setSelectionMode(QAbstractItemView.SingleSelection)

        self._view.setMouseTracking(True)
        gVar.app.installEventFilter(self)

        self._delegate = LocationCompleterDelegate(self)
        self._view.setItemDelegate(self._delegate)

        searchFrame = QFrame(self)
        searchFrame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        searchLayout = QHBoxLayout(searchFrame)
        searchLayout.setContentsMargins(10, 4, 4, 4)

        searchSettingsButton = ToolButton(self)
        searchSettingsButton.setIcon(IconProvider.settingsIcon())
        searchSettingsButton.setToolTip(_('Manage Search Engines'))
        searchSettingsButton.setAutoRaise(True)
        searchSettingsButton.setIconSize(QSize(16, 16))
        searchSettingsButton.clicked.connect(self.searchEnginesDialogRequested)

        searchLabel = QLabel(_('Search with:'))
        self._searchEnginesLayout = QHBoxLayout()

        self._setupSearchEngines()
        gVar.app.searchEnginesManager().enginesChanged.connect(
            self._setupSearchEngines)

        searchLayout.addWidget(searchLabel)
        searchLayout.addLayout(self._searchEnginesLayout)
        searchLayout.addStretch()
        searchLayout.addWidget(searchSettingsButton)

        layout.addWidget(searchFrame)

    def model(self):
        '''
        @return: QAbstractItemModel
        '''
        return self._view.model()

    def setModel(self, model):
        '''
        @param model QAbstractItemModel
        '''
        self._view.setModel(model)

    def selectionModel(self):
        '''
        @return: QItemSelectionModel
        '''
        return self._view.selectionModel()

    def currentIndex(self):
        '''
        @return: QModelIndex
        '''
        return self._view.currentIndex()

    def setCurrentIndex(self, index):
        '''
        @param index QModelIndex
        '''
        self._view.setCurrentIndex(index)

    def adjustSize(self):
        maxItemsCount = 12
        newHeight = self._view.sizeHintForRow(0) * min(maxItemsCount,
                                                       self.model().rowCount())

        if not self._resizeTimer:
            self._resizeTimer = QTimer(self)
            self._resizeTimer.setInterval(200)

            def func():
                if self._resizeHeight > 0:
                    self._view.setFixedHeight(self._resizeHeight)
                    self.setFixedHeight(self.sizeHint().height())
                self._resizeHeight = -1

            self._resizeTimer.timeout.connect(func)

        if not self._forceResize:
            if newHeight == self._resizeHeight:
                return
            elif newHeight == self._view.height():
                self._resizeHeight = -1
                return
            elif newHeight < self._view.height():
                self._resizeHeight = newHeight
                self._resizeTimer.start()
                return

        self._resizeHeight = -1
        self._forceResize = False
        self._view.setFixedHeight(newHeight)
        self.setFixedHeight(self.sizeHint().height())

    # override
    def eventFilter(self, obj, event):  # noqa C901
        '''
        @param obj QObject
        @param event QEvent
        @return: bool
        '''
        # Event filter based on QCompleter::eventFilter from qcompleter.cpp
        if obj == self or obj == self._view or not self.isVisible():
            return False

        evtType = event.type()
        if obj == self._view.viewport():
            if evtType == QEvent.MouseButtonRelease:
                # QMouseEvent
                e = event
                index = self._view.indexAt(e.pos())
                if not index.isValid():
                    return False

                # Qt::MouseButton
                button = e.button()
                # Qt::KeyboardModifiers
                modifiers = e.modifiers()

                if button == Qt.LeftButton and modifiers == Qt.NoModifier:
                    self.indexActivated.emit(index)
                    return True

                if button == Qt.MiddleButton or (button == Qt.LeftButton
                                                 and modifiers
                                                 == Qt.ControlModifier):
                    self.indexCtrlActivated.emit(index)
                    return True

                if button == Qt.LeftButton and modifiers == Qt.ShiftModifier:
                    self.indexShiftActivated.emit(index)
                    return True

            return False

        if evtType == QEvent.KeyPress:
            # QKeyEvent
            keyEvent = event
            evtKey = keyEvent.key()
            modifiers = keyEvent.modifiers()
            index = self._view.currentIndex()
            item = self.model().index(0, 0)
            if item.data(LocationCompleterModel.VisitSearchItemRole):
                visitSearchIndex = item
            else:
                visitSearchIndex = QModelIndex()

            if (evtKey == Qt.Key_Up or evtKey == Qt.Key_Down) and \
                    self._view.currentIndex() != index:
                self._view.setCurrentIndex(index)  # TODO: ?

            if evtKey in (Qt.Key_Return, Qt.Key_Enter):
                if index.isValid():
                    if modifiers == Qt.NoModifier or modifiers == Qt.KeypadModifier:
                        self.indexActivated.emit(index)
                        return True

                    if modifiers == Qt.ControlModifier:
                        self.indexCtrlActivated.emit(index)
                        return True

                    if modifiers == Qt.ShiftModifier:
                        self.indexShiftActivated.emit(index)
                        return True

            elif evtKey == Qt.Key_End:
                if modifiers & Qt.ControlModifier:
                    self._view.setCurrentIndex(self.model().index(
                        self.model().rowCount() - 1, 0))
                    return True
                else:
                    self.close()

            elif evtKey == Qt.Key_Home:
                if modifiers & Qt.ControlModifier:
                    self._view.setCurrentIndex(self.model().index(0, 0))
                    self._view.scrollToTop()
                    return True
                else:
                    self.close()

            elif evtKey == Qt.Key_Escape:
                self.close()
                return True

            elif evtKey == Qt.Key_F4:
                if modifiers == Qt.AltModifier:
                    self.close()
                    return False

            elif evtKey in (Qt.Key_Tab, Qt.Key_Backtab):
                if modifiers != Qt.NoModifier and modifiers != Qt.ShiftModifier:
                    return False
                isBack = evtKey == Qt.Key_Backtab
                if evtKey == Qt.Key_Tab and modifiers == Qt.ShiftModifier:
                    isBack = True
                ev = QKeyEvent(QKeyEvent.KeyPress, isBack and Qt.Key_Up
                               or Qt.Key_Down, Qt.NoModifier)
                QApplication.sendEvent(self.focusProxy(), ev)
                return True

            elif evtKey in (Qt.Key_Up, Qt.Key_PageUp):
                if modifiers != Qt.NoModifier:
                    return False
                step = evtKey == Qt.Key_PageUp and 5 or 1
                if not index.isValid() or index == visitSearchIndex:
                    rowCount = self.model().rowCount()
                    lastIndex = self.model().index(rowCount - 1, 0)
                    self._view.setCurrentIndex(lastIndex)
                elif index.row() == 0:
                    self._view.setCurrentIndex(QModelIndex())
                else:
                    row = max(0, index.row() - step)
                    self._view.setCurrentIndex(self.model().index(row, 0))
                return True

            elif evtKey in (Qt.Key_Down, Qt.Key_PageDown):
                if modifiers != Qt.NoModifier:
                    return False
                step = evtKey == Qt.Key_PageDown and 5 or 1
                if not index.isValid():
                    firstIndex = self.model().index(0, 0)
                    self._view.setCurrentIndex(firstIndex)
                elif index != visitSearchIndex and index.row(
                ) == self.model().rowCount() - 1:
                    self._view.setCurrentIndex(visitSearchIndex)
                    self._view.scrollToTop()
                else:
                    row = min(self.model().rowCount() - 1, index.row() + step)
                    self._view.setCurrentIndex(self.model().index(row, 0))
                return True

            elif evtKey == Qt.Key_Delete:
                if index != visitSearchIndex and self._view.viewport().rect(
                ).contains(self._view.visualRect(index)):
                    self.indexDeleteRequested.emit(index)
                    return True

            elif evtKey == Qt.Key_Shift:
                self._delegate.setForceVisitItem(True)
                self._view.viewport().update()

            # end of switch evtKey

            if self.focusProxy():
                self.focusProxy().event(keyEvent)

            return True

        elif evtType == QEvent.KeyRelease:
            if event.key() == Qt.Key_Shift:
                self._delegate.setForceVisitItem(False)
                self._view.viewport().update()
                return True

        elif evtType in (QEvent.Wheel, QEvent.MouseButtonPress):
            if not self.underMouse():
                self.close()
                return False

        elif evtType == QEvent.FocusOut:
            # QFocusEvent
            focusEvent = event
            reason = focusEvent.reason()
            if reason != Qt.PopupFocusReason and reason != Qt.MouseFocusReason:
                self.close()

        elif evtType in (QEvent.Move, QEvent.Resize):
            w = obj
            if isinstance(w, QWidget) and w.isWindow() and self.focusProxy(
            ) and w == self.focusProxy().window():
                self.close()

        # end of switch evtType
        return False

    # Q_SIGNALS
    closed = pyqtSignal()
    searchEnginesDialogRequested = pyqtSignal()
    loadRequested = pyqtSignal(LoadRequest)

    indexActivated = pyqtSignal(QModelIndex)
    indexCtrlActivated = pyqtSignal(QModelIndex)
    indexShiftActivated = pyqtSignal(QModelIndex)
    indexDeleteRequested = pyqtSignal(QModelIndex)

    # public Q_SLOTS:
    def close(self):
        self.hide()
        self._view.verticalScrollBar().setValue(0)
        self._delegate.setForceVisitItem(False)
        self._forceResize = True

        self.closed.emit()

    # private:
    def _setupSearchEngines(self):
        for idx in (range(self._searchEnginesLayout.count())):
            item = self._searchEnginesLayout.takeAt(0)
            item.deleteLater()

        engines = gVar.app.searchEnginesManager().allEngines()
        for engine in engines:
            button = ToolButton(self)
            button.setIcon(engine.icon)
            button.setToolTip(engine.name)
            button.setAutoRaise(True)
            button.setIconSize(QSize(16, 16))

            def func():
                text = self.model().index(0, 0).data(
                    LocationCompleterModel.SearchStringRole)
                self.loadRequested.emit(
                    gVar.app.searchEngineManager().searchResult(engine, text))

            button.clicked.connect(func)
            self._searchEnginesLayout.addWidget(button)
Ejemplo n.º 12
0
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()
Ejemplo n.º 13
0
class DeviceBar(QWidget):
    """ DeviceBar

        Signals:
            onDeviceUpdated()
            onDeviceSelected(str) # str = id
            onDeviceChanged(str) # str = id

    """

    onDeviceUpdated = pyqtSignal(str, name="onDeviceUpdated")
    onDeviceSelected = pyqtSignal(str, name="onDeviceSelected")
    onDeviceChanged = pyqtSignal(str, name="onDeviceChanged")

    def __init__(self, parent=None, device_type='usb'):
        super().__init__(parent=parent)

        # dont show for local
        if device_type != 'usb':
            return

        self.parent = parent
        self.wait_for_devtype = device_type
        self.is_waiting = True
        self._adb = Adb()

        if not self._adb.min_required:
            raise Exception('Adb missing or no Device')

        self._git = Git()
        self.setAutoFillBackground(True)
        self.setStyleSheet(
            'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;'
        )
        self.setup()
        self._timer = QTimer()
        self._timer.setInterval(500)
        self._timer.timeout.connect(self._on_timer)
        self._timer.start()
        self._timer_step = 0
        frida.get_device_manager().on('added', self._on_device)
        frida.get_device_manager().on('removed', self._on_device)
        self.devices_thread = DevicesUpdateThread(self)
        self.devices_thread.onAddDevice.connect(self.on_add_deviceitem)
        self.devices_thread.onDevicesUpdated.connect(self._on_devices_finished)
        self._update_thread = FridaUpdateThread(self)
        self._update_thread._adb = self._adb
        self._update_thread.onStatusUpdate.connect(self._update_statuslbl)
        self._update_thread.onFinished.connect(self._frida_updated)
        self._update_thread.onError.connect(self._on_download_error)
        self.updated_frida_version = ''
        self.updated_frida_assets_url = {}
        self._device_id = None
        self._devices = []
        remote_frida = self._git.get_frida_version()
        if remote_frida is None:
            self.updated_frida_version = ''
            self.updated_frida_assets_url.clear()
        else:
            self.updated_frida_version = remote_frida['tag_name']
            for asset in remote_frida['assets']:
                if 'name' not in asset:
                    continue

                asset_name = asset['name']

                if not asset_name.startswith('frida-server-'):
                    continue

                if not 'android' in asset_name:
                    continue

                tag_start = asset_name.index('android-')
                if asset_name.index('server') >= 0:
                    tag = asset_name[tag_start + 8:-3]
                    self.updated_frida_assets_url[tag] = asset[
                        'browser_download_url']

    def setup(self):
        """ Setup ui
        """
        h_box = QHBoxLayout()
        h_box.setContentsMargins(0, 0, 0, 0)
        self.update_label = QLabel('Waiting for Device')
        self.update_label.setFixedWidth(self.parent.width())
        self.update_label.setOpenExternalLinks(True)
        self.update_label.setTextFormat(Qt.RichText)
        self.update_label.setFixedHeight(35)
        self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self._install_btn = QPushButton('Install Frida', self.update_label)
        self._install_btn.setStyleSheet('padding: 0; border-color: white;')
        self._install_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._install_btn.clicked.connect(self._on_install_btn)
        self._install_btn.setVisible(False)
        self._start_btn = QPushButton('Start Frida', self.update_label)
        self._start_btn.setStyleSheet('padding: 0; border-color: white;')
        self._start_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                    25)
        self._start_btn.clicked.connect(self._on_start_btn)
        self._start_btn.setVisible(False)
        self._update_btn = QPushButton('Update Frida', self.update_label)
        self._update_btn.setStyleSheet('padding: 0; border-color: white;')
        self._update_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                     25)
        self._update_btn.clicked.connect(self._on_install_btn)
        self._update_btn.setVisible(False)
        self._restart_btn = QPushButton('Restart Frida', self.update_label)
        self._restart_btn.setStyleSheet('padding: 0; border-color: white;')
        self._restart_btn.setGeometry(self.update_label.width() - 110, 5, 100,
                                      25)
        self._restart_btn.clicked.connect(self._on_restart_btn)
        self._restart_btn.setVisible(False)
        self._devices_combobox = QComboBox(self.update_label)
        self._devices_combobox.setStyleSheet(
            'padding: 2px 5px; border-color: white;')
        self._devices_combobox.setGeometry(self.update_label.width() - 320, 5,
                                           200, 25)
        self._devices_combobox.currentIndexChanged.connect(
            self._on_device_changed)
        self._devices_combobox.setVisible(False)
        h_box.addWidget(self.update_label)
        self.setLayout(h_box)

    def on_add_deviceitem(self, device_ident):
        """ Adds an Item to the DeviceComboBox
        """
        if device_ident['type'] == self.wait_for_devtype:
            if device_ident['name'] not in self._devices:
                self._devices.append(device_ident)
            self._timer_step = -1
            self.is_waiting = False

    def _on_device_changed(self, index):
        device = None
        device_id = self._devices_combobox.itemData(index)
        if device_id:
            try:
                device = frida.get_device(device_id)
            except:
                return

            if device:
                self._device_id = device.id
                self._check_device(device)
                self.onDeviceChanged.emit(self._device_id)

    def _check_device(self, frida_device):
        self.update_label.setStyleSheet('background-color: crimson;')
        self._install_btn.setVisible(False)
        self._update_btn.setVisible(False)
        self._start_btn.setVisible(False)
        self._restart_btn.setVisible(False)
        self._adb.device = frida_device.id
        self._device_id = frida_device.id
        if self._adb.available():
            self.update_label.setText('Device: ' + frida_device.name)
            # try getting frida version
            device_frida = self._adb.get_frida_version()
            # frida not found show install button
            if device_frida is None:
                self._install_btn.setVisible(True)
            else:
                # frida is old show update button
                if self.updated_frida_version != device_frida:
                    self._start_btn.setVisible(True)
                    self._update_btn.setVisible(False)
                    # old frida is running allow use of this version
                    if self._adb.is_frida_running():
                        self._start_btn.setVisible(False)
                        if self.updated_frida_assets_url:
                            self._update_btn.setVisible(True)
                        self.update_label.setStyleSheet(
                            'background-color: yellowgreen;')
                        self.onDeviceUpdated.emit(frida_device.id)
                # frida not running show start button
                elif device_frida and not self._adb.is_frida_running():
                    self._start_btn.setVisible(True)
                # frida is running with last version show restart button
                elif device_frida and self._adb.is_frida_running():
                    self.update_label.setStyleSheet(
                        'background-color: yellowgreen;')
                    self._restart_btn.setVisible(True)
                    self.onDeviceUpdated.emit(frida_device.id)

        elif self._adb.non_root_available():
            self.update_label.setText('Device: ' + frida_device.name +
                                      ' (NOROOT!)')
            self.onDeviceUpdated.emit(frida_device.id)

    def _on_devices_finished(self):
        if self._devices:
            if len(self._devices) > 1:
                self._devices_combobox.clear()
                self._devices_combobox.setVisible(True)
                self.update_label.setText('Please select the Device: ')
                for device in self._devices:
                    self._devices_combobox.addItem(device['name'],
                                                   device['id'])
            else:
                self._devices_combobox.setVisible(False)
                try:
                    device = frida.get_device(self._devices[0]['id'])
                    self._check_device(device)
                except:
                    pass

    def _on_timer(self):
        if self._timer_step == -1:
            self._timer.stop()
            return

        if self._timer_step == 0:
            self.update_label.setText(self.update_label.text() + ' .')
            self._timer_step = 1
        elif self._timer_step == 1:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 2
        elif self._timer_step == 2:
            self.update_label.setText(self.update_label.text() + '.')
            self._timer_step = 3
        else:
            self.update_label.setText(
                self.update_label.text()[:-self._timer_step])
            self._timer_step = 0
            if self.is_waiting and self.devices_thread is not None:
                if not self.devices_thread.isRunning():
                    self.devices_thread.start()

    def _on_download_error(self, text):
        self._timer_step = -1
        self.update_label.setStyleSheet('background-color: crimson;')
        self.update_label.setText(text)
        self._install_btn.setVisible(True)
        self._update_btn.setVisible(False)

    def _on_device(self):
        self.update_label.setText('Waiting for Device ...')
        self._timer_step = 3
        self.is_waiting = True
        self._on_timer()

    def _on_install_btn(self):
        # urls are empty
        if not self.updated_frida_assets_url:
            return

        arch = self._adb.get_device_arch()
        request_url = ''

        if arch is not None and len(arch) > 1:
            arch = arch.join(arch.split())

            if arch == 'arm64' or arch == 'arm64-v8a':
                request_url = self.updated_frida_assets_url['arm64']
            elif arch == 'armeabi-v7a':
                request_url = self.updated_frida_assets_url['arm']
            else:
                if arch in self.updated_frida_assets_url:
                    request_url = self.updated_frida_assets_url[arch]

            try:
                if self._adb.available() and request_url.index(
                        'https://') == 0:
                    self._install_btn.setVisible(False)
                    self._update_btn.setVisible(False)
                    qApp.processEvents()
                    if self._update_thread is not None:
                        if not self._update_thread.isRunning():
                            self._update_thread.frida_update_url = request_url
                            self._update_thread.adb = self._adb
                            self._update_thread.start()

            except ValueError:
                # something wrong in .git_cache folder
                print("request_url not set")

    def _update_statuslbl(self, text):
        self._timer.stop()
        self._timer_step = 0
        self._timer.start()
        self.update_label.setText(text)

    def _frida_updated(self):
        # self._timer_step = 3
        # self.is_waiting = True
        self._on_devices_finished()

    def _on_start_btn(self):
        if self._adb.available():
            self._start_btn.setVisible(False)
            qApp.processEvents()
            if self._adb.start_frida():
                # self.onDeviceUpdated.emit(self._device_id)
                self._on_devices_finished()
            else:
                self._start_btn.setVisible(True)

    def _on_restart_btn(self):
        if self._adb.available():
            self._restart_btn.setVisible(False)
            qApp.processEvents()
            if self._adb.start_frida(restart=True):
                self._restart_btn.setVisible(True)
                # self.onDeviceUpdated.emit(self._device_id)
                self._on_devices_finished()
class DashboardQWidget(QWidget):
    """
        Class who manage Host and Services resume QWidgets with number of:

        * Hosts: ``UP``, ``UNREACHABLE``, ``DOWN``
        * Services: ``OK``, ``WARNING``, ``CRITICAL``, ``UNKNWON``, ``UNREACHABLE``
        * Hosts and services: ``NOT_MONITORED``, ``ACKNOWLEDGED``, ``DOWNTIMED``

    """

    def __init__(self, parent=None):
        super(DashboardQWidget, self).__init__(parent)
        # Fields
        self.layout = QGridLayout()
        self.items_nb = {
            'hosts_nb': QLabel(),
            'services_nb': QLabel()
        }
        self.hosts_labels = {
            'hosts_up': QLabel(),
            'hosts_unreachable': QLabel(),
            'hosts_down': QLabel(),
            'hosts_not_monitored': QLabel(),
            'acknowledge': QLabel(),
            'downtime': QLabel()
        }
        self.services_labels = {
            'services_ok': QLabel(),
            'services_warning': QLabel(),
            'services_critical': QLabel(),
            'services_unknown': QLabel(),
            'services_unreachable': QLabel(),
            'services_not_monitored': QLabel(),
            'acknowledge': QLabel(),
            'downtime': QLabel()
        }
        self.hosts_buttons = {
            'hosts_up': QPushButton(),
            'hosts_unreachable': QPushButton(),
            'hosts_down': QPushButton(),
            'hosts_not_monitored': QPushButton(),
            'acknowledge': QPushButton(),
            'downtime': QPushButton()
        }
        self.services_buttons = {
            'services_ok': QPushButton(),
            'services_warning': QPushButton(),
            'services_critical': QPushButton(),
            'services_unknown': QPushButton(),
            'services_unreachable': QPushButton(),
            'services_not_monitored': QPushButton(),
            'acknowledge': QPushButton(),
            'downtime': QPushButton()
        }
        self.refresh_timer = QTimer()
        self.setFixedHeight(85)

    def initialize(self):
        """
        Initialize QWidget

        """

        self.setLayout(self.layout)
        self.layout.setContentsMargins(0, 0, 0, 0)

        self.get_host_resume_widget()
        self.get_services_resume_widget()

        self.update_dashboard()

        update_dashboard = int(settings.get_config('Alignak-app', 'update_dashboard')) * 1000
        self.refresh_timer.setInterval(update_dashboard)
        self.refresh_timer.start()
        self.refresh_timer.timeout.connect(self.update_dashboard)

    def get_host_resume_widget(self):
        """
        Return Host resume QWidget

        """

        self.layout.addWidget(QLabel(_('<b>Hosts:</b>')), 0, 0, 1, 1)
        self.items_nb['hosts_nb'].setObjectName('subtitle')
        self.layout.addWidget(self.items_nb['hosts_nb'], 1, 0, 1, 1)
        row = 1
        for icon in Host.get_available_icons():
            self.hosts_buttons[icon].setIcon(QIcon(settings.get_image(icon)))
            self.hosts_buttons[icon].setFixedSize(48, 24)
            self.hosts_buttons[icon].setObjectName(icon)
            self.hosts_buttons[icon].setToolTip(
                _('Hosts %s. See in WebUI ?') % icon.replace('hosts_', '').upper()
            )
            self.hosts_buttons[icon].clicked.connect(
                lambda: self.open_item_type_url('hosts')
            )
            self.layout.addWidget(self.hosts_buttons[icon], 0, row, 1, 1)
            self.layout.setAlignment(self.hosts_buttons[icon], Qt.AlignCenter)
            self.hosts_labels[icon].setObjectName(icon)
            self.layout.addWidget(self.hosts_labels[icon], 1, row, 1, 1)
            self.layout.setAlignment(self.hosts_labels[icon], Qt.AlignCenter)
            row += 1

    def get_services_resume_widget(self):
        """
        Return Services resume QWidget

        """

        self.layout.addWidget(QLabel('<b>Services:</b>'), 2, 0, 1, 1)
        self.items_nb['services_nb'].setObjectName('subtitle')
        self.layout.addWidget(self.items_nb['services_nb'], 3, 0, 1, 1)
        row = 1
        for icon in Service.get_available_icons():
            self.services_buttons[icon].setIcon(QIcon(settings.get_image(icon)))
            self.services_buttons[icon].setFixedSize(48, 24)
            self.services_buttons[icon].setObjectName(icon)
            self.services_buttons[icon].setToolTip(
                _('Services %s. See in WebUI ?') % icon.replace('services_', '').upper()
            )
            self.services_buttons[icon].clicked.connect(
                lambda: self.open_item_type_url('services')
            )
            self.layout.addWidget(self.services_buttons[icon], 2, row, 1, 1)
            self.layout.setAlignment(self.services_buttons[icon], Qt.AlignCenter)
            self.services_labels[icon].setObjectName(icon)
            self.layout.addWidget(self.services_labels[icon], 3, row, 1, 1)
            self.layout.setAlignment(self.services_labels[icon], Qt.AlignCenter)
            row += 1

    def open_item_type_url(self, item_type):
        """
        Retrieve sender to send right endpoint to open_url() function for item type

        :param item_type: type of item: hosts | services
        :type item_type: str
        """

        endpoint = self.sender().objectName()

        open_url('%s%s' % (item_type, get_url_endpoint_from_icon_name(endpoint)))

    def update_dashboard(self):
        """
        Update number of items in dashboard

        """

        synthesis = data_manager.get_synthesis_count()

        hosts_sum = 0
        for item in synthesis['hosts']:
            hosts_sum += synthesis['hosts'][item]
        services_sum = 0
        for item in synthesis['services']:
            services_sum += synthesis['services'][item]

        # Hosts percentages
        self.items_nb['hosts_nb'].setText("%d hosts" % hosts_sum)
        for icon in Host.get_available_icons():
            host_nb = synthesis['hosts'][icon.replace('hosts_', '')]
            percent = 0.0
            try:
                percent = float(host_nb) * 100.0 / float(hosts_sum)
            except ZeroDivisionError:
                pass
            item_text = '%d (%.02f%%)' % (host_nb, percent)
            self.hosts_labels[icon].setText(item_text)

        # Services percentage
        self.items_nb['services_nb'].setText("%d services" % services_sum)
        for icon in Service.get_available_icons():
            service_nb = synthesis['services'][icon.replace('services_', '')]
            percent = 0.0
            try:
                percent = float(service_nb) * 100.0 / float(services_sum)
            except ZeroDivisionError:
                pass
            item_text = '%d (%.01f%%)' % (service_nb, percent)
            self.services_labels[icon].setText(item_text)

        for button in self.hosts_buttons:
            if settings.get_config('Alignak', 'webui'):
                self.hosts_buttons[button].setEnabled(True)
                self.hosts_buttons[button].setToolTip(
                    _('Hosts %s. See in WebUI ?') % button.replace('hosts_', '').upper()
                )
            else:
                self.hosts_buttons[button].setToolTip(
                    _("Hosts %s. WebUI is not set in configuration file.") % button.replace(
                        'hosts_', '').upper()
                )

        for button in self.services_buttons:
            if settings.get_config('Alignak', 'webui'):
                self.services_buttons[button].setEnabled(True)
                self.services_buttons[button].setToolTip(
                    _('Services %s. See in WebUI ?') % button.replace('services_', '').upper()
                )
            else:
                self.services_buttons[button].setToolTip(
                    _("Services %s. WebUI is not set in configuration file.") % button.replace(
                        'services_', '').upper()
                )
Ejemplo n.º 15
0
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.Link, theme_color(theme, "Link", "fg"))
        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))
            )
            or ""
        )
        try:
            result = json.loads(result)
        except ValueError:
            result = None
        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):
        self.update_data()

    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(for_position_sync=False)
            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"])
class ServiceDataQWidget(QWidget):
    """
        Class who create QWidget with service data
    """

    def __init__(self, parent=None):
        super(ServiceDataQWidget, self).__init__(parent)
        self.setMinimumSize(460, 230)
        # Fields
        self.refresh_timer = QTimer()
        self.service_item = None
        self.labels = {
            'service_icon': QLabel(),
            'service_name': QLabel(),
            'ls_last_check': QLabel(),
            'ls_output': QLabel(),
        }
        self.buttons = {
            'acknowledge': QPushButton(),
            'downtime': QPushButton()
        }
        self.actions_widget = ActionsQWidget()

    def initialize(self):
        """
        Initialize QWidget

        """

        layout = QGridLayout()
        self.setLayout(layout)

        layout.addWidget(self.get_service_icon_widget(), 0, 0, 1, 1)
        layout.addWidget(self.get_actions_widget(), 0, 1, 1, 1)
        layout.addWidget(self.get_last_check_widget(), 1, 0, 1, 2)

        update_service = int(settings.get_config('Alignak-app', 'update_service')) * 1000
        self.refresh_timer.setInterval(update_service)
        self.refresh_timer.start()
        self.refresh_timer.timeout.connect(self.periodic_refresh)

        self.hide()

    def get_service_icon_widget(self):
        """
        Return QWidget with its icon and name

        :return: widget with icon and name
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)

        # Host Icon
        layout.addWidget(self.labels['service_icon'])
        layout.setAlignment(self.labels['service_icon'], Qt.AlignCenter)

        # Host Name
        self.labels['service_name'].setObjectName('itemname')
        self.labels['service_name'].setWordWrap(True)
        layout.addWidget(self.labels['service_name'])
        layout.setAlignment(self.labels['service_name'], Qt.AlignCenter)

        return widget

    def get_last_check_widget(self):
        """
        Return QWidget with last check data

        :return: widget with last check data
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)

        # Title
        check_title = QLabel(_('My last check'))
        check_title.setObjectName('itemtitle')
        check_title.setFixedHeight(30)
        layout.addWidget(check_title, 0, 0, 1, 2)

        # When last check
        when_title = QLabel(_("When:"))
        when_title.setObjectName('title')
        layout.addWidget(when_title, 2, 0, 1, 1)

        layout.addWidget(self.labels['ls_last_check'], 2, 1, 1, 1)

        # Output
        output_title = QLabel(_("Output"))
        output_title.setObjectName('title')
        layout.addWidget(output_title, 3, 0, 1, 1)

        self.labels['ls_output'].setWordWrap(True)
        self.labels['ls_output'].setTextInteractionFlags(Qt.TextSelectableByMouse)
        output_scrollarea = QScrollArea()
        output_scrollarea.setWidget(self.labels['ls_output'])
        output_scrollarea.setWidgetResizable(True)
        output_scrollarea.setObjectName('output')
        layout.addWidget(output_scrollarea, 3, 1, 1, 1)

        return widget

    def get_actions_widget(self):
        """
        Return QWidget with actions buttons

        :return: widget with buttons
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)

        action_title = QLabel(_('Actions:'))
        action_title.setObjectName('title')
        layout.addWidget(action_title)

        self.actions_widget.initialize(self.service_item)
        layout.addWidget(self.actions_widget)

        layout.setAlignment(Qt.AlignCenter)

        return widget

    def update_widget(self, service):
        """
        Update ServiceDataQWidget

        :param service: Service item with its data
        :type service: alignak_app.core.models.service.Service
        """

        self.service_item = service

        monitored = self.service_item.data[
            'passive_checks_enabled'] + self.service_item.data['active_checks_enabled']
        icon_name = get_icon_name(
            'service',
            self.service_item.data['ls_state'],
            self.service_item.data['ls_acknowledged'],
            self.service_item.data['ls_downtimed'],
            monitored
        )
        icon_pixmap = QPixmap(settings.get_image(icon_name))

        self.labels['service_icon'].setPixmap(QPixmap(icon_pixmap))
        self.labels['service_icon'].setScaledContents(True)
        self.labels['service_icon'].setFixedSize(48, 48)
        self.labels['service_icon'].setToolTip(self.service_item.get_tooltip())
        self.labels['service_name'].setText(self.service_item.get_display_name())

        since_last_check = get_diff_since_last_timestamp(
            self.service_item.data['ls_last_check']
        )
        last_check_tooltip = get_date_fromtimestamp(self.service_item.data['ls_last_check'])
        self.labels['ls_last_check'].setText(since_last_check)
        self.labels['ls_last_check'].setToolTip(last_check_tooltip)
        self.labels['ls_output'].setText(self.service_item.data['ls_output'])

        self.actions_widget.item = self.service_item
        self.actions_widget.update_widget()
        self.show()

    def periodic_refresh(self):
        """
        Refresh QWidget periodically

        """

        if self.service_item:
            updated_service = data_manager.get_item('service', '_id', self.service_item.item_id)
            if updated_service:
                self.service_item = updated_service
            self.update_widget(self.service_item)
Ejemplo n.º 17
0
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.""")
        self._wlogy.toggled[bool].connect(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.setMass(0.5)
        self._whistzoom.setOrientation(Qt.Horizontal)
        self._whistzoom.setMaximumWidth(80)
        self._whistzoom.setRange(0, 10)
        self._whistzoom.setSingleStep(0.1)
        self._whistzoom.setPageStepCount(1)
        self._whistzoom.setTickCount(30)
        self._whistzoom.setTracking(False)
        self._whistzoom.valueChanged['double'].connect(self._zoomHistogramFinalize)
        self._whistzoom.wheelMoved['double'].connect(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)
        self._whistzoom_timer.timeout.connect(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 for help)"
        self._wlab_histpos = QLabel(self._wlab_histpos_text, self)
        help_font = QFont()
        help_font.setPointSize(8)
        self._wlab_histpos.setFont(help_font)
        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])
                wslicer.activated[int].connect(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)
                wminus.clicked.connect(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)
                wplus.clicked.connect(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)
        self._wlab_stats.setWordWrap(True)
        self._wlab_stats.setMinimumWidth(384)
        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)
            w.editingFinished.connect(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())
        self._wimap.currentIndexChanged[int].connect(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)
        self._wlogcycles_timer.timeout.connect(self._setIntensityLogCycles)
        lo1.addWidget(self._wlogcycles, 1, 1)
        # self._wlogcycles.setRange(1., 10)  # need to find 6.1.5 change from v5
        self._wlogcycles.setScale(1., 10)
        # self._wlogcycles.setStep(0.1)  # need to find 6.1.5 change from v5
        # self._wlogcycles.setScaleStepSize(0.1)
        self._wlogcycles.setTracking(False)
        self._wlogcycles.valueChanged.connect(self._setIntensityLogCycles)
        self._wlogcycles.sliderMoved.connect(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.setMinimumWidth(192)
        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())
        wlock.clicked[bool].connect(self._rc.lockDisplayRange)
        wlockall.clicked.connect(self._currier.curry(self._imgman.lockAllDisplayRanges, self._rc))
        wunlockall.clicked.connect(self._imgman.unlockAllDisplayRanges)
        self._rc.displayRangeLocked.connect(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,pyqtSignal("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)
        self._wcolmaps.activated[int].connect(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)
                cmap.colormapChanged.connect(self._currier.curry(self._previewColormapParameters, index, cmap))
                cmap.colormapPreviewed.connect(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.signalSlice.connect(self._updateImageSlice)
        self._rc.intensityMapChanged.connect(self._updateIntensityMap)
        self._rc.colorMapChanged.connect(self._updateColorMap)
        self._rc.dataSubsetChanged.connect(self._updateDataSubset)
        self._rc.displayRangeChanged.connect(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:
            btn.clicked.connect(callback)
        return btn

    #  def closeEvent (self,ev):
    #    ev.ignore()
    #    self.hide()

    def hide(self):
        self._geometry = self.geometry()
        QDialog.hide(self)
        self.parent().setVisible(False)

    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.reset_cursor()
        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=QwtPickerClickPointMachine(),
                     rubber_band=QwtPicker.VLineRubberBand, tracker_mode=QwtPicker.ActiveOnly, track=None):
            QwtPlotPicker.__init__(self, QwtPlot.xBottom, QwtPlot.yRight, rubber_band, tracker_mode,
                                   plot.canvas())

            self.setStateMachine(mode)
            self.plot = plot
            self.label = label
            self.track = track
            self.color = QColor(color)
            self.setRubberBandPen(QPen(self.color))
            self.setRubberBand(rubber_band)

        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.RenderAntialiased
            self.imap = None
            self.cmap = None
            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:
        """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.line.setRenderHint(QwtPlotItem.RenderAntialiased)
            self.color = color = color if isinstance(color, QColor) else QColor(color)
            self.line.setPen(QPen(color, linewidth, linestyle))
            self.marker = TiggerPlotMarker()
            self.marker.setRenderHint(QwtPlotItem.RenderAntialiased)
            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.setAxes(QwtPlot.xBottom, yaxis)
            self.marker.setAxes(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._histcurve1.setRenderHint(QwtPlotItem.RenderAntialiased)
        self._histcurve2 = TiggerPlotCurve()
        self._histcurve2.setRenderHint(QwtPlotItem.RenderAntialiased)
        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.setRenderHint(QwtPlotItem.RenderAntialiased)
        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.setRenderHint(QwtPlotItem.RenderAntialiased)
        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.setRenderHint(QwtPlotItem.RenderAntialiased)
        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()
        self._itfmarker.setRenderHint(QwtPlotItem.RenderAntialiased)
        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)
        self._hist_minpicker.selected.connect(self._selectLowLimit)
        self._hist_maxpicker = self.HistLimitPicker(self._histplot, "high: %(x).4g")
        self._hist_maxpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.RightButton)
        self._hist_maxpicker.selected.connect(self._selectHighLimit)
        self._hist_maxpicker1 = self.HistLimitPicker(self._histplot, "high: %(x).4g")
        self._hist_maxpicker1.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.ControlModifier)
        self._hist_maxpicker1.selected.connect(self._selectHighLimit)
        self._hist_zoompicker = self.HistLimitPicker(self._histplot, label="zoom",
                                                     tracker_mode=QwtPicker.AlwaysOn, track=self._trackHistCoordinates,
                                                     color="black",
                                                     mode=QwtPickerClickRectMachine(),
                                                     rubber_band=QwtPicker.RectRubberBand)
        self._hist_zoompicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.ShiftModifier)
        # self._hist_zoompicker.selected[QRectF].connect(self._zoomHistogramIntoRect)
        self._hist_zoompicker.selected.connect(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, int(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()
        busy.reset_cursor()

    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), ("\n 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()
        if not isinstance(busy, bool):
            busy.reset_cursor()

    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)
        busy.reset_cursor()

    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, curry=None):
        """
        Changes histogram limits by specified factor.
        curry=None is due to an error raised from the signal to zoom
        and is unused.
        """
        # 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, QwtLogScaleEngine())
            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()
Ejemplo n.º 18
0
class SequencePlayback():
    def __init__(self):
        self.imageSequence = []
        self.framerate = 0
        self.currentFrame = -1
        self.currentFrameIdx = -1
        self.endFrameIdx = 1
        self.state = ''

        self.timer = QTimer()
        self.timer.timerEvent = self.onTick
        Event.add(AudioPlaybackEvent.TICK, self.onTick)
        Event.add(PlaybackEvent.STATE, self.onState)
        self.setFramerate(24)
        pass

    def onState(self, state):
        self.state = state
        if state == PlayStateType.PLAY:
            self.play()
        elif state == PlayStateType.PAUSE:
            self.pause()
            pass
        pass

    def onTick(self, time):
        self.render()
        pass

    # def load(self, imagesPath=None):
    #     if imagesPath:
    #         for root, dirs, files in os.walk(imagesPath):
    #             for filespath in files:
    #                 filename = os.path.join(root, filespath).replace('\\', '/')
    #                 # todo support image ext
    #                 if filename.find('.png') < 0:
    #                     continue
    #                 simage = SImage(filename)
    #                 self.imageSequence.append(simage)
    #                 simage.frameIdx = len(self.imageSequence)
    #                 self.endFrameIdx = simage.frameIdx
    #                 print('[load img]: ', filename)
    #         Event.dis(ActionEvent.LOAD_SEQ, self.imageSequence)
    #         pass

    def play(self):
        if not self.timer.isActive():
            self.timer.start()
            pass
        pass

    def pause(self):
        if self.timer.isActive():
            self.timer.stop()
            pass
        pass

    def render(self):
        self.currentFrameIdx = (self.currentFrameIdx + 1) % self.endFrameIdx
        event = SequencePlaybackEvent()
        event.type = SequencePlaybackEvent.RENDER_FRAME
        event.frameIdx = self.currentFrameIdx
        Event.dis(SequencePlaybackEvent.RENDER_FRAME, event)

    def setFramerate(self, framerate):
        self.framerate = framerate
        self.timer.setInterval(1000 / self.framerate)
        pass
Ejemplo n.º 19
0
class Comments(HTMLDisplay):  # {{{

    def __init__(self, parent=None):
        HTMLDisplay.__init__(self, parent)
        self.setAcceptDrops(False)
        self.setMaximumWidth(300)
        self.setMinimumWidth(300)
        self.wait_timer = QTimer(self)
        self.wait_timer.timeout.connect(self.update_wait)
        self.wait_timer.setInterval(800)
        self.dots_count = 0
        self.anchor_clicked.connect(self.link_activated)

    def link_activated(self, url):
        from calibre.gui2 import open_url
        if url.scheme() in {'http', 'https'}:
            open_url(url)

    def show_wait(self):
        self.dots_count = 0
        self.wait_timer.start()
        self.update_wait()

    def update_wait(self):
        self.dots_count += 1
        self.dots_count %= 10
        self.dots_count = self.dots_count or 1
        self.setHtml(
            '<h2>'+_('Please wait')+
            '<br><span id="dots">{}</span></h2>'.format('.' * self.dots_count))

    def show_data(self, html):
        self.wait_timer.stop()

        def color_to_string(col):
            ans = '#000000'
            if col.isValid():
                col = col.toRgb()
                if col.isValid():
                    ans = unicode_type(col.name())
            return ans

        c = color_to_string(QApplication.palette().color(QPalette.Normal,
                        QPalette.WindowText))
        templ = '''\
        <html>
            <head>
            <style type="text/css">
                body, td {background-color: transparent; color: %s }
                a { text-decoration: none; }
                div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
                table { margin-bottom: 0; padding-bottom: 0; }
            </style>
            </head>
            <body>
            <div class="description">
            %%s
            </div>
            </body>
        <html>
        '''%(c,)
        self.setHtml(templ%html)

    def sizeHint(self):
        # This is needed, because on windows the dialog cannot be resized to
        # so that this widgets height become < sizeHint().height(). Qt sets the
        # sizeHint to (800, 600), which makes the dialog unusable on smaller
        # screens.
        return QSize(800, 300)
class LivestateQWidget(QWidget):
    """
        Class who display items livestate: hosts, services and number of problems
    """

    def __init__(self):
        super(LivestateQWidget, self).__init__()
        # Fields
        self.labels = {
            'host': None,
            'service': None,
            'problem': None
        }
        self.timer = QTimer()

    def initialize(self):
        """
        Initialize QWidget

        """

        layout = QHBoxLayout()
        self.setLayout(layout)

        item_types = ['host', 'service', 'problem']

        for item_type in item_types:
            item_widget = self.get_item_type_widget(item_type, 0, 0)
            layout.addWidget(item_widget)

        self.update_labels()

        update_livestate = int(settings.get_config('Alignak-app', 'update_livestate')) * 1000
        self.timer.setInterval(update_livestate)
        self.timer.start()
        self.timer.timeout.connect(self.update_labels)

    def get_item_type_widget(self, item_type, problem_nb, total_nb):
        """
        Create and return QWidget with backend data

        :param item_type: type of item: host, service, problem
        :type item_type: str
        :param problem_nb: number of problems for item type
        :type problem_nb: int
        :param total_nb: total number of item type
        :type total_nb: int
        :return: widget with its data
        :rtype: QWidget
        """

        layout = QVBoxLayout()
        widget = QWidget()
        widget.setLayout(layout)

        problem_label = QLabel('%d' % problem_nb)
        problem_label.setObjectName('ok')
        problem_label.setToolTip(
            _('Number of unhandled %s problems') % (
                item_type if 'problem' not in item_type else ''
            )
        )
        layout.addWidget(problem_label)
        layout.setAlignment(problem_label, Qt.AlignCenter)

        icon_label = QLabel()
        icon_label.setFixedSize(64, 64)
        icon_label.setScaledContents(True)
        layout.addWidget(icon_label)
        layout.setAlignment(icon_label, Qt.AlignCenter)

        total_label = QLabel('%d' % total_nb)
        total_label.setObjectName('total')
        total_label.setToolTip(
            _('Number of monitored %s') % (
                item_type if 'problem' not in item_type else 'items'
            )
        )
        layout.addWidget(total_label)
        layout.setAlignment(total_label, Qt.AlignCenter)

        self.labels[item_type] = {
            'problem': problem_label,
            'icon': icon_label,
            'total': total_label
        }

        return widget

    def update_labels(self):
        """
        Update QLabels of QWidget

        """

        items_and_problems = data_manager.get_items_and_problems()

        for item_type in self.labels:
            if items_and_problems[item_type]['problem'] < 1:
                self.labels[item_type]['problem'].setObjectName('ok')
            else:
                self.labels[item_type]['problem'].setObjectName('ko')

            self.labels[item_type]['problem'].style().unpolish(self.labels[item_type]['problem'])
            self.labels[item_type]['problem'].style().polish(self.labels[item_type]['problem'])
            self.labels[item_type]['problem'].update()

            self.labels[item_type]['problem'].setText(
                '%s' % str(items_and_problems[item_type]['problem'])
            )
            self.labels[item_type]['icon'].setPixmap(
                get_icon_item(item_type, items_and_problems[item_type]['problem'])
            )
            self.labels[item_type]['total'].setText(
                '%s' % str(items_and_problems[item_type]['total'])
            )

    def paintEvent(self, _):
        """Override paintEvent to paint background"""

        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)
        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
Ejemplo n.º 21
0
class MaterialManager(QObject):

    materialsUpdated = pyqtSignal()  # Emitted whenever the material lookup tables are updated.
    favoritesUpdated = pyqtSignal()  # Emitted whenever the favorites are changed

    def __init__(self, container_registry, parent = None):
        super().__init__(parent)
        self._application = Application.getInstance()
        self._container_registry = container_registry  # type: ContainerRegistry

        # Material_type -> generic material metadata
        self._fallback_materials_map = dict()  # type: Dict[str, Dict[str, Any]]

        # Root_material_id -> MaterialGroup
        self._material_group_map = dict()  # type: Dict[str, MaterialGroup]

        # Approximate diameter str
        self._diameter_machine_nozzle_buildplate_material_map = dict()  # type: Dict[str, Dict[str, MaterialNode]]

        # We're using these two maps to convert between the specific diameter material id and the generic material id
        # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
        # i.e. generic_pla -> generic_pla_175
        # root_material_id -> approximate diameter str -> root_material_id for that diameter
        self._material_diameter_map = defaultdict(dict)  # type: Dict[str, Dict[str, str]]

        # Material id including diameter (generic_pla_175) -> material root id (generic_pla)
        self._diameter_material_map = dict()  # type: Dict[str, str]

        # This is used in Legacy UM3 send material function and the material management page.
        # GUID -> a list of material_groups
        self._guid_material_groups_map = defaultdict(list)  # type: Dict[str, List[MaterialGroup]]

        # The machine definition ID for the non-machine-specific materials.
        # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
        self._default_machine_definition_id = "fdmprinter"
        self._default_approximate_diameter_for_quality_search = "3"

        # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
        # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
        # react too many time.
        self._update_timer = QTimer(self)
        self._update_timer.setInterval(300)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._updateMaps)

        self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
        self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
        self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)

        self._favorites = set()  # type: Set[str]

    def initialize(self) -> None:
        # Find all materials and put them in a matrix for quick search.
        material_metadatas = {metadata["id"]: metadata for metadata in
                              self._container_registry.findContainersMetadata(type = "material") if
                              metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]

        self._material_group_map = dict()  # type: Dict[str, MaterialGroup]
                
        # Map #1
        #    root_material_id -> MaterialGroup
        for material_id, material_metadata in material_metadatas.items():
            # We don't store empty material in the lookup tables
            if material_id == "empty_material":
                continue

            root_material_id = material_metadata.get("base_file", "")
            if root_material_id not in self._material_group_map:
                self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
                self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
            group = self._material_group_map[root_material_id]

            # Store this material in the group of the appropriate root material.
            if material_id != root_material_id:
                new_node = MaterialNode(material_metadata)
                group.derived_material_node_list.append(new_node)

        # Order this map alphabetically so it's easier to navigate in a debugger
        self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))

        # Map #1.5
        #    GUID -> material group list
        self._guid_material_groups_map = defaultdict(list)  # type: Dict[str, List[MaterialGroup]]
        for root_material_id, material_group in self._material_group_map.items():
            guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
            self._guid_material_groups_map[guid].append(material_group)

        # Map #2
        # Lookup table for material type -> fallback material metadata, only for read-only materials
        grouped_by_type_dict = dict()  # type: Dict[str, Any]
        material_types_without_fallback = set()
        for root_material_id, material_node in self._material_group_map.items():
            material_type = material_node.root_material_node.getMetaDataEntry("material", "")
            if material_type not in grouped_by_type_dict:
                grouped_by_type_dict[material_type] = {"generic": None,
                                                       "others": []}
                material_types_without_fallback.add(material_type)
            brand = material_node.root_material_node.getMetaDataEntry("brand", "")
            if brand.lower() == "generic":
                to_add = True
                if material_type in grouped_by_type_dict:
                    diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
                    if diameter != self._default_approximate_diameter_for_quality_search:
                        to_add = False  # don't add if it's not the default diameter

                if to_add:
                    # Checking this first allow us to differentiate between not read only materials:
                    #  - if it's in the list, it means that is a new material without fallback
                    #  - if it is not, then it is a custom material with a fallback material (parent)
                    if material_type in material_types_without_fallback:
                        grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
                        material_types_without_fallback.remove(material_type)

        # Remove the materials that have no fallback materials
        for material_type in material_types_without_fallback:
            del grouped_by_type_dict[material_type]
        self._fallback_materials_map = grouped_by_type_dict

        # Map #3
        # There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
        # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
        # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
        # for quality search.
        self._material_diameter_map = defaultdict(dict)
        self._diameter_material_map = dict()

        # Group the material IDs by the same name, material, brand, and color but with different diameters.
        material_group_dict = dict()  # type: Dict[Tuple[Any], Dict[str, str]]
        keys_to_fetch = ("name", "material", "brand", "color")
        for root_material_id, machine_node in self._material_group_map.items():
            root_material_metadata = machine_node.root_material_node._metadata

            key_data_list = []  # type: List[Any]
            for key in keys_to_fetch:
                key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
            key_data = cast(Tuple[Any], tuple(key_data_list))  # type: Tuple[Any]

            # If the key_data doesn't exist, it doesn't matter if the material is read only...
            if key_data not in material_group_dict:
                material_group_dict[key_data] = dict()
            else:
                # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
                if not machine_node.is_read_only:
                    continue
            approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
            material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")

        # Map [root_material_id][diameter] -> root_material_id for this diameter
        for data_dict in material_group_dict.values():
            for root_material_id1 in data_dict.values():
                if root_material_id1 in self._material_diameter_map:
                    continue
                diameter_map = data_dict
                for root_material_id2 in data_dict.values():
                    self._material_diameter_map[root_material_id2] = diameter_map

            default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
            if default_root_material_id is None:
                default_root_material_id = list(data_dict.values())[0]  # no default diameter present, just take "the" only one
            for root_material_id in data_dict.values():
                self._diameter_material_map[root_material_id] = default_root_material_id

        # Map #4
        # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
        self._diameter_machine_nozzle_buildplate_material_map = dict()  # type: Dict[str, Dict[str, MaterialNode]]
        for material_metadata in material_metadatas.values():
            self.__addMaterialMetadataIntoLookupTree(material_metadata)

        favorites = self._application.getPreferences().getValue("cura/favorite_materials")
        for item in favorites.split(";"):
            self._favorites.add(item)

        self.materialsUpdated.emit()

    def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
        material_id = material_metadata["id"]

        # We don't store empty material in the lookup tables
        if material_id == "empty_material":
            return

        root_material_id = material_metadata["base_file"]
        definition = material_metadata["definition"]
        approximate_diameter = material_metadata["approximate_diameter"]

        if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}

        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
            approximate_diameter]
        if definition not in machine_nozzle_buildplate_material_map:
            machine_nozzle_buildplate_material_map[definition] = MaterialNode()

        # This is a list of information regarding the intermediate nodes:
        #    nozzle -> buildplate
        nozzle_name = material_metadata.get("variant_name")
        buildplate_name = material_metadata.get("buildplate_name")
        intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
                                       (buildplate_name, VariantType.BUILD_PLATE),
                                       ]

        variant_manager = self._application.getVariantManager()

        machine_node = machine_nozzle_buildplate_material_map[definition]
        current_node = machine_node
        current_intermediate_node_info_idx = 0
        error_message = None  # type: Optional[str]
        while current_intermediate_node_info_idx < len(intermediate_node_info_list):
            variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
            if variant_name is not None:
                # The new material has a specific variant, so it needs to be added to that specific branch in the tree.
                variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
                if variant is None:
                    error_message = "Material {id} contains a variant {name} that does not exist.".format(
                        id = material_metadata["id"], name = variant_name)
                    break

                # Update the current node to advance to a more specific branch
                if variant_name not in current_node.children_map:
                    current_node.children_map[variant_name] = MaterialNode()
                current_node = current_node.children_map[variant_name]

            current_intermediate_node_info_idx += 1

        if error_message is not None:
            Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
            self._container_registry.addWrongContainerId(material_metadata["id"])
            return

        # Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
        # Sanity check: Make sure that there is no duplicated materials.
        if root_material_id in current_node.material_map:
            Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
                       material_id, root_material_id)
            ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
            return

        current_node.material_map[root_material_id] = MaterialNode(material_metadata)

    def _updateMaps(self):
        Logger.log("i", "Updating material lookup data ...")
        self.initialize()

    def _onContainerMetadataChanged(self, container):
        self._onContainerChanged(container)

    def _onContainerChanged(self, container):
        container_type = container.getMetaDataEntry("type")
        if container_type != "material":
            return

        # update the maps
        self._update_timer.start()

    def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
        return self._material_group_map.get(root_material_id)

    def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
        return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)

    def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
        return self._diameter_material_map.get(root_material_id, "")

    def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
        return self._guid_material_groups_map.get(guid)

    #
    # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
    #
    def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
                              buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
            return dict()

        machine_definition_id = machine_definition.getId()

        # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
        machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
        default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
        nozzle_node = None
        buildplate_node = None
        if nozzle_name is not None and machine_node is not None:
            nozzle_node = machine_node.getChildNode(nozzle_name)
            # Get buildplate node if possible
            if nozzle_node is not None and buildplate_name is not None:
                buildplate_node = nozzle_node.getChildNode(buildplate_name)

        nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]

        # Fallback mechanism of finding materials:
        #  1. buildplate-specific material
        #  2. nozzle-specific material
        #  3. machine-specific material
        #  4. generic material (for fdmprinter)
        machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])

        material_id_metadata_dict = dict()  # type: Dict[str, MaterialNode]
        excluded_materials = set()
        for current_node in nodes_to_check:
            if current_node is None:
                continue

            # Only exclude the materials that are explicitly specified in the "exclude_materials" field.
            # Do not exclude other materials that are of the same type.
            for material_id, node in current_node.material_map.items():
                if material_id in machine_exclude_materials:
                    excluded_materials.add(material_id)
                    continue

                if material_id not in material_id_metadata_dict:
                    material_id_metadata_dict[material_id] = node

        if excluded_materials:
            Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))

        return material_id_metadata_dict

    #
    # A convenience function to get available materials for the given machine with the extruder position.
    #
    def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
                                                extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
        buildplate_name = machine.getBuildplateName()
        nozzle_name = None
        if extruder_stack.variant.getId() != "empty_variant":
            nozzle_name = extruder_stack.variant.getName()
        diameter = extruder_stack.approximateMaterialDiameter

        # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
        return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)

    #
    # Gets MaterialNode for the given extruder and machine with the given material name.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
                        buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
                       diameter, rounded_diameter, root_material_id)
            return None

        # If there are nozzle materials, get the nozzle-specific material
        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]  # type: Dict[str, MaterialNode]
        machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
        nozzle_node = None
        buildplate_node = None

        # Fallback for "fdmprinter" if the machine-specific materials cannot be found
        if machine_node is None:
            machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
        if machine_node is not None and nozzle_name is not None:
            nozzle_node = machine_node.getChildNode(nozzle_name)
        if nozzle_node is not None and buildplate_name is not None:
            buildplate_node = nozzle_node.getChildNode(buildplate_name)

        # Fallback mechanism of finding materials:
        #  1. buildplate-specific material
        #  2. nozzle-specific material
        #  3. machine-specific material
        #  4. generic material (for fdmprinter)
        nodes_to_check = [buildplate_node, nozzle_node, machine_node,
                          machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]

        material_node = None
        for node in nodes_to_check:
            if node is not None:
                material_node = node.material_map.get(root_material_id)
                if material_node:
                    break

        return material_node

    #
    # Gets MaterialNode for the given extruder and machine with the given material type.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
                              buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
        node = None
        machine_definition = global_stack.definition
        extruder_definition = global_stack.extruders[position].definition
        if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
            material_diameter = extruder_definition.getProperty("material_diameter", "value")
            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)

            # Look at the guid to material dictionary
            root_material_id = None
            for material_group in self._guid_material_groups_map[material_guid]:
                root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
                break

            if not root_material_id:
                Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
                return None

            node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
                                        material_diameter, root_material_id)
        return node

    #
    # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
    # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
    # the generic material IDs to search for qualities.
    #
    # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
    # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
    # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
    # be "generic_pla". This function is intended to get a generic fallback material for the given material type.
    #
    # This function returns the generic root material ID for the given material type, where material types are "PLA",
    # "ABS", etc.
    #
    def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
        # For safety
        if material_type not in self._fallback_materials_map:
            Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
            return None
        fallback_material = self._fallback_materials_map[material_type]
        if fallback_material:
            return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
        else:
            return None

    ##  Get default material for given global stack, extruder position and extruder nozzle name
    #   you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
    def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
                           extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
        node = None

        buildplate_name = global_stack.getBuildplateName()
        machine_definition = global_stack.definition
        if extruder_definition is None:
            extruder_definition = global_stack.extruders[position].definition

        if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
            # At this point the extruder_definition is not None
            material_diameter = extruder_definition.getProperty("material_diameter", "value")
            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)
            approximate_material_diameter = str(round(material_diameter))
            root_material_id = machine_definition.getMetaDataEntry("preferred_material")
            root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
            node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
                                        material_diameter, root_material_id)
        return node

    def removeMaterialByRootId(self, root_material_id: str):
        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
            return

        nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
        for node in nodes_to_remove:
            self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))

    #
    # Methods for GUI
    #

    #
    # Sets the new name for the given material.
    #
    @pyqtSlot("QVariant", str)
    def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
        root_material_id = material_node.getMetaDataEntry("base_file")
        if root_material_id is None:
            return
        if self._container_registry.isReadOnly(root_material_id):
            Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
            return

        material_group = self.getMaterialGroup(root_material_id)
        if material_group:
            container = material_group.root_material_node.getContainer()
            if container:
                container.setName(name)

    #
    # Removes the given material.
    #
    @pyqtSlot("QVariant")
    def removeMaterial(self, material_node: "MaterialNode") -> None:
        root_material_id = material_node.getMetaDataEntry("base_file")
        if root_material_id is not None:
            self.removeMaterialByRootId(root_material_id)

    #
    # Creates a duplicate of a material, which has the same GUID and base_file metadata.
    # Returns the root material ID of the duplicated material if successful.
    #
    @pyqtSlot("QVariant", result = str)
    def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
        root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))

        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
            return None

        base_container = material_group.root_material_node.getContainer()
        if not base_container:
            return None

        # Ensure all settings are saved.
        self._application.saveSettings()

        # Create a new ID & container to hold the data.
        new_containers = []
        if new_base_id is None:
            new_base_id = self._container_registry.uniqueName(base_container.getId())
        new_base_container = copy.deepcopy(base_container)
        new_base_container.getMetaData()["id"] = new_base_id
        new_base_container.getMetaData()["base_file"] = new_base_id
        if new_metadata is not None:
            for key, value in new_metadata.items():
                new_base_container.getMetaData()[key] = value
        new_containers.append(new_base_container)

        # Clone all of them.
        for node in material_group.derived_material_node_list:
            container_to_copy = node.getContainer()
            if not container_to_copy:
                continue
            # Create unique IDs for every clone.
            new_id = new_base_id
            if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
                new_id += "_" + container_to_copy.getMetaDataEntry("definition")
                if container_to_copy.getMetaDataEntry("variant_name"):
                    nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
                    new_id += "_" + nozzle_name.replace(" ", "_")

            new_container = copy.deepcopy(container_to_copy)
            new_container.getMetaData()["id"] = new_id
            new_container.getMetaData()["base_file"] = new_base_id
            if new_metadata is not None:
                for key, value in new_metadata.items():
                    new_container.getMetaData()[key] = value

            new_containers.append(new_container)

        for container_to_add in new_containers:
            container_to_add.setDirty(True)
            self._container_registry.addContainer(container_to_add)


        # if the duplicated material was favorite then the new material should also be added to favorite.
        if root_material_id in self.getFavorites():
            self.addFavorite(new_base_id)

        return new_base_id

    #
    # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
    # Returns the ID of the newly created material.
    @pyqtSlot(result = str)
    def createMaterial(self) -> str:
        from UM.i18n import i18nCatalog
        catalog = i18nCatalog("cura")
        # Ensure all settings are saved.
        self._application.saveSettings()

        machine_manager = self._application.getMachineManager()
        extruder_stack = machine_manager.activeStack

        approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
        root_material_id = "generic_pla"
        root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
        material_group = self.getMaterialGroup(root_material_id)

        if not material_group:  # This should never happen
            Logger.log("w", "Cannot get the material group of %s.", root_material_id)
            return ""

        # Create a new ID & container to hold the data.
        new_id = self._container_registry.uniqueName("custom_material")
        new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
                        "brand": catalog.i18nc("@label", "Custom"),
                        "GUID": str(uuid.uuid4()),
                        }

        self.duplicateMaterial(material_group.root_material_node,
                               new_base_id = new_id,
                               new_metadata = new_metadata)
        return new_id

    @pyqtSlot(str)
    def addFavorite(self, root_material_id: str) -> None:
        self._favorites.add(root_material_id)
        self.materialsUpdated.emit()

        # Ensure all settings are saved.
        self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
        self._application.saveSettings()

    @pyqtSlot(str)
    def removeFavorite(self, root_material_id: str) -> None:
        self._favorites.remove(root_material_id)
        self.materialsUpdated.emit()

        # Ensure all settings are saved.
        self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
        self._application.saveSettings()

    @pyqtSlot()
    def getFavorites(self):
        return self._favorites
Ejemplo n.º 22
0
class SpyQWidget(QWidget):
    """
        Class who create QWidget for spied hosts
    """

    def __init__(self):
        super(SpyQWidget, self).__init__()
        self.host_services_lbl = QLabel(_('You are not spying on any hosts for now...'))
        self.spy_list_widget = SpyQListWidget()
        self.host_list_widget = QListWidget()
        self.spy_timer = QTimer()

    def initialize(self):
        """
        Intialize Spy QWidget

        """

        layout = QGridLayout()
        self.setLayout(layout)

        spy_icon = QLabel()
        spy_pixmap = QPixmap(settings.get_image('spy'))
        spy_icon.setPixmap(spy_pixmap)
        spy_icon.setScaledContents(True)
        spy_icon.setFixedSize(20, 20)
        layout.addWidget(spy_icon, 0, 0, 1, 1)
        layout.setAlignment(spy_icon, Qt.AlignRight)

        spy_title = QLabel(_('Spy Hosts'))
        spy_title.setObjectName('title')
        spy_title.setMinimumHeight(40)
        layout.addWidget(spy_title, 0, 1, 1, 1)

        hint_lbl = QLabel('Click to refresh, double-click to stop spying')
        hint_lbl.setObjectName('subtitle')
        layout.addWidget(hint_lbl, 1, 0, 1, 1)
        layout.setAlignment(hint_lbl, Qt.AlignCenter)

        self.host_services_lbl.setObjectName('subtitle')
        layout.addWidget(self.host_services_lbl, 1, 1, 1, 1)
        layout.setAlignment(self.host_services_lbl, Qt.AlignCenter)

        self.spy_list_widget.setDragDropMode(QAbstractItemView.DragDrop)
        self.spy_list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.spy_list_widget.doubleClicked.connect(self.remove_event)
        self.spy_list_widget.setAcceptDrops(True)
        self.spy_list_widget.setWordWrap(True)
        self.spy_list_widget.insertItem(0, self.get_hint_item())
        self.spy_list_widget.item_dropped.connect(get_events_widget().remove_event)
        self.spy_list_widget.clicked.connect(
            lambda: self.manage_host_events(self.spy_list_widget.currentRow())
        )
        layout.addWidget(self.spy_list_widget, 2, 0, 1, 1)

        self.host_list_widget.setObjectName('spy')
        # self.host_list_widget.setMinimumWidth(500)
        layout.addWidget(self.host_list_widget, 2, 1, 1, 1)

        spy_interval = int(settings.get_config('Alignak-app', 'spy_interval')) * 1000
        self.spy_timer.setInterval(spy_interval)
        self.spy_timer.start()
        self.spy_timer.timeout.connect(self.send_spy_events)

    @staticmethod
    def get_hint_item():
        """
        Return an EventItem with a hint text

        :return: event item with hint text
        :rtype: EventItem
        """

        drop_hint_item = EventItem()
        drop_hint_item.setText(_('Drop host-related events here to spy on it...'))
        drop_hint_item.setIcon(QIcon(settings.get_image('spy')))
        drop_hint_item.setFlags(Qt.ItemIsDropEnabled)

        return drop_hint_item

    def send_spy_events(self):
        """
        Send event messages for all hosts who are spied

        """

        if self.spy_list_widget.spied_hosts:
            for host_id in self.spy_list_widget.spied_hosts:
                host = data_manager.get_item('host', host_id)

                get_events_widget().add_event(
                    EventItem.get_event_type(host.data),
                    _('Host %s, current state: %s') % (
                        host.get_display_name(), host.data['ls_state']),
                    host=host.item_id
                )

    def manage_host_events(self, row):
        """
        Manage spy events for a host, defined by current row of "spy_list_widget"

        :param row: current row of "spy_list_widget"
        :type row: int
        """

        # Clear QListWidget
        self.host_list_widget.clear()

        # Get Host and its services
        if row < 0:
            item = None
        else:
            item = self.spy_list_widget.item(row)

        if item:
            host = data_manager.get_item('host', item.host)

            if _('(new !)') in item.data(Qt.DisplayRole):
                item.setData(Qt.DisplayRole, item.data(Qt.DisplayRole).replace(_('(new !)'), ''))
                item.setToolTip(item.toolTip().replace(_('(new !)'), ''))

            self.host_services_lbl.setText(_('Problems found for %s:') % host.get_display_name())
            services = data_manager.get_host_services(host.item_id)

            if services:
                problems = False
                for service in services:
                    if data_manager.is_problem('service', service.data):
                        problems = True
                        svc_state = _('Service %s is %s') % (
                            service.get_display_name(), service.data['ls_state']
                        )

                        event = EventItem()
                        event.initialize(
                            service.data['ls_state'],
                            svc_state,
                            host=host.item_id
                        )
                        tooltip = 'Output: %s (%s)' % (
                            service.data['ls_output'],
                            get_date_fromtimestamp(service.data['ls_last_check'])
                        )
                        event.setToolTip(tooltip)

                        self.host_list_widget.insertItem(0, event)

                if not problems:
                    event = EventItem()
                    event.initialize(
                        host.data['ls_state'],
                        _('%s is %s. Services of host seems managed.') % (
                            host.get_display_name(), host.data['ls_state']),
                        host=host.item_id
                    )
                    self.host_list_widget.insertItem(0, event)
            else:
                no_service_event = EventItem()
                no_service_event.initialize(
                    host.data['ls_state'],
                    _('%s is %s. No services.') % (host.get_display_name(), host.data['ls_state'])
                )
                self.host_list_widget.insertItem(0, no_service_event)

    def remove_event(self):
        """
        Remove item when user double click on an item, update parent tab text

        """

        item = self.spy_list_widget.currentItem()

        self.spy_list_widget.spied_hosts.remove(item.data(Qt.UserRole))
        self.spy_list_widget.takeItem(self.spy_list_widget.currentRow())

        if not self.spy_list_widget.spied_hosts:
            self.host_list_widget.clear()
            self.spy_list_widget.insertItem(0, self.get_hint_item())
            self.spy_list_widget.initialized = False
            self.host_services_lbl.setText(_('You are not spying on any hosts for now...'))

        if not self.spy_list_widget.selectedItems():
            self.manage_host_events(self.spy_list_widget.currentRow())

        self.update_parent_spytab()

    def update_parent_spytab(self):  # pragma: no cover - not testable
        """
        Update the parent spy tab text

        """

        if self.parent():
            if self.spy_list_widget.spied_hosts:
                self.parent().parent().setTabText(
                    self.parent().parent().indexOf(self),
                    _('Spied Hosts (%d)') % self.spy_list_widget.count()
                )
            else:
                # Remove hint item from count
                self.parent().parent().setTabText(
                    self.parent().parent().indexOf(self),
                    _('Spied Hosts (%d)') % (self.spy_list_widget.count() - 1)
                )
Ejemplo n.º 23
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_dropped = pyqtSignal(object)

    def __init__(self, parent):
        QListView.__init__(self, parent)
        setup_gestures(self)
        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()
        dpr = self.device_pixel_ratio
        self.thumbnail_cache = ThumbnailCache(
            max_size=gprefs['cover_grid_disk_cache_size'],
            thumbnail_size=(int(dpr * self.delegate.cover_size.width()),
                            int(dpr * 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)
        self.resize_timer = t = QTimer(self)
        t.setInterval(200), t.setSingleShot(True)
        t.timeout.connect(self.update_memory_cover_cache_size)

    def viewportEvent(self, ev):
        ret = gesture_viewport_event(self, ev)
        if ret is not None:
            return ret
        return QListView.viewportEvent(self, ev)

    @property
    def device_pixel_ratio(self):
        try:
            return self.devicePixelRatioF()
        except AttributeError:
            return self.devicePixelRatio()

    @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 or
                gprefs['show_emblems'] != self.delegate.original_show_emblems
                or gprefs['emblem_size'] != self.delegate.orginal_emblem_size
                or gprefs['emblem_position'] !=
                self.delegate.orginal_emblem_position):
            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()
        if size_changed:
            dpr = self.device_pixel_ratio
            self.thumbnail_cache.set_thumbnail_size(
                int(dpr * self.delegate.cover_size.width()),
                int(dpr * 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)
        self.update_memory_cover_cache_size()

    def resizeEvent(self, ev):
        self.resize_timer.start()
        return QListView.resizeEvent(self, ev)

    def update_memory_cover_cache_size(self):
        try:
            sz = self.delegate.item_size
        except AttributeError:
            return
        rows, cols = self.width() // sz.width(), self.height() // sz.height()
        num = (rows + 1) * (cols + 1)
        limit = max(100,
                    num * max(2, gprefs['cover_grid_cache_size_multiple']))
        if limit != self.delegate.cover_cache.limit:
            self.delegate.cover_cache.set_limit(limit)

    def shown(self):
        self.update_memory_cover_cache_size()
        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')
            dpr = self.device_pixel_ratio
            p.setDevicePixelRatio(dpr)
            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,
                        int(dpr * self.delegate.cover_size.width()),
                        int(dpr * self.delegate.cover_size.height()))
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio,
                                     Qt.SmoothTransformation)
                        p.setDevicePixelRatio(dpr)
                    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)
Ejemplo n.º 24
0
class MaterialManager(QObject):

    materialsUpdated = pyqtSignal()  # Emitted whenever the material lookup tables are updated.
    favoritesUpdated = pyqtSignal()  # Emitted whenever the favorites are changed

    def __init__(self, container_registry, parent = None):
        super().__init__(parent)
        self._application = Application.getInstance()
        self._container_registry = container_registry  # type: ContainerRegistry

        # Material_type -> generic material metadata
        self._fallback_materials_map = dict()  # type: Dict[str, Dict[str, Any]]

        # Root_material_id -> MaterialGroup
        self._material_group_map = dict()  # type: Dict[str, MaterialGroup]

        # Approximate diameter str
        self._diameter_machine_nozzle_buildplate_material_map = dict()  # type: Dict[str, Dict[str, MaterialNode]]

        # We're using these two maps to convert between the specific diameter material id and the generic material id
        # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
        # i.e. generic_pla -> generic_pla_175
        # root_material_id -> approximate diameter str -> root_material_id for that diameter
        self._material_diameter_map = defaultdict(dict)  # type: Dict[str, Dict[str, str]]

        # Material id including diameter (generic_pla_175) -> material root id (generic_pla)
        self._diameter_material_map = dict()  # type: Dict[str, str]

        # This is used in Legacy UM3 send material function and the material management page.
        # GUID -> a list of material_groups
        self._guid_material_groups_map = defaultdict(list)  # type: Dict[str, List[MaterialGroup]]

        # The machine definition ID for the non-machine-specific materials.
        # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
        self._default_machine_definition_id = "fdmprinter"
        self._default_approximate_diameter_for_quality_search = "3"

        # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
        # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
        # react too many time.
        self._update_timer = QTimer(self)
        self._update_timer.setInterval(300)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._updateMaps)

        self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
        self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
        self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)

        self._favorites = set()  # type: Set[str]

    def initialize(self) -> None:
        # Find all materials and put them in a matrix for quick search.
        material_metadatas = {metadata["id"]: metadata for metadata in
                              self._container_registry.findContainersMetadata(type = "material") if
                              metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]

        self._material_group_map = dict()  # type: Dict[str, MaterialGroup]
                
        # Map #1
        #    root_material_id -> MaterialGroup
        for material_id, material_metadata in material_metadatas.items():
            # We don't store empty material in the lookup tables
            if material_id == "empty_material":
                continue

            root_material_id = material_metadata.get("base_file", "")
            if root_material_id not in self._material_group_map:
                self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
                self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
            group = self._material_group_map[root_material_id]

            # Store this material in the group of the appropriate root material.
            if material_id != root_material_id:
                new_node = MaterialNode(material_metadata)
                group.derived_material_node_list.append(new_node)

        # Order this map alphabetically so it's easier to navigate in a debugger
        self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))

        # Map #1.5
        #    GUID -> material group list
        self._guid_material_groups_map = defaultdict(list)  # type: Dict[str, List[MaterialGroup]]
        for root_material_id, material_group in self._material_group_map.items():
            guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
            self._guid_material_groups_map[guid].append(material_group)

        # Map #2
        # Lookup table for material type -> fallback material metadata, only for read-only materials
        grouped_by_type_dict = dict()  # type: Dict[str, Any]
        material_types_without_fallback = set()
        for root_material_id, material_node in self._material_group_map.items():
            material_type = material_node.root_material_node.getMetaDataEntry("material", "")
            if material_type not in grouped_by_type_dict:
                grouped_by_type_dict[material_type] = {"generic": None,
                                                       "others": []}
                material_types_without_fallback.add(material_type)
            brand = material_node.root_material_node.getMetaDataEntry("brand", "")
            if brand.lower() == "generic":
                to_add = True
                if material_type in grouped_by_type_dict:
                    diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
                    if diameter != self._default_approximate_diameter_for_quality_search:
                        to_add = False  # don't add if it's not the default diameter

                if to_add:
                    # Checking this first allow us to differentiate between not read only materials:
                    #  - if it's in the list, it means that is a new material without fallback
                    #  - if it is not, then it is a custom material with a fallback material (parent)
                    if material_type in material_types_without_fallback:
                        grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
                        material_types_without_fallback.remove(material_type)

        # Remove the materials that have no fallback materials
        for material_type in material_types_without_fallback:
            del grouped_by_type_dict[material_type]
        self._fallback_materials_map = grouped_by_type_dict

        # Map #3
        # There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
        # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
        # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
        # for quality search.
        self._material_diameter_map = defaultdict(dict)
        self._diameter_material_map = dict()

        # Group the material IDs by the same name, material, brand, and color but with different diameters.
        material_group_dict = dict()  # type: Dict[Tuple[Any], Dict[str, str]]
        keys_to_fetch = ("name", "material", "brand", "color")
        for root_material_id, machine_node in self._material_group_map.items():
            root_material_metadata = machine_node.root_material_node._metadata

            key_data_list = []  # type: List[Any]
            for key in keys_to_fetch:
                key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
            key_data = cast(Tuple[Any], tuple(key_data_list))  # type: Tuple[Any]

            # If the key_data doesn't exist, it doesn't matter if the material is read only...
            if key_data not in material_group_dict:
                material_group_dict[key_data] = dict()
            else:
                # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
                if not machine_node.is_read_only:
                    continue
            approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
            material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")

        # Map [root_material_id][diameter] -> root_material_id for this diameter
        for data_dict in material_group_dict.values():
            for root_material_id1 in data_dict.values():
                if root_material_id1 in self._material_diameter_map:
                    continue
                diameter_map = data_dict
                for root_material_id2 in data_dict.values():
                    self._material_diameter_map[root_material_id2] = diameter_map

            default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
            if default_root_material_id is None:
                default_root_material_id = list(data_dict.values())[0]  # no default diameter present, just take "the" only one
            for root_material_id in data_dict.values():
                self._diameter_material_map[root_material_id] = default_root_material_id

        # Map #4
        # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
        self._diameter_machine_nozzle_buildplate_material_map = dict()  # type: Dict[str, Dict[str, MaterialNode]]
        for material_metadata in material_metadatas.values():
            self.__addMaterialMetadataIntoLookupTree(material_metadata)

        favorites = self._application.getPreferences().getValue("cura/favorite_materials")
        for item in favorites.split(";"):
            self._favorites.add(item)

        self.materialsUpdated.emit()

    def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
        material_id = material_metadata["id"]

        # We don't store empty material in the lookup tables
        if material_id == "empty_material":
            return

        root_material_id = material_metadata["base_file"]
        definition = material_metadata["definition"]
        approximate_diameter = material_metadata["approximate_diameter"]

        if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}

        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
            approximate_diameter]
        if definition not in machine_nozzle_buildplate_material_map:
            machine_nozzle_buildplate_material_map[definition] = MaterialNode()

        # This is a list of information regarding the intermediate nodes:
        #    nozzle -> buildplate
        nozzle_name = material_metadata.get("variant_name")
        buildplate_name = material_metadata.get("buildplate_name")
        intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
                                       (buildplate_name, VariantType.BUILD_PLATE),
                                       ]

        variant_manager = self._application.getVariantManager()

        machine_node = machine_nozzle_buildplate_material_map[definition]
        current_node = machine_node
        current_intermediate_node_info_idx = 0
        error_message = None  # type: Optional[str]
        while current_intermediate_node_info_idx < len(intermediate_node_info_list):
            variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
            if variant_name is not None:
                # The new material has a specific variant, so it needs to be added to that specific branch in the tree.
                variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
                if variant is None:
                    error_message = "Material {id} contains a variant {name} that does not exist.".format(
                        id = material_metadata["id"], name = variant_name)
                    break

                # Update the current node to advance to a more specific branch
                if variant_name not in current_node.children_map:
                    current_node.children_map[variant_name] = MaterialNode()
                current_node = current_node.children_map[variant_name]

            current_intermediate_node_info_idx += 1

        if error_message is not None:
            Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
            self._container_registry.addWrongContainerId(material_metadata["id"])
            return

        # Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
        # Sanity check: Make sure that there is no duplicated materials.
        if root_material_id in current_node.material_map:
            Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
                       material_id, root_material_id)
            ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
            return

        current_node.material_map[root_material_id] = MaterialNode(material_metadata)

    def _updateMaps(self):
        Logger.log("i", "Updating material lookup data ...")
        self.initialize()

    def _onContainerMetadataChanged(self, container):
        self._onContainerChanged(container)

    def _onContainerChanged(self, container):
        container_type = container.getMetaDataEntry("type")
        if container_type != "material":
            return

        # update the maps
        self._update_timer.start()

    def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
        return self._material_group_map.get(root_material_id)

    def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
        return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)

    def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
        return self._diameter_material_map.get(root_material_id, "")

    def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
        return self._guid_material_groups_map.get(guid)

    #
    # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
    #
    def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
                              buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
            return dict()

        machine_definition_id = machine_definition.getId()

        # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
        machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
        default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
        nozzle_node = None
        buildplate_node = None
        if nozzle_name is not None and machine_node is not None:
            nozzle_node = machine_node.getChildNode(nozzle_name)
            # Get buildplate node if possible
            if nozzle_node is not None and buildplate_name is not None:
                buildplate_node = nozzle_node.getChildNode(buildplate_name)

        nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]

        # Fallback mechanism of finding materials:
        #  1. buildplate-specific material
        #  2. nozzle-specific material
        #  3. machine-specific material
        #  4. generic material (for fdmprinter)
        machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])

        material_id_metadata_dict = dict()  # type: Dict[str, MaterialNode]
        excluded_materials = set()
        for current_node in nodes_to_check:
            if current_node is None:
                continue

            # Only exclude the materials that are explicitly specified in the "exclude_materials" field.
            # Do not exclude other materials that are of the same type.
            for material_id, node in current_node.material_map.items():
                if material_id in machine_exclude_materials:
                    excluded_materials.add(material_id)
                    continue

                if material_id not in material_id_metadata_dict:
                    material_id_metadata_dict[material_id] = node

        if excluded_materials:
            Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))

        return material_id_metadata_dict

    #
    # A convenience function to get available materials for the given machine with the extruder position.
    #
    def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
                                                extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
        buildplate_name = machine.getBuildplateName()
        nozzle_name = None
        if extruder_stack.variant.getId() != "empty_variant":
            nozzle_name = extruder_stack.variant.getName()
        diameter = extruder_stack.getApproximateMaterialDiameter()

        # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
        return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)

    #
    # Gets MaterialNode for the given extruder and machine with the given material name.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
                        buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
        # round the diameter to get the approximate diameter
        rounded_diameter = str(round(diameter))
        if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
                       diameter, rounded_diameter, root_material_id)
            return None

        # If there are nozzle materials, get the nozzle-specific material
        machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]  # type: Dict[str, MaterialNode]
        machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
        nozzle_node = None
        buildplate_node = None

        # Fallback for "fdmprinter" if the machine-specific materials cannot be found
        if machine_node is None:
            machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
        if machine_node is not None and nozzle_name is not None:
            nozzle_node = machine_node.getChildNode(nozzle_name)
        if nozzle_node is not None and buildplate_name is not None:
            buildplate_node = nozzle_node.getChildNode(buildplate_name)

        # Fallback mechanism of finding materials:
        #  1. buildplate-specific material
        #  2. nozzle-specific material
        #  3. machine-specific material
        #  4. generic material (for fdmprinter)
        nodes_to_check = [buildplate_node, nozzle_node, machine_node,
                          machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]

        material_node = None
        for node in nodes_to_check:
            if node is not None:
                material_node = node.material_map.get(root_material_id)
                if material_node:
                    break

        return material_node

    #
    # Gets MaterialNode for the given extruder and machine with the given material type.
    # Returns None if:
    #  1. the given machine doesn't have materials;
    #  2. cannot find any material InstanceContainers with the given settings.
    #
    def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
                              buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
        node = None
        machine_definition = global_stack.definition
        extruder_definition = global_stack.extruders[position].definition
        if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
            material_diameter = extruder_definition.getProperty("material_diameter", "value")
            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)

            # Look at the guid to material dictionary
            root_material_id = None
            for material_group in self._guid_material_groups_map[material_guid]:
                root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
                break

            if not root_material_id:
                Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
                return None

            node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
                                        material_diameter, root_material_id)
        return node

    #   There are 2 ways to get fallback materials;
    #   - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
    #   - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
    #       a GUID. This should only be done if the material itself does not have a quality just yet.
    def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
        results = []  # type: List[str]

        material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
        for material_group in material_groups:  # type: ignore
            if material_group.name != material.getId():
                # If the material in the group is read only, put it at the front of the list (since that is the most
                # likely one to get a result)
                if material_group.is_read_only:
                    results.insert(0, material_group.name)
                else:
                    results.append(material_group.name)

        fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
        if fallback is not None:
            results.append(fallback)
        return results

    #
    # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
    # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
    # the generic material IDs to search for qualities.
    #
    # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
    # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
    # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
    # be "generic_pla". This function is intended to get a generic fallback material for the given material type.
    #
    # This function returns the generic root material ID for the given material type, where material types are "PLA",
    # "ABS", etc.
    #
    def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
        # For safety
        if material_type not in self._fallback_materials_map:
            Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
            return None
        fallback_material = self._fallback_materials_map[material_type]
        if fallback_material:
            return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
        else:
            return None

    ##  Get default material for given global stack, extruder position and extruder nozzle name
    #   you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
    def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
                           extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
        node = None

        buildplate_name = global_stack.getBuildplateName()
        machine_definition = global_stack.definition

        # The extruder-compatible material diameter in the extruder definition may not be the correct value because
        # the user can change it in the definition_changes container.
        if extruder_definition is None:
            extruder_stack_or_definition = global_stack.extruders[position]
            is_extruder_stack = True
        else:
            extruder_stack_or_definition = extruder_definition
            is_extruder_stack = False

        if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
            if is_extruder_stack:
                material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
            else:
                material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")

            if isinstance(material_diameter, SettingFunction):
                material_diameter = material_diameter(global_stack)
            approximate_material_diameter = str(round(material_diameter))
            root_material_id = machine_definition.getMetaDataEntry("preferred_material")
            root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
            node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
                                        material_diameter, root_material_id)
        return node

    def removeMaterialByRootId(self, root_material_id: str):
        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
            return

        nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
        for node in nodes_to_remove:
            self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))

    #
    # Methods for GUI
    #

    #
    # Sets the new name for the given material.
    #
    @pyqtSlot("QVariant", str)
    def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
        root_material_id = material_node.getMetaDataEntry("base_file")
        if root_material_id is None:
            return
        if self._container_registry.isReadOnly(root_material_id):
            Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
            return

        material_group = self.getMaterialGroup(root_material_id)
        if material_group:
            container = material_group.root_material_node.getContainer()
            if container:
                container.setName(name)

    #
    # Removes the given material.
    #
    @pyqtSlot("QVariant")
    def removeMaterial(self, material_node: "MaterialNode") -> None:
        root_material_id = material_node.getMetaDataEntry("base_file")
        if root_material_id is not None:
            self.removeMaterialByRootId(root_material_id)

    #
    # Creates a duplicate of a material, which has the same GUID and base_file metadata.
    # Returns the root material ID of the duplicated material if successful.
    #
    @pyqtSlot("QVariant", result = str)
    def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
        root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))

        material_group = self.getMaterialGroup(root_material_id)
        if not material_group:
            Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
            return None

        base_container = material_group.root_material_node.getContainer()
        if not base_container:
            return None

        # Ensure all settings are saved.
        self._application.saveSettings()

        # Create a new ID & container to hold the data.
        new_containers = []
        if new_base_id is None:
            new_base_id = self._container_registry.uniqueName(base_container.getId())
        new_base_container = copy.deepcopy(base_container)
        new_base_container.getMetaData()["id"] = new_base_id
        new_base_container.getMetaData()["base_file"] = new_base_id
        if new_metadata is not None:
            for key, value in new_metadata.items():
                new_base_container.getMetaData()[key] = value
        new_containers.append(new_base_container)

        # Clone all of them.
        for node in material_group.derived_material_node_list:
            container_to_copy = node.getContainer()
            if not container_to_copy:
                continue
            # Create unique IDs for every clone.
            new_id = new_base_id
            if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
                new_id += "_" + container_to_copy.getMetaDataEntry("definition")
                if container_to_copy.getMetaDataEntry("variant_name"):
                    nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
                    new_id += "_" + nozzle_name.replace(" ", "_")

            new_container = copy.deepcopy(container_to_copy)
            new_container.getMetaData()["id"] = new_id
            new_container.getMetaData()["base_file"] = new_base_id
            if new_metadata is not None:
                for key, value in new_metadata.items():
                    new_container.getMetaData()[key] = value

            new_containers.append(new_container)

        for container_to_add in new_containers:
            container_to_add.setDirty(True)
            self._container_registry.addContainer(container_to_add)

        # if the duplicated material was favorite then the new material should also be added to favorite.
        if root_material_id in self.getFavorites():
            self.addFavorite(new_base_id)

        return new_base_id

    #
    # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
    # Returns the ID of the newly created material.
    @pyqtSlot(result = str)
    def createMaterial(self) -> str:
        from UM.i18n import i18nCatalog
        catalog = i18nCatalog("cura")
        # Ensure all settings are saved.
        self._application.saveSettings()

        machine_manager = self._application.getMachineManager()
        extruder_stack = machine_manager.activeStack

        machine_definition = self._application.getGlobalContainerStack().definition
        preferred_material = machine_definition.getMetaDataEntry("preferred_material")

        approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
        root_material_id = preferred_material if preferred_material else "generic_pla"
        root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
        material_group = self.getMaterialGroup(root_material_id)

        if not material_group:  # This should never happen
            Logger.log("w", "Cannot get the material group of %s.", root_material_id)
            return ""

        # Create a new ID & container to hold the data.
        new_id = self._container_registry.uniqueName("custom_material")
        new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
                        "brand": catalog.i18nc("@label", "Custom"),
                        "GUID": str(uuid.uuid4()),
                        }

        self.duplicateMaterial(material_group.root_material_node,
                               new_base_id = new_id,
                               new_metadata = new_metadata)
        return new_id

    @pyqtSlot(str)
    def addFavorite(self, root_material_id: str) -> None:
        self._favorites.add(root_material_id)
        self.materialsUpdated.emit()

        # Ensure all settings are saved.
        self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
        self._application.saveSettings()

    @pyqtSlot(str)
    def removeFavorite(self, root_material_id: str) -> None:
        self._favorites.remove(root_material_id)
        self.materialsUpdated.emit()

        # Ensure all settings are saved.
        self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
        self._application.saveSettings()

    @pyqtSlot()
    def getFavorites(self):
        return self._favorites
class ProblemsQWidget(QWidget):
    """
        Class who create Problems QWidget
    """

    def __init__(self, parent=None):
        super(ProblemsQWidget, self).__init__(parent)
        self.setWindowIcon(QIcon(settings.get_image('icon')))
        # Fields
        self.line_search = QLineEdit()
        self.problems_table = ProblemsQTableView()
        self.problems_title = QLabel()
        self.actions_widget = ActionsQWidget()
        self.spy_widget = None
        self.filter_hosts_btn = ToggleQWidgetButton()
        self.filter_services_btn = ToggleQWidgetButton()
        self.spy_btn = QPushButton()
        self.host_btn = QPushButton()
        self.refresh_timer = QTimer()

    def initialize(self, spy_widget):
        """
        Initialize QWidget and set SpyQWidget

        :param spy_widget: instance of SpyQWidget to manage spy events
        :type spy_widget: alignak_app.qobjects.events.spy.SpyQWidget
        """

        problem_layout = QVBoxLayout()
        problem_layout.setContentsMargins(5, 20, 5, 5)
        self.setLayout(problem_layout)

        self.spy_widget = spy_widget

        self.problems_title.setObjectName('title')
        problem_layout.addWidget(self.problems_title)

        problem_layout.addWidget(self.get_search_widget())

        problem_layout.addWidget(self.get_btn_widget())

        problem_layout.addWidget(self.problems_table)

        self.update_problems_data()

        update_problems = int(settings.get_config('Alignak-app', 'update_problems')) * 1000
        self.refresh_timer.setInterval(update_problems)
        self.refresh_timer.start()
        self.refresh_timer.timeout.connect(self.update_problems_data)

    def get_current_user_role_item(self):
        """
        Return current selected item by ``Qt.UserRole``

        :return: current selected item or None
        :rtype: alignak_app.items.item.Item
        """

        item = self.problems_table.model().data(
            self.problems_table.selectionModel().currentIndex(),
            Qt.UserRole
        )

        return item

    def update_action_buttons(self):
        """
        Update status of action buttons and set current item for ActionsQWidget

        """

        # Get item by UserRole
        item = self.get_current_user_role_item()

        if item:
            # If the elements had been ack or downtimed, they would not be present
            self.actions_widget.acknowledge_btn.setEnabled(True)
            self.actions_widget.downtime_btn.setEnabled(True)
            self.actions_widget.item = item

            if 'service' in item.item_type:
                host_id = item.data['host']
            else:
                host_id = item.item_id
            self.spy_btn.setEnabled(
                bool(host_id not in self.spy_widget.spy_list_widget.spied_hosts)
            )
            self.host_btn.setEnabled(True)
        else:
            self.actions_widget.acknowledge_btn.setEnabled(False)
            self.actions_widget.downtime_btn.setEnabled(False)
            self.host_btn.setEnabled(False)
            self.spy_btn.setEnabled(False)

    def get_search_widget(self):
        """
        Create and return the search QWidget

        :return: search QWidget
        :rtype: QWidget
        """

        widget = QWidget()
        layout = QHBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(5, 20, 5, 10)
        widget.setLayout(layout)

        # Search label
        search_lbl = QLabel(_('Search Problems'))
        search_lbl.setObjectName('bordertitle')
        search_lbl.setFixedHeight(25)
        search_lbl.setToolTip(_('Search Problems'))
        layout.addWidget(search_lbl)

        # QLineEdit
        self.line_search.setFixedHeight(search_lbl.height())
        self.line_search.setPlaceholderText(_('Type text to filter problems...'))
        layout.addWidget(self.line_search)

        # Refresh button
        refresh_btn = QPushButton(_('Refresh'))
        refresh_btn.setObjectName('ok')
        refresh_btn.setFixedSize(120, search_lbl.height())
        refresh_btn.setToolTip(_('Refresh problems'))
        refresh_btn.clicked.connect(self.update_problems_data)
        layout.addWidget(refresh_btn)

        return widget

    def get_btn_widget(self):
        """
        Return QWidget with spy and host synthesis QPushButtons

        :return: widget with spy and host button
        :rtype: QWidget
        """

        widget_btn = QWidget()
        layout_btn = QHBoxLayout()
        layout_btn.setContentsMargins(0, 0, 0, 5)
        widget_btn.setLayout(layout_btn)

        host_filter = QLabel(_('Filter hosts'))
        host_filter.setObjectName('subtitle')
        layout_btn.addWidget(host_filter)
        self.filter_hosts_btn.initialize()
        self.filter_hosts_btn.update_btn_state(False)
        self.filter_hosts_btn.toggle_btn.clicked.connect(lambda: self.update_problems_data('host'))
        layout_btn.addWidget(self.filter_hosts_btn)

        service_filter = QLabel(_('Filter services'))
        service_filter.setObjectName('subtitle')
        layout_btn.addWidget(service_filter)
        self.filter_services_btn.initialize()
        self.filter_services_btn.update_btn_state(False)
        self.filter_services_btn.toggle_btn.clicked.connect(
            lambda: self.update_problems_data('service')
        )
        layout_btn.addWidget(self.filter_services_btn)

        layout_btn.addStretch()

        self.host_btn.setIcon(QIcon(settings.get_image('host')))
        self.host_btn.setFixedSize(80, 20)
        self.host_btn.setEnabled(False)
        self.host_btn.setToolTip(_('See current item in synthesis view'))
        layout_btn.addWidget(self.host_btn)

        self.spy_btn.setIcon(QIcon(settings.get_image('spy')))
        self.spy_btn.setFixedSize(80, 20)
        self.spy_btn.setEnabled(False)
        self.spy_btn.setToolTip(_('Spy current host'))
        self.spy_btn.clicked.connect(self.add_spied_host)
        layout_btn.addWidget(self.spy_btn)

        self.actions_widget.initialize(None)
        self.actions_widget.acknowledge_btn.setEnabled(False)
        self.actions_widget.downtime_btn.setEnabled(False)
        layout_btn.addWidget(self.actions_widget)

        layout_btn.setAlignment(Qt.AlignCenter)

        return widget_btn

    def add_spied_host(self):
        """
        Add a host to spied hosts

        """

        # Get item by UserRole
        item = self.get_current_user_role_item()

        if item:
            if 'service' in item.item_type:
                item_id = item.data['host']
            else:
                item_id = item.item_id

            app_backend.query_services(item_id)
            self.spy_widget.spy_list_widget.add_spy_host(item_id)
            self.spy_widget.update_parent_spytab()

        self.update_action_buttons()

    def update_problems_data(self, item_type=''):
        """
        Update data of Problems QTableWidget and problems title

        :param item_type: type of item to filter
        :type item_type: str
        """

        problems_data = data_manager.get_problems()
        old_research = self.line_search.text()

        if self.parent():
            self.parent().parent().setTabText(
                self.parent().parent().indexOf(self),
                _("Problems (%d)") % len(problems_data['problems'])
            )
            self.problems_title.setText(
                _('There are %d problems to manage (hosts: %d, services: %d)') % (
                    len(problems_data['problems']),
                    problems_data['hosts_nb'],
                    problems_data['services_nb']
                )
            )

        if self.filter_hosts_btn.is_checked() and not self.filter_services_btn.is_checked():
            item_type = 'host'
        if self.filter_services_btn.is_checked() and not self.filter_hosts_btn.is_checked():
            item_type = 'service'
        if not self.filter_services_btn.is_checked() and not self.filter_hosts_btn.is_checked():
            item_type = ''

        if isinstance(item_type, str):
            if 'host' in item_type and self.filter_hosts_btn.is_checked():
                if self.filter_services_btn.is_checked():
                    self.filter_services_btn.update_btn_state(False)
            if 'service' in item_type and self.filter_services_btn.is_checked():
                if self.filter_hosts_btn.is_checked():
                    self.filter_hosts_btn.update_btn_state(False)
            problems_data['problems'] = [
                item for item in problems_data['problems'] if item_type in item.item_type
            ]

        proxy_filter = self.problems_table.update_view(problems_data)
        if problems_data['problems']:
            self.line_search.textChanged.connect(proxy_filter.setFilterRegExp)
        else:
            self.problems_title.setText(_('If problems are found, they will be displayed here.'))

        self.problems_table.selectionModel().selectionChanged.connect(self.update_action_buttons)
        self.update_action_buttons()

        if old_research:
            self.line_search.setText(old_research)
            self.line_search.textChanged.emit(old_research)
Ejemplo n.º 26
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_dropped = pyqtSignal(object)

    def __init__(self, parent):
        QListView.__init__(self, parent)
        setup_gestures(self)
        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()
        dpr = self.device_pixel_ratio
        self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'],
            thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * 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)
        self.resize_timer = t = QTimer(self)
        t.setInterval(200), t.setSingleShot(True)
        t.timeout.connect(self.update_memory_cover_cache_size)

    def viewportEvent(self, ev):
        ret = gesture_viewport_event(self, ev)
        if ret is not None:
            return ret
        return QListView.viewportEvent(self, ev)

    @property
    def device_pixel_ratio(self):
        try:
            return self.devicePixelRatioF()
        except AttributeError:
            return self.devicePixelRatio()

    @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 or
                gprefs['show_emblems'] != self.delegate.original_show_emblems or
                gprefs['emblem_size'] != self.delegate.orginal_emblem_size or
                gprefs['emblem_position'] != self.delegate.orginal_emblem_position):
            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()
        if size_changed:
            dpr = self.device_pixel_ratio
            self.thumbnail_cache.set_thumbnail_size(int(dpr * self.delegate.cover_size.width()), int(dpr*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)
        self.update_memory_cover_cache_size()

    def resizeEvent(self, ev):
        self.resize_timer.start()
        return QListView.resizeEvent(self, ev)

    def update_memory_cover_cache_size(self):
        try:
            sz = self.delegate.item_size
        except AttributeError:
            return
        rows, cols = self.width() // sz.width(), self.height() // sz.height()
        num = (rows + 1) * (cols + 1)
        limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple']))
        if limit != self.delegate.cover_cache.limit:
            self.delegate.cover_cache.set_limit(limit)

    def shown(self):
        self.update_memory_cover_cache_size()
        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')
            dpr = self.device_pixel_ratio
            p.setDevicePixelRatio(dpr)
            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, int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
                        p.setDevicePixelRatio(dpr)
                    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 AppTrayIcon(QSystemTrayIcon):
    """
        Class who create `QMenu` and `QActions` for `QSystemTrayIcon` (displayed in task bar)
    """

    def __init__(self, icon, parent=None):
        QSystemTrayIcon.__init__(self, icon, parent)
        # Fields
        self.menu = QMenu(parent)
        self.app_about = AboutQDialog()
        self.app_main = AppQMainWindow()
        self.connection_timer = QTimer()
        self.connection_nb = 3
        self.tray_actions = {
            'app': QAction(),
            'webui': QAction(),
            'reload': QAction(),
            'about': QAction(),
            'exit': QAction(),
        }

    def build_menu(self):
        """
        Initialize and create each QAction of QMenu

        """

        logger.info("Start TrayIcon...")

        self.connection_timer.setInterval(10000)
        self.connection_timer.start()
        self.connection_timer.timeout.connect(self.check_connection)

        # Create actions
        self.add_alignak_menu()
        self.add_webui_menu()
        self.menu.addSeparator()
        self.add_reload_menu()
        self.add_about_menu()
        self.menu.addSeparator()
        self.add_quit_menu()

        self.setContextMenu(self.menu)
        self.refresh_menus()

    def check_connection(self):
        """
        Check periodically connection to Alignak backend

        """

        if app_backend.connected:
            connect = app_backend.login(check=True)
            logger.info('App check connection: %s', app_backend.connection_status[connect])
            self.connection_nb = 3
        elif not app_backend.connected and self.connection_nb < 1:
            connect = app_backend.login(check=True)
            logger.warning('App check connection: %s', app_backend.connection_status[connect])
            self.connection_nb = 3
        elif not app_backend.connected:
            logger.warning('App check connection in %d0s', self.connection_nb)
            self.connection_nb -= 1
        else:
            pass

        self.app_main.dock.status_widget.update_status()
        self.refresh_menus()

    def add_alignak_menu(self):
        """
        Create and add to menu "app" QAction

        """

        self.app_main.initialize()

        self.tray_actions['app'].setIcon(QIcon(settings.get_image('icon')))
        self.tray_actions['app'].setText(_('Alignak-App'))
        self.tray_actions['app'].setToolTip(_('Display Alignak-App'))
        self.tray_actions['app'].triggered.connect(self.app_main.show)

        self.menu.addAction(self.tray_actions['app'])

    def add_webui_menu(self):
        """
        Create and add to menu "webui" QAction

        """

        self.tray_actions['webui'].setIcon(QIcon(settings.get_image('web')))
        self.tray_actions['webui'].setText(_('Go to WebUI'))
        self.tray_actions['webui'].setToolTip(_('Go to Alignak WebUI'))
        self.tray_actions['webui'].triggered.connect(
            lambda: open_url('livestate')
        )

        self.menu.addAction(self.tray_actions['webui'])

    def refresh_menus(self):
        """
        Refresh menu if needed

        """

        if settings.get_config('Alignak', 'webui'):
            self.tray_actions['webui'].setEnabled(True)
        else:
            self.tray_actions['webui'].setEnabled(False)

    def add_reload_menu(self):
        """
        Create and add to menu "reload" QAction

        """

        self.tray_actions['reload'].setIcon(QIcon(settings.get_image('refresh')))
        self.tray_actions['reload'].setText(_('Reload configuration'))
        self.tray_actions['reload'].setToolTip(_('Reload configuration'))
        self.tray_actions['reload'].triggered.connect(self.reload_configuration)

        self.menu.addAction(self.tray_actions['reload'])

    def add_about_menu(self):
        """
        Create and add to menu "about" QAction

        """

        self.app_about.initialize()

        self.tray_actions['about'].setIcon(QIcon(settings.get_image('about')))
        self.tray_actions['about'].setText(_('About...'))
        self.tray_actions['about'].setToolTip(_('About Alignak-app'))
        self.tray_actions['about'].triggered.connect(self.app_about.show_about)

        self.menu.addAction(self.tray_actions['about'])

    def add_quit_menu(self):
        """
        Create and add to menu "exit" QAction

        """

        self.tray_actions['exit'].setIcon(QIcon(settings.get_image('exit')))
        self.tray_actions['exit'].setText(_('Quit'))
        self.tray_actions['exit'].setToolTip(_('Quit Alignak-app'))
        self.tray_actions['exit'].triggered.connect(self.quit_app)

        self.menu.addAction(self.tray_actions['exit'])

    @staticmethod
    def quit_app():  # pragma: no cover
        """
        Quit application and close all widgets handle by application

        """

        thread_manager.stop_threads()
        logger.info('----- Alignak-App STOP -----')
        sys.exit(0)

    @staticmethod
    def reload_configuration():  # pragma: no cover
        """
        Reload configuration

        """

        logger.info('Reload configuration...')
        settings.init_config()
        settings.init_css()

        send_event('INFO', _('Configuration reloaded'), timer=True)
Ejemplo n.º 28
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_dropped = pyqtSignal(object)
    books_dropped = pyqtSignal(object)

    def __init__(self, parent):
        QListView.__init__(self, parent)
        self._ncols = None
        self.gesture_manager = GestureManager(self)
        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.set_color()
        self.ignore_render_requests = Event()
        dpr = self.device_pixel_ratio
        self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'],
            thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * 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)
        self.resize_timer = t = QTimer(self)
        t.setInterval(200), t.setSingleShot(True)
        t.timeout.connect(self.update_memory_cover_cache_size)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return QListView.viewportEvent(self, ev)

    @property
    def device_pixel_ratio(self):
        try:
            return self.devicePixelRatioF()
        except AttributeError:
            return self.devicePixelRatio()

    @property
    def first_visible_row(self):
        geom = self.viewport().geometry()
        for y in range(geom.top(), (self.spacing()*2) + geom.top(), 5):
            for x in range(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 range(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5):
            for x in range(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 range(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)):
            self.update(m.index(r, 0))

    def start_view_animation(self, index):
        d = self.delegate
        if d.animating is None and not config['disable_animations']:
            d.animating = index
            d.animation.start()

    def double_clicked(self, index):
        self.start_view_animation(index)
        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 or gprefs[
                'show_emblems'] != self.delegate.original_show_emblems or gprefs[
                    'emblem_size'] != self.delegate.orginal_emblem_size or gprefs[
                        'emblem_position'] != self.delegate.orginal_emblem_position):
            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.set_thumbnail_cache_image_size()
        cs = gprefs['cover_grid_disk_cache_size']
        if (cs*(1024**2)) != self.thumbnail_cache.max_size:
            self.thumbnail_cache.set_size(cs)
        self.update_memory_cover_cache_size()

    def set_thumbnail_cache_image_size(self):
        dpr = self.device_pixel_ratio
        self.thumbnail_cache.set_thumbnail_size(
            int(dpr * self.delegate.cover_size.width()), int(dpr*self.delegate.cover_size.height()))

    def resizeEvent(self, ev):
        self._ncols = None
        self.resize_timer.start()
        return QListView.resizeEvent(self, ev)

    def update_memory_cover_cache_size(self):
        try:
            sz = self.delegate.item_size
        except AttributeError:
            return
        rows, cols = self.width() // sz.width(), self.height() // sz.height()
        num = (rows + 1) * (cols + 1)
        limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple']))
        if limit != self.delegate.cover_cache.limit:
            self.delegate.cover_cache.set_limit(limit)

    def shown(self):
        self.update_memory_cover_cache_size()
        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
        dpr = self.device_pixel_ratio
        page_width = int(dpr * self.delegate.cover_size.width())
        page_height = int(dpr * self.delegate.cover_size.height())
        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')
            p.setDevicePixelRatio(dpr)
            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, page_width, page_height)
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
                        p.setDevicePixelRatio(dpr)
                    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[0]-i_x[1]):
            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 selectAll(self):
        # We re-implement this to ensure that only indexes from column 0 are
        # selected. The base class implementation selects all columns. This
        # causes problems with selection syncing, see
        # https://bugs.launchpad.net/bugs/1236348
        m = self.model()
        sm = self.selectionModel()
        sel = QItemSelection(m.index(0, 0), m.index(m.rowCount(QModelIndex())-1, 0))
        sm.select(sel, sm.ClearAndSelect)

    def set_current_row(self, row):
        sm = self.selectionModel()
        sm.setCurrentIndex(self.model().index(row, 0), sm.NoUpdate)

    def set_context_menu(self, menu):
        self.context_menu = menu

    def contextMenuEvent(self, event):
        if self.context_menu is None:
            return
        from calibre.gui2.main_window import clone_menu
        m = clone_menu(self.context_menu) if islinux else self.context_menu
        m.popup(event.globalPos())
        event.accept()

    def get_selected_ids(self):
        m = self.model()
        return [m.id(i) for i in self.selectionModel().selectedIndexes()]

    def restore_vpos(self, vpos):
        self.verticalScrollBar().setValue(vpos)

    def restore_hpos(self, hpos):
        pass

    def handle_mouse_press_event(self, ev):
        if QApplication.keyboardModifiers() & Qt.ShiftModifier:
            # Shift-Click in QListView is broken. It selects extra items in
            # various circumstances, for example, click on some item in the
            # middle of a row then click on an item in the next row, all items
            # in the first row will be selected instead of only items after the
            # middle item.
            index = self.indexAt(ev.pos())
            if not index.isValid():
                return
            ci = self.currentIndex()
            sm = self.selectionModel()
            sm.setCurrentIndex(index, sm.NoUpdate)
            if not ci.isValid():
                return
            if not sm.hasSelection():
                sm.select(index, sm.ClearAndSelect)
                return
            cr = ci.row()
            tgt = index.row()
            top = self.model().index(min(cr, tgt), 0)
            bottom = self.model().index(max(cr, tgt), 0)
            sm.select(QItemSelection(top, bottom), sm.Select)
        else:
            return QListView.mousePressEvent(self, ev)

    def indices_for_merge(self, resolved=True):
        return self.selectionModel().selectedIndexes()

    def number_of_columns(self):
        # Number of columns currently visible in the grid
        if self._ncols is None:
            step = max(10, self.spacing())
            for y in range(step, 500, step):
                for x in range(step, 500, step):
                    i = self.indexAt(QPoint(x, y))
                    if i.isValid():
                        for x in range(self.viewport().width() - step, self.viewport().width() - 300, -step):
                            j = self.indexAt(QPoint(x, y))
                            if j.isValid():
                                self._ncols = j.row() - i.row() + 1
                                return self._ncols
        return self._ncols

    def keyPressEvent(self, ev):
        if handle_enter_press(self, ev, self.start_view_animation, False):
            return
        k = ev.key()
        if ev.modifiers() & Qt.ShiftModifier and k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down):
            ci = self.currentIndex()
            if not ci.isValid():
                return
            c = ci.row()
            delta = {Qt.Key_Left: -1, Qt.Key_Right: 1, Qt.Key_Up: -self.number_of_columns(), Qt.Key_Down: self.number_of_columns()}[k]
            n = max(0, min(c + delta, self.model().rowCount(None) - 1))
            if n == c:
                return
            sm = self.selectionModel()
            rows = {i.row() for i in sm.selectedIndexes()}
            if rows:
                mi, ma = min(rows), max(rows)
                end = mi if c == ma else ma if c == mi else c
            else:
                end = c
            top = self.model().index(min(n, end), 0)
            bottom = self.model().index(max(n, end), 0)
            sm.select(QItemSelection(top, bottom), sm.ClearAndSelect)
            sm.setCurrentIndex(self.model().index(n, 0), sm.NoUpdate)
        else:
            return QListView.keyPressEvent(self, ev)

    @property
    def current_book(self):
        ci = self.currentIndex()
        if ci.isValid():
            try:
                return self.model().db.data.index_to_id(ci.row())
            except (IndexError, ValueError, KeyError, TypeError, AttributeError):
                pass

    def current_book_state(self):
        return self.current_book

    def restore_current_book_state(self, state):
        book_id = state
        self.setFocus(Qt.OtherFocusReason)
        try:
            row = self.model().db.data.id_to_index(book_id)
        except (IndexError, ValueError, KeyError, TypeError, AttributeError):
            return
        self.set_current_row(row)
        self.select_rows((row,))
        self.scrollTo(self.model().index(row, 0), self.PositionAtCenter)

    def marked_changed(self, old_marked, current_marked):
        changed = old_marked | current_marked
        m = self.model()
        for book_id in changed:
            try:
                self.update(m.index(m.db.data.id_to_index(book_id), 0))
            except ValueError:
                pass

    def moveCursor(self, action, modifiers):
        index = QListView.moveCursor(self, action, modifiers)
        if action in (QListView.MoveLeft, QListView.MoveRight) and index.isValid():
            ci = self.currentIndex()
            if ci.isValid() and index.row() == ci.row():
                nr = index.row() + (1 if action == QListView.MoveRight else -1)
                if 0 <= nr < self.model().rowCount(QModelIndex()):
                    index = self.model().index(nr, 0)
        return index

    def selectionCommand(self, index, event):
        if event and event.type() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(GridView, self).selectionCommand(index, event)

    def wheelEvent(self, ev):
        if ev.phase() not in (Qt.ScrollUpdate, 0):
            return
        number_of_pixels = ev.pixelDelta()
        number_of_degrees = ev.angleDelta() / 8.0
        b = self.verticalScrollBar()
        if number_of_pixels.isNull() or islinux:
            # pixelDelta() is broken on linux with wheel mice
            dy = number_of_degrees.y() / 15.0
            # Scroll by approximately half a row
            dy = int(math.ceil((dy) * b.singleStep() / 2.0))
        else:
            dy = number_of_pixels.y()
        if abs(dy) > 0:
            b.setValue(b.value() - dy)

    def paintEvent(self, ev):
        dpr = self.device_pixel_ratio
        page_width = int(dpr * self.delegate.cover_size.width())
        page_height = int(dpr * self.delegate.cover_size.height())
        size_changed = self.thumbnail_cache.set_thumbnail_size(page_width, page_height)
        if size_changed:
            self.delegate.cover_cache.clear()

        return super(GridView, self).paintEvent(ev)
Ejemplo n.º 29
0
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.Link, theme_color(theme, 'Link', 'fg'))
        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_type(
            mf.evaluateJavaScript(
                'window.calibre_preview_integration.live_css(%s, %s)' %
                (json.dumps(sourceline), json.dumps(tags))) or '')
        try:
            result = json.loads(result)
        except ValueError:
            result = None
        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, SelectorError):
                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(
                '%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)):
            qurl = QUrl(href)
            name = qurl.path()[1:]
            if name:
                rule['href'] = name

    @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):
        self.update_data()

    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(for_position_sync=False)
            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'])