Example #1
0
class SearchUserQComboBox(QComboBox):
    def __init__(self):
        super().__init__()

        self.setEditable(True)
        self.results: List[MixcloudUser] = []
        self.selected_result: Any[MixcloudUser, None] = None
        self.search_artist_thread = SearchArtistThread()

        # Connections
        self._connect_with_delay(
            input=self.lineEdit().textEdited,
            slot=self.get_suggestions,
        )
        self.currentIndexChanged.connect(
            lambda user: self.set_selected_result(index=self.currentIndex()))
        self.search_artist_thread.new_result.connect(self.add_result)
        self.search_artist_thread.error_signal.connect(self.show_error)

    def _connect_with_delay(self,
                            input: Callable,
                            slot: Slot,
                            delay_ms: int = 750):
        """Connects a given input to a given Slot with a given delay."""
        self.timer = QTimer()
        self.timer.setInterval(delay_ms)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(slot)
        input.connect(self.timer.start)

    @Slot()
    def get_suggestions(self) -> None:
        phrase = self.currentText()

        if phrase:
            self.clear()
            self.results.clear()

            self.search_artist_thread.phrase = phrase
            self.search_artist_thread.start()

    @Slot()
    def show_error(self, msg: str):
        ErrorDialog(self.parent(), message=msg)

    @Slot()
    def add_result(self, item: MixcloudUser):
        self.results.append(item)

        if len(self.results) == 1:
            self.set_selected_result(index=0)

        self.addItem(f'{item.name} ({item.username})')

    @Slot(MixcloudUser)
    def set_selected_result(self, index: int):
        self.selected_result = self.results[index]
Example #2
0
def queue_text_change(ui, text: str) -> None:
    global text_timer

    if text_timer:
        text_timer.stop()

    text_timer = QTimer()
    text_timer.setSingleShot(True)
    text_timer.timeout.connect(partial(update_button_text, ui, text))
    text_timer.start(500)
Example #3
0
class RefreshOrLogoutThread(QThread):
    def __init__(self,
                 identity_service,
                 token,
                 is_refresh,
                 timeout,
                 parent=None):
        QThread.__init__(self, parent)
        self.__token = token
        self.__is_refresh = is_refresh
        self.__timeout = timeout
        self.__identity_service = identity_service

    def run(self):
        print("Start thread")
        self.__refresh_timer = QTimer()
        callback = self.__refresh_callback if self.__is_refresh else self.__logout_callback
        self.__refresh_timer.timeout.connect(callback)
        self.__refresh_timer.setSingleShot(True)
        self.__refresh_timer.start(self.__timeout)
        self.exec_()
        self.__stop_timer()
        print("End thread")

    def __refresh_callback(self):
        try:
            print("Refresh callback")
            api_url = AppConfig.instance().config['api_url']
            rf_token = self.__token['refresh_token']
            (status, resp) = fqcs_api.refresh_token(api_url, rf_token)
            if (status == True):
                self.__identity_service.save_token_json(resp)
                self.__identity_service.check_token()
            else:
                raise Exception("Resp error")
        except Exception as ex:
            print("Error refresh callback")
            self.__identity_service.log_out()
        finally:
            self.__stop_timer()
            self.quit()
        return

    def __logout_callback(self):
        try:
            self.__identity_service.log_out()
        except:
            print("Log out error")
        finally:
            self.__stop_timer()
            self.quit()
        return

    def __stop_timer(self):
        self.__refresh_timer.stop()
Example #4
0
class ImageAcquisition(QObject):
    signal = Signal(list)
    __instance = None

    @staticmethod
    def getInstance():
        """ Static access method. """
        if ImageAcquisition.__instance == None:
            ImageAcquisition()
        return ImageAcquisition.__instance

    def __init__(self,
                 parent=None,
                 camera_id=0,
                 resolution_x=640,
                 resolution_y=480,
                 time_in_ms=200):
        QObject.__init__(self, parent)

        # cv2.CAP_DSHOW to ensure compatibility with Windows and DirectShow
        if platform.system() == "Windows":
            self.cap = cv2.VideoCapture(cv2.CAP_DSHOW + camera_id)
        else:
            self.cap = cv2.VideoCapture(camera_id)

        self.img_timer = QTimer(self)
        self.img_timer.setSingleShot(False)
        self.img_timer.timeout.connect(self.read_frame)
        self.img_timer.start(time_in_ms)
        self._x = resolution_x
        self._y = resolution_y
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self._x)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self._y)

        if ImageAcquisition.__instance is not None:
            raise Exception("This class is a singleton!")
        else:
            ImageAcquisition.__instance = self

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

    def read_frame(self):
        ret, frame = self.cap.read()
        self.signal.emit(frame)
        return frame

    def __del__(self):
        self.cap.release()
        print('ImageAcquisition closed')
Example #5
0
    def queue_command(self, name, command):
        timer = self.timers.get(name, None)
        if timer is None:
            timer = QTimer()
            timer.setInterval(200)
            timer.setSingleShot(True)
            timer.timeout.connect(self.timeout)
            timer.start()

        setattr(timer, 'command', command)
        self.timers[name] = timer
Example #6
0
 def set_intervals(self, intervals):
     self.timers.clear()
     self.intervals = intervals
     for val in intervals:
         timer = QTimer(self)
         timer.setSingleShot(True)
         timer.setInterval(val * 1000)
         timer.setTimerType(Qt.PreciseTimer)
         timer.timeout.connect(self.target)
         if (val == intervals[-1]): timer.timeout.connect(self.callback)
         self.timers.append(timer)
Example #7
0
def event_loop(msec):
    """Event loop to show the GUI during a unit test. 
    
    https://www.qtcentre.org/threads/23541-Displaying-GUI-events-with-QtTest
    """
    loop = QEventLoop()
    timer = QTimer()
    timer.timeout.connect(loop.quit)
    timer.setSingleShot(True)
    timer.start(msec)
    loop.exec_()
Example #8
0
class MyGlWidget(QGLWidget):
    "PySideApp uses Qt library to create an opengl context, listen to keyboard events, and clean up"

    def __init__(self, renderer, glformat, app):
        "Creates an OpenGL context and a window, and acquires OpenGL resources"
        super(MyGlWidget, self).__init__(glformat)
        self.renderer = renderer
        self.app = app
        # Use a timer to rerender as fast as possible
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.setInterval(0)
        self.timer.timeout.connect(self.render_vr)
        # Accept keyboard events
        self.setFocusPolicy(Qt.StrongFocus)

    def __enter__(self):
        "setup for RAII using 'with' keyword"
        return self

    def __exit__(self, type_arg, value, traceback):
        "cleanup for RAII using 'with' keyword"
        self.dispose_gl()

    def initializeGL(self):
        if self.renderer is not None:
            self.renderer.init_gl()
        self.timer.start()

    def paintGL(self):
        "render scene one time"
        self.renderer.render_scene()
        self.swapBuffers() # Seems OK even in single-buffer mode
        
    def render_vr(self):
        self.makeCurrent()
        self.paintGL()
        self.doneCurrent()
        self.timer.start() # render again real soon now

    def disposeGL(self):
        if self.renderer is not None:
            self.makeCurrent()
            self.renderer.dispose_gl()
            self.doneCurrent()

    def keyPressEvent(self, event):
        "press ESCAPE to quit the application"
        key = event.key()
        if key == Qt.Key_Escape:
            self.app.quit()
Example #9
0
 def _set_up_broker_fields(
     led: Led,
     edit: QLineEdit,
     timer: QTimer,
     timer_callback: Callable,
     kafka_obj_type: Type[KafkaInterface],
 ):
     led.turn_off()
     validator = BrokerAndTopicValidator()
     edit.setValidator(validator)
     validator.is_valid.connect(partial(validate_line_edit, edit))
     edit.textChanged.connect(lambda: timer.start(1000))
     timer.setSingleShot(True)
     timer.timeout.connect(partial(timer_callback, kafka_obj_type))
Example #10
0
class TestThread(QObject):

    data = Signal(dict)

    def __init__(self,timer,name,ptnum,amplIncr,rampPts,parent=None):

        QObject.__init__(self,parent)

        self.pointNum = ptnum
        self.rampPoints = rampPts
        self.counterIncr = 1
        self.amplIncr = amplIncr
        self.name = name
        self.amplCounter = 1
        self.timer = timer
        self.internalTimer = None


    @Slot()
    def startTimer(self):

        self.internalTimer = QTimer(self)
        self.internalTimer.timeout.connect(self.calcData)
        self.internalTimer.setSingleShot(False)
        self.internalTimer.start(self.timer)


    def calcData(self):

        dataX = np.arange(0,self.pointNum)*2*np.pi/self.pointNum
        dataY = np.sin(dataX)*(self.amplIncr*self.amplCounter)
        toSend = {"name":self.name,"x":dataX,"y":dataY}

        self.amplCounter += self.counterIncr
        if self.amplCounter == self.rampPoints or self.amplCounter == -1*self.rampPoints:
            self.counterIncr *= -1
            self.amplCounter += self.counterIncr

        self.data.emit(toSend)


    @Slot()
    def stopTimer(self):

        if self.internalTimer is not None:
            self.internalTimer.stop()
            while self.internalTimer.isActive():
                sleep(0.05)
            self.internalTimer = None
Example #11
0
    def test_controller_and_worker_better(self):
        app = QCoreApplication.instance() or QCoreApplication(sys.argv)
        controller = Controller()
        controller.worker.finished.connect(QCoreApplication.quit,
                                           type=Qt.QueuedConnection)

        timeout_timer = QTimer(parent=controller)
        timeout_timer.setInterval(3000)
        timeout_timer.setSingleShot(True)
        timeout_timer.timeout.connect(lambda: QCoreApplication.exit(-1))
        timeout_timer.start()

        with patch.object(controller, "on_worker_result") as on_result:
            controller.start()
            self.assertEqual(0, app.exec_())
            self.assertEqual(20, len(on_result.mock_calls))
Example #12
0
class EntropyWidget(QWidget):
	def __init__(self, parent, view, data):
		super(EntropyWidget, self).__init__(parent)
		self.view = view
		self.data = data
		self.raw_data = data.file.raw

		self.block_size = (len(self.raw_data) / 4096) + 1
		if self.block_size < 1024:
			self.block_size = 1024
		self.width = int(len(self.raw_data) / self.block_size)
		self.image = QImage(self.width, 1, QImage.Format_ARGB32)
		self.image.fill(QColor(0, 0, 0, 0))

		self.thread = EntropyThread(self.raw_data, self.image, self.block_size)
		self.started = False

		self.timer = QTimer()
		self.timer.timeout.connect(self.timerEvent)
		self.timer.setInterval(100)
		self.timer.setSingleShot(False)
		self.timer.start()

		self.setMinimumHeight(UIContext.getScaledWindowSize(32, 32).height())

	def paintEvent(self, event):
		p = QPainter(self)
		p.drawImage(self.rect(), self.image)
		p.drawRect(self.rect())

	def sizeHint(self):
		return QSize(640, 32)

	def timerEvent(self):
		if not self.started:
			self.thread.start()
			self.started = True
		if self.thread.updated:
			self.thread.updated = False
			self.update()

	def mousePressEvent(self, event):
		if event.button() != Qt.LeftButton:
			return
		frac = float(event.x()) / self.rect().width()
		offset = int(frac * self.width * self.block_size)
		self.view.navigateToFileOffset(offset)
Example #13
0
 def open(self, url, timeout=60):
     """Wait for download to complete and return result"""
     loop = QEventLoop()
     timer = QTimer()
     timer.setSingleShot(True)
     timer.timeout.connect(loop.quit)
     self.loadFinished.connect(loop.quit)
     self.load(QUrl(url))
     timer.start(timeout * 1000)
     loop.exec_()  # delay here until download finished
     if timer.isActive():
         # downloaded successfully
         timer.stop()
         return self.html()
     else:
         # timed out
         print('Request timed out:', url)
Example #14
0
 def open(self, url: str, timeout: int = 10):
     """Wait for download to complete and return result"""
     loop = QEventLoop()
     timer = QTimer()
     timer.setSingleShot(True)
     # noinspection PyUnresolvedReferences
     timer.timeout.connect(loop.quit)
     # noinspection PyUnresolvedReferences
     self.loadFinished.connect(loop.quit)
     self.load(QUrl(url))
     # noinspection PyArgumentList
     timer.start(timeout * 1000)
     loop.exec_()  # delay here until download finished
     if timer.isActive():
         # downloaded successfully
         timer.stop()
     else:
         logger.info('Request timed out: %s' % url)
Example #15
0
def wait_signal(signal, timeout=5000):
    """Block loop until signal emitted, or timeout (ms) elapses."""
    loop = QEventLoop()
    signal.connect(loop.quit)

    yield

    if timeout:
        timer = QTimer()
        timer.setInterval(timeout)
        timer.setSingleShot(True)
        timer.timeout.connect(loop.quit)
        timer.start()
    else:
        timer = None
    loop.exec_()
    signal.disconnect(loop.quit)
    if timer and timer.isActive():
        timer.stop()
Example #16
0
class TestScatter(QObject):

    data = Signal(dict)

    def __init__(self,timer,name,xInc,parent=None):

        QObject.__init__(self,parent)

        self.xInc = xInc

        self.currX = 0
        self.name = name
        self.timer = timer
        self.internalTimer = None


    @Slot()
    def startTimer(self):

        self.internalTimer = QTimer(self)
        self.internalTimer.timeout.connect(self.calcData)
        self.internalTimer.setSingleShot(False)
        self.internalTimer.start(self.timer)


    def calcData(self):

        dataX = self.currX
        dataY = np.sin(dataX)
        self.currX+=self.xInc
        toSend = {self.name:[dataX,dataY]}

        self.data.emit(toSend)


    @Slot()
    def stopTimer(self):

        if self.internalTimer is not None:
            self.internalTimer.stop()
            while self.internalTimer.isActive():
                sleep(0.05)
            self.internalTimer = None
Example #17
0
class IdleDetection(QObject):
    def __init__(self, parent):
        super(IdleDetection, self).__init__(parent)
        self._parent: QWidget = parent

        # Report user inactivity
        self._idle_timer = QTimer()
        self._idle_timer.setSingleShot(True)
        self._idle_timer.setTimerType(Qt.VeryCoarseTimer)
        self._idle_timer.setInterval(10000)

        # Detect inactivity for automatic session save
        self._idle_timer.timeout.connect(self.set_inactive)
        self.idle = False

        self.parent.installEventFilter(self)

    def is_active(self):
        return self.idle

    def set_active(self):
        self.idle = False
        self._idle_timer.stop()

    def set_inactive(self):
        self.idle = True

    def eventFilter(self, obj, eve):
        if eve is None or obj is None:
            return False

        if eve.type() == QEvent.KeyPress or \
           eve.type() == QEvent.MouseMove or \
           eve.type() == QEvent.MouseButtonPress:
            self.set_active()
            return False

        if not self._idle_timer.isActive():
            self._idle_timer.start()

        return False
    def wait():
        # loop = QEventLoop()
        # QTimer.singleShot(delay * 1000, loop.quit)
        # loop.exec_()

        loop = QEventLoop()
        timer = QTimer()
        timer.setSingleShot(True)
        timer.timeout.connect(loop.quit)

        if append:
            TIMER_RUNNING.append(timer)
            LOOP_RUNNING.append(loop)

            timer.start(delay * 1000)
            loop.exec_()

            TIMER_RUNNING.remove(timer)
            LOOP_RUNNING.remove(loop)
        else:
            timer.start(delay * 1000)
            loop.exec_()
Example #19
0
class ViewerApp(QApplication):
    idle_event = Signal()

    def __init__(self, version: str):
        super(ViewerApp, self).__init__(sys.argv)
        self.setApplicationName(f'{APP_NAME}')
        self.setApplicationVersion(version)
        self.setApplicationDisplayName(f'{APP_NAME} v{version}')
        load_style(self)

        self.idle_timer = QTimer()
        self.idle_timer.setSingleShot(True)
        self.idle_timer.setTimerType(Qt.VeryCoarseTimer)
        self.idle_timer.setInterval(3 * 60 * 1000)  # 3 min until idle
        self.idle_timer.timeout.connect(self.set_idle)
        self.installEventFilter(self)

        self.window = ViewerWindow(self)
        self.window.show()

    def eventFilter(self, obj, eve):
        if eve is None or obj is None:
            return False

        if eve.type() == QEvent.KeyPress or \
           eve.type() == QEvent.MouseMove or \
           eve.type() == QEvent.MouseButtonPress:
            self.set_active()
            return False

        return False

    def set_active(self):
        self.idle_timer.start()

    def set_idle(self):
        LOGGER.debug('Application is idle.')
        self.idle_event.emit()
Example #20
0
class QW_AnimCntrl(QWidget):
    updated_cadmshfem = Signal()

    def __init__(self, fem):
        super(QW_AnimCntrl, self).__init__()

        self.fem = fem

        self.tmr_stepTime = QTimer(self)
        self.tmr_stepTime.setSingleShot(False)
        self.tmr_stepTime.timeout.connect(self.stepTime)

        self.btn_Initialize = QPushButton("initialize")

        self.btn_Animate = QPushButton("animate")
        self.btn_Animate.setCheckable(True)
        self.btn_Animate.toggled.connect(self.btn_Animate_toggled)

        self.btn_Initialize.pressed.connect(self.btn_Initialize_pressed)

        self.hl = QHBoxLayout()
        self.hl.addWidget(self.btn_Initialize)
        self.hl.addWidget(self.btn_Animate)
        self.setLayout(self.hl)

    def btn_Animate_toggled(self):
        if self.btn_Animate.isChecked():
            self.tmr_stepTime.start(30)
        else:
            self.tmr_stepTime.stop()

    def btn_Initialize_pressed(self):
        self.fem.initialize()
        self.updated_cadmshfem.emit()

    def stepTime(self):
        self.fem.step_time()
        self.updated_cadmshfem.emit()
Example #21
0
class ImageRecorder(QObject):
    def __init__(self, get_frame_func, subfolder, parent=None):
        QObject.__init__(self, parent)
        self.get_frame_func = get_frame_func
        self.period_timer = QTimer(self)
        self.duration_timer = QTimer(self)
        self.subfolder = subfolder
        # create subfolder if not present
        if os.path.isdir(subfolder) == False:
            os.mkdir(subfolder)

    def start(self, time_duration_in_s, time_period_in_s):
        self.time_duration_in_s = time_duration_in_s
        self.time_period_in_s = time_period_in_s
        self.period_timer.setSingleShot(False)
        self.period_timer.timeout.connect(self.read_frame)
        self.period_timer.start(self.time_period_in_s * 1000)

        self.duration_timer.setSingleShot(True)
        self.duration_timer.timeout.connect(self.period_timer.stop)
        self.duration_timer.start(self.time_duration_in_s * 1000)

    def stop(self):
        self.period_timer.stop()
        self.duration_timer.stop()
        # disconnect all
        QObject.disconnect(self.period_timer)
        QObject.disconnect(self.duration_timer)

    @property
    def timeout(self):
        return self.duration_timer.timeout

    # cyclic started reading of frames
    def read_frame(self):
        unique_filename = datetime.now().strftime("%d-%m-%Y_%H-%M-%S")
        frame = self.get_frame_func()
        cv2.imwrite(self.subfolder + '/' + unique_filename + '.jpg', frame)
Example #22
0
    def __init__(self, settings):
        super().__init__()

        # Dialog UI
        self.ui = SearchUI.Ui_SearchUI()
        self.ui.setupUi(self)

        # Dialog Variables
        self.settings = settings
        self.results = []

        # Connect actions

        # Search Bar
        timer = QTimer()
        timer.setSingleShot(True)
        timer.setInterval(300)
        timer.timeout.connect(self.actSearch)
        self.ui.txtSearchBar.textChanged.connect(lambda: timer.start())

        # List
        self.ui.listResults.currentItemChanged.connect(self.actListSelect)
        self.ui.listResults.itemActivated.connect(self.actListClick)
Example #23
0
    def beam_vector_changed(self):
        if self.mode == ViewType.polar:
            # Polar needs a complete re-draw
            # Only emit this once every 100 milliseconds or so to avoid
            # too many updates if the slider widget is being used.
            if not hasattr(self, '_beam_vec_update_polar_timer'):
                timer = QTimer()
                timer.setSingleShot(True)
                timer.timeout.connect(HexrdConfig().rerender_needed.emit)
                self._beam_vec_update_polar_timer = timer
            HexrdConfig().flag_overlay_updates_for_all_materials()
            self._beam_vec_update_polar_timer.start(100)
            return

        # If it isn't polar, only overlay updates are needed
        if not self.iviewer or not hasattr(self.iviewer, 'instr'):
            return

        # Re-draw all overlays from scratch
        HexrdConfig().clear_overlay_data()

        bvec = HexrdConfig().instrument_config['beam']['vector']
        self.iviewer.instr.beam_vector = (bvec['azimuth'], bvec['polar_angle'])
        self.update_overlays()
Example #24
0
class Win(QMainWindow):
    def __init__(self, id, time):
        super(Win, self).__init__()
        self.regions = []
        self.running = False
        self.layout_id = id
        self.layout_time = time
        self.layout_timer = QTimer()
        self.layout_timer.setSingleShot(True)
        self.layout_timer.timeout.connect(self.stop)
        self.widget = QWidget()
        self.setCentralWidget(self.widget)
        #---- kong ----
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        #self.setAttribute(Qt.WA_TranslucentBackground)
        #----

    def __enter__(self):
        self.play(self.layout_id)
        self.layout_timer.setInterval(self.layout_time * 1000)
        self.layout_timer.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_tb or exc_type or exc_val:
            pass

    def play(self, layout_id):
        path = f'content/{layout_id}.xml'
        layout = yLayout.get_layout(path)

        if not layout:
            print('---- yWin: layout error ----')
            return False

        color = layout['bgcolor']
        self.setStyleSheet(f'background-color: {color}')

        #---- kong ----
        for region in layout['regions']:
            region['layout_id'] = layout_id
            r = yRegion.get_region(region, self.widget)
            self.regions.append(r)

        if self.regions:
            for r in self.regions:
                r.play_end_signal.connect(self.replay)
                r.play()
        #----
        return True

    @Slot()
    def replay(self):
        print('---- yWin: replay ----')
        if self.regions:
            for r in self.regions:
                r.play()

    def stop(self):
        if self.regions:
            for r in self.regions:
                r.stop()

        self.regions = []  #---- del self.regions[:]
        self.widget = None
        #self.widget = QWidget()
        #self.setCentralWidget(self.widget)
        print(f'---- yWin: stop to close ----')
        #---- kong ----
        self.close()
Example #25
0
        print(f'---- yWin: stop to close ----')
        #---- kong ----
        self.close()
        #----


#================================
#
#
if __name__ == '__main__':

    app = QApplication(sys.argv)
    win = app.desktop().screenGeometry()

    signal.signal(signal.SIGINT, lambda s, f: app.quit())

    r = -1
    with Win('2', 10) as w:
        t = QTimer()
        t.setSingleShot(True)
        t.timeout.connect(w.showFullScreen)
        t.start(1000)
        w.setGeometry(win)
        w.show()
        r = app.exec_()

    sys.exit(r)

#
#
#
Example #26
0
class TafelsMainWindow(QMainWindow, Ui_MainWindow):
    test_timed_out: bool
    card_stats: CardStats
    cards_todo: List[Card]
    state: GameState
    test_timer: QTimer
    question_start_time: float
    test_answers: Dict[Card, int]

    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.state = GameState.SETUP
        self.hook_events()
        self.enable_controls()
        self.question.setAlignment(Qt.AlignRight)
        self.sound_ok = QSound(":/sound/sound/ok.wav")
        self.sound_error = QSound(":/sound/sound/error.wav")
        self.test_timed_out = False
        self.card_stats = CardStatsLoader.load(self.get_stats_file())
        self.apply_selections(SelectionsLoader.load(
            self.get_selections_file()))
        print(self.card_stats)

    def hook_events(self):
        for pb in self.numpad_controls():
            pb.clicked.connect(self.numpad_click)
        self.pb_clear.clicked.connect(self.clear_answer)
        self.pb_submit.clicked.connect(self.check_answer)
        self.pb_stop.clicked.connect(self.stop_all)
        self.pb_test.clicked.connect(self.start_test)
        self.pb_practice.clicked.connect(self.start_practice)
        self.answer.returnPressed.connect(self.check_answer)

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def numpad_controls(self) -> Iterable[QPushButton]:
        return [
            self.pushButton_1, self.pushButton_2, self.pushButton_3,
            self.pushButton_4, self.pushButton_5, self.pushButton_6,
            self.pushButton_7, self.pushButton_8, self.pushButton_9,
            self.pushButton_0
        ]

    def enable_controls(self):
        for pb in self.numpad_controls():
            pb.setEnabled(self.is_running())
        self.pb_clear.setEnabled(self.is_running())
        self.pb_submit.setEnabled(self.is_running())
        self.answer.setEnabled(self.is_running())
        self.pb_stop.setEnabled(self.is_running())
        self.pb_practice.setEnabled(self.state == GameState.SETUP)
        self.pb_test.setEnabled(self.state == GameState.SETUP)
        self.lst_selection.setEnabled(self.state == GameState.SETUP)

    def is_running(self):
        return self.state == GameState.TESTING or self.state == GameState.PRACTICE

    def get_selection(self) -> Iterable[int]:
        selection = []
        for i in range(0, self.lst_selection.count()):
            item: QListWidgetItem = self.lst_selection.item(i)
            if item.checkState() == Qt.Checked:
                selection.append(int(item.text()))
        return selection

    def apply_selections(self, selection: Iterable[int]):
        for i in range(0, self.lst_selection.count()):
            item: QListWidgetItem = self.lst_selection.item(i)
            if int(item.text()) in selection:
                item.setCheckState(Qt.Checked)
            else:
                item.setCheckState(Qt.Unchecked)

    @Slot()
    def clear_answer(self):
        self.answer.setText("")

    @Slot()
    def numpad_click(self):
        sender = self.sender()
        self.answer.setText(self.answer.text() + sender.text())

    @Slot()
    def start_test(self):
        SelectionsLoader.store(self.get_selections_file(),
                               self.get_selection())
        self.state = GameState.TESTING
        self.test_timed_out = False
        self.enable_controls()
        self.cards_todo = list(
            self.card_stats.select_for_test(TEST_SIZE, self.get_selection()))
        shuffle(self.cards_todo)
        self.show_question_or_feedback()
        self.feedback.setText("")
        self.test_answers = {}

        self.test_timer = QTimer(self)
        self.test_timer.timeout.connect(self.test_timeout)
        self.test_timer.setInterval(TEST_DURATION_MSEC)
        self.test_timer.setSingleShot(True)
        self.test_timer.start()
        self.progressBar.setValue(0)
        self.progressBar.setMaximum(len(self.cards_todo))

    @Slot()
    def start_practice(self):
        print("starting practice")
        SelectionsLoader.store(self.get_selections_file(),
                               self.get_selection())
        self.state = GameState.PRACTICE
        self.test_timed_out = False
        self.enable_controls()
        self.cards_todo = list(Card.generate(self.get_selection()))
        shuffle(self.cards_todo)
        self.show_question_or_feedback()
        self.feedback.setText("")
        self.progressBar.setValue(0)
        self.progressBar.setMaximum(len(self.cards_todo))

    @Slot()
    def stop_all(self):
        print("stopping")
        self.state = GameState.SETUP
        self.enable_controls()
        self.progressBar.setValue(0)
        if self.state == GameState.TESTING:
            self.test_timer.stop()
            del self.test_timer
            self.test_timed_out = False

    @Slot()
    def test_timeout(self):
        self.test_timed_out = True

    def show_test_results(self):
        print(self.test_answers)
        msgBox = QMessageBox()
        msgBox.setTextFormat(Qt.RichText)
        msgBox.setText(self.generate_report())
        msgBox.exec()

    def current_card(self):
        return self.cards_todo[-1]

    @Slot()
    def check_answer(self):
        try:
            answer = int(self.answer.text())
        except ValueError:
            self.clear_answer()
            return
        stop_time = time()
        if answer == self.current_card().answer():
            self.correct_answer(stop_time)
        else:
            self.wrong_answer()

    def correct_answer(self, stop_time):
        time_delta = stop_time - self.question_start_time
        print(" %s took %f" % (self.current_card(), time_delta))
        self.card_stats.add_correct_answer(self.current_card(), time_delta)
        self.save_stats()
        if self.state == GameState.PRACTICE:
            self.sound_ok.play()
            self.next_card()
        elif self.state == GameState.TESTING:
            self.test_answers[self.current_card()] = int(self.answer.text())
            self.next_card()

    def next_card(self):
        self.cards_todo.pop()
        self.progressBar.setValue(1 + self.progressBar.maximum() -
                                  len(self.cards_todo))
        self.show_question_or_feedback()

    def wrong_answer(self):
        self.card_stats.add_error(self.current_card())
        print(" %s wrong answer %s" %
              (str(self.current_card()), self.answer.text()))
        self.save_stats()
        if self.state == GameState.PRACTICE:
            self.sound_error.play()
            self.style_feedback()
            self.feedback.setText(" " + self.answer.text() + " ")
            self.answer.setText("")
        elif self.state == GameState.TESTING:
            self.test_answers[self.current_card()] = int(self.answer.text())
            self.next_card()

    def style_feedback(self, color=Qt.red, strikeout=True):
        font = self.question.font()
        font.setStrikeOut(strikeout)
        self.feedback.setFont(font)
        palette = self.feedback.palette()
        palette.setColor(self.feedback.foregroundRole(), color)
        self.feedback.setPalette(palette)

    def show_question_or_feedback(self):
        if len(self.cards_todo) == 0 or self.test_timed_out:
            if self.state == GameState.PRACTICE:
                self.style_feedback(Qt.green, False)
                self.feedback.setText("Klaar!")
            else:
                self.show_test_results()
            self.question.setText("")
            self.answer.setText("")
            self.stop_all()
        else:
            self.question.setText(str(self.current_card()) + " =")
            self.answer.setText("")
            self.answer.setFocus()
            self.feedback.setText("")
            self.question_start_time = time()

    def generate_report(self) -> str:
        correct_answers = 0
        for (card, my_answer) in self.test_answers.items():
            correct_answer = card.answer()
            if my_answer == correct_answer:
                correct_answers += 1
        report = "<h1>"
        report += "Resultaat toets = %d / %d" % (correct_answers, TEST_SIZE)
        report += "<img src='%s'></img>" % self.get_report_icon(
            correct_answers / TEST_SIZE)
        report += " </h1>\n<br>"
        for (card, my_answer) in self.test_answers.items():
            correct_answer = card.answer()
            if my_answer == correct_answer:
                report += "<font size='6' color='green'>%s = %d</font><br>\n" % (
                    str(card), my_answer)
            else:
                report += "<font size='6' color='red'>%s&nbsp;=&nbsp;<s>%s</s>&nbsp;</font>" \
                          "<font size='6'>%d</font><br>\n" % (str(card), str(my_answer), correct_answer)
        return report

    @staticmethod
    def get_report_icon(score: float) -> str:
        if score == 1.0:
            return ":/icons/icons/emoji/1F3C6.svg"  # prize
        elif score >= 0.9:
            return ":/icons/icons/emoji/1F600.svg"  # :D
        elif score >= 0.8:
            return ":/icons/icons/emoji/1F642.svg"  # :-)
        elif score >= 0.6:
            return ":/icons/icons/emoji/1F610.svg"  # :-|
        else:
            return ":/icons/icons/emoji/1F61F.svg"  # :-(

    @staticmethod
    def get_stats_file() -> Path:
        dir = user_state_dir("tafels")
        return Path(dir, "cardstate.dat")

    @staticmethod
    def get_selections_file() -> Path:
        dir = user_state_dir("tafels")
        return Path(dir, "selections.dat")

    def save_stats(self):
        CardStatsLoader.store(self.get_stats_file(), self.card_stats)
Example #27
0
def exception_setup(python, thread, where, activeTime_s):
    logging.getLogger(__name__).info("------------------------------------------------------")
    logging.getLogger(__name__).info("Starting exception_setup %d %s %s %f", python, thread, where, activeTime_s)
    from nexxT.services.ConsoleLogger import ConsoleLogger
    logger = ConsoleLogger()
    Services.addService("Logging", logger)
    class LogCollector(logging.StreamHandler):
        def __init__(self):
            super().__init__()
            self.logs = []
        def emit(self, record):
            self.logs.append(record)
    # avoid warning flood about service profiling not found
    Services.addService("Profiling", None)
    collector = LogCollector()
    logging.getLogger().addHandler(collector)
    try:
        t = QTimer()
        t.setSingleShot(True)
        # timeout if test case hangs
        t2 = QTimer()
        t2.start((activeTime_s + 3)*1000)
        try:
            test_json = Path(__file__).parent / "test_except_constr.json"
            with test_json.open("r", encoding='utf-8') as fp:
                cfg = json.load(fp)
            if nexxT.useCImpl and not python:
                cfg["composite_filters"][0]["nodes"][2]["library"] = "binary://../binary/${NEXXT_PLATFORM}/${NEXXT_VARIANT}/test_plugins"
            cfg["composite_filters"][0]["nodes"][2]["thread"] = thread
            cfg["composite_filters"][0]["nodes"][2]["properties"]["whereToThrow"] = where
            mod_json = Path(__file__).parent / "test_except_constr_tmp.json"
            with mod_json.open("w", encoding="utf-8") as fp:
                json.dump(cfg, fp)

            config = Configuration()
            ConfigFileLoader.load(config, mod_json)
            config.activate("testApp")
            app.processEvents()

            aa = Application.activeApplication

            init = True
            def timeout():
                nonlocal init
                if init:
                    init = False
                    aa.stop()
                    aa.close()
                    aa.deinit()
                else:
                    app.exit(0)

            def timeout2():
                print("Application timeout hit!")
                nonlocal init
                if init:
                    init = False
                    aa.stop()
                    aa.close()
                    aa.deinit()
                else:
                    print("application exit!")
                    app.exit(1)
            t2.timeout.connect(timeout2)
            t.timeout.connect(timeout)
            def state_changed(state):
                if state == FilterState.ACTIVE:
                    t.setSingleShot(True)
                    t.start(activeTime_s*1000)
                elif not init and state == FilterState.CONSTRUCTED:
                    t.start(1000)
            aa.stateChanged.connect(state_changed)

            aa.init()
            aa.open()
            aa.start()

            app.exec_()
        finally:
            del t
            del t2
    finally:
        logging.getLogger().removeHandler(collector)
        Services.removeAll()
    return collector.logs
Example #28
0
class Plotter(FigureCanvasQTAgg):
    def __init__(self, xtitle=None, ytitle=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        self.axes.autoscale(enable=True)
        super().__init__(fig)
        self._createDialog()

        self._needsUpdating = False

        self.updateTimer = QTimer()
        self.updateTimer.setSingleShot(False)
        self.updateTimer.setInterval(UPDATE_INTERVAL)
        self.updateTimer.timeout.connect(self._updateFigure)
        self.updateThread = QThread()
        self.updateThread.started.connect(self._refreshFigure)

        self.xdata = list(range(MAXIMUM_POINTS))
        self.ydata = [0] * MAXIMUM_POINTS

        #The following lines may not be correct
        self.xtitle = xtitle
        self.ytitle = ytitle
        self.xtitle("SAMPLE X")

        # self._plot_ref = None
        self._showDialog()
        self.updateTimer.start()

    def _updateFigure(self):
        if self._needsUpdating:
            self.updateThread.start()

    def _refreshFigure(self):
        # if self._plot_ref is None:
        #     self._plot_ref, = self.axes.plot(self.xdata, self.ydata, 'r', marker='o', markersize=12)
        # else:
        #     self._plot_ref.set_xdata(self.xdata)
        #     self._plot_ref.set_ydata(self.ydata)
        self.axes.cla()
        self.axes.plot(self.xdata, self.ydata, 'r', marker='o', markersize=12)

        if self.xtitle is not None:
            self.axes.xtitle(self.xtitle)

        if self.ytitle is not None:
            self.axes.ytitle(self.ytitle)

        self.draw()
        self._needsUpdating = False

    def _createDialog(self):
        self.plotDialog = QDialog()
        self.plotDialog.setWindowTitle("Processing Plot")
        dialogButtons = QDialogButtonBox(QDialogButtonBox.Close)
        dialogButtons.clicked.connect(self._closeDialog)
        layout = QVBoxLayout()
        self.toolbar = NavigationToolbar(self, self.plotDialog)
        layout.addWidget(self.toolbar)
        layout.addWidget(self)
        layout.addWidget(dialogButtons)
        self.plotDialog.setLayout(layout)

    def _closeDialog(self):
        self.plotDialog.close()

    def _showDialog(self):
        self.plotDialog.show()

    def addNewData(self, x, y):
        #Check data validity
        x = np.array(x).flatten()
        y = np.array(y).flatten()
        if x.size != y.size:
            print(
                "Problem with adding new values to the plot, x and y should have the same length"
            )
            return
        #TODO: Add other validation conditions

        #Add data to class
        length = x.shape[0]
        self.xdata = self.xdata[-(MAXIMUM_POINTS - length):] + x.tolist()
        self.ydata = self.ydata[-(MAXIMUM_POINTS - length):] + y.tolist()

        #flag it for updates
        self._needsUpdating = True

    def __del__(self):
        self.plotDialog.close()
        while not self.updateThread.isFinished():
            pass
Example #29
0
class WizardSession:
    settings_dir = CreateZip.settings_dir
    last_session_file = Path(settings_dir, 'last_preset_session.rksession')
    automagic_filter = set()

    def __init__(self, wizard):
        """ Saves and loads all data to the wizard

        :param modules.gui.wizard.wizard.PresetWizard wizard:
        """
        self.wizard = wizard
        self.data = SessionData()

        self.update_options_timer = QTimer()
        self.update_options_timer.setInterval(15)
        self.update_options_timer.setSingleShot(True)
        self.update_options_timer.timeout.connect(self._update_available_options)

        # -- Preset Pages KnechtModels for available PR and Package options --
        self.opt_models = dict()  # ModelCode: KnechtModel
        self.pkg_models = dict()  # ModelCode: KnechtModel

        self._load_default_filter()

    def _load_default_filter(self):
        """ Read Package default filter from qt resources """
        f = QFile(Resource.icon_paths.get('pr_data'))

        try:
            f.open(QIODevice.ReadOnly)
            data: QByteArray = f.readAll()
            data: bytes = data.data()
            Settings.load_json_from_bytes(PrJsonData, data)
        except Exception as e:
            LOGGER.error(e)
        finally:
            f.close()

        self.data.pkg_filter = PrJsonData.package_filter[::]
        self.automagic_filter = set(PrJsonData.wizard_automagic_filter)

        # Update Start Page Package Widget
        if hasattr(self.wizard, 'page_welcome'):
            self.wizard.page_welcome.reload_pkg_filter()

    def _clean_up_import_data(self):
        new_models = list()
        for trim in self.data.import_data.models:
            if trim.model in self.data.import_data.selected_models:
                new_models.append(trim)

        self.data.import_data.models = new_models

    def _load_default_attributes(self):
        """ Make sure that all attributes are present in SessionData after pickle load.
            Previous version may had less attributes.
        """
        default_session = SessionData()

        for k in dir(default_session):
            v = getattr(default_session, k)
            if k.startswith('__') or not isinstance(v, (int, str, float, bool, list, dict, tuple, set)):
                continue

            # Set missing attributes
            if not hasattr(self.data, k):
                LOGGER.debug('Setting default session attribute: %s: %s', k, v)
                setattr(self.data, k, v)

        # Make sure newly added attributes exists in older sessions
        self.data.import_data.options_text_filter = True

    def reset_session(self):
        self.data = SessionData()

        # Reset Preset Page content - available PR-Options/Packages
        self.opt_models = dict()
        self.pkg_models = dict()

        self._load_default_filter()
        self.wizard.page_fakom.result_tree.clear()
        self.clear_preset_pages()

    def load(self, file: Path=None) -> bool:
        if not file:
            file = self.last_session_file
        result = True

        try:
            self.data = Settings.pickle_load(file, compressed=True)
        except Exception as e:
            LOGGER.error('Error loading wizard session: %s', e)
            result = False

        try:
            self._load_default_attributes()
        except Exception as e:
            LOGGER.debug('Error setting default session attributes: %s', e)
            result = False

        if not result:
            # Restore Default Session
            self.data = SessionData()
            self._load_default_filter()

        return result

    def save(self, file: Path=None) -> bool:
        if not file:
            file = self.last_session_file

        for page_id in self.data.preset_page_ids:
            page: PresetWizardPage = self.wizard.page(page_id)

            if not isinstance(page, PresetWizardPage):
                LOGGER.warning('Skipping non existing page %s', page_id)
                continue

            src_item_model = page.preset_tree.model().sourceModel()
            self.data.store_preset_page_content(page.model, page.fakom, src_item_model)

        self._clean_up_import_data()
        return Settings.pickle_save(self.data, file, compressed=True)

    def iterate_preset_pages(self):
        for page_id in self.data.preset_page_ids:
            preset_page: PresetWizardPage = self.wizard.page(page_id)
            if not isinstance(preset_page, PresetWizardPage):
                continue
            yield preset_page

    def clear_preset_pages(self):
        page_id, cleared_pages = self.wizard.page_placeholder.id, 0

        if not page_id > 0:
            return

        while True:
            page_id += 1

            if isinstance(self.wizard.page(page_id), (PresetWizardPage, ResultWizardPage)):
                self.wizard.removePage(page_id)
                cleared_pages += 1
            else:
                break

        LOGGER.debug('Cleared %s preset pages.', cleared_pages)

    def create_preset_pages(self):
        """ Create a Wizard preset page for each selected FaKom item """
        self.clear_preset_pages()
        self.data.preset_page_ids = set()

        for model_code, fakom_ls in self.data.fakom_selection.items():
            # Create available PR-Options and Packages per model
            self._update_preset_pages_item_models(model_code)

            for fakom in fakom_ls:
                preset_page = PresetWizardPage(self.wizard, model_code, fakom)
                page_id = self.wizard.addPage(preset_page)
                self.data.preset_page_ids.add(page_id)
                LOGGER.debug('Creating preset page: %s', page_id)

                # --- Load preset page content if available ---
                saved_model = self.data.load_preset_page_content(model_code, fakom)
                preset_page.load_model(saved_model)

        # Add Results Wizard Page
        self.wizard.addPage(self.wizard.page_result)
        # Populate Navigation Menu
        self.wizard.nav_menu.create_preset_page_entries()

    def update_available_options(self):
        """ This will be called from multiple views after a refresh so we delay the update with
            a timer so that it will update only once.
        """
        self.update_options_timer.start()

    def update_available_options_immediately(self):
        """ Called from automagic routine for immediate updates """
        self._update_available_options(ignore_lock_btn=True)

    def _update_available_options(self, ignore_lock_btn: bool=False):
        """ Update PR-Options and Packages Trees based on Preset Page Content """
        used_pr_families, used_pr_options, visible_pr_options = set(), set(), set()
        visible_pkgs, used_pkgs = set(), set()

        current_page = self.wizard.page(self.wizard.currentId())
        if not isinstance(current_page, PresetWizardPage):
            return

        # -- Add user locked options and packages
        used_pr_options.update(self.data.user_locked_pr)
        used_pkgs.update(self.data.user_locked_pkg)

        # --- Update PR-Options in use by all pages ---
        for preset_page in self.iterate_preset_pages():
            pr_options = self._collect_tree_pr_data(preset_page.preset_tree)[0]
            used_pr_options.update(pr_options)

            for index, item in preset_page.preset_tree.editor.iterator.iterate_view():
                if item.data(Kg.TYPE) == 'package':
                    used_pkgs.add(item.data(Kg.VALUE))

        # -- Update currently used PR-Families on current page --
        used_pr_families = self._collect_tree_pr_data(current_page.preset_tree)[1]
        used_pr_families.update(self.automagic_filter)

        # --- Update available PR-Options ---
        for opt_index, opt_item in current_page.option_tree.editor.iterator.iterate_view():
            # Clear userType and locked style
            opt_item.fixed_userType = 0
            opt_item.style_unlocked()

            item_type = opt_item.data(Kg.TYPE)

            if item_type in used_pr_families or opt_item.data(Kg.NAME) in used_pr_options:
                if current_page.option_lock_btn.isChecked() or ignore_lock_btn:
                    opt_item.fixed_userType = Kg.locked_variant
                    opt_item.style_locked()
                else:
                    opt_item.style_italic()
            else:
                visible_pr_options.add(opt_item.data(Kg.NAME))

        # --- Update available Packages ---
        for pkg_index, pkg_item in current_page.pkg_tree.editor.iterator.iterate_view():
            # Clear userType and locked style
            pkg_item.fixed_userType = 0
            pkg_item.style_unlocked()
            pkg_name = pkg_item.data(Kg.NAME)
            lock_pkg = False

            if pkg_item.data(Kg.VALUE) in used_pkgs:
                lock_pkg = True
            else:
                for pkg_variant in pkg_item.iter_children():
                    if pkg_variant.data(Kg.TYPE) in used_pr_families or pkg_name in used_pr_options:
                        lock_pkg = True
                        break

            # Package Country Filter
            if self.data.pkg_filter_regex and re.search(self.data.pkg_filter_regex, pkg_name):
                lock_pkg = True

            if lock_pkg:
                if current_page.option_lock_btn.isChecked() or ignore_lock_btn:
                    pkg_item.fixed_userType = Kg.locked_preset
                    pkg_item.style_locked()
                else:
                    pkg_item.style_italic()
            else:
                visible_pkgs.add(pkg_item.data(Kg.VALUE))

        # Show or Hide locked PR-Options and Packages
        if current_page.option_hide_btn.isChecked():
            current_page.option_tree.permanent_type_filter = list(visible_pr_options)
            current_page.pkg_tree.permanent_type_filter = list(visible_pkgs)
        else:
            del current_page.option_tree.permanent_type_filter
            del current_page.pkg_tree.permanent_type_filter

    @staticmethod
    def _collect_tree_pr_data(view: KnechtTreeView):
        pr_options, pr_families = set(), set()

        for index, item in view.editor.iterator.iterate_view():
            variant_ls = view.editor.collect.collect_index(index)

            for variant in variant_ls.variants:
                pr_families.add(variant.item_type)
                pr_options.add(variant.name)

        return pr_options, pr_families

    def _update_preset_pages_item_models(self, model_code: str):
        """ Populate preset page models with available pr options and packages """
        if model_code not in self.opt_models:
            # --- Create Knecht item model for available PR-Options ---
            self.opt_models[model_code] = self._create_options_knecht_model(
                model_code, self.data.import_data, is_pr_options=True
                )
        if model_code not in self.pkg_models:
            # --- Create Knecht item model for available PR-Options ---
            self.pkg_models[model_code] = self._create_options_knecht_model(
                model_code, self.data.import_data, is_pr_options=False
                )

    @staticmethod
    def _create_options_knecht_model(model_code, import_data: KnData, is_pr_options=True):
        """ Create Knecht Item Model with either available PR-Options or Packages """
        converter = KnechtDataToModel(import_data)
        opt_item_model = KnechtModel()

        trim = [t for t in import_data.models if t.model == model_code]
        if not trim:
            return opt_item_model
        else:
            trim = trim[0]

        if is_pr_options:
            if import_data.options_text_filter:
                # Create PR-Options matching E
                converter.create_pr_options(trim.iterate_optional_filtered_pr(), opt_item_model.root_item,
                                            ignore_pr_family=False)
            else:
                # Create PR-Options not matching L
                converter.create_pr_options(trim.iterate_optional_pr(), opt_item_model.root_item,
                                            ignore_pr_family=False)
        else:
            converter.create_packages(trim, opt_item_model.root_item, filter_pkg_by_pr_family=False)

        return opt_item_model
Example #30
0
class MaterialMerger(QWidget):
    def __init__(self, ui):
        """ Dialog to merge Materials directory of different models

        :param modules.gui.main_ui.KnechtWindow ui: Main Window
        """
        super(MaterialMerger, self).__init__(ui)
        SetupWidget.from_ui_file(self,
                                 Resource.ui_paths['knecht_material_merger'])
        self.setWindowTitle('AViT Material Merger')

        self.src_path_objects = list()

        self.titleLabel: QLabel
        self.titleLabel.setText('''
        <p>Vergleicht beliebige Quell -Materials- Verzeichnisse mit dem Ziel Material Verzeichnis. Ersetzt nur 
        Unterordner die bereits im Ziel Verzeichniss bestehen. Die Quell-Verzeichnisse werden gewählt 
        anhand der jüngsten enthaltenen CSB Datei. <b>Erstellt kein BackUp!</b></p>
        <table>
            <tr>
                <th>Ziel</th>
                <th>Aktion</th>
                <th>Quelle</th>
            </tr>
            <tr>
                <td>ABC001</td>
                <td> &lt; - - </td>
                <td>ABC001</td>
            </tr>
            <tr>
                <td></td>
                <td>x</td>
                <td>DEF001</td>
            </tr>
            <tr>
                <td>DBC000</td>
                <td> &lt; - - </td>
                <td>DBC000</td>
            </tr>
        </table>
        ''')
        self.titleLabel.setWordWrap(True)

        self.srcGrp: QGroupBox
        self.srcGrp.setTitle(_('Quell-Verzeichnisse'))
        self.targetGrp: QGroupBox
        self.targetGrp.setTitle(_('Ziel-Verzeichnis'))

        self.addSrcBtn: QPushButton
        self.addSrcBtn.setText(_('Quelleordner hinzufügen'))
        self.addSrcBtn.pressed.connect(self.add_source_path_object)

        self.mergeBtn: QPushButton
        self.mergeBtn.setText(_('Vereinen'))
        self.mergeBtn.pressed.connect(self.merge)

        self.target_path_widget = SetDirectoryPath(
            self,
            line_edit=self.targetPathLineEdit,
            tool_button=self.targetPathToolBtn,
            reject_invalid_path_edits=True)

        self.resultBrowser: QTextBrowser
        self.resultBrowser.append('Displaying results.')

        self.copy_thread = None
        self.thread_timer = QTimer()
        self.thread_timer.setInterval(1000)
        self.thread_timer.setSingleShot(False)
        self.thread_timer.timeout.connect(self.update_copy_thread_status)

        # -- Create initial source path widget
        self.add_source_path_object()

        self.sorted_src_dirs = dict()

        self.tex_difference_dirs = dict()

    def remove_source_path_object(self):
        btn = self.sender()
        src_path = [s for s in self.src_path_objects if s == btn.src_path][0]

        for i in range(src_path.layout.count()):
            w = src_path.layout.itemAt(i).widget()
            w.deleteLater()

        self.src_path_objects.remove(src_path)

        src_path.layout.deleteLater()
        src_path.path_widget.deleteLater()
        src_path.deleteLater()

    def add_source_path_object(self):
        src_path = QObject(self)
        h_layout = QHBoxLayout(self.srcGrp)
        label = QLabel(f'Source_{len(self.src_path_objects)}')
        h_layout.addWidget(label)
        line_edit = QLineEdit(self.srcGrp)
        h_layout.addWidget(line_edit)
        tool_btn = QToolButton(self.srcGrp)
        tool_btn.setText('...')
        h_layout.addWidget(tool_btn)
        del_btn = QPushButton(self.srcGrp)
        del_btn.setIcon(IconRsc.get_icon('delete'))
        h_layout.addWidget(del_btn)
        del_btn.src_path = src_path
        del_btn.released.connect(self.remove_source_path_object)
        path_widget = SetDirectoryPath(self,
                                       line_edit=line_edit,
                                       tool_button=tool_btn)

        src_path.layout = h_layout
        src_path.path_widget = path_widget

        # Save widget in path objects list and add widget to source path layout
        self.src_path_objects.append(src_path)
        self.srcLayout.addLayout(h_layout)

    def merge(self):
        self.mergeBtn.setEnabled(False)
        self.resultBrowser.clear()
        self.resultBrowser.append('<h1>' +
                                  _('Vereine Material Verzeichnisse') +
                                  '</h1><br>')
        self.resultBrowser.append(
            f'Target: <i>{self.target_path_widget.path.as_posix()}</i>')

        if self.target_path_widget.path is None or not self.target_path_widget.path.exists(
        ):
            self.resultBrowser.append(
                f'<span style="color: red;">Could not locate Target path: {self.target_path_widget.path}</span>'
            )
            return

        target_dir_names = [
            target_dir.name
            for target_dir in self.target_path_widget.path.iterdir()
        ]

        # -- Collect source Materials
        src_material_csbs = dict()
        for idx, src in enumerate(self.src_path_objects):
            self.resultBrowser.append(
                f'Source #{idx}: <i>{src.path_widget.path.as_posix()}</i>')
            _src_csbs = self._collect_csb_materials(
                src.path_widget.path.iterdir(), target_dir_names)

            for src_dir_name, entry_ls in _src_csbs.items():
                if src_dir_name not in src_material_csbs:
                    src_material_csbs[src_dir_name] = list()
                for entry in entry_ls:
                    src_material_csbs[src_dir_name].append(entry)
                del src_dir_name, entry_ls
        self.resultBrowser.append('<br><br>')

        # -- Sort entries by CSB File change time
        self.sorted_src_dirs = dict()
        for dir_name, entry_list in src_material_csbs.items():

            self.sorted_src_dirs[dir_name] = sorted(entry_list,
                                                    key=lambda k: k['ctime'],
                                                    reverse=True)[0]
            if len(entry_list) > 1:
                self.resultBrowser.append(
                    f'Found <b>{dir_name}</b> in multiple sources. Selecting newer file from: '
                    f'{self.sorted_src_dirs[dir_name]["path"].parent.parent.name} from '
                    f'{datetime.utcfromtimestamp(self.sorted_src_dirs[dir_name]["ctime"]).strftime("%d.%m.%Y %H:%M")}'
                )

                for e in entry_list:
                    self.resultBrowser.append(
                        f'Checked: {e["path"].parent.parent.name} - CSB last modified date: '
                        f'{datetime.utcfromtimestamp(e["ctime"]).strftime("%d.%m.%Y %H:%M")}'
                    )
                self.resultBrowser.append('<br>')

        del src_material_csbs

        # -- Filter out Materials that have differing textures
        for target_dir in self.target_path_widget.path.iterdir():
            if target_dir.name not in self.sorted_src_dirs:
                continue

            # -- Collect source and target texture file paths
            src_tex = self._collect_texture_files(
                self.sorted_src_dirs[target_dir.name]['path'])
            tgt_tex = self._collect_texture_files(target_dir)

            # -- Compare and exclude differing files
            if tgt_tex.symmetric_difference(src_tex):
                # -- Remove Entries that have differing textures
                self.sorted_src_dirs.pop(target_dir.name)
                # -- Add Report Entry
                self.tex_difference_dirs[
                    target_dir.name] = tgt_tex.symmetric_difference(src_tex)

        # -- Replace Material directories in target dir
        thread_signals = ThreadSignals()
        thread_signals.update.connect(self.resultBrowser.append)
        self.copy_thread = Thread(target=copy_material_dirs,
                                  args=(self.target_path_widget.path,
                                        self.sorted_src_dirs, thread_signals))
        self.copy_thread.start()
        self.thread_timer.start()

    def update_copy_thread_status(self):
        if self.copy_thread is None:
            return

        if self.copy_thread.is_alive():
            return

        self.resultBrowser.append('Copy thread finished!<br>')
        self.report_untouched_materials()
        self.export_html_report()
        self.thread_timer.stop()
        self.mergeBtn.setEnabled(True)

    def report_untouched_materials(self):
        """ Report Materials not copied into target
            Directories existing in the target path but not in any source path
        """
        self.resultBrowser.append('<h1>Material Merger Results</h1><br>')
        count = 0
        for target_dir in self.target_path_widget.path.iterdir():
            if target_dir.name in self.sorted_src_dirs or target_dir.name in self.tex_difference_dirs:
                continue
            self.resultBrowser.append(
                f'<b>{target_dir.name}</b> - '
                f'was not in any source directory and was not updated.<br>')
            count += 1

        if not count:
            self.resultBrowser.append(
                'Source and target directories perfectly matched!? Did you cheat?<br>'
            )

        self.resultBrowser.append(
            '<h2>Materials with differing texture files</h2> <br>')
        if not self.tex_difference_dirs:
            self.resultBrowser.append(
                '<i>Found no Materials with differing texture files.</i>')

        for target_dir in self.target_path_widget.path.iterdir():
            if target_dir.name not in self.tex_difference_dirs:
                continue

            filenames = ''.join(
                [f'{f}; ' for f in self.tex_difference_dirs[target_dir.name]])
            self.resultBrowser.append(
                f'<b>{target_dir.name}</b> - '
                f'contained differing texture files: <i>{filenames}</i><br>')

    def export_html_report(self):
        # Report file path
        name = f'MaterialMerger_Report_{datetime.now().strftime("%d%m%Y_%H%M")}.html'
        report_file = QUrl.fromLocalFile(
            os.path.abspath(os.path.expanduser(f'~\\Documents\\{name}')))

        # Result browser html content
        html_data = str(self.resultBrowser.toHtml())

        # Write report
        try:
            with open(report_file.toLocalFile(), 'w') as f:
                f.write(html_data)
        except Exception as e:
            LOGGER.error(e)

        QDesktopServices.openUrl(report_file)

    @staticmethod
    def _collect_texture_files(directory: Path):
        return {
            file.name
            for file in directory.iterdir()
            if file.suffix.casefold() not in ('.csb', '.bak', '.texturepath',
                                              '.db') and not file.is_dir()
        }

    @staticmethod
    def _collect_csb_materials(directories: Union[List[Path]],
                               target_dir_names: List[str]) -> dict:
        dir_data = dict()

        for src_dir in directories:
            # -- Skip directories not in target and non existing
            if src_dir is None or src_dir.is_file(
            ) or not src_dir.exists() or src_dir.name not in target_dir_names:
                continue

            # -- Get contained CSB files
            for d in src_dir.glob('*.csb'):
                # -- Add entry with directory path and CSB last modified time
                path_entry = {'path': src_dir, 'ctime': d.stat().st_mtime}

                if src_dir.name not in dir_data:
                    dir_data[src_dir.name] = list()

                dir_data[src_dir.name].append(path_entry)

        return dir_data