예제 #1
0
class PlayerLabel(QLabel):
    loading_movie = None

    def __init__(self, fontsize, parent=None):
        super().__init__(parent)
        self.fontsize = fontsize

        cls = type(self)
        if not cls.loading_movie:
            cls.loading_movie = QMovie(resource_path("loading.gif"))
            cls.loading_movie.setScaledSize(QSize(MOVIEWIDTH, MOVIEWIDTH))
            cls.loading_movie.start()
        # self.player_heading.setGeometry(0, 140, self.rect().width(), 50)
        # self.player_heading.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        f = self.font()
        f.setPointSize(self.fontsize)
        self.setFont(f)
        self.setAutoFillBackground(True)

        self.setMovie(cls.loading_movie)

        self.blink_timer = None

    def buzz_hint(self):
        self.setStyleSheet("QLabel { background-color : grey}")
        self.blink_timer = QTimer()
        # self.blink_timer.moveToThread(QApplication.instance().thread())
        self.blink_timer.timeout.connect(self._buzz_hint_callback)
        self.blink_timer.start(100)

    def _buzz_hint_callback(self):
        self.setStyleSheet("QLabel { background-color : none}")
예제 #2
0
class KeysWidget(QWidget):
    keyPressed = QtCore.pyqtSignal(QtCore.QEvent)

    def __init__(self, parent=None):
        super(KeysWidget, self).__init__(parent)
        self.classifyExercises = None
        if parent is not None:
            self.classifyExercises = parent.classifyExercises
            self.infoLabel = parent.infoLabel

        self.ui = Ui_KeysPanel()
        self.ui.setupUi(self)
        self.monitor = KeyMonitor()
        self.monitor.start_monitoring()

        self.timer = QTimer()
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start()

        self.ui.saveProfile.clicked.connect(self.saveBindings)

    def saveBindings(self):
        if self.classifyExercises.subject is not None:
            key_list = [x.serialize() for x in self.classifyExercises.exercises.values()]
            content = {
                self.classifyExercises.subject: key_list
            }
            print(content)
            with open(MAPPED_KEYS_PATH + self.classifyExercises.subject + '.json', "w") as f:
                json.dump(content, f)

    def onTimeout(self):
        if self.monitor.released:
            for b in self.ui.buttons:
                b.setStyleSheet(
                    """ QPushButton
                    {
                        border: 1px solid grey;
                        background-color: white;
                    }
                    """)
        else:
            for ind in range(0, len(self.ui.exercises)):
                if self.monitor.currentKey == self.ui.exercises[ind].assigned_key[1]:
                    self.ui.buttons[ind].setStyleSheet(
                    """ QPushButton
                    {
                        border: 1px solid green;
                        background-color: #7FFFD4;
                    }
                    """)
예제 #3
0
    def _scheduleDelayedCallEvent(self, event: "_CallFunctionEvent") -> None:
        if event.delay is None:
            return

        timer = QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(event.delay * 1000 * (1 + self.TIME_TOLERANCE))
        timer_callback = lambda e=event: self._onDelayReached(e)
        timer.timeout.connect(timer_callback)
        timer.start()
        self._delayed_events[event] = {
            "event": event,
            "timer": timer,
            "timer_callback": timer_callback,
        }
예제 #4
0
class QDoublePushButton(QPushButton):
    """加入了双击事件的按钮"""
    doubleClicked = pyqtSignal()
    clicked = pyqtSignal()

    def __init__(self, *args, **kwargs):
        QPushButton.__init__(self, *args, **kwargs)
        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.clicked.emit)
        super().clicked.connect(self.checkDoubleClick)

    def checkDoubleClick(self):
        if self.timer.isActive():
            self.doubleClicked.emit()
            self.timer.stop()
        else:
            self.timer.start(250)
예제 #5
0
class WinForm(QWidget):
    def __init__(self, parent=None):
        super(WinForm, self).__init__(parent)
        self.setWindowTitle("QTimer demo")
        self.listFile = QListWidget()
        self.label = QLabel("显示当前时间")
        self.startButton = QPushButton("开始")
        self.endButton = QPushButton("结束")
        layout = QGridLayout(self)

        # 初始化定时器
        self.timer = QTimer(self)
        # 显示时间
        self.timer.timeout.connect(
            self.showTime)  # timeout 信号连接到特定的槽,当定时器超时,发出 timeout 信号

        layout.addWidget(self.label, 0, 0, 1, 2)
        layout.addWidget(self.startButton, 1, 0)
        layout.addWidget(self.endButton, 1, 1)

        self.startButton.clicked.connect(self.start_timer)
        self.endButton.clicked.connect(self.end_timer)

        self.setLayout(layout)

    def showTime(self):
        # 获取当前系统时间
        time = QDateTime.currentDateTime()
        # 设置时间格式
        timeDisplay = time.toString("yyyy-MM-dd hh:mm:ss dddd")
        self.label.setText(timeDisplay)

    def start_timer(self):
        # 设置时间间隔并启动定时器
        self.timer.start(1000)  # start 内设置时间间隔,启动或重新启动计时器,如果计时器在运行,则重启
        self.startButton.setEnabled(False)
        self.endButton.setEnabled(True)

    def end_timer(self):
        self.timer.stop()  # 停止计时器
        self.startButton.setEnabled(True)
        self.endButton.setEnabled(False)
예제 #6
0
class ProgressBar(QProgressBar):
    def __init__(self, *args, **kwargs):
        super(ProgressBar, self).__init__(*args, **kwargs)
        self.setValue(0)
        if self.minimum() != self.maximum():
            self.timer = QTimer(self)
            self.timer.timeout.connect(self.onTimeout)
            self.timer.start(100)

    def start(self):
        self.timer.start(100)

    def stop(self):
        self.timer.stop()

    def onTimeout(self):
        if self.value() >= 100:
            self.timer.stop()
            self.timer.deleteLater()
            del self.timer
            return
        self.setValue(self.value() + 1)
예제 #7
0
        self.btn1.setStyleSheet('background-color:grey')
        self.btn1.setFont(QFont('san serif', 15))
        self.btn0.clicked.connect(self.clickButton0)
        self.btn1.clicked.connect(self.clickButton1)

    def clickButton0(self):
        self.btn0.setStyleSheet('background-color:grey')
        self.btn1.setStyleSheet('background-color:red')
        if self.oneClick:
            main.start()
        self.oneClick = False

    def clickButton1(self):
        self.btn1.setStyleSheet('background-color:grey')
        self.btn0.setStyleSheet('background-color:green')
        main.end()
        self.oneClick = True
        
    def checkCount(self):
        text = 'word count: ' + str(htmlparse.wordCount)
        self.label.setText(text)


app = QApplication(sys.argv)
window = Window()
window.show()
timer = QTimer()
timer.timeout.connect(window.checkCount)
timer.start(1000)
sys.exit(app.exec())
예제 #8
0
    global y
    lock.acquire()
    for i in range(len(toAdd)):
        w.updateImage(2079-x, y, toAdd[i])
        x += 1
        if x > 2079:
            x = 0
            y += 1
    toAdd = []
    lock.release()
    w.refreshImage()


loop_thread = threading.Thread(target=loop)
loop_thread.start()

timer = QTimer()
timer.timeout.connect(update_gui)
timer.start(1)

# Run the app until we close
app_ret = app.exec()
# Tell thread to finish
keepRunning = False
# Wait for thread to finish
loop_thread.join()
update_gui()
w.saveImage()
# Exit
sys.exit(app_ret)
예제 #9
0
class TabAntiWarping(Tool):
    def __init__(self):
        super().__init__()
        
        # variable for menu dialog        
        self._UseSize = 0.0
        self._UseOffset = 0.0
        self._AsCapsule = False
        self._Nb_Layer = 1


        # Shortcut
        if not VERSION_QT5:
            self._shortcut_key = Qt.Key.Key_I
        else:
            self._shortcut_key = Qt.Key_I
            
        self._controller = self.getController()

        self._selection_pass = None

        # self._i18n_catalog = None
        
        self.Major=1
        self.Minor=0

        # Logger.log('d', "Info Version CuraVersion --> " + str(Version(CuraVersion)))
        Logger.log('d', "Info CuraVersion --> " + str(CuraVersion))
        
        # Test version for Cura Master
        # https://github.com/smartavionics/Cura
        if "master" in CuraVersion :
            self.Major=4
            self.Minor=20
        else:
            try:
                self.Major = int(CuraVersion.split(".")[0])
                self.Minor = int(CuraVersion.split(".")[1])
            except:
                pass
        
        self.setExposedProperties("SSize", "SOffset", "SCapsule", "NLayer")
        
        CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
        
         
        # Note: if the selection is cleared with this tool active, there is no way to switch to
        # another tool than to reselect an object (by clicking it) because the tool buttons in the
        # toolbar will have been disabled. That is why we need to ignore the first press event
        # after the selection has been cleared.
        Selection.selectionChanged.connect(self._onSelectionChanged)
        self._had_selection = False
        self._skip_press = False

        self._had_selection_timer = QTimer()
        self._had_selection_timer.setInterval(0)
        self._had_selection_timer.setSingleShot(True)
        self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
        
        # set the preferences to store the default value
        self._preferences = CuraApplication.getInstance().getPreferences()
        self._preferences.addPreference("customsupportcylinder/p_size", 10)
        # convert as float to avoid further issue
        self._UseSize = float(self._preferences.getValue("customsupportcylinder/p_size"))
 
        self._preferences.addPreference("customsupportcylinder/p_offset", 0.16)
        # convert as float to avoid further issue
        self._UseOffset = float(self._preferences.getValue("customsupportcylinder/p_offset"))

        self._preferences.addPreference("customsupportcylinder/as_capsule", False)
        # convert as float to avoid further issue
        self._AsCapsule = bool(self._preferences.getValue("customsupportcylinder/as_capsule"))   

        self._preferences.addPreference("customsupportcylinder/nb_layer", 1)
        # convert as float to avoid further issue
        self._Nb_Layer = int(self._preferences.getValue("customsupportcylinder/nb_layer"))        
                
    def event(self, event):
        super().event(event)
        modifiers = QApplication.keyboardModifiers()
        if not VERSION_QT5:
            ctrl_is_active = modifiers & Qt.KeyboardModifier.ControlModifier
        else:
            ctrl_is_active = modifiers & Qt.ControlModifier

        if event.type == Event.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled():
            if ctrl_is_active:
                self._controller.setActiveTool("TranslateTool")
                return

            if self._skip_press:
                # The selection was previously cleared, do not add/remove an support mesh but
                # use this click for selection and reactivating this tool only.
                self._skip_press = False
                return

            if self._selection_pass is None:
                # The selection renderpass is used to identify objects in the current view
                self._selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection")
            picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
            if not picked_node:
                # There is no slicable object at the picked location
                return

            node_stack = picked_node.callDecoration("getStack")

            
            if node_stack:
            
                if node_stack.getProperty("support_mesh", "value"):
                    self._removeSupportMesh(picked_node)
                    return

                elif node_stack.getProperty("anti_overhang_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("support_mesh", "value"):
                    # Only "normal" meshes can have support_mesh added to them
                    return

            # Create a pass for picking a world-space location from the mouse location
            active_camera = self._controller.getScene().getActiveCamera()
            picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
            picking_pass.render()

            picked_position = picking_pass.getPickedPosition(event.x, event.y)

            # Add the support_mesh cube at the picked location
            self._createSupportMesh(picked_node, picked_position)

    def _createSupportMesh(self, parent: CuraSceneNode, position: Vector):
        node = CuraSceneNode()

        node.setName("RoundTab")
            
        node.setSelectable(True)
        
        # long=Support Height
        _long=position.y

        # get layer_height_0 used to define pastille height
        _id_ex=0
        
        # This function can be triggered in the middle of a machine change, so do not proceed if the machine change
        # has not done yet.
        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        #extruder = global_container_stack.extruderList[int(_id_ex)] 
        extruder_stack = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks()[0]        
        _layer_h_i = extruder_stack.getProperty("layer_height_0", "value")
        _layer_height = extruder_stack.getProperty("layer_height", "value")
        _line_w = extruder_stack.getProperty("line_width", "value")
        # Logger.log('d', 'layer_height_0 : ' + str(_layer_h_i))
        _layer_h = (_layer_h_i * 1.2) + (_layer_height * (self._Nb_Layer -1) )
        _line_w = _line_w * 1.2 
        
        if self._AsCapsule:
             # Capsule creation Diameter , Increment angle 4°, length, layer_height_0*1.2 , line_width
            mesh = self._createCapsule(self._UseSize,4,_long,_layer_h,_line_w)       
        else:
            # Cylinder creation Diameter , Increment angle 4°, length, layer_height_0*1.2
            mesh = self._createPastille(self._UseSize,4,_long,_layer_h)
        
        node.setMeshData(mesh.build())

        active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
        node.addDecorator(BuildPlateDecorator(active_build_plate))
        node.addDecorator(SliceableObjectDecorator())

        stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode
        settings = stack.getTop()

        # support_mesh type
        definition = stack.getSettingDefinition("support_mesh")
        new_instance = SettingInstance(definition, settings)
        new_instance.setProperty("value", True)
        new_instance.resetState()  # Ensure that the state is not seen as a user state.
        settings.addInstance(new_instance)

        definition = stack.getSettingDefinition("support_mesh_drop_down")
        new_instance = SettingInstance(definition, settings)
        new_instance.setProperty("value", False)
        new_instance.resetState()  # Ensure that the state is not seen as a user state.
        settings.addInstance(new_instance)
 
        if self._AsCapsule:
            s_p = global_container_stack.getProperty("support_type", "value")
            if s_p ==  'buildplate' :
                Message(text = "Info modification current profile support_type parameter\nNew value : everywhere", title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show()
                Logger.log('d', 'support_type different : ' + str(s_p))
                # Define support_type=everywhere
                global_container_stack.setProperty("support_type", "value", 'everywhere')
                
            
        # Define support_xy_distance
        definition = stack.getSettingDefinition("support_xy_distance")
        new_instance = SettingInstance(definition, settings)
        new_instance.setProperty("value", self._UseOffset)
        # new_instance.resetState()  # Ensure that the state is not seen as a user state.
        settings.addInstance(new_instance)

        # Fix some settings in Cura to get a better result
        id_ex=0
        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        extruder_stack = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks()[0]
        #extruder = global_container_stack.extruderList[int(id_ex)]    
        
        # hop to fix it in a futur release
        # https://github.com/Ultimaker/Cura/issues/9882
        # if self.Major < 5 or ( self.Major == 5 and self.Minor < 1 ) :
        _xy_distance = extruder_stack.getProperty("support_xy_distance", "value")
        if self._UseOffset !=  _xy_distance :
            _msg = "New value : %8.3f" % (self._UseOffset) 
            Message(text = "Info modification current profile support_xy_distance parameter\nNew value : %8.3f" % (self._UseOffset), title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show()
            Logger.log('d', 'support_xy_distance different : ' + str(_xy_distance))
            # Define support_xy_distance
            extruder_stack.setProperty("support_xy_distance", "value", self._UseOffset)
 
        if self._Nb_Layer >1 :
            s_p = int(extruder_stack.getProperty("support_infill_rate", "value"))
            Logger.log('d', 'support_infill_rate actual : ' + str(s_p))
            if s_p < 99 :
                Message(text = "Info modification current profile support_infill_rate parameter\nNew value : 100%", title = catalog.i18nc("@info:title", "Warning ! Tab Anti Warping")).show()
                Logger.log('d', 'support_infill_rate different : ' + str(s_p))
                # Define support_infill_rate=100%
                extruder_stack.setProperty("support_infill_rate", "value", 100)
                
        
        
        op = GroupedOperation()
        # First add node to the scene at the correct position/scale, before parenting, so the support mesh does not get scaled with the parent
        op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
        op.addOperation(SetParentOperation(node, parent))
        op.push()
        node.setPosition(position, CuraSceneNode.TransformSpace.World)

        CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)

    def _removeSupportMesh(self, node: CuraSceneNode):
        parent = node.getParent()
        if parent == self._controller.getScene().getRoot():
            parent = None

        op = RemoveSceneNodeOperation(node)
        op.push()

        if parent and not Selection.isSelected(parent):
            Selection.add(parent)

        CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)

    def _updateEnabled(self):
        plugin_enabled = False

        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        if global_container_stack:
            plugin_enabled = global_container_stack.getProperty("support_mesh", "enabled")

        CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
    
    def _onSelectionChanged(self):
        # When selection is passed from one object to another object, first the selection is cleared
        # and then it is set to the new object. We are only interested in the change from no selection
        # to a selection or vice-versa, not in a change from one object to another. A timer is used to
        # "merge" a possible clear/select action in a single frame
        if Selection.hasSelection() != self._had_selection:
            self._had_selection_timer.start()

    def _selectionChangeDelay(self):
        has_selection = Selection.hasSelection()
        if not has_selection and self._had_selection:
            self._skip_press = True
        else:
            self._skip_press = False

        self._had_selection = has_selection
 
    # Capsule creation
    def _createCapsule(self, size, nb , lg, He, lw):
        mesh = MeshBuilder()
        # Per-vertex normals require duplication of vertices
        r = size / 2
        # First layer length
        sup = -lg + He
        if self._Nb_Layer >1 :
            sup_c = -lg + (He * 2)
        else:
            sup_c = -lg + (He * 3)
        l = -lg
        rng = int(360 / nb)
        ang = math.radians(nb)

        r_sup=math.tan(math.radians(45))*(He * 3)+r
        # Top inside radius 
        ri=r_sup-(1.8*lw)
        # Top radius 
        rit=r-(1.8*lw)
            
        verts = []
        for i in range(0, rng):
            # Top
            verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)])
            verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)])
            verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)])
            
            verts.append([ri*math.cos((i+1)*ang), sup_c, ri*math.sin((i+1)*ang)])
            verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)])
            verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)])

            #Side 1a
            verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)])
            verts.append([r_sup*math.cos((i+1)*ang), sup_c, r_sup*math.sin((i+1)*ang)])
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)])
            
            #Side 1b
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)])
            verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)])
            verts.append([r_sup*math.cos(i*ang), sup_c, r_sup*math.sin(i*ang)])
 
            #Side 2a
            verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)])
            verts.append([ri*math.cos((i+1)*ang), sup_c, ri*math.sin((i+1)*ang)])
            verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)])
            
            #Side 2b
            verts.append([ri*math.cos(i*ang), sup_c, ri*math.sin(i*ang)])
            verts.append([rit*math.cos(i*ang), sup, rit*math.sin(i*ang)])
            verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)])
                
            #Bottom Top
            verts.append([0, sup, 0])
            verts.append([rit*math.cos((i+1)*ang), sup, rit*math.sin((i+1)*ang)])
            verts.append([rit*math.cos(i*ang), sup, rit*math.sin(i*ang)])
            
            #Bottom 
            verts.append([0, l, 0])
            verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)])
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) 
            
            
        mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))

        indices = []
        # for every angle increment 24 Vertices
        tot = rng * 24
        for i in range(0, tot, 3): # 
            indices.append([i, i+1, i+2])
        mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))

        mesh.calculateNormals()
        return mesh
        
    # Cylinder creation
    def _createPastille(self, size, nb , lg, He):
        mesh = MeshBuilder()
        # Per-vertex normals require duplication of vertices
        r = size / 2
        # First layer length
        sup = -lg + He
        l = -lg
        rng = int(360 / nb)
        ang = math.radians(nb)
        
        verts = []
        for i in range(0, rng):
            # Top
            verts.append([0, sup, 0])
            verts.append([r*math.cos((i+1)*ang), sup, r*math.sin((i+1)*ang)])
            verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)])
            #Side 1a
            verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)])
            verts.append([r*math.cos((i+1)*ang), sup, r*math.sin((i+1)*ang)])
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)])
            #Side 1b
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)])
            verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)])
            verts.append([r*math.cos(i*ang), sup, r*math.sin(i*ang)])
            #Bottom 
            verts.append([0, l, 0])
            verts.append([r*math.cos(i*ang), l, r*math.sin(i*ang)])
            verts.append([r*math.cos((i+1)*ang), l, r*math.sin((i+1)*ang)]) 
            
            
        mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))

        indices = []
        # for every angle increment 12 Vertices
        tot = rng * 12
        for i in range(0, tot, 3): # 
            indices.append([i, i+1, i+2])
        mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))

        mesh.calculateNormals()
        return mesh
    
    def getSSize(self) -> float:
        """ 
            return: golabl _UseSize  in mm.
        """           
        return self._UseSize
  
    def setSSize(self, SSize: str) -> None:
        """
        param SSize: Size in mm.
        """
 
        try:
            s_value = float(SSize)
        except ValueError:
            return

        if s_value <= 0:
            return
        
        #Logger.log('d', 's_value : ' + str(s_value))        
        self._UseSize = s_value
        self._preferences.setValue("customsupportcylinder/p_size", s_value)
 
    def getNLayer(self) -> int:
        """ 
            return: golabl _Nb_Layer
        """           
        return self._Nb_Layer
  
    def setNLayer(self, NLayer: str) -> None:
        """
        param NLayer: NLayer as integer >1
        """
 
        try:
            i_value = int(NLayer)
            
        except ValueError:
            return
 
        if i_value < 1:
            return
            
        Logger.log('d', 'i_value : ' + str(i_value))        
        self._Nb_Layer = i_value
        self._preferences.setValue("customsupportcylinder/nb_layer", i_value)
        
    def getSOffset(self) -> float:
        """ 
            return: golabl _UseOffset  in mm.
        """           
        return self._UseOffset
  
    def setSOffset(self, SOffset: str) -> None:
        """
        param SOffset: SOffset in mm.
        """
 
        try:
            s_value = float(SOffset)
        except ValueError:
            return
        
        #Logger.log('d', 's_value : ' + str(s_value))        
        self._UseOffset = s_value
        self._preferences.setValue("customsupportcylinder/p_offset", s_value)

    def getSCapsule(self) -> bool:
        """ 
            return: golabl _AsCapsule  as boolean
        """           
        return self._AsCapsule
  
    def setSCapsule(self, SCapsule: bool) -> None:
        """
        param SCapsule: as boolean.
        """
        self._AsCapsule = SCapsule
        self._preferences.setValue("customsupportcylinder/as_capsule", SCapsule)
class MPPIOptionsWindow(QWidget):
    def __init__(self):
        super(MPPIOptionsWindow, self).__init__()

        self.horizon_steps = controller_mppi.mpc_samples
        self.num_rollouts = controller_mppi.num_rollouts
        self.dd_weight = controller_mppi.dd_weight
        self.ep_weight = controller_mppi.ep_weight
        self.ekp_weight = controller_mppi.ekp_weight * 1.0e1
        self.ekc_weight = controller_mppi.ekc_weight * 1.0e-1
        self.cc_weight = controller_mppi.cc_weight * 1.0e-2
        self.ccrc_weight = controller_mppi.ccrc_weight * 1.0e-2
        self.R = controller_mppi.R  # How much to punish Q
        self.LBD = controller_mppi.LBD  # Cost parameter lambda
        self.NU = controller_mppi.NU  # Exploration variance

        layout = QVBoxLayout()

        ### Set Horizon Length
        horizon_options_layout = QVBoxLayout()

        self.horizon_label = QLabel("")
        self.horizon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        horizon_options_layout.addWidget(self.horizon_label)

        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(10, 300)
        slider.setValue(self.horizon_steps)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        horizon_options_layout.addWidget(slider)

        slider.valueChanged.connect(self.horizon_length_changed)

        ### Set Number of Rollouts
        rollouts_options_layout = QVBoxLayout()

        self.rollouts_label = QLabel("")
        self.rollouts_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        rollouts_options_layout.addWidget(self.rollouts_label)

        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(10, 3000)
        slider.setValue(self.num_rollouts)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        rollouts_options_layout.addWidget(slider)

        slider.valueChanged.connect(self.num_rollouts_changed)

        ### Set Cost Weights
        cost_weight_layout = QVBoxLayout()

        # Distance difference cost
        self.dd_weight_label = QLabel("")
        self.dd_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.dd_weight_label)
        self.dd_label = QLabel("")
        self.dd_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.dd_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 990)
        slider.setValue(self.dd_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.dd_weight_changed)

        # Potential energy cost
        self.ep_weight_label = QLabel("")
        self.ep_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ep_weight_label)
        self.ep_label = QLabel("")
        self.ep_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ep_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 1e5 - 1e3)
        slider.setValue(self.ep_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1e3)
        slider.setSingleStep(1e3)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ep_weight_changed)

        # Pole kinetic energy cost
        self.ekp_weight_label = QLabel("")
        self.ekp_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekp_weight_label)
        self.ekp_label = QLabel("")
        self.ekp_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekp_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ekp_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ekp_weight_changed)

        # Cart kinetic energy cost
        self.ekc_weight_label = QLabel("")
        self.ekc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekc_weight_label)
        self.ekc_label = QLabel("")
        self.ekc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ekc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ekc_weight_changed)

        # Control cost
        self.cc_weight_label = QLabel("")
        self.cc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.cc_weight_label)
        self.cc_label = QLabel("")
        self.cc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.cc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.cc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.cc_weight_changed)

        # Control change rate cost
        self.ccrc_weight_label = QLabel("")
        self.ccrc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ccrc_weight_label)
        self.ccrc_label = QLabel("")
        self.ccrc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ccrc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ccrc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ccrc_weight_changed)

        ### Set some more MPPI constants
        mppi_constants_layout = QVBoxLayout()

        # Quadratic cost penalty R
        textbox = QLineEdit()
        textbox.setText(str(self.R))
        textbox.textChanged.connect(self.R_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Quadratic input cost penalty R ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Quadratic cost penalty LBD
        textbox = QLineEdit()
        textbox.setText(str(self.LBD))
        textbox.textChanged.connect(self.LBD_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Importance of higher-cost rollouts LBD ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Quadratic cost penalty NU
        textbox = QLineEdit()
        textbox.setText(str(self.NU))
        textbox.textChanged.connect(self.NU_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Exploration variance NU ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Sampling type
        h_layout = QHBoxLayout()
        btn1 = QRadioButton("iid")
        if btn1.text() == controller_mppi.SAMPLING_TYPE: btn1.setChecked(True)
        btn1.toggled.connect(lambda: self.toggle_button(btn1))
        h_layout.addWidget(btn1)
        btn2 = QRadioButton("random_walk")
        if btn2.text() == controller_mppi.SAMPLING_TYPE: btn2.setChecked(True)
        btn2.toggled.connect(lambda: self.toggle_button(btn2))
        h_layout.addWidget(btn2)
        btn3 = QRadioButton("uniform")
        if btn3.text() == controller_mppi.SAMPLING_TYPE: btn3.setChecked(True)
        btn3.toggled.connect(lambda: self.toggle_button(btn3))
        h_layout.addWidget(btn3)
        btn4 = QRadioButton("repeated")
        if btn4.text() == controller_mppi.SAMPLING_TYPE: btn4.setChecked(True)
        btn4.toggled.connect(lambda: self.toggle_button(btn4))
        h_layout.addWidget(btn4)
        btn5 = QRadioButton("interpolated")
        if btn5.text() == controller_mppi.SAMPLING_TYPE: btn5.setChecked(True)
        btn5.toggled.connect(lambda: self.toggle_button(btn5))
        h_layout.addWidget(btn5)
        mppi_constants_layout.addWidget(QLabel("Sampling type:"))
        mppi_constants_layout.addLayout(h_layout)

        ### Put together layout
        self.update_labels()
        self.update_slider_labels()
        layout.addLayout(horizon_options_layout)
        layout.addLayout(rollouts_options_layout)
        layout.addLayout(cost_weight_layout)
        layout.addLayout(mppi_constants_layout)

        self.setLayout(layout)
        self.setWindowFlags(self.windowFlags()
                            | Qt.WindowType.WindowStaysOnTopHint)
        self.setGeometry(0, 0, 400, 50)

        self.show()
        self.setWindowTitle("MPPI Options")

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_labels)
        self.timer.start(100)

    def horizon_length_changed(self, val: int):
        self.horizon_steps = val
        # TODO: Replace by setter method
        controller_mppi.mpc_samples = self.horizon_steps
        self.update_slider_labels()

    def num_rollouts_changed(self, val: int):
        self.num_rollouts = val
        controller_mppi.num_rollouts = self.num_rollouts
        self.update_slider_labels()

    def dd_weight_changed(self, val: int):
        self.dd_weight = val
        # TODO: Replace by setter method
        controller_mppi.dd_weight = self.dd_weight * 1.0
        self.update_slider_labels()

    def ep_weight_changed(self, val: int):
        self.ep_weight = val
        # TODO: Replace by setter method
        controller_mppi.ep_weight = self.ep_weight * 1.0
        self.update_slider_labels()

    def ekp_weight_changed(self, val: int):
        self.ekp_weight = val
        # TODO: Replace by setter method
        controller_mppi.ekp_weight = self.ekp_weight * 1.0e-1
        self.update_slider_labels()

    def ekc_weight_changed(self, val: int):
        self.ekc_weight = val
        # TODO: Replace by setter method
        controller_mppi.ekc_weight = self.ekc_weight * 1.0e1
        self.update_slider_labels()

    def cc_weight_changed(self, val: int):
        self.cc_weight = val
        # TODO: Replace by setter method
        controller_mppi.cc_weight = self.cc_weight * 1.0e2
        self.update_slider_labels()

    def ccrc_weight_changed(self, val: int):
        self.ccrc_weight = val
        # TODO: Replace by setter method
        controller_mppi.ccrc_weight = self.ccrc_weight * 1.0e2
        self.update_slider_labels()

    def R_changed(self, val: str):
        if val == '': val = '0'
        val = float(val)
        self.R = val
        controller_mppi.R = self.R

    def LBD_changed(self, val: str):
        if val == '': val = '0'
        val = float(val)
        if val == 0: val = 1.0
        self.LBD = val
        controller_mppi.LBD = self.LBD

    def NU_changed(self, val: str):
        if val == '': val = '0'
        val = float(val)
        if val == 0: val = 1.0
        self.NU = val
        controller_mppi.NU = self.NU

    def toggle_button(self, b):
        if b.isChecked(): controller_mppi.SAMPLING_TYPE = b.text()

    def update_slider_labels(self):
        self.horizon_label.setText(
            f"Horizon: {self.horizon_steps} steps = {round(self.horizon_steps * controller_mppi.dt, 2)} s"
        )
        self.rollouts_label.setText(f"Rollouts: {self.num_rollouts}")
        self.dd_weight_label.setText(
            f"Distance difference cost weight: {round(self.dd_weight, 2)}")
        self.ep_weight_label.setText(
            f"Pole angle cost weight: {round(self.ep_weight, 2)}")
        self.ekp_weight_label.setText(
            f"Pole kinetic energy cost weight: {round(self.ekp_weight * 1.0e-1, 4)}"
        )
        self.ekc_weight_label.setText(
            f"Cart kinetic energy cost weight: {round(self.ekc_weight * 1.0e1, 3)}"
        )
        self.cc_weight_label.setText(
            f"Control cost weight: {round(self.cc_weight * 1.0e2, 3)}")
        self.ccrc_weight_label.setText(
            f"Control change rate cost weight: {round(self.ccrc_weight * 1.0e2, 3)}"
        )

    def update_labels(self):
        self.dd_label.setText(f"{round(controller_mppi.gui_dd.item(), 2)}")
        self.ep_label.setText(f"{round(controller_mppi.gui_ep.item(), 2)}")
        self.ekp_label.setText(f"{round(controller_mppi.gui_ekp.item(), 2)}")
        self.ekc_label.setText(f"{round(controller_mppi.gui_ekc.item(), 2)}")
        self.cc_label.setText(f"{round(controller_mppi.gui_cc.item(), 2)}")
        self.ccrc_label.setText(f"{round(controller_mppi.gui_ccrc.item(), 2)}")
예제 #11
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setFixedSize(800, 410)
        self.setWindowTitle("PyLX16A Servo Testing Software")

        self.port_selection_box = QComboBox(self)
        self.port_selection_box.setFixedSize(200, 27)
        self.port_selection_box.move(30, 65)
        port_selection_box_label = QLabel("Select Port:", self)
        port_selection_box_label.move(30, 35)

        self.port_selection_box_refresh_button = QPushButton("Refresh", self)
        self.port_selection_box_refresh_button.setFixedSize(60, 23)
        self.port_selection_box_refresh_button.move(170, 38)

        self.id_selection_box = QListWidget(self)
        self.id_selection_box.setFixedSize(200, 200)
        self.id_selection_box.move(30, 135)
        id_selection_box_label = QLabel("Connected Servos:", self)
        id_selection_box_label.setFixedWidth(200)
        id_selection_box_label.move(30, 105)

        self.id_selection_box_refresh_button = QPushButton("Refresh", self)
        self.id_selection_box_refresh_button.setFixedSize(60, 23)
        self.id_selection_box_refresh_button.move(170, 108)

        self.set_id_line_edit = QLineEdit(self)
        self.set_id_line_edit.setFixedSize(50, 27)
        self.set_id_line_edit.move(80, 355)
        set_id_line_edit_label = QLabel("Set ID:", self)
        set_id_line_edit_label.move(30, 355)
        set_id_line_edit_label.setFixedSize(50, 27)

        self.set_id_button = QPushButton("Change ID!", self)
        self.set_id_button.setFixedSize(85, 27)
        self.set_id_button.move(145, 355)

        self.position_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_slider.setMinimum(0)
        self.position_slider.setMaximum(240)
        self.position_slider.setFixedWidth(200)
        self.position_slider.move(300, 55)
        self.position_slider_readout = QLabel("0.00°", self)
        self.position_slider_readout.setFixedWidth(50)
        self.position_slider_readout.move(450, 30)
        self.position_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_slider_label = QLabel("Angle (degrees):", self)
        position_slider_label.move(300, 30)

        self.position_offset_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_offset_slider.setMinimum(-30)
        self.position_offset_slider.setMaximum(30)
        self.position_offset_slider.setFixedWidth(200)
        self.position_offset_slider.move(300, 125)
        self.position_offset_slider_readout = QLabel("0.00°", self)
        self.position_offset_slider_readout.setFixedWidth(50)
        self.position_offset_slider_readout.move(450, 100)
        self.position_offset_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_offset_slider_label = QLabel("Angle offset (degrees):", self)
        position_offset_slider_label.setFixedWidth(200)
        position_offset_slider_label.move(300, 100)

        self.angle_lower_limit_textentry = QLineEdit(self)
        self.angle_lower_limit_textentry.setFixedWidth(50)
        self.angle_lower_limit_textentry.move(450, 175)
        self.angle_lower_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_upper_limit_textentry = QLineEdit(self)
        self.angle_upper_limit_textentry.setFixedWidth(50)
        self.angle_upper_limit_textentry.move(450, 210)
        self.angle_upper_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_lower_limit_textentry_label = QLabel(
            "Lower Limit (degrees):", self)
        self.angle_lower_limit_textentry_label.move(300, 175)
        self.angle_lower_limit_textentry_label.setFixedWidth(150)
        self.angle_upper_limit_textentry_label = QLabel(
            "Upper Limit (degrees):", self)
        self.angle_upper_limit_textentry_label.move(300, 210)
        self.angle_upper_limit_textentry_label.setFixedWidth(150)

        self.vin_lower_limit_textentry = QLineEdit(self)
        self.vin_lower_limit_textentry.setFixedWidth(50)
        self.vin_lower_limit_textentry.move(450, 265)
        self.vin_lower_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_upper_limit_textentry = QLineEdit(self)
        self.vin_upper_limit_textentry.setFixedWidth(50)
        self.vin_upper_limit_textentry.move(450, 300)
        self.vin_upper_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_lower_limit_textentry_label = QLabel(
            "Voltage Lower Limit (mV):", self)
        self.vin_lower_limit_textentry_label.move(300, 265)
        self.vin_lower_limit_textentry_label.setFixedWidth(150)
        self.vin_upper_limit_textentry_label = QLabel(
            "Voltage Upper Limit (mV):", self)
        self.vin_upper_limit_textentry_label.move(300, 300)
        self.vin_upper_limit_textentry_label.setFixedWidth(150)

        self.temp_limit_textentry = QLineEdit(self)
        self.temp_limit_textentry.setFixedWidth(50)
        self.temp_limit_textentry.move(450, 355)
        self.temp_limit_textentry.setValidator(QIntValidator(50, 100, self))
        self.temp_limit_textentry_label = QLabel("Temp Limit (°C):", self)
        self.temp_limit_textentry_label.move(300, 355)
        self.temp_limit_textentry_label.setFixedWidth(150)

        self.servo_mode_radio_button = QRadioButton("Servo Mode", self)
        self.servo_mode_radio_button.move(565, 50)
        self.motor_mode_radio_button = QRadioButton("Motor Mode", self)
        self.motor_mode_radio_button.move(565, 75)

        self.motor_speed_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.motor_speed_slider.setMinimum(-1000)
        self.motor_speed_slider.setMaximum(1000)
        self.motor_speed_slider.setFixedWidth(200)
        self.motor_speed_slider.move(565, 125)
        motor_speed_slider_label = QLabel("Motor Speed:", self)
        motor_speed_slider_label.move(565, 100)

        self.torque_enabled_checkbox = QCheckBox("Torque Enabled", self)
        self.torque_enabled_checkbox.move(565, 175)
        self.torque_enabled_checkbox.setFixedWidth(200)

        self.led_enabled_checkbox = QCheckBox("LED Enabled", self)
        self.led_enabled_checkbox.move(565, 210)
        self.led_enabled_checkbox.setFixedWidth(200)

        self.led_over_temp_checkbox = QCheckBox("LED Over Temperature", self)
        self.led_over_temp_checkbox.move(565, 258)
        self.led_over_temp_checkbox.setFixedWidth(200)
        self.led_over_voltage_checkbox = QCheckBox("LED Over Voltage", self)
        self.led_over_voltage_checkbox.move(565, 283)
        self.led_over_voltage_checkbox.setFixedWidth(200)
        self.led_rotor_locked_checkbox = QCheckBox("LED Rotor Locked", self)
        self.led_rotor_locked_checkbox.move(565, 308)
        self.led_rotor_locked_checkbox.setFixedWidth(200)

        self.physical_position_readout = QLabel("--°", self)
        self.physical_position_readout.move(565, 367)
        self.physical_position_readout.setFixedWidth(200)
        self.physical_position_readout_label = QLabel("Position", self)
        self.physical_position_readout_label.move(565, 347)

        self.temperature_readout = QLabel("-- °C", self)
        self.temperature_readout.move(635, 367)
        self.temperature_readout.setFixedWidth(200)
        self.temperature_readout_label = QLabel("Temperature", self)
        self.temperature_readout_label.move(635, 347)

        self.voltage_readout = QLabel("-- V", self)
        self.voltage_readout.move(730, 367)
        self.voltage_readout.setFixedWidth(200)
        self.voltage_readout_label = QLabel("Voltage", self)
        self.voltage_readout_label.move(730, 347)

        self.readout_update_timer = QTimer(self)
        self.readout_update_timer.timeout.connect(self.update_readouts)
        self.readout_update_timer.start(250)

        self.active_servo: LX16A = None

        self.position_slider.setValue(0)
        self.position_offset_slider.setValue(0)
        self.motor_speed_slider.setValue(0)
        self.id_selection_box_refresh_button.setEnabled(False)
        self.disable_widgets()

        self.port_selection_box.currentTextChanged.connect(
            self.port_selection_box_changed)
        self.port_selection_box_refresh_button.clicked.connect(
            self.port_refresh_button_clicked)
        self.id_selection_box.currentTextChanged.connect(
            self.id_selection_box_changed)
        self.id_selection_box_refresh_button.clicked.connect(
            self.id_refresh_button_clicked)
        self.set_id_button.pressed.connect(self.id_updated)
        self.position_slider.sliderMoved.connect(self.position_slider_updated)
        self.position_offset_slider.sliderMoved.connect(
            self.position_offset_slider_updated)
        self.angle_lower_limit_textentry.textChanged.connect(
            self.angle_lower_limit_updated)
        self.angle_upper_limit_textentry.textChanged.connect(
            self.angle_upper_limit_updated)
        self.vin_lower_limit_textentry.textChanged.connect(
            self.vin_lower_limit_updated)
        self.vin_upper_limit_textentry.textChanged.connect(
            self.vin_upper_limit_updated)
        self.temp_limit_textentry.textChanged.connect(self.temp_limit_updated)
        self.servo_mode_radio_button.toggled.connect(
            self.servo_mode_radio_button_toggled)
        self.motor_mode_radio_button.toggled.connect(
            self.motor_mode_radio_button_toggled)
        self.motor_speed_slider.valueChanged.connect(
            self.motor_speed_slider_updated)
        self.torque_enabled_checkbox.stateChanged.connect(
            self.torque_enabled_checkbox_toggled)
        self.led_enabled_checkbox.stateChanged.connect(
            self.led_enabled_checkbox_toggled)
        self.led_over_temp_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_over_voltage_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_rotor_locked_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)

        self.scan_for_ports()

    def disable_widgets(self):
        self.set_id_line_edit.setEnabled(False)
        self.position_slider.setEnabled(False)
        self.position_offset_slider.setEnabled(False)
        self.angle_lower_limit_textentry.setEnabled(False)
        self.angle_upper_limit_textentry.setEnabled(False)
        self.vin_lower_limit_textentry.setEnabled(False)
        self.vin_upper_limit_textentry.setEnabled(False)
        self.temp_limit_textentry.setEnabled(False)
        self.servo_mode_radio_button.setEnabled(False)
        self.motor_mode_radio_button.setEnabled(False)
        self.motor_speed_slider.setEnabled(False)
        self.torque_enabled_checkbox.setEnabled(False)
        self.led_enabled_checkbox.setEnabled(False)
        self.led_over_temp_checkbox.setEnabled(False)
        self.led_over_voltage_checkbox.setEnabled(False)
        self.led_rotor_locked_checkbox.setEnabled(False)

    def enable_widgets(self):
        self.set_id_line_edit.setEnabled(True)
        self.position_slider.setEnabled(True)
        self.position_offset_slider.setEnabled(True)
        self.angle_lower_limit_textentry.setEnabled(True)
        self.angle_upper_limit_textentry.setEnabled(True)
        self.vin_lower_limit_textentry.setEnabled(True)
        self.vin_upper_limit_textentry.setEnabled(True)
        self.temp_limit_textentry.setEnabled(True)
        self.servo_mode_radio_button.setEnabled(True)
        self.motor_mode_radio_button.setEnabled(True)
        self.motor_speed_slider.setEnabled(True)
        self.torque_enabled_checkbox.setEnabled(True)
        self.led_enabled_checkbox.setEnabled(True)
        self.led_over_temp_checkbox.setEnabled(True)
        self.led_over_voltage_checkbox.setEnabled(True)
        self.led_rotor_locked_checkbox.setEnabled(True)

    def clear_servo(self):
        self.active_servo = None

    @catch_disconnection
    def set_servo_id(self, id_):
        if not id_.isdigit():
            return

        self.active_servo = LX16A(int(id_))
        self.active_servo.enable_torque()

        self.position_slider.setValue(
            int(self.active_servo.get_physical_angle()))
        self.position_slider_readout.setText(
            f"{int(self.active_servo.get_physical_angle() * 25 / 6) * 6 / 25:0.2f}°"
        )
        self.position_offset_slider.setValue(
            int(self.active_servo.get_angle_offset()))
        self.position_offset_slider_readout.setText(
            f"{int(self.active_servo.get_angle_offset() * 25 / 6) * 6 / 25:0.2f}°"
        )
        self.angle_lower_limit_textentry.setText(
            str(int(self.active_servo.get_angle_limits()[0])))
        self.angle_upper_limit_textentry.setText(
            str(int(self.active_servo.get_angle_limits()[1])))
        self.vin_lower_limit_textentry.setText(
            str(self.active_servo.get_vin_limits()[0]))
        self.vin_upper_limit_textentry.setText(
            str(self.active_servo.get_vin_limits()[1]))
        self.temp_limit_textentry.setText(
            str(self.active_servo.get_temp_limit()))
        self.motor_speed_slider.setValue(self.active_servo.get_motor_speed(
        ) if self.active_servo.is_motor_mode() else 0)
        if self.active_servo.is_motor_mode():
            self.motor_mode_radio_button.setChecked(True)
        else:
            self.servo_mode_radio_button.setChecked(True)
        self.motor_speed_slider.setEnabled(self.active_servo.is_motor_mode())
        self.torque_enabled_checkbox.setChecked(
            self.active_servo.is_torque_enabled())
        self.led_enabled_checkbox.setChecked(
            self.active_servo.is_led_power_on())
        self.led_over_temp_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[0])
        self.led_over_voltage_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[1])
        self.led_rotor_locked_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[2])

    @catch_disconnection
    def scan_for_servos(self, port):
        self.setCursor(Qt.CursorShape.WaitCursor)

        LX16A.initialize(port)

        self.id_selection_box.clear()

        for i in range(0, 254):
            try:
                servo = LX16A(i)
                self.id_selection_box.addItem(str(i))
            except:
                pass

        self.setCursor(Qt.CursorShape.ArrowCursor)

    @catch_disconnection
    def scan_for_ports(self):
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_selection_box.addItem(port.device)

    @catch_disconnection
    def update_readouts(self):
        if self.active_servo is None:
            return

        try:
            self.physical_position_readout.setText(
                f"{self.active_servo.get_physical_angle():0.2f}°")
            self.temperature_readout.setText(
                f"{self.active_servo.get_temp()} °C")
            self.voltage_readout.setText(
                f"{self.active_servo.get_vin() / 1000} V")
        except (ServoTimeoutError, ServoChecksumError):
            pass

    @catch_disconnection
    def id_updated(self):
        new_id = self.set_id_line_edit.text()

        try:
            servo = LX16A(int(new_id))
        except ServoTimeoutError:
            # Meaning this ID is not taken
            self.active_servo.set_id(int(new_id))
            self.id_selection_box.item(
                self.id_selection_box.currentRow()).setText(new_id)

            return

        QMessageBox.warning(None, "Error", "ID already taken")

    @catch_disconnection
    def position_slider_updated(self, pos):
        if float(self.voltage_readout.text()[:-2]) < 5:
            QMessageBox.warning(
                None,
                "Error",
                "The voltage going through the servo is too low. Is your battery powered on?",
            )

            return
        self.active_servo.move(pos)
        self.position_slider_readout.setText(
            f"{int(pos * 25 / 6) * 6 / 25:0.2f}°")

    @catch_disconnection
    def position_offset_slider_updated(self, pos):
        self.active_servo.set_angle_offset(pos)
        self.position_offset_slider_readout.setText(
            f"{int(pos * 25 / 6) * 6 / 25:0.2f}°")

    @catch_disconnection
    def angle_lower_limit_updated(self, text):
        if (QIntValidator(0, 240, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) > int(self.angle_upper_limit_textentry.text()):
            return

        self.active_servo.set_angle_limits(
            int(text), int(self.angle_upper_limit_textentry.text()))

    @catch_disconnection
    def angle_upper_limit_updated(self, text):
        if (QIntValidator(0, 240, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) < int(self.angle_lower_limit_textentry.text()):
            return

        self.active_servo.set_angle_limits(
            int(self.angle_lower_limit_textentry.text()), int(text))

    @catch_disconnection
    def vin_lower_limit_updated(self, text):
        if (QIntValidator(4500, 12000, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) > int(self.vin_upper_limit_textentry.text()):
            return

        self.active_servo.set_vin_limits(
            int(text), int(self.vin_upper_limit_textentry.text()))

    @catch_disconnection
    def vin_upper_limit_updated(self, text):
        if (QIntValidator(4500, 12000, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) < int(self.vin_lower_limit_textentry.text()):
            return

        self.active_servo.set_vin_limits(
            int(self.vin_lower_limit_textentry.text()), int(text))

    @catch_disconnection
    def temp_limit_updated(self, text):
        if (QIntValidator(50, 100, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        self.active_servo.set_temp_limit(int(text))

    @catch_disconnection
    def servo_mode_radio_button_toggled(self, checked):
        if checked:
            self.active_servo.servo_mode()
            self.motor_speed_slider.setEnabled(False)
            self.position_slider.setEnabled(True)
            self.position_offset_slider.setEnabled(True)
        else:
            self.active_servo.motor_mode(int(self.motor_speed_slider.value()))
            self.motor_speed_slider.setEnabled(True)
            self.position_slider.setEnabled(False)
            self.position_offset_slider.setEnabled(False)

    @catch_disconnection
    def motor_mode_radio_button_toggled(self, checked):
        if checked:
            self.active_servo.motor_mode(int(self.motor_speed_slider.value()))
            self.motor_speed_slider.setEnabled(True)
            self.position_slider.setEnabled(False)
            self.position_offset_slider.setEnabled(False)
        else:
            self.active_servo.servo_mode()
            self.motor_speed_slider.setEnabled(False)
            self.position_slider.setEnabled(True)
            self.position_offset_slider.setEnabled(True)

    @catch_disconnection
    def motor_speed_slider_updated(self, pos):
        self.active_servo.motor_mode(pos)

    @catch_disconnection
    def torque_enabled_checkbox_toggled(self, checked):
        if checked:
            self.active_servo.enable_torque()
        else:
            self.active_servo.disable_torque()

        self.position_slider.setEnabled(checked)
        self.position_offset_slider.setEnabled(checked)
        self.servo_mode_radio_button.setEnabled(checked)
        self.motor_mode_radio_button.setEnabled(checked)
        self.motor_speed_slider.setEnabled(checked)

    @catch_disconnection
    def led_enabled_checkbox_toggled(self, checked):
        if checked:
            self.active_servo.led_power_on()
        else:
            self.active_servo.led_power_off()

    @catch_disconnection
    def led_error_triggers_checkbox_toggled(self):
        self.active_servo.set_led_error_triggers(
            self.led_over_voltage_checkbox.isChecked(),
            self.led_over_temp_checkbox.isChecked(),
            self.led_rotor_locked_checkbox.isChecked(),
        )

    @catch_disconnection
    def port_refresh_button_clicked(self, value):
        self.id_selection_box_refresh_button.setEnabled(False)
        self.disable_widgets()
        self.port_selection_box.clear()
        self.id_selection_box.clear()
        self.scan_for_ports()

    @catch_disconnection
    def id_refresh_button_clicked(self, value):
        self.disable_widgets()
        self.id_selection_box.clear()
        self.scan_for_servos(self.port_selection_box.currentText())

    @catch_disconnection
    def port_selection_box_changed(self, text):
        if text == "":
            return

        self.id_selection_box_refresh_button.setEnabled(True)
        self.disable_widgets()
        self.id_selection_box.clear()
        self.clear_servo()
        self.scan_for_servos(text)

    @catch_disconnection
    def id_selection_box_changed(self, text):
        if text == "":
            return

        self.enable_widgets()
        self.set_servo_id(text)
예제 #12
0
class ChangeKeyDialog(QDialog):
    def __init__(
        self,
        parent=None,
        buttons=None,
        exercises=None,
        index: int = None,
    ):
        super(ChangeKeyDialog, self).__init__(parent)
        layout = QVBoxLayout(self)
        self.setLayout(layout)
        widget = QWidget()
        keyLayout = QVBoxLayout()
        widget.setStyleSheet("""
        QWidget{
            border-radius: 12px;
            border: 1px solid grey;
            background-color: #b5b5b5;
            color: white;
            font-size: 40px;
        }
        """)
        # widget.setFixedSize(100, 100)
        self.currentKeyLabel = QLabel('W')
        keyLayout.addWidget(self.currentKeyLabel)
        keyLayout.setAlignment(self.currentKeyLabel, Qt.Alignment.AlignCenter)
        widget.setLayout(keyLayout)

        label = QLabel("Press a key to swap")
        emptyKey = QPushButton('Use empty slot')
        emptyKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        emptyKey.clicked.connect(self.useEmpty)

        acceptKey = QPushButton('Accept')
        acceptKey.clicked.connect(self.accept)
        acceptKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus)

        layout.addWidget(label)
        layout.addWidget(widget)
        actions = QHBoxLayout()
        actions.addWidget(emptyKey)
        actions.addWidget(acceptKey)
        layout.addLayout(actions)
        layout.setAlignment(widget, Qt.Alignment.AlignCenter)
        self.buttons = buttons
        self.exercises = exercises
        self.index = index

        self.monitor = KeyMonitor()
        self.monitor.start_monitoring()
        self.currentKey = self.monitor.currentKey

        self.timer = QTimer()
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start()
        print("Dialog init done!")

    def accept(self):
        currentKeyScheme = [e.assigned_key for e in self.exercises]
        print(self.exercises[self.index].assigned_key)

        # Check if pressed key is among
        if self.currentKey in currentKeyScheme:
            for name, key in SUPPORTED_KEYS.items(
            ):  # for name, age in dictionary.iteritems():  (for Python 2.x)
                if key == self.currentKey:
                    old_exercise = None
                    old_button = None
                    for exercise, button in zip(self.exercises, self.buttons):
                        if exercise.assigned_key == (
                                name, key) and button.text() == name:
                            old_exercise = exercise
                            old_button = button

                    # Set new keys and labels
                    if old_exercise is not None and old_button is not None:
                        self.exercises[
                            self.index].assigned_key, old_exercise.assigned_key = old_exercise.assigned_key, \
                                                                                  self.exercises[
                                                                                      self.index].assigned_key
                        old_label = old_button.text()
                        old_button.setText(self.buttons[self.index].text())
                        self.buttons[self.index].setText(old_label)
                        print("old key:", old_exercise.assigned_key)
                        print("new key:",
                              self.exercises[self.index].assigned_key)
                        self.timer.stop()
                        self.close()
        else:
            self.exercises[self.index].assigned_key = self.currentKey
            print(
                "pos:",
                list(SUPPORTED_KEYS.keys())[list(
                    SUPPORTED_KEYS.values()).index(self.currentKey[1])])
            self.buttons[self.index].setText(
                list(SUPPORTED_KEYS.keys())[list(
                    SUPPORTED_KEYS.values()).index(self.currentKey[1])])
            self.timer.stop()
            self.close()

    def useEmpty(self):
        self.currentKey = ("Empty", None)
        self.exercises[self.index].assigned_key = self.currentKey
        self.buttons[self.index].setText(self.currentKey[0])
        self.timer.stop()
        self.close()

    def onTimeout(self):
        if self.monitor is not None:
            if self.monitor.currentKey in list(SUPPORTED_KEYS.values()):
                if type(self.monitor.currentKey) is KeyCode:
                    self.currentKeyLabel.setText(self.monitor.currentKey.char)
                else:
                    self.currentKeyLabel.setText(str(self.monitor.currentKey))
                name = list(SUPPORTED_KEYS.keys())[list(
                    SUPPORTED_KEYS.values()).index(self.monitor.currentKey)]
                self.currentKey = (name, self.monitor.currentKey)

    def writeExerciseKeyMap(self):
        with open(os.getcwd() + MAPPED_KEYS_PATH + self.subject + '.json',
                  'w') as fp:
            json.dump(self.exercises, fp)

    # static method to create the dialog and return (date, time, accepted)
    @staticmethod
    def getSwapper(parent=None):
        dialog = ChangeKeyDialog(parent)
        result = dialog.exec()
        return result == QDialog.DialogCode.Accepted
예제 #13
0
class SettingPropertyProvider(QObject):
    """This class provides the value and change notifications for the properties of a single setting

    Since setting values and other properties are provided by a stack, we need some way to
    query the stack from QML to provide us with those values. This class takes care of that.

    This class provides the property values through QObject dynamic properties so that they
    are available from QML.
    """
    def __init__(self, parent=None) -> None:
        super().__init__(parent=parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)

    def setContainerStack(self, stack: Optional[ContainerStack]) -> None:
        if self._stack == stack:
            return  # Nothing to do, attempting to set stack to the same value.

        if self._stack:
            self._stack.propertiesChanged.disconnect(self._onPropertiesChanged)
            self._stack.containersChanged.disconnect(self._containersChanged)

        self._stack = stack

        if self._stack:
            self._stack.propertiesChanged.connect(self._onPropertiesChanged)
            self._stack.containersChanged.connect(self._containersChanged)

        self._validator = None
        self._updateDelayed()
        self.containerStackChanged.emit()

    def setContainerStackId(self, stack_id: str) -> None:
        """Set the containerStackId property."""

        if stack_id == self.containerStackId:
            return  # No change.

        if stack_id:
            if stack_id == "global":
                self.setContainerStack(
                    Application.getInstance().getGlobalContainerStack())
            else:
                stacks = ContainerRegistry.getInstance().findContainerStacks(
                    id=stack_id)
                if stacks:
                    self.setContainerStack(stacks[0])
        else:
            self.setContainerStack(None)

    containerStackIdChanged = pyqtSignal()
    """Emitted when the containerStackId property changes."""

    @pyqtProperty(str,
                  fset=setContainerStackId,
                  notify=containerStackIdChanged)
    def containerStackId(self) -> str:
        """The ID of the container stack we should query for property values."""

        if self._stack:
            return self._stack.id

        return ""

    containerStackChanged = pyqtSignal()

    @pyqtProperty(QObject,
                  fset=setContainerStack,
                  notify=containerStackChanged)
    def containerStack(self) -> Optional[ContainerInterface]:
        return self._stack

    removeUnusedValueChanged = pyqtSignal()

    def setRemoveUnusedValue(self, remove_unused_value: bool) -> None:
        if self._remove_unused_value != remove_unused_value:
            self._remove_unused_value = remove_unused_value
            self.removeUnusedValueChanged.emit()

    @pyqtProperty(bool,
                  fset=setRemoveUnusedValue,
                  notify=removeUnusedValueChanged)
    def removeUnusedValue(self) -> bool:
        return self._remove_unused_value

    def setWatchedProperties(self, properties: List[str]) -> None:
        """Set the watchedProperties property."""

        if properties != self._watched_properties:
            self._watched_properties = properties
            self._updateDelayed()
            self.watchedPropertiesChanged.emit()

    watchedPropertiesChanged = pyqtSignal()
    """Emitted when the watchedProperties property changes."""

    @pyqtProperty("QStringList",
                  fset=setWatchedProperties,
                  notify=watchedPropertiesChanged)
    def watchedProperties(self) -> List[str]:
        """A list of property names that should be watched for changes."""

        return self._watched_properties

    def setKey(self, key: str) -> None:
        """Set the key property."""

        if key != self._key:
            self._key = key
            self._validator = None
            self._updateDelayed()
            self.keyChanged.emit()

    keyChanged = pyqtSignal()
    """Emitted when the key property changes."""

    @pyqtProperty(str, fset=setKey, notify=keyChanged)
    def key(self):
        """The key of the setting that we should provide property values for."""

        return self._key

    propertiesChanged = pyqtSignal()

    @pyqtProperty(QQmlPropertyMap, notify=propertiesChanged)
    def properties(self):
        return self._property_map

    @pyqtSlot()
    def forcePropertiesChanged(self):
        self._onPropertiesChanged(self._key, self._watched_properties)

    def setStoreIndex(self, index):
        if index != self._store_index:
            self._store_index = index
            self.storeIndexChanged.emit()

    storeIndexChanged = pyqtSignal()

    @pyqtProperty(int, fset=setStoreIndex, notify=storeIndexChanged)
    def storeIndex(self):
        return self._store_index

    stackLevelChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=stackLevelChanged)
    def stackLevels(self):
        """At what levels in the stack does the value(s) for this setting occur?"""

        if not self._stack:
            return [-1]
        return self._stack_levels

    @pyqtSlot(str, "QVariant")
    def setPropertyValue(self, property_name, property_value):
        """Set the value of a property.

        :param stack_index: At which level in the stack should this property be set?
        :param property_name: The name of the property to set.
        :param property_value: The value of the property to set.
        """

        if not self._stack or not self._key:
            return

        if property_name not in self._watched_properties:
            Logger.log("w",
                       "Tried to set a property that is not being watched")
            return

        container = self._stack.getContainer(self._store_index)
        if isinstance(container, DefinitionContainer):
            return

        # In some cases we clean some stuff and the result is as when nothing as been changed manually.
        if property_name == "value" and self._remove_unused_value:
            for index in self._stack_levels:
                if index > self._store_index:
                    old_value = self.getPropertyValue(property_name, index)

                    key_state = str(
                        self._stack.getContainer(
                            self._store_index).getProperty(self._key, "state"))

                    # The old_value might be a SettingFunction, like round(), sum(), etc.
                    #  In this case retrieve the value to compare
                    if isinstance(old_value, SettingFunction):
                        old_value = old_value(self._stack)

                    # sometimes: old value is int, property_value is float
                    # (and the container is not removed, so the revert button appears)
                    if str(old_value) == str(
                            property_value
                    ) and key_state != "InstanceState.Calculated":
                        # If we change the setting so that it would be the same as a deeper setting, we can just remove
                        # the value. Note that we only do this when this is not caused by the calculated state
                        # In this case the setting does need to be set, as it needs to be stored in the user settings.
                        self.removeFromContainer(self._store_index)
                        return
                    else:  # First value that we encountered was different, stop looking & continue as normal.
                        break

        # _remove_unused_value is used when the stack value differs from the effective value
        # i.e. there is a resolve function
        if self._property_map.value(
                property_name) == property_value and self._remove_unused_value:
            return

        container.setProperty(self._key, property_name, property_value)

    @pyqtSlot(str, int, result="QVariant")
    def getPropertyValue(self, property_name: str, stack_level: int) -> Any:
        """Manually request the value of a property.
        The most notable difference with the properties is that you have more control over at what point in the stack
        you want the setting to be retrieved (instead of always taking the top one)

        :param property_name: The name of the property to get the value from.
        :param stack_level: the index of the container to get the value from.
        """

        try:
            # Because we continue to count if there are multiple linked stacks, we need to check what stack is targeted
            current_stack = self._stack
            while current_stack:
                num_containers = len(current_stack.getContainers())
                if stack_level >= num_containers:
                    stack_level -= num_containers
                    current_stack = current_stack.getNextStack()
                else:
                    break  # Found the right stack

            if not current_stack:
                Logger.log(
                    "w",
                    "Could not find the right stack for setting %s at stack level %d while trying to get property %s",
                    self._key, stack_level, property_name)
                return None

            value = current_stack.getContainers()[stack_level].getProperty(
                self._key, property_name)
        except IndexError:  # Requested stack level does not exist
            Logger.log(
                "w",
                "Tried to get property of type %s from %s but it did not exist on requested index %d",
                property_name, self._key, stack_level)
            return None
        return value

    @pyqtSlot(str, result=str)
    def getPropertyValueAsString(self, property_name: str) -> str:
        return self._getPropertyValue(property_name)

    @pyqtSlot(int)
    def removeFromContainer(self, index: int) -> None:
        current_stack = self._stack
        while current_stack:
            num_containers = len(current_stack.getContainers())
            if index >= num_containers:
                index -= num_containers
                current_stack = current_stack.getNextStack()
            else:
                break  # Found the right stack

        if not current_stack:
            Logger.log(
                "w",
                "Unable to remove instance from container because the right stack at stack level %d could not be found",
                index)
            return

        container = current_stack.getContainer(index)
        if not container or not isinstance(container, InstanceContainer):
            Logger.log(
                "w",
                "Unable to remove instance from container as it was either not found or not an instance container"
            )
            return

        container.removeInstance(self._key)

    isValueUsedChanged = pyqtSignal()

    @pyqtProperty(bool, notify=isValueUsedChanged)
    def isValueUsed(self) -> bool:
        if self._value_used is not None:
            return self._value_used
        if not self._stack:
            return False
        definition = self._stack.getSettingDefinition(self._key)
        if not definition:
            return False

        relation_count = 0
        value_used_count = 0
        for key in self._relations:
            # If the setting is not a descendant of this setting, ignore it.
            if not definition.isDescendant(key):
                continue

            relation_count += 1

            if self._stack.getProperty(key, "state") != InstanceState.User:
                value_used_count += 1
                break

            # If the setting has a formula the value is still used.
            if isinstance(self._stack.getRawProperty(key, "value"),
                          SettingFunction):
                value_used_count += 1
                break

        self._value_used = relation_count == 0 or (relation_count > 0
                                                   and value_used_count != 0)
        return self._value_used

    def _onPropertiesChanged(self, key: str,
                             property_names: List[str]) -> None:
        if key != self._key:
            if key in self._relations:
                self._value_used = None
                try:
                    self.isValueUsedChanged.emit()
                except RuntimeError:
                    # QtObject has been destroyed, no need to handle the signals anymore.
                    # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                    # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                    # logic to emit pyqtSignals is gone.
                    return
            return

        has_values_changed = False
        for property_name in property_names:
            if property_name not in self._watched_properties:
                continue

            has_values_changed = True
            try:
                self._property_map.insert(
                    property_name, self._getPropertyValue(property_name))
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

        self._updateStackLevels()
        if has_values_changed:
            try:
                self.propertiesChanged.emit()
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

    def _update(self, container=None):
        if not self._stack or not self._watched_properties or not self._key:
            return

        self._updateStackLevels()
        relations = self._stack.getProperty(self._key, "relations")
        if relations:  # If the setting doesn't have the property relations, None is returned
            for relation in filter(
                    lambda r: r.type == RelationType.RequiredByTarget and r.
                    role == "value", relations):
                self._relations.add(relation.target.key)

        for property_name in self._watched_properties:
            self._property_map.insert(property_name,
                                      self._getPropertyValue(property_name))

        # Notify that the properties have been changed.Kewl
        self.propertiesChanged.emit()

        # Force update of value_used
        self._value_used = None
        self.isValueUsedChanged.emit()

    def _updateDelayed(self, container=None) -> None:
        try:
            self._update_timer.start()
        except RuntimeError:
            # Sometimes the python object is not yet deleted, but the wrapped part is already gone.
            # In that case there is nothing else to do but ignore this.
            pass

    def _containersChanged(self, container=None):
        self._updateDelayed()

    def _storeIndexChanged(self):
        self._updateDelayed()

    def _updateStackLevels(self) -> None:
        """Updates the self._stack_levels field, which indicates at which levels in the stack the property is set."""

        levels = []
        # Start looking at the stack this provider is attached to.
        current_stack = self._stack
        index = 0
        while current_stack:
            for container in current_stack.getContainers():
                try:
                    if container.getProperty(self._key, "value") is not None:
                        levels.append(index)
                except AttributeError:
                    pass
                index += 1
            # If there is a next stack, check that one as well.
            current_stack = current_stack.getNextStack()

        if levels != self._stack_levels:
            self._stack_levels = levels
            self.stackLevelChanged.emit()

    def _getPropertyValue(self, property_name):
        # Use the evaluation context to skip certain containers
        context = PropertyEvaluationContext(self._stack)
        context.context["evaluate_from_container_index"] = self._store_index

        property_value = self._stack.getProperty(self._key,
                                                 property_name,
                                                 context=context)
        if isinstance(property_value, SettingFunction):
            property_value = property_value(self._stack)

        if property_name == "value":
            setting_type = self._stack.getProperty(self._key, "type")
            if setting_type is not None:
                property_value = SettingDefinition.settingValueToString(
                    setting_type, property_value)
            else:
                property_value = ""
        elif property_name == "validationState":
            # Setting is not validated. This can happen if there is only a setting definition.
            # We do need to validate it, because a setting defintions value can be set by a function, which could
            # be an invalid setting.
            if property_value is None:
                if not self._validator:
                    definition = self._stack.getSettingDefinition(self._key)
                    if definition:
                        validator_type = SettingDefinition.getValidatorForType(
                            definition.type)
                        if validator_type:
                            self._validator = validator_type(self._key)
                if self._validator:
                    property_value = self._validator(self._stack)
        return str(property_value)
예제 #14
0
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupTrayicon()
        self.setupVariables()
        self.setupUi()
        self.setupConnections()
        self.show()

    def setupVariables(self):
        settings = QSettings()
        self.workEndTime = QTime(
            int(settings.value(workHoursKey, 0)),
            int(settings.value(workMinutesKey, 25)),
            int(settings.value(workSecondsKey, 0)),
        )
        self.restEndTime = QTime(
            int(settings.value(restHoursKey, 0)),
            int(settings.value(restMinutesKey, 5)),
            int(settings.value(restSecondsKey, 0)),
        )
        self.timeFormat = "hh:mm:ss"
        self.time = QTime(0, 0, 0, 0)
        self.workTime = QTime(0, 0, 0, 0)
        self.restTime = QTime(0, 0, 0, 0)
        self.totalTime = QTime(0, 0, 0, 0)
        self.currentMode = Mode.work
        self.maxRepetitions = -1
        self.currentRepetitions = 0

    def setupConnections(self):
        """ Create button connections """
        self.startButton.clicked.connect(self.startTimer)
        self.startButton.clicked.connect(
            lambda: self.startButton.setDisabled(True))
        self.startButton.clicked.connect(
            lambda: self.pauseButton.setDisabled(False))
        self.startButton.clicked.connect(
            lambda: self.resetButton.setDisabled(False))
        self.pauseButton.clicked.connect(self.pauseTimer)
        self.pauseButton.clicked.connect(
            lambda: self.startButton.setDisabled(False))
        self.pauseButton.clicked.connect(
            lambda: self.pauseButton.setDisabled(True))
        self.pauseButton.clicked.connect(
            lambda: self.resetButton.setDisabled(False))
        self.pauseButton.clicked.connect(
            lambda: self.startButton.setText("continue"))
        self.resetButton.clicked.connect(self.resetTimer)
        self.resetButton.clicked.connect(
            lambda: self.startButton.setDisabled(False))
        self.resetButton.clicked.connect(
            lambda: self.pauseButton.setDisabled(True))
        self.resetButton.clicked.connect(
            lambda: self.resetButton.setDisabled(True))
        self.resetButton.clicked.connect(
            lambda: self.startButton.setText("start"))
        self.acceptTaskButton.pressed.connect(self.insertTask)
        self.deleteTaskButton.pressed.connect(self.deleteTask)
        """ Create spinbox  connections """
        self.workHoursSpinBox.valueChanged.connect(self.updateWorkEndTime)
        self.workMinutesSpinBox.valueChanged.connect(self.updateWorkEndTime)
        self.workSecondsSpinBox.valueChanged.connect(self.updateWorkEndTime)
        self.restHoursSpinBox.valueChanged.connect(self.updateRestEndTime)
        self.restMinutesSpinBox.valueChanged.connect(self.updateRestEndTime)
        self.restSecondsSpinBox.valueChanged.connect(self.updateRestEndTime)
        self.repetitionsSpinBox.valueChanged.connect(self.updateMaxRepetitions)
        """ Create combobox connections """
        self.modeComboBox.currentTextChanged.connect(self.updateCurrentMode)
        """ Create tablewidget connections """
        self.tasksTableWidget.cellDoubleClicked.connect(
            self.markTaskAsFinished)

    def setupUi(self):
        self.size_policy = QSizePolicy(QSizePolicy.Policy.Expanding,
                                       QSizePolicy.Policy.Expanding)
        """ Create tabwidget """
        self.tabWidget = QTabWidget()
        """ Create tab widgets """
        timerWidget = self.setupTimerTab()
        tasksWidget = self.setupTasksTab()
        statisticsWidget = self.setupStatisticsTab()
        """ add tab widgets to tabwidget"""
        self.timerTab = self.tabWidget.addTab(timerWidget, makeIcon("timer"),
                                              "Timer")
        self.tasksTab = self.tabWidget.addTab(tasksWidget, makeIcon("tasks"),
                                              "Tasks")
        self.statisticsTab = self.tabWidget.addTab(statisticsWidget,
                                                   makeIcon("statistics"),
                                                   "Statistics")
        """ Set mainwindows central widget """
        self.setCentralWidget(self.tabWidget)

    def setupTimerTab(self):
        settings = QSettings()
        self.timerContainer = QWidget(self)
        self.timerContainerLayout = QVBoxLayout(self.timerContainer)
        self.timerContainer.setLayout(self.timerContainerLayout)
        """ Create work groupbox"""
        self.workGroupBox = QGroupBox("Work")
        self.workGroupBoxLayout = QHBoxLayout(self.workGroupBox)
        self.workGroupBox.setLayout(self.workGroupBoxLayout)
        self.workHoursSpinBox = QSpinBox(
            minimum=0,
            maximum=24,
            value=int(settings.value(workHoursKey, 0)),
            suffix="h",
            sizePolicy=self.size_policy,
        )
        self.workMinutesSpinBox = QSpinBox(
            minimum=0,
            maximum=60,
            value=int(settings.value(workMinutesKey, 25)),
            suffix="m",
            sizePolicy=self.size_policy,
        )
        self.workSecondsSpinBox = QSpinBox(
            minimum=0,
            maximum=60,
            value=int(settings.value(workSecondsKey, 0)),
            suffix="s",
            sizePolicy=self.size_policy,
        )
        """ Create rest groupbox"""
        self.restGroupBox = QGroupBox("Rest")
        self.restGroupBoxLayout = QHBoxLayout(self.restGroupBox)
        self.restGroupBox.setLayout(self.restGroupBoxLayout)
        self.restHoursSpinBox = QSpinBox(
            minimum=0,
            maximum=24,
            value=int(settings.value(restHoursKey, 0)),
            suffix="h",
            sizePolicy=self.size_policy,
        )
        self.restMinutesSpinBox = QSpinBox(
            minimum=0,
            maximum=60,
            value=int(settings.value(restMinutesKey, 5)),
            suffix="m",
            sizePolicy=self.size_policy,
        )
        self.restSecondsSpinBox = QSpinBox(
            minimum=0,
            maximum=60,
            value=int(settings.value(restSecondsKey, 0)),
            suffix="s",
            sizePolicy=self.size_policy,
        )
        self.restGroupBoxLayout.addWidget(self.restHoursSpinBox)
        self.restGroupBoxLayout.addWidget(self.restMinutesSpinBox)
        self.restGroupBoxLayout.addWidget(self.restSecondsSpinBox)
        """ Create other groupbox"""
        self.otherGroupBox = QGroupBox("Other")
        self.otherGroupBoxLayout = QHBoxLayout(self.otherGroupBox)
        self.otherGroupBox.setLayout(self.otherGroupBoxLayout)
        self.repetitionsLabel = QLabel("Repetitions")
        self.repetitionsSpinBox = QSpinBox(
            minimum=0,
            maximum=10000,
            value=0,
            sizePolicy=self.size_policy,
            specialValueText="∞",
        )
        self.modeLabel = QLabel("Mode")
        self.modeComboBox = QComboBox(sizePolicy=self.size_policy)
        self.modeComboBox.addItems(["work", "rest"])
        self.otherGroupBoxLayout.addWidget(self.repetitionsLabel)
        self.otherGroupBoxLayout.addWidget(self.repetitionsSpinBox)
        self.otherGroupBoxLayout.addWidget(self.modeLabel)
        self.otherGroupBoxLayout.addWidget(self.modeComboBox)
        """ Create timer groupbox"""
        self.lcdDisplayGroupBox = QGroupBox("Time")
        self.lcdDisplayGroupBoxLayout = QHBoxLayout(self.lcdDisplayGroupBox)
        self.lcdDisplayGroupBox.setLayout(self.lcdDisplayGroupBoxLayout)
        self.timeDisplay = QLCDNumber(8, sizePolicy=self.size_policy)
        self.timeDisplay.setFixedHeight(100)
        self.timeDisplay.display("00:00:00")
        self.lcdDisplayGroupBoxLayout.addWidget(self.timeDisplay)
        """ Create pause, start and reset buttons"""
        self.buttonContainer = QWidget()
        self.buttonContainerLayout = QHBoxLayout(self.buttonContainer)
        self.buttonContainer.setLayout(self.buttonContainerLayout)
        self.startButton = self.makeButton("start", disabled=False)
        self.resetButton = self.makeButton("reset")
        self.pauseButton = self.makeButton("pause")
        """ Add widgets to container """
        self.workGroupBoxLayout.addWidget(self.workHoursSpinBox)
        self.workGroupBoxLayout.addWidget(self.workMinutesSpinBox)
        self.workGroupBoxLayout.addWidget(self.workSecondsSpinBox)
        self.timerContainerLayout.addWidget(self.workGroupBox)
        self.timerContainerLayout.addWidget(self.restGroupBox)
        self.timerContainerLayout.addWidget(self.otherGroupBox)
        self.timerContainerLayout.addWidget(self.lcdDisplayGroupBox)
        self.buttonContainerLayout.addWidget(self.pauseButton)
        self.buttonContainerLayout.addWidget(self.startButton)
        self.buttonContainerLayout.addWidget(self.resetButton)
        self.timerContainerLayout.addWidget(self.buttonContainer)
        return self.timerContainer

    def setupTasksTab(self):
        settings = QSettings()
        """ Create vertical tasks container """
        self.tasksWidget = QWidget(self.tabWidget)
        self.tasksWidgetLayout = QVBoxLayout(self.tasksWidget)
        self.tasksWidget.setLayout(self.tasksWidgetLayout)
        """ Create horizontal input container """
        self.inputContainer = QWidget()
        self.inputContainer.setFixedHeight(50)
        self.inputContainerLayout = QHBoxLayout(self.inputContainer)
        self.inputContainerLayout.setContentsMargins(0, 0, 0, 0)
        self.inputContainer.setLayout(self.inputContainerLayout)
        """ Create text edit """
        self.taskTextEdit = QTextEdit(
            placeholderText="Describe your task briefly.",
            undoRedoEnabled=True)
        """ Create vertical buttons container """
        self.inputButtonContainer = QWidget()
        self.inputButtonContainerLayout = QVBoxLayout(
            self.inputButtonContainer)
        self.inputButtonContainerLayout.setContentsMargins(0, 0, 0, 0)
        self.inputButtonContainer.setLayout(self.inputButtonContainerLayout)
        """ Create buttons """
        self.acceptTaskButton = QToolButton(icon=makeIcon("check"))
        self.deleteTaskButton = QToolButton(icon=makeIcon("trash"))
        """ Create tasks tablewidget """
        self.tasksTableWidget = QTableWidget(0, 1)
        self.tasksTableWidget.setHorizontalHeaderLabels(["Tasks"])
        self.tasksTableWidget.horizontalHeader().setStretchLastSection(True)
        self.tasksTableWidget.verticalHeader().setVisible(False)
        self.tasksTableWidget.setWordWrap(True)
        self.tasksTableWidget.setTextElideMode(Qt.TextElideMode.ElideNone)
        self.tasksTableWidget.setEditTriggers(
            QAbstractItemView.EditTriggers.NoEditTriggers)
        self.tasksTableWidget.setSelectionMode(
            QAbstractItemView.SelectionMode.SingleSelection)
        self.insertTasks(*settings.value(tasksKey, []))
        """ Add widgets to container widgets """
        self.inputButtonContainerLayout.addWidget(self.acceptTaskButton)
        self.inputButtonContainerLayout.addWidget(self.deleteTaskButton)
        self.inputContainerLayout.addWidget(self.taskTextEdit)
        self.inputContainerLayout.addWidget(self.inputButtonContainer)
        self.tasksWidgetLayout.addWidget(self.inputContainer)
        self.tasksWidgetLayout.addWidget(self.tasksTableWidget)
        return self.tasksWidget

    def setupStatisticsTab(self):
        """ Create statistics container """
        self.statisticsContainer = QWidget()
        self.statisticsContainerLayout = QVBoxLayout(self.statisticsContainer)
        self.statisticsContainer.setLayout(self.statisticsContainerLayout)
        """ Create work time groupbox """
        self.statisticsWorkTimeGroupBox = QGroupBox("Work Time")
        self.statisticsWorkTimeGroupBoxLayout = QHBoxLayout()
        self.statisticsWorkTimeGroupBox.setLayout(
            self.statisticsWorkTimeGroupBoxLayout)
        self.statisticsWorkTimeDisplay = QLCDNumber(8)
        self.statisticsWorkTimeDisplay.display("00:00:00")
        self.statisticsWorkTimeGroupBoxLayout.addWidget(
            self.statisticsWorkTimeDisplay)
        """ Create rest time groupbox """
        self.statisticsRestTimeGroupBox = QGroupBox("Rest Time")
        self.statisticsRestTimeGroupBoxLayout = QHBoxLayout()
        self.statisticsRestTimeGroupBox.setLayout(
            self.statisticsRestTimeGroupBoxLayout)
        self.statisticsRestTimeDisplay = QLCDNumber(8)
        self.statisticsRestTimeDisplay.display("00:00:00")
        self.statisticsRestTimeGroupBoxLayout.addWidget(
            self.statisticsRestTimeDisplay)
        """ Create total time groupbox """
        self.statisticsTotalTimeGroupBox = QGroupBox("Total Time")
        self.statisticsTotalTimeGroupBoxLayout = QHBoxLayout()
        self.statisticsTotalTimeGroupBox.setLayout(
            self.statisticsTotalTimeGroupBoxLayout)
        self.statisticsTotalTimeDisplay = QLCDNumber(8)
        self.statisticsTotalTimeDisplay.display("00:00:00")
        self.statisticsTotalTimeGroupBoxLayout.addWidget(
            self.statisticsTotalTimeDisplay)
        """ Add widgets to container """
        self.statisticsContainerLayout.addWidget(
            self.statisticsTotalTimeGroupBox)
        self.statisticsContainerLayout.addWidget(
            self.statisticsWorkTimeGroupBox)
        self.statisticsContainerLayout.addWidget(
            self.statisticsRestTimeGroupBox)
        return self.statisticsContainer

    def setupTrayicon(self):
        self.trayIcon = QSystemTrayIcon(makeIcon("tomato"))
        self.trayIcon.setContextMenu(QMenu())
        self.quitAction = self.trayIcon.contextMenu().addAction(
            makeIcon("exit"), "Quit", self.exit)
        self.quitAction.triggered.connect(self.exit)
        self.trayIcon.activated.connect(self.onActivate)
        self.trayIcon.show()
        self.trayIcon.setToolTip("Pomodoro")
        self.toast = ToastNotifier()

    def leaveEvent(self, event):
        super(MainWindow, self).leaveEvent(event)
        self.tasksTableWidget.clearSelection()

    def closeEvent(self, event):
        super(MainWindow, self).closeEvent(event)
        settings = QSettings()
        settings.setValue(workHoursKey, self.workHoursSpinBox.value())
        settings.setValue(
            workMinutesKey,
            self.workMinutesSpinBox.value(),
        )
        settings.setValue(
            workSecondsKey,
            self.workSecondsSpinBox.value(),
        )
        settings.setValue(restHoursKey, self.restHoursSpinBox.value())
        settings.setValue(
            restMinutesKey,
            self.restMinutesSpinBox.value(),
        )
        settings.setValue(
            restSecondsKey,
            self.restSecondsSpinBox.value(),
        )

        tasks = []
        for i in range(self.tasksTableWidget.rowCount()):
            item = self.tasksTableWidget.item(i, 0)
            if not item.font().strikeOut():
                tasks.append(item.text())
        settings.setValue(tasksKey, tasks)

    def startTimer(self):
        try:
            if not self.timer.isActive():
                self.createTimer()
        except:
            self.createTimer()

    def createTimer(self):
        self.timer = QTimer()
        self.timer.timeout.connect(self.updateTime)
        self.timer.timeout.connect(self.maybeChangeMode)
        self.timer.setInterval(1000)
        self.timer.setSingleShot(False)
        self.timer.start()

    def pauseTimer(self):
        try:
            self.timer.stop()
            self.timer.disconnect()
        except:
            pass

    def resetTimer(self):
        try:
            self.pauseTimer()
            self.time = QTime(0, 0, 0, 0)
            self.displayTime()
        except:
            pass

    def maybeStartTimer(self):
        if self.currentRepetitions != self.maxRepetitions:
            self.startTimer()
            started = True
        else:
            self.currentRepetitions = 0
            started = False
        return started

    def updateWorkEndTime(self):
        self.workEndTime = QTime(
            self.workHoursSpinBox.value(),
            self.workMinutesSpinBox.value(),
            self.workSecondsSpinBox.value(),
        )

    def updateRestEndTime(self):
        self.restEndTime = QTime(
            self.restHoursSpinBox.value(),
            self.restMinutesSpinBox.value(),
            self.restSecondsSpinBox.value(),
        )

    def updateCurrentMode(self, mode: str):
        self.currentMode = Mode.work if mode == "work" else Mode.rest

    def updateTime(self):
        self.time = self.time.addSecs(1)
        self.totalTime = self.totalTime.addSecs(1)
        if self.modeComboBox.currentText() == "work":
            self.workTime = self.workTime.addSecs(1)
        else:
            self.restTime = self.restTime.addSecs(1)
        self.displayTime()

    def updateMaxRepetitions(self, value):
        if value == 0:
            self.currentRepetitions = 0
            self.maxRepetitions = -1
        else:
            self.maxRepetitions = 2 * value

    def maybeChangeMode(self):
        if self.currentMode is Mode.work and self.time >= self.workEndTime:
            self.resetTimer()
            self.modeComboBox.setCurrentIndex(1)
            self.incrementCurrentRepetitions()
            started = self.maybeStartTimer()
            self.showWindowMessage(
                Status.workFinished if started else Status.repetitionsReached)
            if not started:
                self.resetButton.click()
        elif self.currentMode is Mode.rest and self.time >= self.restEndTime:
            self.resetTimer()
            self.modeComboBox.setCurrentIndex(0)
            self.incrementCurrentRepetitions()
            started = self.maybeStartTimer()
            self.showWindowMessage(
                Status.restFinished if started else Status.repetitionsReached)
            if not started:
                self.resetButton.click()

    def incrementCurrentRepetitions(self):
        if self.maxRepetitions > 0:
            self.currentRepetitions += 1

    def insertTask(self):
        task = self.taskTextEdit.toPlainText()
        self.insertTasks(task)

    def insertTasks(self, *tasks):
        for task in tasks:
            if task:
                rowCount = self.tasksTableWidget.rowCount()
                self.tasksTableWidget.setRowCount(rowCount + 1)
                self.tasksTableWidget.setItem(rowCount, 0,
                                              QTableWidgetItem(task))
                self.tasksTableWidget.resizeRowsToContents()
                self.taskTextEdit.clear()

    def deleteTask(self):
        selectedIndexes = self.tasksTableWidget.selectedIndexes()
        if selectedIndexes:
            self.tasksTableWidget.removeRow(selectedIndexes[0].row())

    def markTaskAsFinished(self, row, col):
        item = self.tasksTableWidget.item(row, col)
        font = self.tasksTableWidget.item(row, col).font()
        font.setStrikeOut(False if item.font().strikeOut() else True)
        item.setFont(font)

    def displayTime(self):
        self.timeDisplay.display(self.time.toString(self.timeFormat))
        self.statisticsRestTimeDisplay.display(
            self.restTime.toString(self.timeFormat))
        self.statisticsWorkTimeDisplay.display(
            self.workTime.toString(self.timeFormat))
        self.statisticsTotalTimeDisplay.display(
            self.totalTime.toString(self.timeFormat))

    def showWindowMessage(self, status):
        if status is Status.workFinished:
            title, text = "Break", choice(work_finished_phrases)
        elif status is Status.restFinished:
            title, text = "Work", choice(rest_finished_phrases)
        else:
            title, text = "Finished", choice(work_finished_phrases)
        self.trayIcon.showMessage(title, text, makeIcon("tomato"))
        self.toast.show_toast(title,
                              text,
                              icon_path="pomodoro/data/icons/tomato.ico",
                              duration=10,
                              threaded=True)

    def makeButton(self, text, iconName=None, disabled=True):
        button = QPushButton(text, sizePolicy=self.size_policy)
        if iconName:
            button.setIcon(makeIcon(iconName))
        button.setDisabled(disabled)
        return button

    def exit(self):
        self.close()
        app = QApplication.instance()
        if app:
            app.quit()

    def onActivate(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            self.show()
예제 #15
0
class TranslateTool(Tool):
    """Provides the tool to move meshes and groups.

    The tool exposes a ToolHint to show the distance of the current operation.
    """
    def __init__(self) -> None:
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle(
        )  #type: TranslateToolHandle.TranslateToolHandle #Because for some reason MyPy thinks this variable contains Optional[ToolHandle].
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key.Key_T

        self._distance_update_time = None  #type: Optional[float]
        self._distance = None  #type: Optional[Vector]

        self.setExposedProperties("ToolHint", "X", "Y", "Z",
                                  SceneNodeSettings.LockPosition)

        self._update_selection_center_timer = QTimer()
        self._update_selection_center_timer.setInterval(50)
        self._update_selection_center_timer.setSingleShot(True)
        self._update_selection_center_timer.timeout.connect(
            self.propertyChanged.emit)

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(
            self._onSelectionCenterChanged)

        # CURA-5966 Make sure to render whenever objects get selected/deselected.
        Selection.selectionChanged.connect(self.propertyChanged)

    def _onSelectionCenterChanged(self):
        self._update_selection_center_timer.start()

    def getX(self) -> float:
        """Get the x-location of the selection bounding box center.

        :return: X location in mm.
        """
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    def getY(self) -> float:
        """Get the y-location of the selection bounding box center.

        :return: Y location in mm.
        """
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    def getZ(self) -> float:
        """Get the z-location of the selection bounding box bottom

        The bottom is used as opposed to the center, because the biggest use
        case is to push the selection into the build plate.
        :return: Z location in mm.
        """
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    @staticmethod
    def _parseFloat(str_value: str) -> float:
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    def setX(self, x: str) -> None:
        """Set the x-location of the selected object(s) by translating relative to

        the selection bounding box center.
        :param x: Location in mm.
        """
        parsed_x = self._parseFloat(x)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
                ):
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        x=parsed_x +
                        (world_position.x - bounding_box.center.x))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
                ):
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        x=parsed_x +
                        (world_position.x - bounding_box.center.x))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()

        self._controller.toolOperationStopped.emit(self)

    def setY(self, y: str) -> None:
        """Set the y-location of the selected object(s) by translating relative to

        the selection bounding box center.
        :param y: Location in mm.
        """
        parsed_y = self._parseFloat(y)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note; The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        z=parsed_y +
                        (world_position.z - bounding_box.center.z))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        z=parsed_y +
                        (world_position.z - bounding_box.center.z))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()

        self._controller.toolOperationStopped.emit(self)

    def setZ(self, z: str) -> None:
        """Set the y-location of the selected object(s) by translating relative to

        the selection bounding box bottom.
        :param z: Location in mm.
        """
        parsed_z = self._parseFloat(z)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note: The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        y=parsed_z + (world_position.y - bounding_box.bottom))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        y=parsed_z + (world_position.y - bounding_box.bottom))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()
        self._controller.toolOperationStopped.emit(self)

    def setEnabledAxis(self, axis: List[int]) -> None:
        """Set which axis/axes are enabled for the current translate operation

        :param axis: List of axes (expressed as ToolHandle enum).
        """

        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    def setLockPosition(self, value: bool) -> None:
        """Set lock setting to the object. This setting will be used to prevent

        model movement on the build plate.
        :param value: The setting state.
        """
        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            selected_node.setSetting(SceneNodeSettings.LockPosition,
                                     str(value))

    def getLockPosition(self) -> Union[str, bool]:
        total_size = Selection.getCount()
        false_state_counter = 0
        true_state_counter = 0
        if not Selection.hasSelection():
            return False

        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            if selected_node.getSetting(SceneNodeSettings.LockPosition,
                                        "False") != "False":
                true_state_counter += 1
            else:
                false_state_counter += 1

        if total_size == false_state_counter:  # No locked positions
            return False
        elif total_size == true_state_counter:  # All selected objects are locked
            return True
        else:
            return "partially"  # At least one, but not all are locked

    def event(self, event: Event) -> bool:
        """Handle mouse and keyboard events.

        :param event: The event to handle.
        :return: Whether this event has been caught by this tool (True) or should
        be passed on (False).
        """
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and cast(
                KeyEvent, event).key == KeyEvent.ShiftKey:
            return False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a translate operation
            if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
                return False

            if not self._selection_pass:
                return False
            id = self._selection_pass.getIdAtPosition(
                cast(MouseEvent, event).x,
                cast(MouseEvent, event).y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False

            camera = self._controller.getScene().getActiveCamera()
            if not camera:
                return False
            camera_direction = camera.getPosition().normalized()

            abs_x = abs(camera_direction.x)
            abs_y = abs(camera_direction.y)

            # We have to define a plane vector that is suitable for the selected toolhandle axis
            # and at the same time the camera direction should not be exactly perpendicular to the plane vector
            if id == ToolHandle.XAxis:
                plane_vector = Vector(0, camera_direction.y,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.YAxis:
                plane_vector = Vector(camera_direction.x, 0,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.ZAxis:
                plane_vector = Vector(camera_direction.x, camera_direction.y,
                                      0).normalized()
            else:
                if abs_y > DIRECTION_TOLERANCE:
                    plane_vector = Vector(0, 1, 0)
                elif abs_x > DIRECTION_TOLERANCE:
                    plane_vector = Vector(1, 0, 0)
                    self.setLockedAxis(
                        ToolHandle.ZAxis)  # Do not move y / vertical
                else:
                    plane_vector = Vector(0, 0, 1)
                    self.setLockedAxis(
                        ToolHandle.XAxis)  # Do not move y / vertical

            self.setDragPlane(Plane(plane_vector, 0))
            return True

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            x = cast(MouseEvent, event).x
            y = cast(MouseEvent, event).y

            if not self.getDragStart():
                self.setDragStart(x, y)
                return False

            drag = self.getDragVector(x, y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y=0, z=0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x=0, z=0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x=0, y=0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors(
                )
                if len(selected_nodes) > 1:
                    op = GroupedOperation()
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition,
                                           "False") == "False":
                            op.addOperation(TranslateOperation(node, drag))
                    op.push()
                else:
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition,
                                           "False") == "False":
                            TranslateOperation(node, drag).push()

                if not self._distance:
                    self._distance = Vector(0, 0, 0)
                self._distance += drag

            self.setDragStart(x, y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(ToolHandle.NoAxis)
                self.setDragPlane(None)
                self.setDragStart(
                    cast(MouseEvent, event).x,
                    cast(MouseEvent, event).y)
                return True

        return False

    def getToolHint(self) -> Optional[str]:
        """Return a formatted distance of the current translate operation.

        :return: Fully formatted string showing the distance by which the
        mesh(es) are dragged.
        """
        return "%.2f mm" % self._distance.length() if self._distance else None
예제 #16
0
class Application:
    def __init__(self):
        self._clock_fmt = '%Y-%m-%d %H:%M:%S %Z%z'
        self._tz_utc = pytz.timezone('UTC')
        self._tz_eastern = pytz.timezone('US/Eastern')
        self._tz_central = pytz.timezone('US/Central')
        self._tz_mountain = pytz.timezone('US/Mountain')
        self._tz_pacific = pytz.timezone('US/Pacific')
        self._tz_berlin = pytz.timezone('Europe/Berlin')
        self._tz_london = pytz.timezone('Europe/London')
        self._tz_paris = pytz.timezone('Europe/Paris')
        self._timer = QTimer()
        self._timer.timeout.connect(self._show_clock)

    def _create_label(self, owner, labelText, yPos, x1, x2):
        lab = QLabel(owner)
        lab.setText(labelText)
        lab.move(x1, yPos)
        result = QLabel(owner)
        result.setText(' ' * 60)
        result.move(x2, yPos)
        return result

    def _show_clock(self):
        utc_dt = datetime.now(self._tz_utc)
        eastern_dt = utc_dt.astimezone(self._tz_eastern)
        central_dt = utc_dt.astimezone(self._tz_central)
        mountain_dt = utc_dt.astimezone(self._tz_mountain)
        pacific_dt = utc_dt.astimezone(self._tz_pacific)
        berlin_dt = utc_dt.astimezone(self._tz_berlin)
        london_dt = utc_dt.astimezone(self._tz_london)
        paris_dt = utc_dt.astimezone(self._tz_paris)
        self._utc_label.setText(utc_dt.strftime(self._clock_fmt))
        self._eastern_label.setText(eastern_dt.strftime(self._clock_fmt))
        self._central_label.setText(central_dt.strftime(self._clock_fmt))
        self._mountain_label.setText(mountain_dt.strftime(self._clock_fmt))
        self._pacific_label.setText(pacific_dt.strftime(self._clock_fmt))
        self._berlin_label.setText(berlin_dt.strftime(self._clock_fmt))
        self._london_label.setText(london_dt.strftime(self._clock_fmt))
        self._paris_label.setText(paris_dt.strftime(self._clock_fmt))

    def execute(self):
        app = QApplication([])
        tab1 = 20
        tab2 = 110
        yStart = 10
        yInc = 20
        w = QWidget()
        w.setGeometry(100, 100, 320, 240)
        w.setWindowTitle('World Clock')
        self._utc_label = self._create_label(w, 'UTC:', yStart, tab1, tab2)
        self._eastern_label = self._create_label(w, 'US/Eastern:',
                                                 yStart + yInc * 2, tab1, tab2)
        self._central_label = self._create_label(w, 'US/Central:',
                                                 yStart + yInc * 3, tab1, tab2)
        self._mountain_label = self._create_label(w, 'US/Mountain:',
                                                  yStart + yInc * 4, tab1,
                                                  tab2)
        self._pacific_label = self._create_label(w, 'US/Pacific:',
                                                 yStart + yInc * 5, tab1, tab2)
        self._berlin_label = self._create_label(w, 'Berlin:',
                                                yStart + yInc * 7, tab1, tab2)
        self._london_label = self._create_label(w, 'London:',
                                                yStart + yInc * 8, tab1, tab2)
        self._paris_label = self._create_label(w, 'Paris:', yStart + yInc * 9,
                                               tab1, tab2)
        w.show()
        self._timer.start(1000)
        exit(app.exec())
예제 #17
0
class InstanceContainersModel(ListModel):
    """Model that holds instance containers. By setting the filter property the instances held by this model can be
    changed.
    """

    NameRole = Qt.ItemDataRole.UserRole + 1  # Human readable name (string)
    IdRole = Qt.ItemDataRole.UserRole + 2  # Unique ID of the InstanceContainer
    MetaDataRole = Qt.ItemDataRole.UserRole + 3
    ReadOnlyRole = Qt.ItemDataRole.UserRole + 4
    SectionRole = Qt.ItemDataRole.UserRole + 5

    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.addRoleName(self.NameRole, "name")
        self.addRoleName(self.IdRole, "id")
        self.addRoleName(self.MetaDataRole, "metadata")
        self.addRoleName(self.ReadOnlyRole, "readOnly")
        self.addRoleName(self.SectionRole, "section")

        #We keep track of two sets: One for normal containers that are already fully loaded, and one for containers of which only metadata is known.
        #Both of these are indexed by their container ID.
        self._instance_containers = {}  #type: Dict[str, InstanceContainer]
        self._instance_containers_metadata = {
        }  # type: Dict[str, Dict[str, Any]]

        self._section_property = ""

        # Listen to changes
        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerChanged)
        ContainerRegistry.getInstance().containerRemoved.connect(
            self._onContainerChanged)
        ContainerRegistry.getInstance().containerLoadComplete.connect(
            self._onContainerLoadComplete)

        self._container_change_timer = QTimer()
        self._container_change_timer.setInterval(150)
        self._container_change_timer.setSingleShot(True)
        self._container_change_timer.timeout.connect(self._update)

        # List of filters for queries. The result is the union of the each list of results.
        self._filter_dicts = []  # type: List[Dict[str, str]]
        self._container_change_timer.start()

    def _onContainerChanged(self, container: ContainerInterface) -> None:
        """Handler for container added / removed events from registry"""

        # We only need to update when the changed container is a instanceContainer
        if isinstance(container, InstanceContainer):
            self._container_change_timer.start()

    def _update(self) -> None:
        """Private convenience function to reset & repopulate the model."""

        #You can only connect on the instance containers, not on the metadata.
        #However the metadata can't be edited, so it's not needed.
        for container in self._instance_containers.values():
            container.metaDataChanged.disconnect(self._updateMetaData)

        self._instance_containers, self._instance_containers_metadata = self._fetchInstanceContainers(
        )

        for container in self._instance_containers.values():
            container.metaDataChanged.connect(self._updateMetaData)

        new_items = list(self._recomputeItems())
        if new_items != self._items:
            self.setItems(new_items)

    def _recomputeItems(self) -> Generator[Dict[str, Any], None, None]:
        """Computes the items that need to be in this list model.

        This does not set the items in the list itself. It is intended to be
        overwritten by subclasses that add their own roles to the model.
        """

        registry = ContainerRegistry.getInstance()
        result = []
        for container in self._instance_containers.values():
            result.append({
                "name":
                container.getName(),
                "id":
                container.getId(),
                "metadata":
                container.getMetaData().copy(),
                "readOnly":
                registry.isReadOnly(container.getId()),
                "section":
                container.getMetaDataEntry(self._section_property, ""),
                "weight":
                int(container.getMetaDataEntry("weight", 0))
            })
        for container_metadata in self._instance_containers_metadata.values():
            result.append({
                "name":
                container_metadata["name"],
                "id":
                container_metadata["id"],
                "metadata":
                container_metadata.copy(),
                "readOnly":
                registry.isReadOnly(container_metadata["id"]),
                "section":
                container_metadata.get(self._section_property, ""),
                "weight":
                int(container_metadata.get("weight", 0))
            })
        yield from sorted(result, key=self._sortKey)

    def _fetchInstanceContainers(
        self
    ) -> Tuple[Dict[str, InstanceContainer], Dict[str, Dict[str, Any]]]:
        """Fetch the list of containers to display.

        This method is intended to be overridable by subclasses.

        :return: A tuple of an ID-to-instance mapping that includes all fully loaded containers, and
        an ID-to-metadata mapping that includes the containers of which only the metadata is known.
        """

        registry = ContainerRegistry.getInstance()  #Cache this for speed.
        containers = {
        }  #type: Dict[str, InstanceContainer] #Mapping from container ID to container.
        metadatas = {
        }  #type: Dict[str, Dict[str, Any]] #Mapping from container ID to metadata.
        for filter_dict in self._filter_dicts:
            this_filter = registry.findInstanceContainersMetadata(
                **filter_dict)
            for metadata in this_filter:
                if metadata["id"] not in containers and metadata[
                        "id"] not in metadatas:  #No duplicates please.
                    if registry.isLoaded(
                            metadata["id"]
                    ):  #Only add it to the full containers if it's already fully loaded.
                        containers[metadata["id"]] = cast(
                            InstanceContainer,
                            registry.findContainers(id=metadata["id"])[0])
                    else:
                        metadatas[metadata["id"]] = metadata
        return containers, metadatas

    def setSectionProperty(self, property_name: str) -> None:
        if self._section_property != property_name:
            self._section_property = property_name
            self.sectionPropertyChanged.emit()
            self._container_change_timer.start()

    sectionPropertyChanged = pyqtSignal()

    @pyqtProperty(str, fset=setSectionProperty, notify=sectionPropertyChanged)
    def sectionProperty(self) -> str:
        return self._section_property

    def setFilter(self, filter_dict: Dict[str, str]) -> None:
        """Set the filter of this model based on a string.

        :param filter_dict: :type{Dict} Dictionary to do the filtering by.
        """

        self.setFilterList([filter_dict])

    filterChanged = pyqtSignal()

    @pyqtProperty("QVariantMap", fset=setFilter, notify=filterChanged)
    def filter(self) -> Dict[str, str]:
        return self._filter_dicts[0] if len(self._filter_dicts) != 0 else {}

    def setFilterList(self, filter_list: List[Dict[str, str]]) -> None:
        """Set a list of filters to use when fetching containers.

        :param filter_list: List of filter dicts to fetch multiple sets of
        containers. The final result is the union of these sets.
        """

        if filter_list != self._filter_dicts:
            self._filter_dicts = filter_list
            self.filterChanged.emit()
            self._container_change_timer.start()

    @pyqtProperty("QVariantList", fset=setFilterList, notify=filterChanged)
    def filterList(self) -> List[Dict[str, str]]:
        return self._filter_dicts

    @pyqtSlot(str, result="QVariantList")
    def getFileNameFilters(self, io_type: str) -> List[str]:
        """Gets a list of the possible file filters that the plugins have registered they can read or write.
        The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers,
        but not when listing writers.

        :param io_type: Name of the needed IO type
        :return: A list of strings indicating file name filters for a file dialog.
        """

        #TODO: This function should be in UM.Resources!
        filters = []
        all_types = []
        for plugin_id, meta_data in self._getIOPlugins(io_type):
            for io_plugin in meta_data[io_type]:
                filters.append(io_plugin["description"] + " (*." +
                               io_plugin["extension"] + ")")
                all_types.append("*.{0}".format(io_plugin["extension"]))

        if "_reader" in io_type:
            # if we're listing readers, add the option to show all supported files as the default option
            filters.insert(
                0,
                catalog.i18nc("@item:inlistbox", "All Supported Types ({0})",
                              " ".join(all_types)))

            filters.append(
                catalog.i18nc("@item:inlistbox", "All Files (*)")
            )  # Also allow arbitrary files, if the user so prefers.
        return filters

    @pyqtSlot(result=QUrl)
    def getDefaultPath(self) -> QUrl:
        return QUrl.fromLocalFile(os.path.expanduser("~/"))

    def _getIOPlugins(self, io_type: str) -> List[Tuple[str, Dict[str, Any]]]:
        """Gets a list of profile reader or writer plugins

        :return: List of tuples of (plugin_id, meta_data).
        """

        pr = PluginRegistry.getInstance()
        active_plugin_ids = pr.getActivePlugins()

        result = []
        for plugin_id in active_plugin_ids:
            meta_data = pr.getMetaData(plugin_id)
            if io_type in meta_data:
                result.append((plugin_id, meta_data))
        return result

    def _sortKey(self, item: Dict[str, Any]) -> List[Any]:
        result = []
        if self._section_property:
            result.append(item.get(self._section_property, ""))

        result.append(
            not ContainerRegistry.getInstance().isReadOnly(item["id"]))
        result.append(int(item.get("weight", 0)))
        result.append(item["name"])

        return result

    def _updateMetaData(self, container: InstanceContainer) -> None:
        index = self.find("id", container.id)

        if self._section_property:
            self.setProperty(
                index, "section",
                container.getMetaDataEntry(self._section_property, ""))

        self.setProperty(index, "metadata", container.getMetaData())
        self.setProperty(index, "name", container.getName())
        self.setProperty(index, "id", container.getId())

    def _onContainerLoadComplete(self, container_id: str) -> None:
        """If a container has loaded fully (rather than just metadata) we need to
        move it from the dict of metadata to the dict of full containers.
        """

        if container_id in self._instance_containers_metadata:
            del self._instance_containers_metadata[container_id]
            self._instance_containers[
                container_id] = ContainerRegistry.getInstance().findContainers(
                    id=container_id)[0]
            self._instance_containers[container_id].metaDataChanged.connect(
                self._updateMetaData)
예제 #18
0
class MPSM2PrintJobOutputModel(PrintJobOutputModel):
    """Print Job Output Model."""
    def __init__(self, output_controller: PrinterOutputController) -> None:
        """Constructor.

    Args:
      output_controller: Printer's output controller.
    """
        super().__init__(output_controller=output_controller, key='', name='')
        self._state = 'not_started'
        self._progress = 0  # type: int
        self._elapsed_print_time_millis = 0  # type: int
        self._elapsed_percentage_points = None  # type: Optional[int]
        # Estimated printing time left, in seconds.
        self._remaining_print_time_secs = _MAX_REMAINING_TIME_SECS
        self._stopwatch = QTimer(self)
        self._stopwatch.timeout.connect(self._tick)
        self._reset()

    @pyqtProperty(int)
    def progress(self) -> int:
        """UI label for printing progress.

    Superclass computes progress based on elapsed time.
    MPSM2 printers do not provide elapsed time, but progress in percentage.

    Returns:
      Print job progress from 0 to 100.
    """
        return self._progress

    @pyqtProperty(str)
    def estimated_time_left(self) -> str:
        """UI label for estimated time left.

    Returns:
       Human-readable estimated printing time left.
    """
        if self._elapsed_percentage_points is None:
            return ''
        if self._elapsed_percentage_points < _MIN_PERCENT_POINTS:
            return ''
        return TimeUtils.get_human_readable_countdown(
            seconds=self._remaining_print_time_secs)

    def update_progress(self, progress: int) -> None:
        """Updates job progress and calculates estimated printing time left.

    Args:
      progress: Job progress from 0 to 100.
    """
        if progress < 0 or progress > 100:
            raise ValueError(f'Invalid printing progress: {progress}.')
        if progress == 0:
            self._reset()
        elif self._progress != progress:
            if self._elapsed_percentage_points is None:
                # Skip first seen percent point.
                self._elapsed_percentage_points = 0
            elif self._elapsed_percentage_points == 0:
                self._elapsed_percentage_points += 1
                if not self._stopwatch.isActive():
                    # New percent point seen. Start measuring.
                    self._stopwatch.start(_POLL_INTERVAL_MILLIS)
            else:
                self._remaining_print_time_secs = self._calculate_remaining_print_time(
                )
                self._elapsed_percentage_points += 1
        self._progress = progress

    def _reset(self) -> None:
        """Resets variables to calculate estimated print time left."""
        self._elapsed_print_time_millis = 0
        self._elapsed_percentage_points = None
        self._remaining_print_time_secs = _MAX_REMAINING_TIME_SECS
        if self._stopwatch.isActive():
            self._stopwatch.stop()

    def _calculate_remaining_print_time(self) -> int:
        """Calculates remaining print time.

    Calculation is based on the printing time and progress so far.

    Returns
      Remaining print time in seconds.
    """
        return int((100 - self._progress) * self._elapsed_print_time_millis /
                   self._elapsed_percentage_points / 1000)

    def _tick(self) -> None:
        """Updates stopwatch."""
        self._elapsed_print_time_millis += _POLL_INTERVAL_MILLIS
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # region Create CartPole instance and load initial settings

        # Create CartPole instance
        self.initial_state = create_cartpole_state()
        self.CartPoleInstance = CartPole(initial_state=self.initial_state)

        # Set timescales
        self.CartPoleInstance.dt_simulation = dt_simulation
        self.CartPoleInstance.dt_controller = controller_update_interval
        self.CartPoleInstance.dt_save = save_interval

        # set other settings
        self.CartPoleInstance.set_controller(controller_init)
        self.CartPoleInstance.stop_at_90 = stop_at_90_init
        self.set_random_experiment_generator_init_params()

        # endregion

        # region Decide whether to save the data in "CartPole memory" or not
        self.save_history = save_history_init
        self.show_experiment_summary = show_experiment_summary_init
        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

        # endregion

        # region Other variables initial values as provided in gui_default_parameters.py

        # Start user controlled experiment/ start random experiment/ load and replay - on start button
        self.simulator_mode = simulator_mode_init
        self.slider_on_click = slider_on_click_init  # Update slider on click/update slider while hoovering over it
        self.speedup = speedup_init  # Default simulation speed-up

        # endregion

        # region Initialize loop-timer
        # This timer allows to relate the simulation time to user time
        # And (if your computer is fast enough) run simulation
        # slower or faster than real-time by predefined factor (speedup)
        self.looper = loop_timer(
            dt_target=(self.CartPoleInstance.dt_simulation / self.speedup))
        # endregion

        # region Variables controlling the state of various processes (DO NOT MODIFY)

        self.terminate_experiment_or_replay_thread = False  # True: gives signal causing thread to terminate
        self.pause_experiment_or_replay_thread = False  # True: gives signal causing the thread to pause

        self.run_set_labels_thread = True  # True if gauges (labels) keep being repeatedly updated
        # Stop threads by setting False

        # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed.
        # Can take values "START!" or "STOP!"
        self.start_or_stop_action = "START!"
        # Flag indicating whether the pause button should pause or unpause.
        self.pause_or_unpause_action = "PAUSE"

        # Flag indicating that saving of experiment recording to csv file has finished
        self.experiment_or_replay_thread_terminated = False

        self.user_time_counter = 0  # Measures the user time

        # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance
        # if the option updating slider "on-click" is enabled.
        self.slider_instant_value = self.CartPoleInstance.slider_value

        self.noise = 'OFF'
        self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise

        # endregion

        # region Create GUI Layout

        # region - Create container for top level layout
        layout = QVBoxLayout()
        # endregion

        # region - Change geometry of the main window
        self.setGeometry(300, 300, 2500, 1000)
        # endregion

        # region - Matplotlib figures (CartPole drawing and Slider)
        # Draw Figure
        self.fig = Figure(
            figsize=(25, 10)
        )  # Regulates the size of Figure in inches, before scaling to window size.
        self.canvas = FigureCanvas(self.fig)
        self.fig.AxCart = self.canvas.figure.add_subplot(211)
        self.fig.AxSlider = self.canvas.figure.add_subplot(212)
        self.fig.AxSlider.set_ylim(0, 1)

        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)

        # Attach figure to the layout
        lf = QVBoxLayout()
        lf.addWidget(self.canvas)

        # endregion

        # region - Radio buttons selecting current controller
        self.rbs_controllers = []
        for controller_name in self.CartPoleInstance.controller_names:
            self.rbs_controllers.append(QRadioButton(controller_name))

        # Ensures that radio buttons are exclusive
        self.controllers_buttons_group = QButtonGroup()
        for button in self.rbs_controllers:
            self.controllers_buttons_group.addButton(button)

        lr_c = QVBoxLayout()
        lr_c.addStretch(1)
        for rb in self.rbs_controllers:
            rb.clicked.connect(self.RadioButtons_controller_selection)
            lr_c.addWidget(rb)
        lr_c.addStretch(1)

        self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked(
            True)

        # endregion

        # region - Create central part of the layout for figures and radio buttons and add it to the whole layout
        lc = QHBoxLayout()
        lc.addLayout(lf)
        lc.addLayout(lr_c)
        layout.addLayout(lc)

        # endregion

        # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...)

        # First row
        ld = QHBoxLayout()
        # User time
        self.labTime = QLabel("User's time (s): ")
        self.timer = QTimer()
        self.timer.setInterval(100)  # Tick every 1/10 of the second
        self.timer.timeout.connect(self.set_user_time_label)
        self.timer.start()
        ld.addWidget(self.labTime)
        # Speed, angle, motor power (Q)
        self.labSpeed = QLabel('Speed (m/s):')
        self.labAngle = QLabel('Angle (deg):')
        self.labMotor = QLabel('')
        self.labTargetPosition = QLabel('')
        ld.addWidget(self.labSpeed)
        ld.addWidget(self.labAngle)
        ld.addWidget(self.labMotor)
        ld.addWidget(self.labTargetPosition)
        layout.addLayout(ld)

        # Second row of labels
        # Simulation time, Measured (real) speed-up, slider-value
        ld2 = QHBoxLayout()
        self.labTimeSim = QLabel('Simulation Time (s):')
        ld2.addWidget(self.labTimeSim)
        self.labSpeedUp = QLabel('Speed-up (measured):')
        ld2.addWidget(self.labSpeedUp)
        self.labSliderInstant = QLabel('')
        ld2.addWidget(self.labSliderInstant)
        layout.addLayout(ld2)

        # endregion

        # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT"
        self.bss = QPushButton("START!")
        self.bss.pressed.connect(self.start_stop_button)
        self.bp = QPushButton("PAUSE")
        self.bp.pressed.connect(self.pause_unpause_button)
        bq = QPushButton("QUIT")
        bq.pressed.connect(self.quit_application)
        lspb = QHBoxLayout()  # Sub-Layout for Start/Stop and Pause Buttons
        lspb.addWidget(self.bss)
        lspb.addWidget(self.bp)

        # endregion

        # region - Sliders setting initial state and buttons for kicking the pole

        # Sliders setting initial position and angle
        lb = QVBoxLayout()  # Layout for buttons
        lb.addLayout(lspb)
        lb.addWidget(bq)
        ip = QHBoxLayout()  # Layout for initial position sliders
        self.initial_position_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_position_slider.setRange(
            -int(float(1000 * TrackHalfLength)),
            int(float(1000 * TrackHalfLength)))
        self.initial_position_slider.setValue(0)
        self.initial_position_slider.setSingleStep(1)
        self.initial_position_slider.valueChanged.connect(
            self.update_initial_position)
        self.initial_angle_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_angle_slider.setRange(-int(float(100 * np.pi)),
                                           int(float(100 * np.pi)))
        self.initial_angle_slider.setValue(0)
        self.initial_angle_slider.setSingleStep(1)
        self.initial_angle_slider.valueChanged.connect(
            self.update_initial_angle)
        ip.addWidget(QLabel("Initial position:"))
        ip.addWidget(self.initial_position_slider)
        ip.addWidget(QLabel("Initial angle:"))
        ip.addWidget(self.initial_angle_slider)
        ip.addStretch(0.01)

        # Slider setting latency
        self.LATENCY_SLIDER_RANGE_INT = 1000
        self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal)
        self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT)
        self.latency_slider.setValue(
            int(self.CartPoleInstance.LatencyAdderInstance.latency *
                self.LATENCY_SLIDER_RANGE_INT /
                self.CartPoleInstance.LatencyAdderInstance.max_latency))
        self.latency_slider.setSingleStep(1)
        self.latency_slider.valueChanged.connect(self.update_latency)
        ip.addWidget(QLabel("Latency:"))
        ip.addWidget(self.latency_slider)
        self.labLatency = QLabel('Latency (ms): {:.1f}'.format(
            self.CartPoleInstance.LatencyAdderInstance.latency * 1000))
        ip.addWidget(self.labLatency)

        # Buttons activating noise
        self.rbs_noise = []
        for mode_name in ['ON', 'OFF']:
            self.rbs_noise.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.noise_buttons_group = QButtonGroup()
        for button in self.rbs_noise:
            self.noise_buttons_group.addButton(button)

        lr_n = QHBoxLayout()
        lr_n.addWidget(QLabel('Noise:'))
        for rb in self.rbs_noise:
            rb.clicked.connect(self.RadioButtons_noise_on_off)
            lr_n.addWidget(rb)

        self.rbs_noise[1].setChecked(True)

        ip.addStretch(0.01)
        ip.addLayout(lr_n)
        ip.addStretch(0.01)

        # Buttons giving kick to the pole
        kick_label = QLabel("Kick pole:")
        kick_left_button = QPushButton()
        kick_left_button.setText("Left")
        kick_left_button.adjustSize()
        kick_left_button.clicked.connect(self.kick_pole)
        kick_right_button = QPushButton()
        kick_right_button.setText("Right")
        kick_right_button.adjustSize()
        kick_right_button.clicked.connect(self.kick_pole)
        ip.addWidget(kick_label)
        ip.addWidget(kick_left_button)
        ip.addWidget(kick_right_button)

        lb.addLayout(ip)
        layout.addLayout(lb)

        # endregion

        # region - Text boxes and Combobox to provide settings concerning generation of random experiment
        l_generate_trace = QHBoxLayout()
        l_generate_trace.addWidget(QLabel('Random experiment settings:'))
        l_generate_trace.addWidget(QLabel('Length (s):'))
        self.textbox_length = QLineEdit()
        l_generate_trace.addWidget(self.textbox_length)
        l_generate_trace.addWidget(QLabel('Turning Points (m):'))
        self.textbox_turning_points = QLineEdit()
        l_generate_trace.addWidget(self.textbox_turning_points)
        l_generate_trace.addWidget(QLabel('Interpolation:'))
        self.cb_interpolation = QComboBox()
        self.cb_interpolation.addItems(
            ['0-derivative-smooth', 'linear', 'previous'])
        self.cb_interpolation.currentIndexChanged.connect(
            self.cb_interpolation_selectionchange)
        self.cb_interpolation.setCurrentText(
            self.CartPoleInstance.interpolation_type)
        l_generate_trace.addWidget(self.cb_interpolation)

        layout.addLayout(l_generate_trace)

        # endregion

        # region - Textbox to provide csv file name for saving or loading data
        l_text = QHBoxLayout()
        textbox_title = QLabel('CSV file name:')
        self.textbox = QLineEdit()
        l_text.addWidget(textbox_title)
        l_text.addWidget(self.textbox)
        layout.addLayout(l_text)

        # endregion

        # region - Make strip of layout for checkboxes
        l_cb = QHBoxLayout()
        # endregion

        # region - Textbox to provide the target speed-up value
        l_text_speedup = QHBoxLayout()
        tx_speedup_title = QLabel('Speed-up (target):')
        self.tx_speedup = QLineEdit()
        l_text_speedup.addWidget(tx_speedup_title)
        l_text_speedup.addWidget(self.tx_speedup)
        self.tx_speedup.setText(str(self.speedup))
        l_cb.addLayout(l_text_speedup)

        self.wrong_speedup_msg = QMessageBox()
        self.wrong_speedup_msg.setWindowTitle("Speed-up value problem")
        self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical)
        # endregion

        # region - Checkboxes

        # region -- Checkbox: Save/don't save experiment recording
        self.cb_save_history = QCheckBox('Save results', self)
        if self.save_history:
            self.cb_save_history.toggle()
        self.cb_save_history.toggled.connect(self.cb_save_history_f)
        l_cb.addWidget(self.cb_save_history)
        # endregion

        # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates
        self.cb_show_experiment_summary = QCheckBox('Show experiment summary',
                                                    self)
        if self.show_experiment_summary:
            self.cb_show_experiment_summary.toggle()
        self.cb_show_experiment_summary.toggled.connect(
            self.cb_show_experiment_summary_f)
        l_cb.addWidget(self.cb_show_experiment_summary)
        # endregion

        # region -- Checkbox: Block pole if it reaches +/-90 deg
        self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self)
        if self.CartPoleInstance.stop_at_90:
            self.cb_stop_at_90_deg.toggle()
        self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f)
        l_cb.addWidget(self.cb_stop_at_90_deg)
        # endregion

        # region -- Checkbox: Update slider on click/update slider while hoovering over it
        self.cb_slider_on_click = QCheckBox('Update slider on click', self)
        if self.slider_on_click:
            self.cb_slider_on_click.toggle()
        self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f)
        l_cb.addWidget(self.cb_slider_on_click)

        # endregion

        # endregion

        # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay

        # List available simulator modes - constant
        self.available_simulator_modes = [
            'Slider-Controlled Experiment', 'Random Experiment', 'Replay'
        ]
        self.rbs_simulator_mode = []
        for mode_name in self.available_simulator_modes:
            self.rbs_simulator_mode.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.simulator_mode_buttons_group = QButtonGroup()
        for button in self.rbs_simulator_mode:
            self.simulator_mode_buttons_group.addButton(button)

        lr_sm = QHBoxLayout()
        lr_sm.addStretch(1)
        lr_sm.addWidget(QLabel('Simulator mode:'))
        for rb in self.rbs_simulator_mode:
            rb.clicked.connect(self.RadioButtons_simulator_mode)
            lr_sm.addWidget(rb)
        lr_sm.addStretch(1)

        self.rbs_simulator_mode[self.available_simulator_modes.index(
            self.simulator_mode)].setChecked(True)

        l_cb.addStretch(1)
        l_cb.addLayout(lr_sm)
        l_cb.addStretch(1)

        # endregion

        # region - Add checkboxes to layout
        layout.addLayout(l_cb)
        # endregion

        # region - Create an instance of a GUI window
        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()
        self.setWindowTitle('CartPole Simulator')

        # endregion

        # endregion

        # region Open controller-specific popup windows
        self.open_additional_controller_widget()
        # endregion

        # region Activate functions capturing mouse movements and clicks over the slider

        # This line links function capturing the mouse position on the canvas of the Figure
        self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement)
        # This line links function capturing the mouse position on the canvas of the Figure click
        self.canvas.mpl_connect("button_press_event", self.on_mouse_click)

        # endregion

        # region Introducing multithreading
        # To ensure smooth functioning of the app,
        # the calculations and redrawing of the figures have to be done in a different thread
        # than the one capturing the mouse position and running the animation
        self.threadpool = QThreadPool()
        # endregion

        # region Starts a thread repeatedly redrawing gauges (labels) of the GUI
        # It runs till the QUIT button is pressed
        worker_labels = Worker(self.set_labels_thread)
        self.threadpool.start(worker_labels)
        # endregion

        # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider)
        # This animation runs ALWAYS when the GUI is open
        # The buttons of GUI only decide if new parameters are calculated or not
        self.anim = self.CartPoleInstance.run_animation(self.fig)
        # endregion

    # region Thread performing CartPole experiment, slider-controlled or random
    # It iteratively updates  CartPole state and save data to a .csv file
    # It also put simulation time in relation to user time
    def experiment_thread(self):

        # Necessary only for debugging in Visual Studio Code IDE
        try:
            ptvsd.debug_this_thread()
        except:
            pass

        self.looper.start_loop()
        while not self.terminate_experiment_or_replay_thread:
            if self.pause_experiment_or_replay_thread:
                time.sleep(0.1)
            else:
                # Calculations of the Cart state in the next timestep
                self.CartPoleInstance.update_state()

                # Terminate thread if random experiment reached its maximal length
                if ((self.CartPoleInstance.use_pregenerated_target_position is
                     True) and (self.CartPoleInstance.time >=
                                self.CartPoleInstance.t_max_pre)):
                    self.terminate_experiment_or_replay_thread = True

                # FIXME: when Speedup empty in GUI I expected inf speedup but got error Loop timer was not initialized properly
                self.looper.sleep_leftover_time()

        # Save simulation history if user chose to do so at the end of the simulation
        if self.save_history:
            csv_name = self.textbox.text()
            self.CartPoleInstance.save_history_csv(
                csv_name=csv_name,
                mode='init',
                length_of_experiment=np.around(
                    self.CartPoleInstance.dict_history['time'][-1],
                    decimals=2))
            self.CartPoleInstance.save_history_csv(csv_name=csv_name,
                                                   mode='save offline')

        self.experiment_or_replay_thread_terminated = True

    # endregion

    # region Thread replaying a saved experiment recording
    def replay_thread(self):

        # Necessary only for debugging in Visual Studio Code IDE
        try:
            ptvsd.debug_this_thread()
        except:
            pass

        # Check what is in the csv textbox
        csv_name = self.textbox.text()

        # Load experiment history
        history_pd, filepath = self.CartPoleInstance.load_history_csv(
            csv_name=csv_name)

        # Set cartpole in the right mode (just to ensure slider behaves properly)
        with open(filepath, newline='') as f:
            reader = csv.reader(f)
            for line in reader:
                line = line[0]
                if line[:len('# Controller: ')] == '# Controller: ':
                    controller_set = self.CartPoleInstance.set_controller(
                        line[len('# Controller: '):].rstrip("\n"))
                    if controller_set:
                        self.rbs_controllers[self.CartPoleInstance.
                                             controller_idx].setChecked(True)
                    else:
                        self.rbs_controllers[1].setChecked(
                            True)  # Set first, but not manual stabilization
                    break

        # Augment the experiment history with simulation time step size
        dt = []
        row_iterator = history_pd.iterrows()
        _, last = next(row_iterator)  # take first item from row_iterator
        for i, row in row_iterator:
            dt.append(row['time'] - last['time'])
            last = row
        dt.append(dt[-1])
        history_pd['dt'] = np.array(dt)

        # Initialize loop timer (with arbitrary dt)
        replay_looper = loop_timer(dt_target=0.0)

        # Start looping over history
        replay_looper.start_loop()
        global L
        for index, row in history_pd.iterrows():
            self.CartPoleInstance.s[POSITION_IDX] = row['position']
            self.CartPoleInstance.s[POSITIOND_IDX] = row['positionD']
            self.CartPoleInstance.s[ANGLE_IDX] = row['angle']
            self.CartPoleInstance.time = row['time']
            self.CartPoleInstance.dt = row['dt']
            try:
                self.CartPoleInstance.u = row['u']
            except KeyError:
                pass
            self.CartPoleInstance.Q = row['Q']
            self.CartPoleInstance.target_position = row['target_position']
            if self.CartPoleInstance.controller_name == 'manual-stabilization':
                self.CartPoleInstance.slider_value = self.CartPoleInstance.Q
            else:
                self.CartPoleInstance.slider_value = self.CartPoleInstance.target_position / TrackHalfLength

            # TODO: Make it more general for all possible parameters
            try:
                L[...] = row['L']
            except KeyError:
                pass
            except:
                print('Error while assigning L')
                print("Unexpected error:", sys.exc_info()[0])
                print("Unexpected error:", sys.exc_info()[1])

            dt_target = (self.CartPoleInstance.dt / self.speedup)
            replay_looper.dt_target = dt_target

            replay_looper.sleep_leftover_time()

            if self.terminate_experiment_or_replay_thread:  # Means that stop button was pressed
                break

            while self.pause_experiment_or_replay_thread:  # Means that pause button was pressed
                time.sleep(0.1)

        if self.show_experiment_summary:
            self.CartPoleInstance.dict_history = history_pd.loc[:index].to_dict(
                orient='list')

        self.experiment_or_replay_thread_terminated = True

    # endregion

    # region "START! / STOP!" button -> run/stop slider-controlled experiment, random experiment or replay experiment recording

    # Actions to be taken when "START! / STOP!" button is clicked
    def start_stop_button(self):

        # If "START! / STOP!" button in "START!" mode...
        if self.start_or_stop_action == 'START!':
            self.bss.setText("STOP!")
            self.start_thread()

        # If "START! / STOP!" button in "STOP!" mode...
        elif self.start_or_stop_action == 'STOP!':
            self.bss.setText("START!")
            self.bp.setText("PAUSE")
            # This flag is periodically checked by thread. It terminates if set True.
            self.terminate_experiment_or_replay_thread = True
            # The stop_thread function is called automatically by the thread when it terminates
            # It is implemented this way, because thread my terminate not only due "STOP!" button
            # (e.g. replay thread when whole experiment is replayed)

    def pause_unpause_button(self):
        # Only Pause if experiment is running
        if self.pause_or_unpause_action == 'PAUSE' and self.start_or_stop_action == 'STOP!':
            self.pause_or_unpause_action = 'UNPAUSE'
            self.pause_experiment_or_replay_thread = True
            self.bp.setText("UNPAUSE")
        elif self.pause_or_unpause_action == 'UNPAUSE' and self.start_or_stop_action == 'STOP!':
            self.pause_or_unpause_action = 'PAUSE'
            self.pause_experiment_or_replay_thread = False
            self.bp.setText("PAUSE")

    # Run thread. works for all simulator modes.
    def start_thread(self):

        # Check if value provided in speed-up textbox makes sense
        # If not abort start
        speedup_updated = self.get_speedup()
        if not speedup_updated:
            return

        # Disable GUI elements for features which must not be changed in runtime
        # For other features changing in runtime may not cause errors, but will stay without effect for current run
        self.cb_save_history.setEnabled(False)
        for rb in self.rbs_simulator_mode:
            rb.setEnabled(False)
        for rb in self.rbs_controllers:
            rb.setEnabled(False)
        if self.simulator_mode != 'Replay':
            self.cb_show_experiment_summary.setEnabled(False)

        # Set user-provided initial values for state (or its part) of the CartPole
        # Search implementation for more detail
        # The following line is important as it let the user to set with the slider the starting target position
        # After the slider was reset at the end of last experiment
        # With the small sliders he can also adjust starting initial_state
        self.reset_variables(
            2,
            s=np.copy(self.initial_state),
            target_position=self.CartPoleInstance.target_position)

        if self.simulator_mode == 'Random Experiment':

            self.CartPoleInstance.use_pregenerated_target_position = True

            if self.textbox_length.text() == '':
                self.CartPoleInstance.length_of_experiment = length_of_experiment_init
            else:
                self.CartPoleInstance.length_of_experiment = float(
                    self.textbox_length.text())

            turning_points_list = []
            if self.textbox_turning_points.text() != '':
                for turning_point in self.textbox_turning_points.text().split(
                        ', '):
                    turning_points_list.append(float(turning_point))
            self.CartPoleInstance.turning_points = turning_points_list

            self.CartPoleInstance.setup_cartpole_random_experiment()

        self.looper.dt_target = self.CartPoleInstance.dt_simulation / self.speedup
        # Pass the function to execute
        if self.simulator_mode == "Replay":
            worker = Worker(self.replay_thread)
        elif self.simulator_mode == 'Slider-Controlled Experiment' or self.simulator_mode == 'Random Experiment':
            worker = Worker(self.experiment_thread)
        worker.signals.finished.connect(self.finish_thread)
        # Execute
        self.threadpool.start(worker)

        # Determine what should happen when "START! / STOP!" is pushed NEXT time
        self.start_or_stop_action = "STOP!"

    # finish_threads works for all simulation modes
    # Some lines mya be redundant for replay,
    # however as they do not take much computation time we leave them here
    # As it my code shorter, while hopefully still clear.
    # It is called automatically at the end of experiment_thread
    def finish_thread(self):

        self.CartPoleInstance.use_pregenerated_target_position = False
        self.initial_state = create_cartpole_state()
        self.initial_position_slider.setValue(0)
        self.initial_angle_slider.setValue(0)
        self.CartPoleInstance.s = self.initial_state

        # Some controllers may collect they own statistics about their usage and print it after experiment terminated
        if self.simulator_mode != 'Replay':
            try:
                self.CartPoleInstance.controller.controller_report()
            except:
                pass

        if self.show_experiment_summary:
            self.w_summary = SummaryWindow(
                summary_plots=self.CartPoleInstance.summary_plots)

        # Reset variables and redraw the figures
        self.reset_variables(0)

        # Draw figures
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

        # Enable back all elements of GUI:
        self.cb_save_history.setEnabled(True)
        self.cb_show_experiment_summary.setEnabled(True)
        for rb in self.rbs_simulator_mode:
            rb.setEnabled(True)
        for rb in self.rbs_controllers:
            rb.setEnabled(True)

        self.start_or_stop_action = "START!"  # What should happen when "START! / STOP!" is pushed NEXT time

    # endregion

    # region Methods: "Get, set, reset, quit"

    # Set parameters from gui_default_parameters related to generating a random experiment target position
    def set_random_experiment_generator_init_params(self):
        self.CartPoleInstance.track_relative_complexity = track_relative_complexity_init
        self.CartPoleInstance.length_of_experiment = length_of_experiment_init
        self.CartPoleInstance.interpolation_type = interpolation_type_init
        self.CartPoleInstance.turning_points_period = turning_points_period_init
        self.CartPoleInstance.start_random_target_position_at = start_random_target_position_at_init
        self.CartPoleInstance.end_random_target_position_at = end_random_target_position_at_init
        self.CartPoleInstance.turning_points = turning_points_init

    # Method resetting variables which change during experimental run
    def reset_variables(self, reset_mode=1, s=None, target_position=None):
        self.CartPoleInstance.set_cartpole_state_at_t0(
            reset_mode, s=s, target_position=target_position)
        self.user_time_counter = 0
        # "Try" because this function is called for the first time during initialisation of the Window
        # when the timer label instance is not yer there.
        try:
            self.labt.setText("Time (s): " +
                              str(float(self.user_time_counter) / 10.0))
        except:
            pass
        self.experiment_or_replay_thread_terminated = False  # This is a flag informing thread terminated
        self.terminate_experiment_or_replay_thread = False  # This is a command to terminate a thread
        self.pause_experiment_or_replay_thread = False  # This is a command to pause a thread
        self.start_or_stop_action = "START!"
        self.pause_or_unpause_action = "PAUSE"
        self.looper.first_call_done = False

    ######################################################################################################

    # (Marcin) Below are methods with less critical functions.

    # A thread redrawing labels (except for timer, which has its own function) of GUI every 0.1 s
    def set_labels_thread(self):
        while (self.run_set_labels_thread):
            self.labSpeed.setText(
                "Speed (m/s): " +
                str(np.around(self.CartPoleInstance.s[POSITIOND_IDX], 2)))
            self.labAngle.setText("Angle (deg): " + str(
                np.around(
                    self.CartPoleInstance.s[ANGLE_IDX] * 360 /
                    (2 * np.pi), 2)))
            self.labMotor.setText("Motor power (Q): {:.3f}".format(
                np.around(self.CartPoleInstance.Q, 2)))
            if self.CartPoleInstance.controller_name == 'manual-stabilization':
                self.labTargetPosition.setText("")
            else:
                self.labTargetPosition.setText(
                    "Target position (m): " +
                    str(np.around(self.CartPoleInstance.target_position, 2)))

            if self.CartPoleInstance.controller_name == 'manual_stabilization':
                self.labSliderInstant.setText(
                    "Slider instant value (-): " +
                    str(np.around(self.slider_instant_value, 2)))
            else:
                self.labSliderInstant.setText(
                    "Slider instant value (m): " +
                    str(np.around(self.slider_instant_value, 2)))

            self.labTimeSim.setText('Simulation time (s): {:.2f}'.format(
                self.CartPoleInstance.time))

            mean_dt_real = np.mean(self.looper.circ_buffer_dt_real)
            if mean_dt_real > 0:
                self.labSpeedUp.setText('Speed-up (measured): x{:.2f}'.format(
                    self.CartPoleInstance.dt_simulation / mean_dt_real))
            sleep(0.1)

    # Function to measure the time of simulation as experienced by user
    # It corresponds to the time of simulation according to equations only if real time mode is on
    # TODO (Marcin) I just retained this function from some example being my starting point
    #   It seems it sometimes counting time to slow. Consider replacing in future
    def set_user_time_label(self):
        # "If": Increment time counter only if simulation is running
        if self.start_or_stop_action == "STOP!":  # indicates what start button was pressed and some process is running
            self.user_time_counter += 1
            # The updates are done smoother if the label is updated here
            # and not in the separate thread
            self.labTime.setText("Time (s): " +
                                 str(float(self.user_time_counter) / 10.0))

    # The actions which has to be taken to properly terminate the application
    # The method is evoked after QUIT button is pressed
    # TODO: Can we connect it somehow also the the default cross closing the application?
    def quit_application(self):
        # Stops animation (updating changing elements of the Figure)
        self.anim._stop()
        # Stops the two threads updating the GUI labels and updating the state of Cart instance
        self.run_set_labels_thread = False
        self.terminate_experiment_or_replay_thread = True
        self.pause_experiment_or_replay_thread = False
        # Closes the GUI window
        self.close()
        # The standard command
        # It seems however not to be working by its own
        # I don't know how it works
        QApplication.quit()

    # endregion

    # region Mouse interaction
    """
    These are some methods GUI uses to capture mouse effect while hoovering or clicking over/on the charts
    """

    # Function evoked at a mouse movement
    # If the mouse cursor is over the lower chart it reads the corresponding value
    # and updates the slider
    def on_mouse_movement(self, event):
        if self.simulator_mode == 'Slider-Controlled Experiment':
            if event.xdata == None or event.ydata == None:
                pass
            else:
                if event.inaxes == self.fig.AxSlider:
                    self.slider_instant_value = event.xdata
                    if not self.slider_on_click:
                        self.CartPoleInstance.update_slider(
                            mouse_position=event.xdata)

    # Function evoked at a mouse click
    # If the mouse cursor is over the lower chart it reads the corresponding value
    # and updates the slider
    def on_mouse_click(self, event):
        if self.simulator_mode == 'Slider-Controlled Experiment':
            if event.xdata == None or event.ydata == None:
                pass
            else:
                if event.inaxes == self.fig.AxSlider:
                    self.CartPoleInstance.update_slider(
                        mouse_position=event.xdata)

    # endregion

    # region Changing "static" options: radio buttons, text boxes, combo boxes, check boxes
    """
    This section collects methods used to change some ''static option'':
    e.g. change current controller, switch between saving and not saving etc.
    These are functions associated with radio buttons, check boxes, textfilds etc.
    The functions of "START! / STOP!" button is much more complex
    and we put them hence in a separate section.
    """

    # region - Radio buttons

    # Chose the controller method which should be used with the CartPole
    def RadioButtons_controller_selection(self):
        # Change the mode variable depending on the Radiobutton state
        for i in range(len(self.rbs_controllers)):
            if self.rbs_controllers[i].isChecked():
                self.CartPoleInstance.set_controller(controller_idx=i)

        # Reset the state of GUI and of the Cart instance after the mode has changed
        # TODO: Do I need the follwowing lines?
        self.reset_variables(0)
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

        self.open_additional_controller_widget()

    # Chose the simulator mode - effect of start/stop button
    def RadioButtons_simulator_mode(self):
        # Change the mode variable depending on the Radiobutton state
        for i in range(len(self.rbs_simulator_mode)):
            sleep(0.001)
            if self.rbs_simulator_mode[i].isChecked():
                self.simulator_mode = self.available_simulator_modes[i]

        # Reset the state of GUI and of the Cart instance after the mode has changed
        # TODO: Do I need the follwowing lines?
        self.reset_variables(0)
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

    # Chose the noise mode - effect of start/stop button
    def RadioButtons_noise_on_off(self):
        # Change the mode variable depending on the Radiobutton state
        if self.rbs_noise[0].isChecked():
            self.noise = 'ON'
            self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise
        elif self.rbs_noise[1].isChecked():
            self.noise = 'OFF'
            self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise
        else:
            raise Exception('Something wrong with ON/OFF button for noise')

        self.open_additional_noise_widget()

    # endregion

    # region - Text Boxes

    # Read speedup provided by user from appropriate GUI textbox
    def get_speedup(self):
        """
        Get speedup provided by user from appropriate textbox.
        Speed-up gives how many times faster or slower than real time the simulation or replay should run.
        The provided values may not always be reached due to computer speed limitation
        """
        speedup = self.tx_speedup.text()
        if speedup == '':
            self.speedup = np.inf
            return True
        else:
            try:
                speedup = float(speedup)
            except ValueError:
                self.wrong_speedup_msg.setText(
                    'You have provided the input for speed-up which is not convertible to a number'
                )
                x = self.wrong_speedup_msg.exec_()
                return False
            if speedup == 0.0:
                self.wrong_speedup_msg.setText(
                    'You cannot run an experiment with 0 speed-up (stopped time flow)'
                )
                x = self.wrong_speedup_msg.exec_()
                return False
            else:
                self.speedup = speedup
                return True

    # endregion

    # region - Combo Boxes

    # Select how to interpolate between turning points of randomly chosen target positions
    def cb_interpolation_selectionchange(self, i):
        """
        Select interpolation type for random target positions of randomly generated experiment
        """
        self.CartPoleInstance.interpolation_type = self.cb_interpolation.currentText(
        )

    # endregion

    # region - Check boxes

    # Action toggling between saving and not saving simulation results
    def cb_save_history_f(self, state):

        if state:
            self.save_history = 1
        else:
            self.save_history = 0

        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

    # Action toggling between saving and not saving simulation results
    def cb_show_experiment_summary_f(self, state):

        if state:
            self.show_experiment_summary = 1
        else:
            self.show_experiment_summary = 0

        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

    # Action toggling between stopping (or not) the pole if it reaches 90 deg
    def cb_stop_at_90_deg_f(self, state):

        if state:
            self.CartPoleInstance.stop_at_90 = True
        else:
            self.CartPoleInstance.stop_at_90 = False

    # Action toggling between updating CarPole slider value on click or by hoovering over it
    def cb_slider_on_click_f(self, state):

        if state:
            self.slider_on_click = True
        else:
            self.slider_on_click = False

    # endregion

    # region - Additional GUI Popups

    def open_additional_controller_widget(self):
        # Open up additional options widgets depending on the controller type
        if self.CartPoleInstance.controller_name == 'mppi':
            self.optionsControllerWidget = MPPIOptionsWindow()
        else:
            try:
                self.optionsControllerWidget.close()
            except:
                pass
            self.optionsControllerWidget = None

    def open_additional_noise_widget(self):
        # Open up additional options widgets depending on the controller type
        if self.noise == 'ON':
            self.optionsNoiseWidget = NoiseOptionsWindow()
        else:
            try:
                self.optionsNoiseWidget.close()
            except:
                pass
            self.optionsNoiseWidget = None

    # endregion

    # region - Sliders setting initial position and angle of the CartPole

    def update_initial_position(self, value: str):
        self.initial_state[POSITION_IDX] = float(value) / 1000.0

    def update_initial_angle(self, value: str):
        self.initial_state[ANGLE_IDX] = float(value) / 100.0

    # endregion

    # region - Slider setting latency of the controller

    def update_latency(self, value: str):
        latency_slider = float(value)
        latency = latency_slider * self.CartPoleInstance.LatencyAdderInstance.max_latency / self.LATENCY_SLIDER_RANGE_INT  # latency in seconds
        self.CartPoleInstance.LatencyAdderInstance.set_latency(latency)
        self.labLatency.setText('{:.1f} ms'.format(latency *
                                                   1000.0))  # latency in ms

    # endregion

    # region Buttons for providing a kick to the pole

    def kick_pole(self):
        if self.sender().text() == "Left":
            self.CartPoleInstance.s[ANGLED_IDX] += .6
        elif self.sender().text() == "Right":
            self.CartPoleInstance.s[ANGLED_IDX] -= .6
예제 #20
0
class Window(QMainWindow):

    # The following attributes are dynamically loaded from the .ui file
    startButton: QPushButton
    stopButton: QPushButton
    leftEyeThreshold: QSlider
    rightEyeThreshold: QSlider

    def __init__(self, video_source: FrameSource, capture: Capture):
        super(Window, self).__init__()
        loadUi(settings.GUI_FILE_PATH, self)
        with open(settings.STYLE_FILE_PATH, "r") as css:
            self.setStyleSheet(css.read())

        self.startButton.clicked.connect(self.start)
        self.stopButton.clicked.connect(self.stop)
        self.timer = None
        self.video_source = video_source
        self.capture = capture

    def start(self):
        self.video_source.start()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_frame)
        self.timer.start(settings.REFRESH_PERIOD)

    def stop(self):
        self.timer.stop()
        self.video_source.stop()

    def update_frame(self):
        frame = self.video_source.next_frame()
        face, l_eye, r_eye = self.capture.process(
            frame, self.leftEyeThreshold.value(),
            self.rightEyeThreshold.value())

        if face is not None:
            self.display_image(self.opencv_to_qt(frame))

        if l_eye is not None:
            self.display_image(self.opencv_to_qt(l_eye), window="leftEyeBox")

        if r_eye is not None:
            self.display_image(self.opencv_to_qt(r_eye), window="rightEyeBox")

    @staticmethod
    def opencv_to_qt(img) -> QImage:
        """
        Convert OpenCV image to PyQT image
        by changing format to RGB/RGBA from BGR
        """
        qformat = QImage.Format.Format_Indexed8
        if len(img.shape) == 3:
            if img.shape[2] == 4:  # RGBA
                qformat = QImage.Format.Format_RGBA8888
            else:  # RGB
                qformat = QImage.Format.Format_RGB888

        img = numpy.require(img, numpy.uint8, "C")
        out_image = QImage(img, img.shape[1], img.shape[0], img.strides[0],
                           qformat)  # BGR to RGB
        out_image = out_image.rgbSwapped()

        return out_image

    def display_image(self, img: QImage, window="baseImage"):
        """
        Display the image on a window - which is a label specified in the GUI .ui file
        """

        display_label: QLabel = getattr(self, window, None)
        if display_label is None:
            raise ValueError(f"No such display window in GUI: {window}")

        display_label.setPixmap(QPixmap.fromImage(img))
        display_label.setScaledContents(True)
예제 #21
0
class HttpRequestData(QObject):

    # Add some tolerance for scheduling the QTimer to check for timeouts, because the QTimer may trigger the event a
    # little earlier. For example, with a 5000ms interval, the timer event can be triggered after 4752ms, so a request
    # may never timeout if we don't add some tolerance.
    # 4752ms (actual) and 5000ms (expected) has about 6% difference, so here I use 15% to be safer.
    TIMEOUT_CHECK_TOLERANCE = 0.15

    def __init__(self,
                 request_id: str,
                 http_method: str,
                 request: "QNetworkRequest",
                 manager_timeout_callback: Callable[["HttpRequestData"], None],
                 data: Optional[Union[bytes, bytearray]] = None,
                 callback: Optional[Callable[["QNetworkReply"], None]] = None,
                 error_callback: Optional[
                     Callable[["QNetworkReply", "QNetworkReply.NetworkError"],
                              None]] = None,
                 download_progress_callback: Optional[Callable[[int, int],
                                                               None]] = None,
                 upload_progress_callback: Optional[Callable[[int, int],
                                                             None]] = None,
                 timeout: Optional[float] = None,
                 reply: Optional["QNetworkReply"] = None,
                 parent: Optional["QObject"] = None) -> None:
        super().__init__(parent=parent)

        # Sanity checks
        if timeout is not None and timeout <= 0:
            raise ValueError(
                "Timeout must be a positive value, but got [%s] instead." %
                timeout)

        self._request_id = request_id
        self.http_method = http_method
        self.request = request
        self.data = data
        self.callback = callback
        self.error_callback = error_callback
        self.download_progress_callback = download_progress_callback
        self.upload_progress_callback = upload_progress_callback
        self._timeout = timeout
        self.reply = reply

        # For benchmarking. For calculating the time a request spent pending.
        self._create_time = time.time()

        # The timestamp when this request was initially issued to the QNetworkManager. This field to used to track and
        # manage timeouts (if set) for the requests.
        self._start_time = None  # type: Optional[float]
        self.is_aborted_due_to_timeout = False

        self._last_response_time = float(0)
        self._timeout_timer = QTimer(parent=self)
        if self._timeout is not None:
            self._timeout_timer.setSingleShot(True)
            timeout_check_interval = int(self._timeout * 1000 *
                                         (1 + self.TIMEOUT_CHECK_TOLERANCE))
            self._timeout_timer.setInterval(timeout_check_interval)
            self._timeout_timer.timeout.connect(self._onTimeoutTimerTriggered)

        self._manager_timeout_callback = manager_timeout_callback

    @property
    def request_id(self) -> str:
        return self._request_id

    @property
    def timeout(self) -> Optional[float]:
        return self._timeout

    @property
    def start_time(self) -> Optional[float]:
        return self._start_time

    # For benchmarking. Time in seconds that this request stayed in the pending queue.
    @property
    def pending_time(self) -> Optional[float]:
        if self._start_time is None:
            return None
        return self._start_time - self._create_time

    # Sets the start time of this request. This is called when this request is issued to the QNetworkManager.
    def setStartTime(self, start_time: float) -> None:
        self._start_time = start_time

        # Prepare timeout handling
        if self._timeout is not None:
            self._last_response_time = start_time
            self._timeout_timer.start()

    # Do some cleanup, such as stopping the timeout timer.
    def setDone(self) -> None:
        if self._timeout is not None:
            self._timeout_timer.stop()
            self._timeout_timer.timeout.disconnect(
                self._onTimeoutTimerTriggered)

    # Since Qt 5.12, pyqtSignal().connect() will return a Connection instance that represents a connection. This
    # Connection instance can later be used to disconnect for cleanup purpose. We are using Qt 5.10 and this feature
    # is not available yet, and I'm not sure if disconnecting a lambda can potentially cause issues. For this reason,
    # I'm using the following facade callback functions to handle the lambda function cases.

    def onDownloadProgressCallback(self, bytes_received: int,
                                   bytes_total: int) -> None:
        # Update info for timeout handling
        if self._timeout is not None:
            now = time.time()
            time_last = now - self._last_response_time
            self._last_response_time = time.time()
            # We've got a response, restart the timeout timer
            self._timeout_timer.start()

        if self.download_progress_callback is not None:
            self.download_progress_callback(bytes_received, bytes_total)

    def onUploadProgressCallback(self, bytes_sent: int,
                                 bytes_total: int) -> None:
        # Update info for timeout handling
        if self._timeout is not None:
            now = time.time()
            time_last = now - self._last_response_time
            self._last_response_time = time.time()
            # We've got a response, restart the timeout timer
            self._timeout_timer.start()

        if self.upload_progress_callback is not None:
            self.upload_progress_callback(bytes_sent, bytes_total)

    def _onTimeoutTimerTriggered(self) -> None:
        # Make typing happy
        if self._timeout is None:
            return
        if self.reply is None:
            return

        now = time.time()
        time_last = now - self._last_response_time
        if self.reply.isRunning() and time_last >= self._timeout:
            self._manager_timeout_callback(self)
        else:
            self._timeout_timer.start()

    def __str__(self) -> str:
        data = "no-data"
        if self.data:
            data = str(self.data[:10])
            if len(self.data) > 10:
                data += "..."

        return "request[{id}][{method}][{url}][timeout={timeout}][{data}]".format(
            id=self._request_id[:8],
            method=self.http_method,
            url=self.request.url(),
            timeout=self._timeout,
            data=data)
예제 #22
0

def backgroundBoxCleaner():
    try:
        for box in ui.boxes:
            if box != 'no_devices' and not ui.boxes[box].isVisible(
            ) and not ui.in_settings:
                ui.boxes[box].deleteLater()
                ui.boxes.pop(box)
    except RuntimeError:
        return


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    app.setWindowIcon(QtGui.QIcon(app_icon))
    MainWindow = QtWidgets.QMainWindow()
    ui = Window()
    ui.setupUi()
    MainWindow.show()

    updater = QTimer()
    updater.timeout.connect(checkDevicesActuality)
    updater.start(300)

    cleaner = QTimer()
    cleaner.timeout.connect(backgroundBoxCleaner)
    cleaner.start(500)

    sys.exit(app.exec())
예제 #23
0
class MainWindow(QMainWindow):
    """Main application window"""
    minesite_changed = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.app = QApplication.instance()
        self.setWindowTitle(gbl.title)
        self.setMinimumSize(QSize(1000, 400))
        self.minesite_changed.connect(self.update_minesite_label)
        self.minesite_label = QLabel(
            self
        )  # permanent label for status bar so it isnt changed by statusTips
        self.minesite_label.setToolTip(
            'Global MineSite > Set with [Ctrl + Shift + M]')
        self.rows_label = QLabel(self)
        self.statusBar().addPermanentWidget(self.rows_label)
        self.statusBar().addPermanentWidget(self.minesite_label)

        # Settings
        s = QSettings('sms', 'smseventlog', self)

        screen_point = s.value('window position', False)
        screen_size = s.value('window size', False)

        # if screen size/left anchor pt values are not set or out of range, use default
        if not (screen_point and screen_size
                and gbl.check_screen_point(screen_point)):
            screen_point = QPoint(50, 50)
            screen_size = QSize(1200, 1000)

        # move/resize MainWindow to last position/size
        self.resize(screen_size)
        self.move(screen_point)
        self.settings = s

        self.menus = {}
        self.create_actions()

        self.tabs = TabWidget(self)
        self.setCentralWidget(self.tabs)
        self.update_minesite_label()

        self.threadpool = QThreadPool(self)
        log.debug('Mainwindow init finished.')

    @property
    def minesite(self) -> str:
        """Global minesite setting"""
        return self.settings.value('minesite', defaultValue='FortHills')
        # return self._minesite

    @minesite.setter
    def minesite(self, val: Any) -> None:
        """Save minesite back to settings"""
        # self._minesite = val
        self.settings.setValue('minesite', val)
        self.minesite_changed.emit(val)

    def update_minesite_label(self, *args):
        """minesite_label is special label to always show current minesite (bottom right)"""
        self.minesite_label.setText(f'Minesite: {self.minesite}')

    def update_rows_label(self, *args):
        view = self.active_table()
        if view is None:
            return  # not init yet

        model = view.data_model
        visible_rows = model.visible_rows
        total_rows = model.total_rows

        if total_rows == visible_rows:
            num_rows = visible_rows
        else:
            num_rows = f'{visible_rows}/{total_rows}'

        self.rows_label.setText(f'Rows: {num_rows}')

    def warn_not_implemented(self) -> None:
        """Let user know feature not implemented"""
        self.update_statusbar('Warning: This feature not yet implemented.')

    def update_statusbar(self,
                         msg: str = None,
                         warn: bool = False,
                         success: bool = False,
                         log_: bool = False,
                         *args) -> None:
        """Statusbar shows temporary messages that disappear on any context event"""
        if not msg is None:

            # allow warn or success status to be passed with msg as dict
            if isinstance(msg, dict):
                warn = msg.get('warn', False)
                success = msg.get('success', False)
                msg = msg.get('msg', None)  # kinda sketch

            if log_:
                log.info(msg)

            bar = self.statusBar()
            self.prev_status = bar.currentMessage()
            bar.showMessage(msg)

            msg_lower = msg.lower()
            if warn or 'warn' in msg_lower or 'error' in msg_lower:
                color = '#ff5454'  # '#fa7070'
            elif success or 'success' in msg_lower:
                color = '#70ff94'
            else:
                color = 'white'

            palette = bar.palette()
            palette.setColor(QPalette.ColorRole.WindowText, QColor(color))
            bar.setPalette(palette)

            self.app.processEvents()

    def revert_status(self):
        # revert statusbar to previous status
        if not hasattr(self, 'prev_status'):
            self.prev_status = ''

        self.update_statusbar(msg=self.prev_status)

    @er.errlog()
    def after_init(self):
        """Steps to run before MainWindow is shown.
        - Everything in here must suppress errors and continue
        """
        self.username = self.get_username()
        self.init_sentry()

        self.u = users.User(username=self.username, mainwindow=self).login()
        log.debug('user init')

        last_tab_name = self.settings.value('active table', 'Event Log')
        self.tabs.init_tabs()
        self.tabs.activate_tab(title=last_tab_name)
        log.debug('last tab activated')

        # initialize updater
        self.updater = Updater(mw=self,
                               dev_channel=self.get_setting('dev_channel'))
        log.debug(f'updater initialized, channel={self.updater.channel}')

        t = self.active_table_widget()
        if t.refresh_on_init:
            t.refresh(default=True, save_query=False)
            log.debug('last table refreshed')

        # startup update checks can allow ignoring dismissed versions
        self.check_update(allow_dismissed=True)
        self.start_update_timer()
        log.debug('Finished after_init')

    def start_update_timer(self, mins: int = 180) -> None:
        """Check for updates every 3 hrs"""
        if not cf.SYS_FROZEN:
            return

        msec = mins * 60 * 1000

        self.update_timer = QTimer(parent=self)
        self.update_timer.timeout.connect(self.check_update)
        self.update_timer.start(msec)

    @er.errlog('Failed to check for update!', display=True)
    def check_update(self, allow_dismissed: bool = False, *args):
        """Check for update and download in a worker thread
        """
        if not cf.SYS_FROZEN:
            self.update_statusbar('App not frozen, not checking for updates.')
            return

        if self.updater.update_available:
            # update has been previously checked and downloaded but user declined to install initially
            self._install_update(updater=self.updater,
                                 allow_dismissed=allow_dismissed)
        else:
            Worker(func=self.updater.check_update, mw=self) \
                .add_signals(signals=(
                    'result',
                    dict(func=lambda updater: self._install_update(updater, allow_dismissed=allow_dismissed)))) \
                .start()

    def _install_update(self,
                        updater: Updater = None,
                        ask_user: bool = True,
                        allow_dismissed: bool = False) -> None:
        """Ask if user wants to update and show changelog

        Parameters
        ----------
        updater : Updater, optional
            Updater obj, default None
        ask_user : bool, optional
            prompt user to update or just install, default True
        allow_dismissed : bool, optional
            allow ignoring patch updates if user has dismissed once
        """

        # update check failed, None result from thread
        if updater is None:
            return

        v_current = updater.version
        v_latest = updater.ver_latest

        # check if PATCH update has been dismissed
        if not updater.needs_update and allow_dismissed:
            log.info('User declined current update. current:' +
                     f'{v_latest}, dismissed: {updater.ver_dismissed}')
            return

        # show changelog between current installed and latest version
        markdown_msg = updater.get_changelog_new()

        # prompt user to install update and restart
        msg = 'An updated version of the Event Log is available.\n\n' \
            + f'Current: {v_current}\n' \
            + f'Latest: {v_latest}\n\n' \
            + 'Would you like to restart and update now?' \
            + '\n\nNOTE - Patch updates (eg x.x.1) can be dismissed. Use Help > Check for Update ' \
            + 'to prompt again.'

        if ask_user:
            if not dlgs.msgbox(msg=msg, yesno=True, markdown_msg=markdown_msg):

                # mark version as dismissed
                self.settings.setValue('ver_dismissed', str(v_latest))
                self.update_statusbar(
                    f'User dismissed update version: {v_latest}', log_=True)
                return

        Worker(func=updater.install_update, mw=self).start()
        self.update_statusbar('Extracting update and restarting...')

    def show_full_changelog(self) -> None:
        """Show full changelog"""
        msg = self.updater.get_changelog_full()
        dlgs.msgbox(msg='Changelog:', markdown_msg=msg)

    def init_sentry(self):
        """Add user-related scope information to sentry"""
        with configure_scope() as scope:  # type: ignore
            scope.user = dict(username=self.username,
                              email=self.get_setting('email'))
            # scope.set_extra('version', VERSION) # added to sentry release field

    def active_table_widget(self) -> tbls.TableWidget:
        """Current active TableWidget"""
        return self.tabs.currentWidget()

    @property
    def t(self) -> tbls.TableWidget:
        """Convenience property wrapper for active TableWidget"""
        return self.active_table_widget()

    def active_table(self) -> Union[tbls.TableView, None]:
        """Current active TableView"""
        table_widget = self.active_table_widget()
        if not table_widget is None:
            return table_widget.view

    @property
    def tv(self) -> Union[tbls.TableView, None]:
        """Convenience property wrapper for active TableView"""
        return self.active_table()

    def show_changeminesite(self):
        dlg = dlgs.ChangeMinesite(parent=self)
        return dlg.exec()

    @er.errlog('Close event failed.')
    def closeEvent(self, event):
        s = self.settings
        s.setValue('window size', self.size())
        s.setValue('window position', self.pos())
        s.setValue('screen', self.geometry().center())
        s.setValue('minesite', self.minesite)
        s.setValue('active table', self.active_table_widget().title)

        # save current TableView column state
        self.tv.save_header_state()

        # update on closeEvent if update available... maybe not yet
        # if self.updater.update_available:
        #     self._install_update(updater=self.updater, ask_user=False)

    def get_setting(self, key: str, default: Any = None) -> Any:
        """Convenience accessor to global settings"""
        return gbl.get_setting(key=key, default=default)

    def get_username(self):
        s = self.settings
        username = self.get_setting('username')
        email = self.get_setting('email')

        if username is None or email is None:
            self.set_username()
            username = self.username

        return username

    def set_username(self):
        # show username dialog and save first/last name to settings
        s = self.settings
        dlg = dlgs.InputUserName(self)
        if not dlg.exec():
            return

        s.setValue('username', dlg.username)
        s.setValue('email', dlg.email)
        self.username = dlg.username

        if hasattr(self, 'u'):
            self.u.username = dlg.username
            self.u.email = dlg.email

    @property
    def driver(self) -> Union[WebDriver, None]:
        """Save global Chrome WebDriver to reuse etc for TSI or SAP"""
        return self._driver if hasattr(self, '_driver') else None

    @driver.setter
    def driver(self, driver: WebDriver):
        self._driver = driver

    def open_sap(self):
        from smseventlog.utils.web import SuncorWorkRemote
        self.sc = SuncorWorkRemote(mw=self, _driver=self.driver)

        Worker(func=self.sc.open_sap, mw=self) \
            .add_signals(signals=('result', dict(func=self.handle_sap_result))) \
            .start()
        self.update_statusbar('Opening SAP...')

    def handle_sap_result(self, sc=None):
        """just need to keep a referece to the driver in main thread so chrome doesnt close"""
        if sc is None:
            log.warning('SAP not opened properly')
            return

        self.driver = sc.driver
        self.update_statusbar('SAP started.', success=True)

    def get_menu(self, name: Union[str, 'QMenu']) -> 'QMenu':
        """Get QMenu if exists or create

        Returns
        -------
        QMenu
            menu bar
        """
        if isinstance(name, str):
            menu = self.menus.get(name, None)

            if menu is None:
                bar = self.menuBar()
                menu = bar.addMenu(name.title())
                self.menus[name] = menu
        else:
            menu = name

        return menu

    def add_action(self,
                   name: str,
                   func: Callable,
                   menu: str = None,
                   shortcut: str = None,
                   tooltip: str = None,
                   label_text: str = None,
                   parent: QWidget = None,
                   **kw) -> QAction:
        """Convenience func to create QAction and add to menu bar

        Parameters
        ----------
        name : str
            Action name

        Returns
        -------
        QAction
        """
        name_action = name.replace(' ', '_').lower()
        name_key = f'act_{name_action}'
        name = f.nice_title(name.replace(
            '_', ' ')) if label_text is None else label_text

        if parent is None:
            parent = self

        act = QAction(name, parent, triggered=func, **kw)

        if not shortcut is None:
            act.setShortcut(QKeySequence(shortcut))

        act.setToolTip(tooltip)
        # act.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
        act.setShortcutVisibleInContextMenu(True)

        setattr(parent, name_key, act)

        if not menu is None:
            menu = self.get_menu(menu)

            menu.addAction(act)
        else:
            parent.addAction(act)

        return act

    def add_actions(self,
                    actions: dict,
                    menu: Union[str, 'QMenu'] = None) -> None:
        """Add dict of multiple actions to menu bar

        Parameters
        ----------
        actions : dict
            dict of menu_name: {action: func|kw}
        """
        menu = self.get_menu(menu)

        for name, kw in actions.items():
            if not isinstance(kw, dict):
                kw = dict(func=kw)

            if 'submenu' in name:
                # create submenu, recurse
                submenu = menu.addMenu(name.replace('submenu_', '').title())
                self.add_actions(menu=submenu, actions=kw)

            else:
                if 'sep' in kw:
                    kw.pop('sep')
                    menu.addSeparator()

                self.add_action(name=name, menu=menu, **kw)

    def create_actions(self) -> None:
        """Initialize menubar actions"""
        t, tv = self.active_table_widget, self.active_table

        menu_actions = dict(
            file=dict(
                add_new_row=dict(func=lambda: t().show_addrow(),
                                 shortcut='Ctrl+Shift+N'),
                refresh_menu=dict(sep=True,
                                  func=lambda: t().show_refresh(),
                                  shortcut='Ctrl+R'),
                refresh_all_open=dict(
                    func=lambda: t().refresh_allopen(default=True),
                    shortcut='Ctrl+Shift+R'),
                reload_last_query=dict(
                    func=lambda: t().refresh(last_query=True),
                    shortcut='Ctrl+Shift+L'),
                previous_tab=dict(sep=True,
                                  func=lambda: self.tabs.activate_previous(),
                                  shortcut='Meta+Tab'),
                change_minesite=dict(func=self.show_changeminesite,
                                     shortcut='Ctrl+Shift+M'),
                view_folder=dict(func=lambda: t().view_folder(),
                                 shortcut='Ctrl+Shift+V'),
                submenu_reports=dict(
                    fleet_monthly_report=lambda: self.create_monthly_report(
                        'Fleet Monthly'),
                    FC_report=lambda: self.create_monthly_report('FC'),
                    SMR_report=lambda: self.create_monthly_report('SMR'),
                    PLM_report=dict(sep=True, func=self.create_plm_report),
                    import_PLM_manual=self.import_plm_manual),
                import_downloads=dict(sep=True, func=self.import_downloads),
                preferences=dict(sep=True,
                                 func=self.show_preferences,
                                 shortcut='Ctrl+,')),
            edit=dict(
                find=dict(func=lambda: tv().show_search(), shortcut='Ctrl+F')),
            table=dict(
                email_table=lambda: t().email_table(),
                email_table_selection=lambda: t().email_table(selection=True),
                export_table_excel=lambda: t().export_df('xlsx'),
                export_table_CSV=lambda: t().export_df('csv'),
                toggle_color=dict(sep=True,
                                  func=lambda: tv().data_model.toggle_color()),
                jump_first_last_row=dict(func=lambda: tv().jump_top_bottom(),
                                         shortcut='Ctrl+Shift+J'),
                reset_column_layout=dict(
                    func=lambda: tv().reset_header_state())),
            rows=dict(open_tsi=dict(func=lambda: t().open_tsi(),
                                    label_text='Open TSI'),
                      delete_row=lambda: t().remove_row(),
                      update_component=lambda: t().show_component(),
                      details_view=dict(func=lambda: t().show_details(),
                                        shortcut='Ctrl+Shift+D')),
            database=dict(
                update_component_SMR=update_comp_smr,
                update_FC_status_clipboard=lambda: fc.
                update_scheduled_sap(exclude=dlgs.inputbox(
                    msg=
                    '1. Enter FCs to exclude\n2. Copy FC Data from SAP to clipboard\n\nExclude:',
                    title='Update Scheduled FCs SAP'),
                                     table_widget=t()),
                reset_database_connection=dict(sep=True, func=db.reset),
                reset_database_tables=db.clear_saved_tables,
                open_SAP=dict(sep=True, func=self.open_sap)),
            help=dict(about=dlgs.about,
                      check_for_update=self.check_update,
                      show_changelog=self.show_full_changelog,
                      email_error_logs=self.email_err_logs,
                      open_documentation=lambda: f.open_url(cf.config['url'][
                          'docs']),
                      submit_issue=dict(
                          func=lambda: f.open_url(cf.config['url']['issues']),
                          label_text='Submit issue or Feature Request'),
                      reset_username=dict(sep=True, func=self.set_username),
                      test_error=self.test_error))

        # reset credentials prompts
        for c in ('TSI', 'SMS', 'exchange', 'SAP'):
            menu_actions['help'][
                f'reset_{c}_credentials'] = lambda x, c=c: CredentialManager(
                    c).prompt_credentials()

        for menu, m_act in menu_actions.items():
            self.add_actions(actions=m_act, menu=menu)

        # other actions which don't go in menubar
        other_actions = dict(
            refresh_last_week=lambda: t().refresh_lastweek(base=True),
            refresh_last_month=lambda: t().refresh_lastmonth(base=True),
            update_SMR=dict(
                func=lambda: t().update_smr(),
                tooltip='Update selected event with SMR from database.'),
            show_SMR_history=lambda: t().show_smr_history())

        self.add_actions(actions=other_actions)

    def test_error(self) -> None:
        """Just raise test error"""
        raise RuntimeError('This is a test error.')

    def contextMenuEvent(self, event):
        """Add actions to right click menu, dependent on currently active table
        """
        child = self.childAt(event.pos())

        menu = QMenu(self)
        # menu.setToolTipsVisible(True)

        table_widget = self.active_table_widget()
        for section in table_widget.context_actions.values():
            for action in section:
                name_action = f'act_{action}'
                try:
                    menu.addAction(getattr(self, name_action))
                except Exception as e:
                    try:
                        menu.addAction(getattr(table_widget, name_action))
                    except Exception as e:
                        log.warning(
                            f'Couldn\'t add action to context menu: {action}')

            menu.addSeparator()

        action = menu.exec(self.mapToGlobal(event.pos()))

    def create_monthly_report(self, name: str):
        """Create report in worker thread from dialog menu

        Parameters
        ----------
        name : str
            ['Fleet Monthly', 'FC']
        """

        dlg = dlgs.BaseReportDialog(window_title=f'{name} Report')
        if not dlg.exec():
            return

        from smseventlog.reports import FCReport, FleetMonthlyReport
        from smseventlog.reports import Report as _Report
        from smseventlog.reports import SMRReport
        Report = {
            'Fleet Monthly': FleetMonthlyReport,
            'FC': FCReport,
            'SMR': SMRReport
        }[name]  # type: _Report

        rep = Report(d=dlg.d, minesite=dlg.items['MineSite'])  # type: ignore

        Worker(func=rep.create_pdf, mw=self) \
            .add_signals(signals=('result', dict(func=self.handle_monthly_report_result))) \
            .start()

        self.update_statusbar('Creating Fleet Monthly Report...')

    def handle_monthly_report_result(self, rep=None):
        if rep is None:
            return
        rep.open_()

        msg = f'Report:\n\n"{rep.title}"\n\nsuccessfully created. Email now?'
        if dlgs.msgbox(msg=msg, yesno=True):
            rep.email()

    def import_plm_manual(self):
        """Allow user to manually select haulcycle files to upload"""
        t = self.active_table_widget()
        e = t.e
        if not e is None:
            from smseventlog import eventfolders as efl
            unit, dateadded = e.Unit, e.DateAdded
            uf = efl.UnitFolder(unit=unit)
            p = uf.p_unit
        else:
            # No unit selected, try to get minesite equip path
            p = cf.p_drive / cf.config['EquipPaths'].get(
                self.minesite.replace('-', ''), '')

        if p is None:
            p = Path.home() / 'Desktop'

        lst_csv = dlgs.select_multi_files(p_start=p)
        if not lst_csv:
            return  # user didn't select anything

        from smseventlog.data.internal import utils as utl
        Worker(func=utl.combine_import_csvs, mw=self, lst_csv=lst_csv, ftype='plm') \
            .add_signals(('result', dict(func=self.handle_import_result_manual))) \
            .start()

        self.update_statusbar(
            'Importing haul cylce files from network drive (this may take a few minutes)...'
        )

    def create_plm_report(self):
        """Trigger plm report from current unit selected in table"""
        from smseventlog.data.internal import plm

        view = self.active_table()
        try:
            e = view.e
            unit, d_upper = e.Unit, e.DateAdded
        except er.NoRowSelectedError:
            # don't set dialog w unit and date, just default
            unit, d_upper, e = None, None, None

        # Report dialog will always set final unit etc
        dlg = dlgs.PLMReport(unit=unit, d_upper=d_upper)
        ok = dlg.exec()
        if not ok:
            return  # user exited

        m = dlg.get_items(lower=True)  # unit, d_upper, d_lower

        # check if unit selected matches event selected
        if not e is None:
            if not e.Unit == m['unit']:
                e = None

        m['e'] = e
        # NOTE could make a func 'rename_dict_keys'
        m['d_upper'], m['d_lower'] = m['date upper'], m['date lower']

        # check max date in db
        maxdate = plm.max_date_plm(unit=m['unit'])

        if maxdate + delta(days=5) < m['d_upper']:
            # worker will call back and make report when finished
            if not fl.drive_exists(warn=False):
                msg = 'Can\'t connect to P Drive. Create report without updating records first?'
                if dlgs.msgbox(msg=msg, yesno=True):
                    self.make_plm_report(**m)

                return

            Worker(func=plm.update_plm_single_unit, mw=self, unit=m['unit']) \
                .add_signals(
                    signals=('result', dict(
                        func=self.handle_import_result,
                        kw=m))) \
                .start()

            msg = f'Max date in db: {maxdate:%Y-%m-%d}. ' \
                + 'Importing haul cylce files from network drive, this may take a few minutes...'
            self.update_statusbar(msg=msg)

        else:
            # just make report now
            self.make_plm_report(**m)

    def handle_import_result_manual(self, rowsadded=None, **kw):
        if not rowsadded is None:
            msg = dict(msg=f'PLM records added to database: {rowsadded}',
                       success=rowsadded > 0)
        else:
            msg = 'Warning: Failed to import PLM records.'

        self.update_statusbar(msg)

    def handle_import_result(self, m_results=None, **kw):
        if m_results is None:
            return

        rowsadded = m_results['rowsadded']
        self.update_statusbar(f'PLM records added to database: {rowsadded}',
                              success=True)

        self.make_plm_report(**kw)

    def make_plm_report(self, e=None, **kw):
        """Actually make the report pdf"""
        from smseventlog import eventfolders as efl
        from smseventlog.reports import PLMUnitReport
        rep = PLMUnitReport(mw=self, **kw)

        if not e is None:
            ef = efl.EventFolder.from_model(e)
            p = ef._p_event
        else:
            ef = None

        # If cant get event folder, ask to create at desktop
        if ef is None or not ef.check(check_pics=False, warn=False):
            p = Path.home() / 'Desktop'
            msg = 'Can\'t get event folder, create report at desktop?'
            if not dlgs.msgbox(msg=msg, yesno=True):
                return

        Worker(func=rep.create_pdf, mw=self, p_base=p) \
            .add_signals(signals=('result', dict(func=self.handle_plm_result, kw=kw))) \
            .start()

        self.update_statusbar(f'Creating PLM report for unit {kw["unit"]}...')

    def handle_plm_result(self, rep=None, unit=None, **kw):
        if rep is False:
            # not super robust, but just warn if no rows in query
            msg = 'No rows returned in query, can\'t create report!'
            dlgs.msg_simple(msg=msg, icon='warning')

        if not rep or not rep.p_rep.exists():
            self.update_statusbar('Failed to create PLM report.', warn=True)
            return

        self.update_statusbar(f'PLM report created for unit {unit}',
                              success=True)

        msg = f'Report:\n\n"{rep.title}"\n\nsuccessfully created. Open now?'
        if dlgs.msgbox(msg=msg, yesno=True):
            rep.open_()

    def email_err_logs(self):
        """Collect and email error logs to simplify for user"""
        docs = []

        def _collect_logs(p):
            return [p for p in p.glob('*log*')] if p.exists() else []

        # collect sms logs
        p_sms = cf.p_applocal / 'logging'
        docs.extend(_collect_logs(p_sms))

        # collect pyupdater logs
        i = 1 if cf.is_win else 0
        p_pyu = cf.p_applocal.parents[1] / 'Digital Sapphire/PyUpdater/logs'
        docs.extend(_collect_logs(p_pyu))

        from smseventlog.utils import email as em

        subject = f'Error Logs - {self.username}'
        body = 'Thanks Jayme,<br><br>I know you\'re trying your best. \
            The Event Log is amazing and we appreciate all your hard work!'

        msg = em.Message(subject=subject,
                         body=body,
                         to_recip=['*****@*****.**'],
                         show_=False)
        msg.add_attachments(docs)
        msg.show()

    def import_downloads(self) -> None:
        """Select and import dls files to p-drive"""
        if not fl.drive_exists():
            return

        from smseventlog.data.internal import dls

        # get dls filepath
        lst_dls = dlgs.select_multi_folders(p_start=cf.desktop)
        if lst_dls is None:
            msg = 'User failed to select downloads folders.'
            self.update_statusbar(msg=msg, warn=True)
            return

        # start uploads for each dls folder selected
        for p_dls in lst_dls:
            Worker(func=dls.import_dls, mw=self, p=p_dls) \
                .add_signals(signals=('result', dict(func=self.handle_dls_result))) \
                .start()

        self.update_statusbar(msg='Started downloads upload in worker thread.')

    def handle_dls_result(self, result: dict = None, **kw):
        if isinstance(result, dict):
            name, time_total = '', ''
            try:
                name = result.pop('name')
                time_total = f.mins_secs(result.pop('time_total'))

                # join remaining processed files/times
                msg_result = ', '.join([
                    f'{k}: ({m["num"]}, {f.mins_secs(m["time"])})'
                    for k, m in result.items()
                ])
            except:
                msg_result = ''
                log.warning('Failed to build upload string')

            msg = f'Successfully uploaded downloads folder "{name}", ({time_total}). \
            Files processed/rows imported: {msg_result}'

            msg = dict(msg=msg, success=True)

        else:
            msg = dict(msg='Failed to upload downloads.', warn=True)

        self.update_statusbar(msg=msg)

    def show_preferences(self) -> None:
        """Show preferences dialog to allow user to change global settings"""
        dlg = dlgs.Preferences(parent=self)
        dlg.exec()
예제 #24
0
class PrintJobUploadProgressMessage(Message):
    """Message displayed when a print upload is in progress."""

    MIN_CALCULATION_TIME_MILLIS = 5000
    POLL_TIME_MILLIS = 10
    MAX_REMAINING_MILLIS = 24 * 60 * 60 * 1000  # arbitrary max
    CALCULATING_TEXT = I18N_CATALOG.i18nc('@info:status',
                                          'Calculating time left...')

    def __init__(self, on_cancelled: Callable) -> None:
        """Constructor.

    Args:
      on_cancelled: Called when user cancels printer upload.
    """
        super().__init__(title=I18N_CATALOG.i18nc(
            '@info:status', 'Uploading model to printer'),
                         text=self.CALCULATING_TEXT,
                         progress=-1,
                         lifetime=0,
                         dismissable=False,
                         use_inactivity_timer=False)
        self._on_cancelled = on_cancelled
        self._elapsed_upload_time_millis = 0
        self._remaining_time_millis = self.MAX_REMAINING_MILLIS
        self.addAction('cancel', I18N_CATALOG.i18nc('@action:button',
                                                    'Cancel'), 'cancel',
                       I18N_CATALOG.i18nc('@action', 'Cancels job upload.'))
        self.actionTriggered.connect(self._on_action_triggered)
        self._stopwatch = QTimer(self)
        self._stopwatch.timeout.connect(self._tick)
        self._reset_calculation_time()

    def show(self) -> None:
        """See base class."""
        self.setProgress(0)
        super().show()
        self._stopwatch.start(self.POLL_TIME_MILLIS)
        self._reset_calculation_time()

    def hide(self, send_signal=True) -> None:
        """See base class."""
        super().hide()
        self._stopwatch.stop()
        self._reset_calculation_time()

    def update(self, bytes_sent: int, bytes_total: int) -> None:
        """Updates the progress bar.

    Args:
      bytes_sent: Number of bytes sent.
      bytes_total: Target bytes.
    """
        percentage = (bytes_sent / bytes_total) if bytes_total else 0
        self.setProgress(percentage * 100)
        if self._elapsed_upload_time_millis > self.MIN_CALCULATION_TIME_MILLIS:
            speed = bytes_sent / self._elapsed_upload_time_millis
            remaining_millis = (bytes_total -
                                bytes_sent) / speed if speed else 0
            # Only go down.
            if remaining_millis < self._remaining_time_millis:
                self._remaining_time_millis = remaining_millis
                self.setText(
                    TimeUtils.get_human_readable_countdown(
                        seconds=int(remaining_millis / 1000)))

    def _reset_calculation_time(self) -> None:
        """Resets the estimated calculation time."""
        self._elapsed_upload_time_millis = 0
        self._remaining_time_millis = self.MAX_REMAINING_MILLIS
        self.setText(self.CALCULATING_TEXT)

    def _on_action_triggered(self, message: str, action: str) -> None:
        """Called when an action from user was triggered.

    Args:
      message: Message (ignored).
      action: Action triggered.
    """
        if action == 'cancel':
            self._on_cancelled()

    def _tick(self) -> None:
        """Updates stopwatch."""
        self._elapsed_upload_time_millis += self.POLL_TIME_MILLIS