Beispiel #1
0
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.counter = 0

        layout = QVBoxLayout()
        self.l = QLabel("Start")
        b = QPushButton("DANGER!")
        b.pressed.connect(self.oh_no)

        layout.addWidget(self.l)
        layout.addWidget(b)

        w = QWidget()
        w.setLayout(layout)

        self.setCentralWidget(w)
        self.show()

        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    def oh_no(self):
        time.sleep(5)

    def recurring_timer(self):
        self.counter += 1
        self.l.setText("Count: %d" % self.counter)
Beispiel #2
0
class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.threadpool = QtCore.QThreadPool()  # 初始化线程池
        print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())

        self.counter = 0

        layout = QtWidgets.QVBoxLayout()

        self.l = QtWidgets.QLabel("Start")

        b = QtWidgets.QPushButton("DANGER!")
        b.pressed.connect(self.oh_no)

        layout.addWidget(self.l)
        layout.addWidget(b)

        w = QWidget()
        w.setLayout(layout)

        self.setCentralWidget(w)

        self.show()

        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    def oh_no(self):
        worker = Worker(iterations=random.randint(10, 50))
        worker.signals.result.connect(self.worker_output)
        worker.signals.finished.connect(self.worker_complete)
        worker.signals.error.connect(self.worker_error)
        self.threadpool.start(worker)

    def worker_output(self, s):
        print("RESULT", s)

    def worker_complete(self):
        print("THREAD COMPLETE!")

    def worker_error(self, t):
        print("ERROR: %s" % t)

    def recurring_timer(self):
        self.counter += 1
        self.l.setText("Counter: %d" % self.counter)
Beispiel #3
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)
Beispiel #4
0
class Backend(QObject):

    updated = Signal(str, arguments=['time'])

    def __init__(self):
        super().__init__()

        # Define timer.
        self.timer = QTimer()
        self.timer.setInterval(100)  # msecs 100 = 1/10th sec
        self.timer.timeout.connect(self.update_time)
        self.timer.start()

    def update_time(self):
        # Pass the current time to QML.
        curr_time = strftime("%H:%M:%S", localtime())
        self.updated.emit(curr_time)
Beispiel #5
0
class ImageViewerModel(QObject):
    image_url_changed = Signal(str)

    def __init__(self, dirname, parent=None):
        super().__init__(parent)
        self.__image_url = ''
        self.__dirname = dirname
        self.__image_list = []
        self.__timer = QTimer(self)
        self.__timer.setInterval(IMAGE_INTERVAL)
        self.__timer.timeout.connect(self.on_timeout)

    @Property(str, notify=image_url_changed)
    def image_url(self):
        return self.__image_url

    @image_url.setter
    def image_url(self, value):
        if self.__image_url != value:
            self.__image_url = value
            self.image_url_changed.emit(self.__image_url)

    @Slot()
    def start_view(self):
        self.init_image_list()
        self.random_set_image()
        self.__timer.start()

    def init_image_list(self):
        self.__image_list = [
            str(x) for x in Path(self.__dirname).iterdir() if x.is_file()
        ]

    def random_set_image(self):
        if not self.__image_list:
            return
        image = random.choice(self.__image_list)
        self.__image_list.remove(image)
        self.image_url = f'file:///{path.join(self.__dirname, image).replace(path.sep, "/")}'

    def on_timeout(self):
        if not self.__image_list:
            self.init_image_list()
        self.random_set_image()
class TimerQT(TimerBase):
    def __init__(self, *args, **kwargs):
        self._timer = QTimer()
        self._timer.timeout.connect(self._on_timer)
        TimerBase.__init__(self, *args, **kwargs)

    def __del__(self):
        self._timer_stop()

    def _timer_set_single_shot(self):
        self._timer.setSingleShot(self._single)

    def _timer_set_interval(self):
        self._timer.setInterval(self._interval)

    def _timer_start(self):
        self._timer.start()

    def _timer_stop(self):
        self._timer.stop()
Beispiel #7
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        layout = QVBoxLayout()

        self.progress = QProgressBar()

        button = QPushButton("START IT UP")
        button.pressed.connect(self.execute)

        self.status = QLabel("0 workers")

        layout.addWidget(self.progress)
        layout.addWidget(button)
        layout.addWidget(self.status)

        w = QWidget()
        w.setLayout(layout)

        # Dictionary holds the progress of current workers.
        self.worker_progress = {}

        self.setCentralWidget(w)

        self.show()

        self.threadpool = QThreadPool()
        print("Multithreading with maximum %d threads" %
              self.threadpool.maxThreadCount())

        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.refresh_progress)
        self.timer.start()

    def execute(self):
        worker = Worker()
        worker.signals.progress.connect(self.update_progress)
        worker.signals.finished.connect(self.cleanup)  # 任务完成后清除对应进度

        # Execute
        self.threadpool.start(worker)

    def cleanup(self, job_id):
        if job_id in self.worker_progress:
            del self.worker_progress[job_id]  # 对完成的任务,删除
            # Update the progress bar if we've removed a value.
            self.refresh_progress()

    def update_progress(self, job_id, progress):
        self.worker_progress[job_id] = progress

    def calculate_progress(self):
        if not self.worker_progress:
            return 0

        return sum(v for v in self.worker_progress.values()) / len(
            self.worker_progress)

    def refresh_progress(self):
        # Calculate total progress.
        progress = self.calculate_progress()
        print(self.worker_progress)
        self.progress.setValue(progress)
        self.status.setText("%d workers" % len(self.worker_progress))
Beispiel #8
0
class ModList(QTableView):
    def __init__(self, parent: QWidget, model: Model) -> None:
        super().__init__(parent)

        settings = QSettings()

        self.hoverIndexRow = -1
        self.modmodel = model
        self.installLock = asyncio.Lock()

        self.setMouseTracking(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setWordWrap(False)
        self.setSortingEnabled(True)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self.setEditTriggers(QTableView.EditKeyPressed
                             | QTableView.DoubleClicked)
        self.setShowGrid(False)

        self.setStyleSheet('''
            QTableView {
                gridline-color: rgba(255,255,255,1);
            }
            QTableView::item:!selected:hover {
                background-color: rgb(217, 235, 249);
            }
            ''')

        self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showContextMenu)

        self.verticalHeader().hide()
        self.verticalHeader().setVisible(False)
        self.setSectionSize(settings.value('compactMode', 'False') == 'True')

        self.setCornerButtonEnabled(False)
        self.horizontalHeader().setHighlightSections(False)
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().setSectionsMovable(True)

        self.listmodel = ModListModel(self, model)
        self.filtermodel = ModListFilterModel(self, self.listmodel)
        self.setModel(self.filtermodel)

        self.setItemDelegate(ModListItemDelegate(self))
        self.setSelectionModel(ModListSelectionModel(self, self.filtermodel))

        if len(model):
            self.modCountLastUpdate = len(model)
            self.resizeColumnsToContents()
        else:
            self.modCountLastUpdate = -1

        if settings.value('modlistHorizontalHeaderState'):
            self.horizontalHeader().restoreState(
                settings.value('modlistHorizontalHeaderState'))  # type: ignore

        self.horizontalHeader().sectionMoved.connect(
            lambda: self.headerChangedEvent())
        self.horizontalHeader().sectionResized.connect(
            lambda: self.headerChangedEvent())

        self.setFocus()

        self.sortByColumn(3, Qt.AscendingOrder, False)
        self.sortByColumn(2, Qt.AscendingOrder, False)
        self.sortByColumn(1, Qt.AscendingOrder, False)
        if settings.value('modlistSortColumn') is not None and \
           settings.value('modlistSortOrder') is not None:
            try:
                self.sortByColumn(
                    cast(int, settings.value('modlistSortColumn', 1,
                                             int)), Qt.DescendingOrder
                    if cast(int, settings.value('modlistSortOrder', 1, int))
                    else Qt.AscendingOrder, False)
            except Exception as e:
                logger.exception(f'could not restore sort order: {e}')
        self.horizontalHeader().sortIndicatorChanged.connect(self.sortByColumn)

        self.doubleClicked.connect(self.doubleClickEvent)
        model.updateCallbacks.append(self.modelUpdateEvent)

        # setup viewport caching to counter slow resizing with many table elements
        self.resizeTimer = QTimer(self)
        self.resizeTimer.setSingleShot(True)
        self.resizeTimer.setInterval(250)
        self.resizeTimer.timeout.connect(lambda: [
            self.resizeTimer.stop(),
            self.viewport().repaint(),
        ])
        self.viewportCache = QPixmap()
        self.viewportCacheSize = QSize(0, 0)

        # TODO: enhancement: offer option to read readme and other additional text files

    def setSectionSize(self, compact: bool) -> None:
        if compact:
            self.verticalHeader().setDefaultSectionSize(25)
        else:
            self.verticalHeader().setDefaultSectionSize(30)

    @debounce(200)
    async def headerChangedEvent(self) -> None:
        settings = QSettings()
        state = self.horizontalHeader().saveState()
        # call later to work around pyqt5 StopIteration exception
        asyncio.get_running_loop().call_later(
            25 / 1000.0,
            lambda: settings.setValue('modlistHorizontalHeaderState', state))

    def modelUpdateEvent(self, model: Model) -> None:
        if not self.modCountLastUpdate and len(self.modmodel):
            # if list was empty before, auto resize columns
            self.resizeColumnsToContents()
        self.modCountLastUpdate = len(self.modmodel)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        self.hoverIndexRow = self.indexAt(event.pos()).row()
        return super().mouseMoveEvent(event)

    def wheelEvent(self, event: QWheelEvent) -> None:
        result = super().wheelEvent(event)
        # repaint previously hovered row on scroll avoid repaint artifacts
        index = self.model().index(self.hoverIndexRow, 0)
        self.hoverIndexRow = self.indexAt(event.position().toPoint()).row()
        rect = self.visualRect(index)
        rect.setLeft(0)
        rect.setRight(self.viewport().width())
        self.viewport().repaint(rect)
        return result

    def leaveEvent(self, event: QEvent) -> None:
        index = self.model().index(self.hoverIndexRow, 0)
        # unset row hover state and repaint previously hovered row
        self.hoverIndexRow = -1
        rect = self.visualRect(index)
        rect.setLeft(0)
        rect.setRight(self.viewport().width())
        self.viewport().repaint(rect)
        return super().leaveEvent(event)

    def doubleClickEvent(self, index: QModelIndex) -> None:
        if self.filtermodel.mapToSource(index).column() == 0:
            mod = self.modmodel[self.filtermodel.mapToSource(index).row()]
            if mod.enabled:
                asyncio.create_task(self.modmodel.disable(mod))
            else:
                asyncio.create_task(self.modmodel.enable(mod))

    def resizeEvent(self, event: QResizeEvent) -> None:
        super().resizeEvent(event)
        if not self.resizeTimer.isActive(
        ) and event.size() != self.viewportCacheSize:
            self.viewportCacheSize = event.size()
            self.viewportCache = self.viewport().grab()
            self.resizeTimer.start()

    def paintEvent(self, event: QPaintEvent) -> None:
        if self.resizeTimer.isActive():
            painter = QPainter(self.viewport())
            painter.drawPixmap(0, 0, self.viewportCache)
        else:
            super().paintEvent(event)

    def selectionChanged(self, selected: QItemSelection,
                         deselected: QItemSelection) -> None:
        super().selectionChanged(selected, deselected)

    def eventFilter(self, obj: QObject, event: QEvent) -> bool:
        return super().eventFilter(obj, event)

    def sortByColumn(self,
                     col: int,
                     order: Qt.SortOrder,
                     save: bool = True) -> None:  # type: ignore
        if save and col is not None and order is not None:
            settings = QSettings()
            settings.setValue('modlistSortColumn', col)
            settings.setValue('modlistSortOrder',
                              0 if order == Qt.AscendingOrder else 1)
        super().sortByColumn(col, order)

    def showContextMenu(self, pos: QPoint) -> None:
        mods = self.getSelectedMods()
        if not mods:
            return
        menu = QMenu(self)
        actionOpen = menu.addAction('&Open Directory')
        actionOpen.setIcon(
            QIcon(str(getRuntimePath('resources/icons/open-folder.ico'))))
        actionOpen.triggered.connect(lambda: [
            util.openDirectory(self.modmodel.getModPath(mod))  # type: ignore
            for mod in mods
        ])
        menu.addSeparator()
        actionEnable = menu.addAction('&Enable')
        actionEnable.triggered.connect(
            lambda: [asyncio.create_task(self.enableSelectedMods(True))])
        actionEnable.setEnabled(not all(mod.enabled for mod in mods))
        actionDisable = menu.addAction('&Disable')
        actionDisable.triggered.connect(
            lambda: [asyncio.create_task(self.enableSelectedMods(False))])
        actionDisable.setEnabled(not all(not mod.enabled for mod in mods))
        menu.addSeparator()
        actionUninstall = menu.addAction('&Uninstall')
        actionUninstall.triggered.connect(
            lambda: [asyncio.create_task(self.deleteSelectedMods())])
        menu.addSeparator()
        actionOpenNexus = menu.addAction('Open &Nexus Mods page')
        actionOpenNexus.setIcon(
            QIcon(str(getRuntimePath('resources/icons/browse.ico'))))
        actionOpenNexus.triggered.connect(lambda: [
            QDesktopServices.openUrl(
                QUrl(f'https://www.nexusmods.com/witcher3/mods/{modid}'))
            for modid in {mod.modid
                          for mod in mods if mod.modid > 0}
        ])
        actionOpenNexus.setEnabled(not all(mod.modid <= 0 for mod in mods))

        menu.popup(self.viewport().mapToGlobal(pos))

    def selectRowChecked(self, row: int) -> None:
        nums: int = self.filtermodel.rowCount()
        if row < nums and row >= 0:
            self.selectRow(row)
        elif nums > 0:
            self.selectRow(nums - 1)

    def getSelectedMods(self) -> List[Mod]:
        return [
            self.modmodel[self.filtermodel.mapToSource(index).row()]
            for index in self.selectionModel().selectedRows()
        ]

    async def enableSelectedMods(self, enable: bool = True) -> None:
        if not self.selectionModel().hasSelection():
            return
        mods = self.getSelectedMods()
        self.setDisabled(True)
        for mod in mods:
            try:
                if enable:
                    await self.modmodel.enable(mod)
                else:
                    await self.modmodel.disable(mod)
            except Exception as e:
                logger.bind(name=mod.filename).exception(
                    f'Could not enable/disable mod: {e}')
        self.setDisabled(False)
        self.setFocus()

    async def deleteSelectedMods(self) -> None:
        if not self.selectionModel().hasSelection():
            return
        self.setDisabled(True)
        mods = self.getSelectedMods()
        # TODO: incomplete: ask if selected mods should really be removed
        inds = self.selectedIndexes()
        self.selectionModel().clear()
        for mod in mods:
            try:
                await self.modmodel.remove(mod)
            except Exception as e:
                logger.bind(
                    name=mod.filename).exception(f'Could not delete mod: {e}')
        asyncio.get_running_loop().call_later(
            100 / 1000.0, partial(self.selectRowChecked, inds[0].row()))
        self.setDisabled(False)
        self.setFocus()

    async def updateModDetails(self, mod: Mod) -> bool:
        logger.bind(name=mod.filename,
                    dots=True).debug('Requesting details for mod')
        if not mod.md5hash:
            logger.bind(name=mod.filename).warning(
                'Could not get details for mod not installed from archive')
            return False
        try:
            details = await getModInformation(mod.md5hash)
        except Exception as e:
            logger.bind(name=mod.filename).warning(f'{e}')
            return False
        try:
            package = str(details[0]['mod']['name'])
            summary = str(details[0]['mod']['summary'])
            modid = int(details[0]['mod']['mod_id'])
            category = int(details[0]['mod']['category_id'])
            version = str(details[0]['file_details']['version'])
            fileid = int(details[0]['file_details']['file_id'])
            uploadname = str(details[0]['file_details']['name'])
            uploadtime = str(details[0]['file_details']['uploaded_time'])
            mod.package = package
            mod.summary = summary
            mod.modid = modid
            mod.category = getCategoryName(category)
            mod.version = version
            mod.fileid = fileid
            mod.uploadname = uploadname
            uploaddate = dateparser.parse(uploadtime)
            if uploaddate:
                mod.uploaddate = uploaddate.astimezone(tz=timezone.utc)
            else:
                logger.bind(name=mod.filename).debug(
                    f'Could not parse date {uploadtime} in mod information response'
                )
        except KeyError as e:
            logger.bind(name=mod.filename).exception(
                f'Could not find key "{str(e)}" in mod information response')
            return False
        try:
            await self.modmodel.update(mod)
        except Exception as e:
            logger.bind(
                name=mod.filename).exception(f'Could not update mod: {e}')
            return False
        return True

    async def updateSelectedModsDetails(self) -> None:
        if not self.selectionModel().hasSelection():
            return
        self.setDisabled(True)
        updatetime = datetime.now(tz=timezone.utc)
        mods = self.getSelectedMods()
        logger.bind(
            newline=True,
            output=False).debug(f'Requesting details for {len(mods)} mods')
        results = await asyncio.gather(
            *[self.updateModDetails(mod) for mod in mods],
            loop=asyncio.get_running_loop(),
            return_exceptions=True)
        successes = sum(results)
        errors = len(results) - successes
        message = 'Updated details for {0} mods{1}'.format(
            successes, f' ({errors} errors)' if errors else '')
        if errors:
            logger.warning(message)
        else:
            logger.success(message)
        self.modmodel.setLastUpdateTime(updatetime)
        self.setDisabled(False)
        self.setFocus()

    async def changeSelectedModsPriority(self, delta: int) -> None:
        mods = self.getSelectedMods()
        await asyncio.gather(*[
            self.modmodel.setPriority(
                mod, max(-1, min(9999, int(mod.priority + delta))))
            for mod in mods if mod.datatype in (
                'mod',
                'udf',
            )
        ],
                             loop=asyncio.get_running_loop())
        self.modmodel.setLastUpdateTime(datetime.now(tz=timezone.utc))

    def keyPressEvent(self, event: QKeyEvent) -> None:
        if event.key() == Qt.Key_Escape:
            self.selectionModel().clear()
        elif event.matches(QKeySequence.Delete):
            asyncio.create_task(self.deleteSelectedMods())
        elif event.modifiers(
        ) & Qt.ControlModifier == Qt.ControlModifier and event.key(
        ) == Qt.Key_Up:
            asyncio.create_task(self.changeSelectedModsPriority(1))
        elif event.modifiers(
        ) & Qt.ControlModifier == Qt.ControlModifier and event.key(
        ) == Qt.Key_Down:
            asyncio.create_task(self.changeSelectedModsPriority(-1))
        elif event.modifiers(
        ) & Qt.ControlModifier == Qt.ControlModifier and event.key(
        ) == Qt.Key_P:
            index = self.selectionModel().selectedRows()[0]
            index = index.sibling(index.row(), 5)
            if index.flags() & Qt.ItemIsEditable:
                self.setCurrentIndex(index)
                self.edit(index)
        else:
            super().keyPressEvent(event)

    def setFilter(self, search: str) -> None:
        self.filtermodel.setFilterRegularExpression(
            QRegularExpression(search,
                               QRegularExpression.CaseInsensitiveOption))

    async def checkInstallFromURLs(self,
                                   paths: List[Union[str, QUrl]],
                                   local: bool = True,
                                   web: bool = True) -> None:
        await self.installLock.acquire()
        installed = 0
        errors = 0
        installtime = datetime.now(tz=timezone.utc)
        # remove duplicate paths
        paths = list(set(paths))
        logger.bind(newline=True,
                    output=False).debug('Starting install from URLs')
        try:
            results = await asyncio.gather(*[
                self.installFromURL(path, local, web, installtime)
                for path in paths
            ],
                                           loop=asyncio.get_running_loop())
            for result in results:
                installed += result[0]
                errors += result[1]
        except Exception as e:
            # we should never land here, but don't lock up the UI if it happens
            logger.exception(str(e))
            errors += 1

        if installed > 0 or errors > 0:
            log = logger.bind(modlist=bool(installed))
            message = 'Installed {0} mods{1}'.format(
                installed, f' ({errors} errors)' if errors else '')
            if installed > 0 and errors > 0:
                log.warning(message)
            elif installed > 0:
                log.success(message)
            else:
                log.error(message)
        self.setDisabled(False)
        self.setFocus()
        self.installLock.release()

    async def installFromURL(
            self,
            path: Union[str, QUrl],
            local: bool = True,
            web: bool = True,
            installtime: Optional[datetime] = None) -> Tuple[int, int]:
        installed = 0
        errors = 0
        if not installtime:
            installtime = datetime.now(tz=timezone.utc)
        if isinstance(path, QUrl):
            path = path.toString()
        if web and isValidModDownloadUrl(path):
            self.setDisabled(True)
            logger.bind(dots=True, path=path).info(f'Installing mods from')
            i, e = await self.installFromFileDownload(path, installtime)
            installed += i
            errors += e
        elif local and isValidFileUrl(path):
            self.setDisabled(True)
            path = QUrl(path)
            logger.bind(dots=True, path=Path(
                path.toLocalFile())).info(f'Installing mods from')
            i, e = await self.installFromFile(Path(path.toLocalFile()),
                                              installtime)
            installed += i
            errors += e
        else:
            logger.bind(path=path).error('Could not install mods from')
        return installed, errors

    async def installFromFileDownload(
            self,
            url: str,
            installtime: Optional[datetime] = None) -> Tuple[int, int]:
        installed = 0
        errors = 0
        if not installtime:
            installtime = datetime.now(tz=timezone.utc)
        try:
            target = Path(urlparse(url).path)
            filename = re.sub(r'[^\w\-_\. ]', '_', unquote(target.name))
            target = Path(tempfile.gettempdir()).joinpath(
                'w3modmanager/download').joinpath(f'{filename}')
        except ValueError:
            logger.bind(name=url).exception('Wrong request URL')
            return 0, 1
        try:
            target.parent.mkdir(parents=True, exist_ok=True)
            logger.bind(name=url).info('Starting to download file')
            await downloadFile(url, target)
            installed, errors = await self.installFromFile(target, installtime)
        except (RequestError, ResponseError, Exception) as e:
            logger.bind(name=url).exception(f'Failed to download file: {e}')
            return 0, 1
        except Exception as e:
            logger.exception(str(e))
            return 0, 1
        finally:
            if target.is_file():
                target.unlink()
        return installed, errors

    async def installFromFile(
            self,
            path: Path,
            installtime: Optional[datetime] = None) -> Tuple[int, int]:
        originalpath = path
        installed = 0
        errors = 0
        archive = path.is_file()
        source = None
        md5hash = ''
        details = None
        detailsrequest: Optional[asyncio.Task] = None

        if not installtime:
            installtime = datetime.now(tz=timezone.utc)
        try:
            if archive:
                # unpack archive, set source and request details
                md5hash = getMD5Hash(path)
                source = path
                settings = QSettings()
                if settings.value('nexusGetInfo', 'False') == 'True':
                    logger.bind(
                        path=str(path),
                        dots=True).debug('Requesting details for archive')
                    detailsrequest = asyncio.create_task(
                        getModInformation(md5hash))
                logger.bind(path=str(path),
                            dots=True).debug('Unpacking archive')
                path = await extractMod(source)

            # validate and read mod
            valid, exhausted = containsValidMod(path, searchlimit=8)
            if not valid:
                if not exhausted and self.showContinueSearchDialog(
                        searchlimit=8):
                    if not containsValidMod(path):
                        raise InvalidPathError(path, 'Invalid mod')
                elif not exhausted:
                    raise InvalidPathError(path, 'Stopped searching for mod')
                else:
                    raise InvalidPathError(path, 'Invalid mod')
            mods = await Mod.fromDirectory(path, searchCommonRoot=not archive)

            installedMods = []
            # update mod details and add mods to the model
            for mod in mods:
                mod.md5hash = md5hash
                try:
                    # TODO: incomplete: check if mod is installed, ask if replace
                    await self.modmodel.add(mod)
                    installedMods.append(mod)
                    installed += 1
                except ModExistsError:
                    logger.bind(path=source if source else mod.source,
                                name=mod.filename).error(f'Mod exists')
                    errors += 1
                    continue

            # wait for details response if requested
            if detailsrequest:
                try:
                    details = await detailsrequest
                except (RequestError, ResponseError, Exception) as e:
                    logger.warning(
                        f'Could not get information for {source.name if source else path.name}: {e}'
                    )

            # update mod with additional information
            if source or details:
                for mod in installedMods:
                    if source:
                        # set source if it differs from the scan directory, e.g. an archive
                        mod.source = source
                    if details:
                        # set additional details if requested and available
                        try:
                            package = str(details[0]['mod']['name'])
                            summary = str(details[0]['mod']['summary'])
                            modid = int(details[0]['mod']['mod_id'])
                            category = int(details[0]['mod']['category_id'])
                            version = str(
                                details[0]['file_details']['version'])
                            fileid = int(details[0]['file_details']['file_id'])
                            uploadname = str(
                                details[0]['file_details']['name'])
                            uploadtime = str(
                                details[0]['file_details']['uploaded_time'])
                            mod.package = package
                            mod.summary = summary
                            mod.modid = modid
                            mod.category = getCategoryName(category)
                            mod.version = version
                            mod.fileid = fileid
                            mod.uploadname = uploadname
                            uploaddate = dateparser.parse(uploadtime)
                            if uploaddate:
                                mod.uploaddate = uploaddate.astimezone(
                                    tz=timezone.utc)
                            else:
                                logger.bind(name=mod.filename).debug(
                                    f'Could not parse date {uploadtime} in mod information response'
                                )
                        except KeyError as e:
                            logger.bind(name=mod.filename).exception(
                                f'Could not find key "{str(e)}" in mod information response'
                            )
                    try:
                        await self.modmodel.update(mod)
                    except Exception:
                        logger.bind(name=mod.filename).warning(
                            'Could not update mod details')

        except ModelError as e:
            logger.bind(path=e.path).error(e.message)
            errors += 1
        except InvalidPathError as e:
            # TODO: enhancement: better install error message
            logger.bind(path=e.path).error(e.message)
            errors += 1
        except FileNotFoundError as e:
            logger.bind(
                path=e.filename).error(e.strerror if e.strerror else str(e))
            errors += 1
        except OSError as e:
            logger.bind(
                path=e.filename).error(e.strerror if e.strerror else str(e))
            errors += 1
        except Exception as e:
            logger.exception(str(e))
            errors += 1
        finally:
            if detailsrequest and not detailsrequest.done():
                detailsrequest.cancel()
            if archive and not path == originalpath:
                try:
                    util.removeDirectory(path)
                except Exception:
                    logger.bind(path=path).warning(
                        'Could not remove temporary directory')
            self.modmodel.setLastUpdateTime(installtime)
            self.repaint()
        return installed, errors

    def showContinueSearchDialog(self, searchlimit: int) -> bool:
        messagebox = QMessageBox(self)
        messagebox.setWindowTitle('Unusual search depth')
        messagebox.setText(f'''
            <p>No mod detected after searching through {searchlimit} directories.</p>
            <p>Are you sure this is a valid mod?</p>
            ''')
        messagebox.setTextFormat(Qt.RichText)
        messagebox.setStandardButtons(QMessageBox.Cancel)
        yes: QPushButton = QPushButton(' Yes, continue searching ', messagebox)
        yes.setAutoDefault(True)
        yes.setDefault(True)
        messagebox.addButton(yes, QMessageBox.YesRole)
        messagebox.exec_()
        return messagebox.clickedButton() == yes

    def dropEvent(self, event: QDropEvent) -> None:
        event.accept()
        self.setDisabled(True)
        self.repaint()
        asyncio.create_task(self.checkInstallFromURLs(event.mimeData().urls()))

    def dragEnterEvent(self, event: QDragEnterEvent) -> None:
        self.setDisabled(True)
        self.repaint()
        urls = event.mimeData().urls()
        if not urls:
            self.setDisabled(False)
            self.setFocus()
            event.ignore()
            return
        for url in urls:
            try:
                parse = urlparse(url.toString())
                if parse.scheme not in ['file']:
                    self.setDisabled(False)
                    event.ignore()
                    return
                filepath = Path(url.toLocalFile())
                if isArchive(filepath) or containsValidMod(filepath,
                                                           searchlimit=8)[0]:
                    self.setDisabled(False)
                    event.accept()
                    return
            except Exception as e:
                logger.debug(str(e))
        self.setDisabled(False)
        self.setFocus()
        event.ignore()

    def dragMoveEvent(self, event: QDragMoveEvent) -> None:
        event.accept()

    def dragLeaveEvent(self, event: QDragLeaveEvent) -> None:
        event.accept()
class GameConnection(QObject, ConnectionBackend):
    Updated = Signal()

    _dt: float = 2.5
    _last_status: Any = None
    _permanent_pickups: List[Tuple[str, PickupEntry]]

    def __init__(self, executor: MemoryOperationExecutor):
        super().__init__()
        ConnectionBackend.__init__(self, executor)
        self._permanent_pickups = []

        self._timer = QTimer(self)
        self._timer.timeout.connect(self._auto_update)
        self._timer.setInterval(self._dt * 1000)
        self._timer.setSingleShot(True)
        self._notify_status()

    def set_executor(self, executor: MemoryOperationExecutor):
        self.executor = executor
        self._notify_status()

    async def start(self):
        self._timer.start()

    async def stop(self):
        self._timer.stop()

    @asyncSlot()
    async def _auto_update(self):
        try:
            await self.update(self._dt)
            self._notify_status()
        finally:
            self._timer.start()

    def _notify_status(self):
        new_status = self.current_status
        inventory = self.get_current_inventory()

        if self._last_status != (new_status, self.executor, inventory):
            self._last_status = (new_status, self.executor,
                                 copy.copy(inventory))
            self.Updated.emit()

    @property
    def pretty_current_status(self) -> str:
        return f"{self.backend_choice.pretty_text}: {self.current_status.pretty_text}"

    @property
    def current_game_name(self) -> Optional[str]:
        if self.connector is not None:
            return self.connector.game_enum.long_name

    @property
    def name(self) -> str:
        raise ValueError("bleh")

    def set_location_collected_listener(self,
                                        listener: Optional[LocationListener]):
        super(ConnectionBackend,
              self).set_location_collected_listener(listener)
        self.checking_for_collected_index = listener is not None
Beispiel #10
0
    class MarqueeText(QScrollArea):
        def __init__(self, parent=None):
            QScrollArea.__init__(self)
            self.setParent(parent)
            self.text = ""

            # Set the background colour of the marquee text to white
            self.setStyleSheet("QScrollArea { background-color: rgba(255, 255, 255, 1)}")

            # Initialise the base label and text
            self.label = QLabel()

            # Set the font for marquee
            font = QFont()
            font.setItalic(True)
            font.setBold(True)
            font.setPixelSize(25)
            self.label.setFont(font)

            # Set the base label as the base widget of QScrollArea
            self.setWidget(self.label)

            # Set QScrollBar Policies
            self.setWidgetResizable(True)
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

            # Initialise timer and associated variables, used for marquee effect
            self.timer = QTimer(self)
            self.x = 0
            self.speed = 0
            
            # Connect a function to the timer, which controls the marquee effect
            self.timer.timeout.connect(self.updatePos)

            # TODO: Set a nominal speed
            self.setSpeed(33)

        def updatePos(self):
            self.x = (self.x + 1) % self.label.fontMetrics().horizontalAdvance(self.text)
            self.horizontalScrollBar().setValue(self.x)

        def setText(self, text):
            # Sets the text of the marquee label
            # text: (str) the text to be displayed

            # TODO: Change the separator bit when adding universal settings
            self.text = text + "          "
            self.x = 0

            # First need the widths of the current label windowspace and the text itself
            windowWidth = self.window().geometry().width()
            textWidth = self.label.fontMetrics().horizontalAdvance(text)
            # Concatenate the text on itself as many times as needed.
            self.label.setText(self.text + (self.text * (windowWidth // textWidth + (windowWidth % textWidth > 0))))

            # Finally, start the timer to start the effect
            self.timer.start(self.timer.interval())

        def setSpeed(self, speed):
            # Sets the speed of the scroll
            # speed: (int) how many pixels to move per second

            # Reset the timer with new interval.
            self.timer.setInterval(1000 / speed)
class ImageView(MouseEventMixin, QGraphicsView):
    def __init__(self, dirname, parent=None):
        super(ImageView, self).__init__()
        super(MouseEventMixin, self).__init__(parent)
        self.__dirname = dirname
        self.__image = None
        self.__image_list = []
        self.__timer = QTimer(self)
        self.init_ui()

    def init_ui(self):
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setRenderHints(QPainter.Antialiasing
                            | QPainter.SmoothPixmapTransform
                            | QPainter.TextAntialiasing)

        self.__timer.setInterval(IMAGE_INTERVAL)
        self.__timer.timeout.connect(self.on_timeout)

    def init_image_list(self):
        self.__image_list = [
            str(x) for x in Path(self.__dirname).iterdir() if x.is_file()
        ]

    def start_view(self):
        self.init_image_list()
        self.random_set_image()
        self.__timer.start()

    def set_image(self, filename):
        self.setUpdatesEnabled(False)
        self.__image = QImage(filename)
        self.repaint()
        self.setUpdatesEnabled(True)

    def random_set_image(self):
        if not self.__image_list:
            return
        image = random.choice(self.__image_list)
        self.__image_list.remove(image)
        self.set_image(path.join(self.__dirname, image))

    def on_timeout(self):
        if not self.__image_list:
            self.init_image_list()
        self.random_set_image()

    def paintEvent(self, event):
        super(MouseEventMixin, self).paintEvent(event)

        if not self.__image:
            return

        if self.__image.height() == 0 or self.height(
        ) == 0 or self.__image.width() == 0:
            return

        image_aspect_ratio = self.__image.width() / self.__image.height()
        view_aspect_ratio = self.width() / self.height()
        if view_aspect_ratio <= image_aspect_ratio:
            image_height = self.width() / image_aspect_ratio
            rect = QRectF(0, (self.height() - image_height) / 2, self.width(),
                          image_height)
        else:
            image_widh = self.height() * image_aspect_ratio
            rect = QRectF((self.width() - image_widh) / 2, 0, image_widh,
                          self.height())

        painter = QPainter(self.viewport())
        painter.drawImage(rect, self.__image)
        painter.end()
Beispiel #12
0
class ByteView(QAbstractScrollArea, View):
    def __init__(self, parent, data):
        QAbstractScrollArea.__init__(self, parent)
        View.__init__(self)
        View.setBinaryDataNavigable(self, True)
        self.setupView(self)
        self.data = data
        self.byte_mapping = [
            u' ', u'☺', u'☻', u'♥', u'♦', u'♣', u'♠', u'•', u'◘', u'○', u'◙',
            u'♂', u'♀', u'♪', u'♫', u'☼', u'▸', u'◂', u'↕', u'‼', u'¶', u'§',
            u'▬', u'↨', u'↑', u'↓', u'→', u'←', u'∟', u'↔', u'▴', u'▾', u' ',
            u'!', u'"', u'#', u'$', u'%', u'&', u'\'', u'(', u')', u'*', u'+',
            u',', u'-', u'.', u'/', u'0', u'1', u'2', u'3', u'4', u'5', u'6',
            u'7', u'8', u'9', u':', u';', u'<', u'=', u'>', u'?', u'@', u'A',
            u'B', u'C', u'D', u'E', u'F', u'G', u'H', u'I', u'J', u'K', u'L',
            u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W',
            u'X', u'Y', u'Z', u'[', u'\\', u']', u'^', u'_', u'`', u'a', u'b',
            u'c', u'd', u'e', u'f', u'g', u'h', u'i', u'j', u'k', u'l', u'm',
            u'n', u'o', u'p', u'q', u'r', u's', u't', u'u', u'v', u'w', u'x',
            u'y', u'z', u'{', u'|', u'}', u'~', u'⌂', u'Ç', u'ü', u'é', u'â',
            u'ä', u'à', u'å', u'ç', u'ê', u'ë', u'è', u'ï', u'î', u'ì', u'Ä',
            u'Å', u'É', u'æ', u'Æ', u'ô', u'ö', u'ò', u'û', u'ù', u'ÿ', u'Ö',
            u'Ü', u'¢', u'£', u'¥', u'₧', u'ƒ', u'á', u'í', u'ó', u'ú', u'ñ',
            u'Ñ', u'ª', u'º', u'¿', u'⌐', u'¬', u'½', u'¼', u'¡', u'«', u'»',
            u'░', u'▒', u'▓', u'│', u'┤', u'╡', u'╢', u'╖', u'╕', u'╣', u'║',
            u'╗', u'╝', u'╜', u'╛', u'┐', u'└', u'┴', u'┬', u'├', u'─', u'┼',
            u'╞', u'╟', u'╚', u'╔', u'╩', u'╦', u'╠', u'═', u'╬', u'╧', u'╨',
            u'╤', u'╥', u'╙', u'╘', u'╒', u'╓', u'╫', u'╪', u'┘', u'┌', u'█',
            u'▄', u'▌', u'▐', u'▀', u'α', u'ß', u'Γ', u'π', u'Σ', u'σ', u'µ',
            u'τ', u'Φ', u'Θ', u'Ω', u'δ', u'∞', u'φ', u'ε', u'∩', u'≡', u'±',
            u'≥', u'≤', u'⌠', u'⌡', u'÷', u'≈', u'°', u'∙', u'·', u'√', u'ⁿ',
            u'²', u'■', u' '
        ]

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setFocusPolicy(Qt.StrongFocus)

        self.cursorAddr = self.data.start
        self.prevCursorAddr = self.cursorAddr
        self.selectionStartAddr = self.cursorAddr
        self.topAddr = self.cursorAddr
        self.topLine = 0
        self.selectionVisible = False
        self.caretVisible = False
        self.caretBlink = True
        self.leftButtonDown = False
        self.cols = 128
        self.updatesRequired = False
        self.visibleRows = 1
        self.lines = []

        self.updateRanges()

        areaSize = self.viewport().size()
        self.adjustSize(areaSize.width(), areaSize.height())

        if self.allocatedLength > 0x7fffffff:
            self.scrollBarMultiplier = (self.allocatedLength // 0x7fffffff) + 1
        else:
            self.scrollBarMultiplier = 1
        self.wheelDelta = 0
        self.updatingScrollBar = False
        self.verticalScrollBar().setRange(0, (self.allocatedLength - 1) //
                                          self.scrollBarMultiplier)
        self.verticalScrollBar().sliderMoved.connect(self.scrollBarMoved)
        self.verticalScrollBar().actionTriggered.connect(self.scrollBarAction)

        self.cursorTimer = QTimer(self)
        self.cursorTimer.setInterval(500)
        self.cursorTimer.setSingleShot(False)
        self.cursorTimer.timeout.connect(self.cursorTimerEvent)
        self.cursorTimer.start()

        self.updateTimer = QTimer(self)
        self.updateTimer.setInterval(200)
        self.updateTimer.setSingleShot(False)
        #self.updateTimer.timeout.connect(self.updateTimerEvent)

        self.actionHandler().bindAction("Move Cursor Up",
                                        UIAction(lambda ctxt: self.up(False)))
        self.actionHandler().bindAction(
            "Move Cursor Down", UIAction(lambda ctxt: self.down(False)))
        self.actionHandler().bindAction(
            "Move Cursor Left", UIAction(lambda ctxt: self.left(1, False)))
        self.actionHandler().bindAction(
            "Move Cursor Right", UIAction(lambda ctxt: self.right(1, False)))
        self.actionHandler().bindAction(
            "Move Cursor Word Left",
            UIAction(lambda ctxt: self.left(8, False)))
        self.actionHandler().bindAction(
            "Move Cursor Word Right",
            UIAction(lambda ctxt: self.right(8, False)))
        self.actionHandler().bindAction("Extend Selection Up",
                                        UIAction(lambda ctxt: self.up(True)))
        self.actionHandler().bindAction("Extend Selection Down",
                                        UIAction(lambda ctxt: self.down(True)))
        self.actionHandler().bindAction(
            "Extend Selection Left", UIAction(lambda ctxt: self.left(1, True)))
        self.actionHandler().bindAction(
            "Extend Selection Right",
            UIAction(lambda ctxt: self.right(1, True)))
        self.actionHandler().bindAction(
            "Extend Selection Word Left",
            UIAction(lambda ctxt: self.left(8, True)))
        self.actionHandler().bindAction(
            "Extend Selection Word Right",
            UIAction(lambda ctxt: self.right(8, True)))
        self.actionHandler().bindAction(
            "Page Up", UIAction(lambda ctxt: self.pageUp(False)))
        self.actionHandler().bindAction(
            "Page Down", UIAction(lambda ctxt: self.pageDown(False)))
        self.actionHandler().bindAction(
            "Extend Selection Page Up",
            UIAction(lambda ctxt: self.pageUp(True)))
        self.actionHandler().bindAction(
            "Extend Selection Page Down",
            UIAction(lambda ctxt: self.pageDown(True)))
        self.actionHandler().bindAction(
            "Move Cursor to Start of Line",
            UIAction(lambda ctxt: self.moveToStartOfLine(False)))
        self.actionHandler().bindAction(
            "Move Cursor to End of Line",
            UIAction(lambda ctxt: self.moveToEndOfLine(False)))
        self.actionHandler().bindAction(
            "Move Cursor to Start of View",
            UIAction(lambda ctxt: self.moveToStartOfView(False)))
        self.actionHandler().bindAction(
            "Move Cursor to End of View",
            UIAction(lambda ctxt: self.moveToEndOfView(False)))
        self.actionHandler().bindAction(
            "Extend Selection to Start of Line",
            UIAction(lambda ctxt: self.moveToStartOfLine(True)))
        self.actionHandler().bindAction(
            "Extend Selection to End of Line",
            UIAction(lambda ctxt: self.moveToEndOfLine(True)))
        self.actionHandler().bindAction(
            "Extend Selection to Start of View",
            UIAction(lambda ctxt: self.moveToStartOfView(True)))
        self.actionHandler().bindAction(
            "Extend Selection to End of View",
            UIAction(lambda ctxt: self.moveToEndOfView(True)))

    def getData(self):
        return self.data

    def getStart(self):
        return self.data.start

    def getEnd(self):
        return self.data.end

    def getLength(self):
        return self.getEnd() - self.getStart()

    def getCurrentOffset(self):
        return self.cursorAddr

    def getSelectionOffsets(self):
        start = self.selectionStartAddr
        end = self.cursorAddr
        if end < start:
            t = start
            start = end
            end = t
        return (start, end)

    def updateRanges(self):
        self.ranges = self.data.allocated_ranges
        # Remove regions not backed by the file
        for i in self.data.segments:
            if i.data_length < len(i):
                self.removeRange(i.start + i.data_length, i.end)
        self.allocatedLength = 0
        for i in self.ranges:
            self.allocatedLength += i.end - i.start

    def removeRange(self, begin, end):
        newRanges = []
        for i in self.ranges:
            if (end <= i.start) or (begin >= i.end):
                newRanges.append(i)
            elif (begin <= i.start) and (end >= i.end):
                continue
            elif (begin <= i.start) and (end < i.end):
                newRanges.append(AddressRange(end, i.end))
            elif (begin > i.start) and (end >= i.end):
                newRanges.append(AddressRange(i.start, begin))
            else:
                newRanges.append(AddressRange(i.start, begin))
                newRanges.append(AddressRange(end, i.end))
        self.ranges = newRanges

    def setTopToAddress(self, addr):
        for i in self.ranges:
            if (addr >= i.start) and (addr <= i.end):
                self.topAddr = addr - ((addr - i.start) % self.cols)
                if self.topAddr < i.start:
                    self.topAddr = i.start
                return
            if i.start > addr:
                self.topAddr = i.start
                return
        self.topAddr = self.data.end

    def navigate(self, addr):
        if addr < self.getStart():
            return False
        if addr > self.getEnd():
            return False
        self.cursorAddr = self.getStart()
        for i in self.ranges:
            if i.start > addr:
                break
            if addr > i.end:
                self.cursorAddr = i.end
            elif addr >= i.start:
                self.cursorAddr = addr
            else:
                self.cursorAddr = i.start
        self.setTopToAddress(self.cursorAddr)
        self.refreshLines()
        self.showContextAroundTop()
        self.selectNone()
        self.repositionCaret()
        return True

    def updateFonts(self):
        areaSize = self.viewport().size()
        self.adjustSize(areaSize.width(), areaSize.height())

    def getFont(self):
        userFont = binaryninjaui.getMonospaceFont(self)
        if sys.platform == "darwin":
            # Some fonts aren't fixed width across all characters, use a known good one
            font = QFont("Menlo", userFont.pointSize())
            font.setKerning(False)
        else:
            font = userFont
        return font

    def createRenderContext(self):
        render = RenderContext(self)
        render.setFont(self.getFont())
        return render

    def adjustSize(self, width, height):
        self.addrWidth = max(len("%x" % self.data.end), 8)
        render = self.createRenderContext()
        cols = ((width - 4) // render.getFontWidth()) - (self.addrWidth + 2)
        if cols < 1:
            cols = 1
        if cols != self.cols:
            self.cols = cols
            if self.topLine < len(self.lines):
                self.setTopToAddress(self.lines[self.topLine].address)
            else:
                self.setTopToAddress(self.cursorAddr)
            self.refreshLines()
        self.visibleRows = (height - 4) // render.getFontHeight()
        self.verticalScrollBar().setPageStep(self.visibleRows * self.cols //
                                             self.scrollBarMultiplier)
        self.refreshAtCurrentLocation()
        self.viewport().update()

    def getContiguousOffsetForAddress(self, addr):
        offset = 0
        for i in self.ranges:
            if (addr >= i.start) and (addr <= i.end):
                offset += addr - i.start
                break
            offset += i.end - i.start
        return offset

    def getAddressForContiguousOffset(self, offset):
        cur = 0
        for i in self.ranges:
            if offset < (cur + (i.end - i.start)):
                return i.start + (offset - cur)
            cur += i.end - i.start
        return self.data.end

    def refreshLines(self):
        addr = self.topAddr
        self.lines = []
        self.topLine = 0
        self.bottomAddr = self.topAddr
        self.updateRanges()
        if self.allocatedLength > 0x7fffffff:
            self.scrollBarMultiplier = (self.allocatedLength // 0x7fffffff) + 1
        else:
            self.scrollBarMultiplier = 1
        self.updatingScrollBar = True
        self.verticalScrollBar().setRange(0, (self.allocatedLength - 1) //
                                          self.scrollBarMultiplier)
        self.verticalScrollBar().setValue(
            self.getContiguousOffsetForAddress(addr) //
            self.scrollBarMultiplier)
        self.updatingScrollBar = False
        self.updateCache()
        self.viewport().update()
        UIContext.updateStatus()

    def refreshAtCurrentLocation(self):
        if self.topLine < len(self.lines):
            self.topAddr = self.lines[self.topLine].address
        self.refreshLines()

    def createLine(self, addr, length, separator):
        if separator:
            return ByteViewLine(addr, length, u'', True)
        else:
            data = self.data.read(addr, length)
            text = u''.join([self.byte_mapping[value] for value in data])
            return ByteViewLine(addr, length, text, False)

    def cachePreviousLines(self):
        prevEnd = None
        for i in self.ranges:
            if (self.topAddr > i.start) and (self.topAddr <= i.end):
                startLine = self.topAddr - (
                    (self.topAddr - i.start) % self.cols)
                if startLine == self.topAddr:
                    startLine -= self.cols
                if startLine < i.start:
                    startLine = i.start
                line = self.createLine(startLine, self.topAddr - startLine,
                                       False)
                self.lines.insert(0, line)
                self.topLine += 1
                self.topAddr = startLine
                return True
            elif i.start >= self.topAddr:
                if prevEnd is None:
                    return False
                line = self.createLine(prevEnd, i.start - prevEnd, True)
                self.lines.insert(0, line)
                self.topLine += 1
                self.topAddr = prevEnd
                return True
            prevEnd = i.end
        if prevEnd is None:
            return False
        line = self.createLine(prevEnd, self.topAddr - prevEnd, True)
        self.lines.insert(0, line)
        self.topLine += 1
        self.topAddr = prevEnd
        return True

    def cacheNextLines(self):
        lastAddr = self.data.start
        for i in self.ranges:
            if (self.bottomAddr >= i.start) and (self.bottomAddr < i.end):
                endLine = self.bottomAddr + self.cols
                if endLine > i.end:
                    endLine = i.end
                line = self.createLine(self.bottomAddr,
                                       endLine - self.bottomAddr, False)
                self.lines.append(line)
                self.bottomAddr = endLine
                return True
            elif i.start > self.bottomAddr:
                line = self.createLine(self.bottomAddr,
                                       i.start - self.bottomAddr, True)
                self.lines.append(line)
                self.bottomAddr = i.start
                return True
            lastAddr = i.end
        if self.bottomAddr == lastAddr:
            # Ensure there is a place for the cursor at the end of the file
            if (len(self.lines) > 0) and (self.lines[-1].length != self.cols):
                return False
            line = self.createLine(lastAddr, 0, False)
            self.lines.append(line)
            self.bottomAddr += 1
            return True
        return False

    def updateCache(self):
        # Cache enough for the current page and the next page
        while (len(self.lines) - self.topLine) <= (self.visibleRows * 2):
            if not self.cacheNextLines():
                break
        # Cache enough for the previous page
        while self.topLine <= self.visibleRows:
            if not self.cachePreviousLines():
                break
        # Trim cache
        if self.topLine > (self.visibleRows * 4):
            self.lines = self.lines[self.topLine - (self.visibleRows * 4):]
            self.topLine = self.visibleRows * 4
            self.topAddr = self.lines[0].address
        if (len(self.lines) - self.topLine) > (self.visibleRows * 5):
            self.bottomAddr = self.lines[self.topLine +
                                         (self.visibleRows * 5)].address
            self.lines = self.lines[0:self.topLine + (self.visibleRows * 5)]

    def scrollLines(self, count):
        newOffset = self.topLine + count
        if newOffset < 0:
            self.topLine = 0
        elif newOffset >= len(self.lines):
            self.topLine = len(self.lines) - 1
        else:
            self.topLine = newOffset
        self.updateCache()
        self.viewport().update()
        if self.topLine < len(self.lines):
            self.updatingScrollBar = True
            addr = self.lines[self.topLine].address
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(addr) //
                self.scrollBarMultiplier)
            self.updatingScrollBar = False

    def showContextAroundTop(self):
        scroll = self.visibleRows // 4
        if scroll > self.topLine:
            self.topLine = 0
        else:
            self.topLine -= scroll
        if self.topLine < len(self.lines):
            self.updatingScrollBar = True
            addr = self.lines[self.topLine].address
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(addr) //
                self.scrollBarMultiplier)
            self.updatingScrollBar = False
        self.updateCache()

    def repositionCaret(self):
        self.updateCache()
        found = False
        for i in range(0, len(self.lines)):
            if (((self.cursorAddr >= self.lines[i].address) and
                 (self.cursorAddr <
                  (self.lines[i].address + self.lines[i].length)))
                    or (((i + 1) == len(self.lines)) and
                        (self.cursorAddr
                         == (self.lines[i].address + self.lines[i].length)))):
                if i < self.topLine:
                    self.topLine = i
                elif i > (self.topLine + self.visibleRows - 1):
                    self.topLine = i - (self.visibleRows - 1)
                self.updatingScrollBar = True
                addr = self.lines[self.topLine].address
                self.verticalScrollBar().setValue(
                    self.getContiguousOffsetForAddress(addr) //
                    self.scrollBarMultiplier)
                self.updatingScrollBar = False
                self.updateCache()
                self.viewport().update()
                found = True
                break
        if not found:
            self.setTopToAddress(self.cursorAddr)
            self.refreshLines()
            self.showContextAroundTop()
        # Force caret to be visible and repaint
        self.caretBlink = True
        self.cursorTimer.stop()
        self.cursorTimer.start()
        self.updateCaret()
        UIContext.updateStatus()

    def updateCaret(self):
        # Rerender both the old caret position and the new caret position
        render = self.createRenderContext()
        for i in range(self.topLine,
                       min(len(self.lines), self.topLine + self.visibleRows)):
            if (((self.prevCursorAddr >= self.lines[i].address) and
                 (self.prevCursorAddr <=
                  (self.lines[i].address + self.lines[i].length)))
                    or ((self.cursorAddr >= self.lines[i].address) and
                        (self.cursorAddr <=
                         (self.lines[i].address + self.lines[i].length)))):
                self.viewport().update(0, (i - self.topLine) *
                                       render.getFontHeight(),
                                       self.viewport().size().width(),
                                       render.getFontHeight() + 3)

    def resizeEvent(self, event):
        self.adjustSize(event.size().width(), event.size().height())

    def paintEvent(self, event):
        p = QPainter(self.viewport())
        render = self.createRenderContext()
        render.init(p)
        charWidth = render.getFontWidth()
        charHeight = render.getFontHeight()

        # Compute range that needs to be updated
        topY = event.rect().y()
        botY = topY + event.rect().height()
        topY = (topY - 2) // charHeight
        botY = ((botY - 2) // charHeight) + 1

        # Compute selection range
        selection = False
        selStart, selEnd = self.getSelectionOffsets()
        if selStart != selEnd:
            selection = True

        # Draw selection
        if selection:
            startY = None
            endY = None
            startX = None
            endX = None
            for i in range(0, len(self.lines)):
                if selStart >= self.lines[i].address:
                    startY = i - self.topLine
                    startX = selStart - self.lines[i].address
                    if startX > self.cols:
                        startX = self.cols
                if selEnd >= self.lines[i].address:
                    endY = i - self.topLine
                    endX = selEnd - self.lines[i].address
                    if endX > self.cols:
                        endX = self.cols

            if startY is not None and endY is not None:
                p.setPen(binaryninjaui.getThemeColor(
                    ThemeColor.SelectionColor))
                p.setBrush(
                    binaryninjaui.getThemeColor(ThemeColor.SelectionColor))
                if startY == endY:
                    p.drawRect(2 + (self.addrWidth + 2 + startX) * charWidth,
                               2 + startY * charHeight,
                               (endX - startX) * charWidth, charHeight + 1)
                else:
                    p.drawRect(2 + (self.addrWidth + 2 + startX) * charWidth,
                               2 + startY * charHeight,
                               (self.cols - startX) * charWidth,
                               charHeight + 1)
                    if endX > 0:
                        p.drawRect(2 + (self.addrWidth + 2) * charWidth,
                                   2 + endY * charHeight, endX * charWidth,
                                   charHeight + 1)
                if (endY - startY) > 1:
                    p.drawRect(2 + (self.addrWidth + 2) * charWidth,
                               2 + (startY + 1) * charHeight,
                               self.cols * charWidth,
                               ((endY - startY) - 1) * charHeight + 1)

        # Paint each line
        color = self.palette().color(QPalette.WindowText)
        for y in range(topY, botY):
            if (y + self.topLine) < 0:
                continue
            if (y + self.topLine) >= len(self.lines):
                break
            if self.lines[y + self.topLine].separator:
                render.drawLinearDisassemblyLineBackground(
                    p,
                    LinearDisassemblyLineType.NonContiguousSeparatorLineType,
                    QRect(0, 2 + y * charHeight,
                          event.rect().width(), charHeight), 0)
                continue

            lineStartAddr = self.lines[y + self.topLine].address
            addrStr = "%.8x" % lineStartAddr
            length = self.lines[y + self.topLine].length
            text = self.lines[y + self.topLine].text

            cursorCol = None
            if (((self.cursorAddr >= lineStartAddr) and
                 (self.cursorAddr < (lineStartAddr + length)))
                    or (((y + self.topLine + 1) >= len(self.lines)) and
                        (self.cursorAddr == (lineStartAddr + length)))):
                cursorCol = self.cursorAddr - lineStartAddr

            render.drawText(
                p, 2, 2 + y * charHeight,
                binaryninjaui.getThemeColor(ThemeColor.AddressColor), addrStr)
            render.drawText(p, 2 + (self.addrWidth + 2) * charWidth,
                            2 + y * charHeight, color, text)

            if self.caretVisible and self.caretBlink and not selection and cursorCol is not None:
                p.setPen(Qt.NoPen)
                p.setBrush(self.palette().color(QPalette.WindowText))
                p.drawRect(2 + (self.addrWidth + 2 + cursorCol) * charWidth,
                           2 + y * charHeight, charWidth, charHeight + 1)
                caretTextColor = self.palette().color(QPalette.Base)
                byteValue = self.data.read(lineStartAddr + cursorCol, 1)
                if len(byteValue) == 1:
                    byteStr = self.byte_mapping[byteValue[0]]
                    render.drawText(
                        p, 2 + (self.addrWidth + 2 + cursorCol) * charWidth,
                        2 + y * charHeight, caretTextColor, byteStr)

    def wheelEvent(self, event):
        if event.orientation() == Qt.Horizontal:
            return
        self.wheelDelta -= event.delta()
        if (self.wheelDelta <= -40) or (self.wheelDelta >= 40):
            lines = self.wheelDelta // 40
            self.wheelDelta -= lines * 40
            self.scrollLines(lines)

    def scrollBarMoved(self, value):
        if self.updatingScrollBar:
            return
        self.wheelDelta = 0
        addr = self.getAddressForContiguousOffset(value *
                                                  self.scrollBarMultiplier)
        self.setTopToAddress(addr)
        self.refreshLines()
        for i in range(1, len(self.lines)):
            if (self.lines[i].address + self.lines[i].length) > addr:
                self.topLine = i - 1
                break
        self.updateCache()

    def scrollBarAction(self, action):
        if action == QAbstractSlider.SliderSingleStepAdd:
            self.wheelDelta = 0
            self.scrollLines(1)
        elif action == QAbstractSlider.SliderSingleStepSub:
            self.wheelDelta = 0
            self.scrollLines(-1)
        elif action == QAbstractSlider.SliderPageStepAdd:
            self.wheelDelta = 0
            self.scrollLines(self.visibleRows)
        elif action == QAbstractSlider.SliderPageStepSub:
            self.wheelDelta = 0
            self.scrollLines(-self.visibleRows)
        elif action == QAbstractSlider.SliderToMinimum:
            self.wheelDelta = 0
            self.setTopToAddress(self.getStart())
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(self.topAddr) //
                self.scrollBarMultiplier)
            self.refreshLines()
        elif action == QAbstractSlider.SliderToMaximum:
            self.wheelDelta = 0
            self.setTopToAddress(self.getEnd())
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(self.topAddr) //
                self.scrollBarMultiplier)
            self.refreshLines()

    def cursorTimerEvent(self):
        self.caretBlink = not self.caretBlink
        self.updateCaret()

    def focusInEvent(self, event):
        self.caretVisible = True
        self.updateCaret()

    def focusOutEvent(self, event):
        self.caretVisible = False
        self.leftButtonDown = False
        self.updateCaret()

    def selectNone(self):
        for i in self.lines:
            if (self.cursorAddr >=
                    i.address) and (self.cursorAddr <
                                    (i.address + i.length)) and i.separator:
                self.cursorAddr = i.address + i.length
                break
        self.selectionStartAddr = self.cursorAddr
        if self.selectionVisible:
            self.viewport().update()
        self.repositionCaret()
        UIContext.updateStatus()

    def selectAll(self):
        self.selectionStartAddr = self.getStart()
        self.cursorAddr = self.getEnd()
        self.viewport().update()
        UIContext.updateStatus()

    def adjustAddressAfterBackwardMovement(self):
        lastAddr = self.getStart()
        for i in self.ranges:
            if (self.cursorAddr >= i.start) and (self.cursorAddr < i.end):
                break
            if i.start > self.cursorAddr:
                self.cursorAddr = lastAddr
                break
            lastAddr = i.end - 1

    def adjustAddressAfterForwardMovement(self):
        for i in self.ranges:
            if (self.cursorAddr >= i.start) and (self.cursorAddr < i.end):
                break
            if i.start > self.cursorAddr:
                self.cursorAddr = i.start
                break

    def left(self, count, selecting):
        if self.cursorAddr > (self.getStart() + count):
            self.cursorAddr -= count
        else:
            self.cursorAddr = self.getStart()
        self.adjustAddressAfterBackwardMovement()
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def right(self, count, selecting):
        if self.cursorAddr <= (self.getEnd() - count):
            self.cursorAddr += count
        else:
            self.cursorAddr = self.getEnd()
        self.adjustAddressAfterForwardMovement()
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def up(self, selecting):
        self.left(self.cols, selecting)

    def down(self, selecting):
        self.right(self.cols, selecting)

    def pageUp(self, selecting):
        for i in range(0, len(self.lines)):
            if (((self.cursorAddr >= self.lines[i].address) and
                 (self.cursorAddr <
                  (self.lines[i].address + self.lines[i].length)))
                    or (((i + 1) == len(self.lines)) and
                        (self.cursorAddr
                         == (self.lines[i].address + self.lines[i].length)))):
                if i < self.visibleRows:
                    self.cursorAddr = self.getStart()
                else:
                    lineOfs = self.cursorAddr - self.lines[i].address
                    self.cursorAddr = self.lines[
                        i - self.visibleRows].address + lineOfs
                    if self.cursorAddr < self.lines[i -
                                                    self.visibleRows].address:
                        self.cursorAddr = self.lines[i -
                                                     self.visibleRows].address
                    elif self.cursorAddr >= (
                            self.lines[i - self.visibleRows].address +
                            self.lines[i - self.visibleRows].length):
                        self.cursorAddr = self.lines[
                            i - self.visibleRows].address + self.lines[
                                i - self.visibleRows].length - 1
                    break
        self.adjustAddressAfterBackwardMovement()
        if self.topLine > self.visibleRows:
            self.topLine -= self.visibleRows
        else:
            self.topLine = 0
        if self.topLine < len(self.lines):
            self.updatingScrollBar = True
            addr = self.lines[self.topLine].address
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(addr) //
                self.scrollBarMultiplier)
            self.updatingScrollBar = False
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        self.viewport().update()

    def pageDown(self, selecting):
        for i in range(0, len(self.lines)):
            if (((self.cursorAddr >= self.lines[i].address) and
                 (self.cursorAddr <
                  (self.lines[i].address + self.lines[i].length)))
                    or (((i + 1) == len(self.lines)) and
                        (self.cursorAddr
                         == (self.lines[i].address + self.lines[i].length)))):
                if i >= (len(self.lines) - self.visibleRows):
                    self.cursorAddr = self.getEnd()
                else:
                    lineOfs = self.cursorAddr - self.lines[i].address
                    self.cursorAddr = self.lines[
                        i + self.visibleRows].address + lineOfs
                    if self.cursorAddr < self.lines[i +
                                                    self.visibleRows].address:
                        self.cursorAddr = self.lines[i +
                                                     self.visibleRows].address
                    elif self.cursorAddr >= (
                            self.lines[i + self.visibleRows].address +
                            self.lines[i + self.visibleRows].length):
                        self.cursorAddr = self.lines[
                            i + self.visibleRows].address + self.lines[
                                i + self.visibleRows].length - 1
                    break
        self.adjustAddressAfterForwardMovement()
        if (self.topLine + self.visibleRows) < len(self.lines):
            self.topLine += self.visibleRows
        elif len(self.lines) > 0:
            self.topLine = len(self.lines) - 1
        if self.topLine < len(self.lines):
            self.updatingScrollBar = True
            addr = self.lines[self.topLine].address
            self.verticalScrollBar().setValue(
                self.getContiguousOffsetForAddress(addr) //
                self.scrollBarMultiplier)
            self.updatingScrollBar = False
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        self.viewport().update()

    def moveToStartOfLine(self, selecting):
        for i in self.lines:
            if (self.cursorAddr >= i.address) and (self.cursorAddr <
                                                   (i.address + i.length)):
                self.cursorAddr = i.address
                break
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def moveToEndOfLine(self, selecting):
        for i in self.lines:
            if (self.cursorAddr >= i.address) and (self.cursorAddr <
                                                   (i.address + i.length)):
                self.cursorAddr = i.address + i.length - 1
                break
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def moveToStartOfView(self, selecting):
        self.cursorAddr = self.getStart()
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def moveToEndOfView(self, selecting):
        self.cursorAddr = self.getEnd()
        if not selecting:
            self.selectNone()
        self.repositionCaret()
        if self.selectionVisible or selecting:
            self.viewport().update()

    def addressFromLocation(self, x, y):
        if y < 0:
            y = 0
        if x < 0:
            x = 0
        if x > self.cols:
            x = self.cols
        if (y + self.topLine) >= len(self.lines):
            return self.getEnd()
        if self.lines[y + self.topLine].separator:
            return self.lines[y + self.topLine].address - 1
        result = self.lines[y + self.topLine].address + x
        if result >= (self.lines[y + self.topLine].address +
                      self.lines[y + self.topLine].length):
            if (y + self.topLine) == (len(self.lines) - 1):
                return self.getEnd()
            else:
                return self.lines[y + self.topLine].address + self.lines[
                    y + self.topLine].length - 1
        return result

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return
        render = self.createRenderContext()
        x = (event.x() - 2) // render.getFontWidth() - (self.addrWidth + 2)
        y = (event.y() - 2) // render.getFontHeight()
        self.lastMouseX = x
        self.lastMouseY = y
        self.cursorAddr = self.addressFromLocation(x, y)
        if (event.modifiers() & Qt.ShiftModifier) == 0:
            self.selectNone()
        self.repositionCaret()
        if (event.modifiers() & Qt.ShiftModifier) != 0:
            self.viewport().update()
        self.leftButtonDown = True

    def mouseMoveEvent(self, event):
        if not self.leftButtonDown:
            return
        render = self.createRenderContext()
        x = (event.x() - 2) // render.getFontWidth() - (self.addrWidth + 2)
        y = (event.y() - 2) // render.getFontHeight()
        if (x == self.lastMouseX) and (y == self.lastMouseY):
            return
        self.lastMouseX = x
        self.lastMouseY = y
        self.cursorAddr = self.addressFromLocation(x, y)
        self.repositionCaret()
        self.viewport().update()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return
        self.leftButtonDown = False
Beispiel #13
0
class DebugView(QWidget, View):
	class DebugViewHistoryEntry(HistoryEntry):
		def __init__(self, memory_addr, address, is_raw):
			HistoryEntry.__init__(self)

			self.memory_addr = memory_addr
			self.address = address
			self.is_raw = is_raw

		def __repr__(self):
			if self.is_raw:
				return "<raw history: {}+{:0x} (memory: {:0x})>".format(self.address['module'], self.address['offset'], self.memory_addr)
			return "<code history: {:0x} (memory: {:0x})>".format(self.address, self.memory_addr)

	def __init__(self, parent, data):
		if not type(data) == BinaryView:
			raise Exception('expected widget data to be a BinaryView')

		self.bv = data

		self.debug_state = binjaplug.get_state(data)
		memory_view = self.debug_state.memory_view
		self.debug_state.ui.debug_view = self

		QWidget.__init__(self, parent)
		self.controls = ControlsWidget.DebugControlsWidget(self, "Controls", data, self.debug_state)
		View.__init__(self)

		self.setupView(self)

		self.current_offset = 0

		self.splitter = QSplitter(Qt.Orientation.Horizontal, self)

		frame = ViewFrame.viewFrameForWidget(self)
		self.memory_editor = LinearView(memory_view, frame)
		self.binary_editor = DisassemblyContainer(frame, data, frame)

		self.binary_text = TokenizedTextView(self, memory_view)
		self.is_raw_disassembly = False
		self.raw_address = 0

		self.is_navigating_history = False
		self.memory_history_addr = 0

		# TODO: Handle these and change views accordingly
		# Currently they are just disabled as the DisassemblyContainer gets confused
		# about where to go and just shows a bad view
		self.binary_editor.getDisassembly().actionHandler().bindAction("View in Hex Editor", UIAction())
		self.binary_editor.getDisassembly().actionHandler().bindAction("View in Linear Disassembly", UIAction())
		self.binary_editor.getDisassembly().actionHandler().bindAction("View in Types View", UIAction())

		self.memory_editor.actionHandler().bindAction("View in Hex Editor", UIAction())
		self.memory_editor.actionHandler().bindAction("View in Disassembly Graph", UIAction())
		self.memory_editor.actionHandler().bindAction("View in Types View", UIAction())

		small_font = QApplication.font()
		small_font.setPointSize(11)

		bv_layout = QVBoxLayout()
		bv_layout.setSpacing(0)
		bv_layout.setContentsMargins(0, 0, 0, 0)

		bv_label = QLabel("Loaded File")
		bv_label.setFont(small_font)
		bv_layout.addWidget(bv_label)
		bv_layout.addWidget(self.binary_editor)

		self.bv_widget = QWidget()
		self.bv_widget.setLayout(bv_layout)

		disasm_layout = QVBoxLayout()
		disasm_layout.setSpacing(0)
		disasm_layout.setContentsMargins(0, 0, 0, 0)

		disasm_label = QLabel("Raw Disassembly at PC")
		disasm_label.setFont(small_font)
		disasm_layout.addWidget(disasm_label)
		disasm_layout.addWidget(self.binary_text)

		self.disasm_widget = QWidget()
		self.disasm_widget.setLayout(disasm_layout)

		memory_layout = QVBoxLayout()
		memory_layout.setSpacing(0)
		memory_layout.setContentsMargins(0, 0, 0, 0)

		memory_label = QLabel("Debugged Process")
		memory_label.setFont(small_font)
		memory_layout.addWidget(memory_label)
		memory_layout.addWidget(self.memory_editor)

		self.memory_widget = QWidget()
		self.memory_widget.setLayout(memory_layout)

		self.splitter.addWidget(self.bv_widget)
		self.splitter.addWidget(self.memory_widget)

		# Equally sized
		self.splitter.setSizes([0x7fffffff, 0x7fffffff])

		layout = QVBoxLayout()
		layout.setContentsMargins(0, 0, 0, 0)
		layout.setSpacing(0)
		layout.addWidget(self.controls)
		layout.addWidget(self.splitter, 100)
		self.setLayout(layout)

		self.needs_update = True
		self.update_timer = QTimer(self)
		self.update_timer.setInterval(200)
		self.update_timer.setSingleShot(False)
		self.update_timer.timeout.connect(lambda: self.updateTimerEvent())

		self.add_scripting_ref()

		# set initial breakpoint when view is switched
		if self.debug_state.bv and self.debug_state.bv.entry_point:
			local_entry_offset = self.debug_state.bv.entry_point - self.debug_state.bv.start
			if not self.debug_state.breakpoints.contains_offset(self.debug_state.bv.file.original_filename, local_entry_offset):
				self.debug_state.breakpoints.add_offset(self.debug_state.bv.file.original_filename, local_entry_offset)
				if self.debug_state.ui is not None:
					self.debug_state.ui.breakpoint_tag_add(self.debug_state.bv.entry_point)
					self.debug_state.ui.update_highlights()
					self.debug_state.ui.update_breakpoints()

	def add_scripting_ref(self):
		# Hack: The interpreter is just a thread, so look through all threads
		# and assign our state to the interpreter's locals
		for thread in threading.enumerate():
			if type(thread) == PythonScriptingInstance.InterpreterThread:
				thread.locals["dbg"] = self.debug_state

	def getData(self):
		return self.bv

	def getFont(self):
		return binaryninjaui.getMonospaceFont(self)

	def getCurrentOffset(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentOffset()
		return self.raw_address

	def getSelectionOffsets(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getSelectionOffsets()
		return (self.raw_address, self.raw_address)

	def getCurrentFunction(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentFunction()
		return None

	def getCurrentBasicBlock(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentBasicBlock()
		return None

	def getCurrentArchitecture(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentArchitecture()
		return None

	def getCurrentLowLevelILFunction(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentLowLevelILFunction()
		return None

	def getCurrentMediumLevelILFunction(self):
		if not self.is_raw_disassembly:
			return self.binary_editor.getDisassembly().getCurrentMediumLevelILFunction()
		return None

	def getHistoryEntry(self):
		if self.is_navigating_history:
			return None
		memory_addr = self.memory_editor.getCurrentOffset()
		if memory_addr != self.memory_history_addr:
			self.memory_history_addr = memory_addr
		if self.is_raw_disassembly and self.debug_state.connected:
			rel_addr = self.debug_state.modules.absolute_addr_to_relative(self.raw_address)
			return DebugView.DebugViewHistoryEntry(memory_addr, rel_addr, True)
		else:
			address = self.binary_editor.getDisassembly().getCurrentOffset()
			return DebugView.DebugViewHistoryEntry(memory_addr, address, False)

	def navigateToFunction(self, func, offset):
		return self.navigate(offset)

	def navigateToHistoryEntry(self, entry):
		self.is_navigating_history = True
		if hasattr(entry, 'is_raw'):
			self.memory_editor.navigate(entry.memory_addr)
			if entry.is_raw:
				if self.debug_state.connected:
					address = self.debug_state.modules.relative_addr_to_absolute(entry.address)
					self.navigate_raw(address)
			else:
				self.navigate_live(entry.address)

		View.navigateToHistoryEntry(self, entry)
		self.is_navigating_history = False

	def navigate(self, addr):
		# If we're not connected we cannot even check if the address is remote
		if not self.debug_state.connected:
			return self.navigate_live(addr)

		if self.debug_state.memory_view.is_local_addr(addr):
			local_addr = self.debug_state.memory_view.remote_addr_to_local(addr)
			if self.debug_state.bv.read(local_addr, 1) and len(self.debug_state.bv.get_functions_containing(local_addr)) > 0:
				return self.navigate_live(local_addr)

		# This runs into conflicts if some other address space is mapped over
		# where the local BV is currently loaded, but this is was less likely
		# than the user navigating to a function from the UI
		if self.debug_state.bv.read(addr, 1) and len(self.debug_state.bv.get_functions_containing(addr)) > 0:
			return self.navigate_live(addr)

		return self.navigate_raw(addr)

	def navigate_live(self, addr):
		self.show_raw_disassembly(False)
		return self.binary_editor.getDisassembly().navigate(addr)

	def navigate_raw(self, addr):
		if not self.debug_state.connected:
			# Can't navigate to remote addr when disconnected
			return False
		self.raw_address = addr
		self.show_raw_disassembly(True)
		self.load_raw_disassembly(addr)
		return True

	def notifyMemoryChanged(self):
		self.needs_update = True

	def updateTimerEvent(self):
		if self.needs_update:
			self.needs_update = False

			# Refresh the editor
			if not self.debug_state.connected:
				self.memory_editor.navigate(0)
				return

			# self.memory_editor.navigate(self.debug_state.stack_pointer)

	def showEvent(self, event):
		if not event.spontaneous():
			self.update_timer.start()
			self.add_scripting_ref()

	def hideEvent(self, event):
		if not event.spontaneous():
			self.update_timer.stop()

	def shouldBeVisible(self, view_frame):
		if view_frame is None:
			return False
		else:
			return True

	def load_raw_disassembly(self, start_ip):
		# Read a few instructions from rip and disassemble them
		inst_count = 50

		arch_dis = self.debug_state.remote_arch
		rip = self.debug_state.ip

		# Assume the worst, just in case
		read_length = arch_dis.max_instr_length * inst_count
		data = self.debug_state.memory_view.read(start_ip, read_length)

		lines = []

		# Append header line
		tokens = [InstructionTextToken(InstructionTextTokenType.TextToken, "(Code not backed by loaded file, showing only raw disassembly)")]
		contents = DisassemblyTextLine(tokens, start_ip)
		line = LinearDisassemblyLine(LinearDisassemblyLineType.BasicLineType, None, None, contents)
		lines.append(line)

		total_read = 0
		for i in range(inst_count):
			line_addr = start_ip + total_read
			(insn_tokens, length) = arch_dis.get_instruction_text(data[total_read:], line_addr)

			if insn_tokens is None:
				insn_tokens = [InstructionTextToken(InstructionTextTokenType.TextToken, "??")]
				length = arch_dis.instr_alignment
				if length == 0:
					length = 1

			# terrible libshiboken workaround, see #101
			for tok in insn_tokens:
				if tok.value.bit_length() == 64:
					tok.value ^= 0x8000000000000000

			tokens = []
			color = HighlightStandardColor.NoHighlightColor
			if line_addr == rip:
				if self.debug_state.breakpoints.contains_absolute(start_ip + total_read):
					# Breakpoint & pc
					tokens.append(InstructionTextToken(InstructionTextTokenType.TagToken, self.debug_state.ui.get_breakpoint_tag_type().icon + ">", width=5))
					color = HighlightStandardColor.RedHighlightColor
				else:
					# PC
					tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, " ==> "))
					color = HighlightStandardColor.BlueHighlightColor
			else:
				if self.debug_state.breakpoints.contains_absolute(start_ip + total_read):
					# Breakpoint
					tokens.append(InstructionTextToken(InstructionTextTokenType.TagToken, self.debug_state.ui.get_breakpoint_tag_type().icon, width=5))
					color = HighlightStandardColor.RedHighlightColor
				else:
					# Regular line
					tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, "     "))
			# Address
			tokens.append(InstructionTextToken(InstructionTextTokenType.AddressDisplayToken, hex(line_addr)[2:], line_addr))
			tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, "  "))
			tokens.extend(insn_tokens)

			# Convert to linear disassembly line
			contents = DisassemblyTextLine(tokens, line_addr, color=color)
			line = LinearDisassemblyLine(LinearDisassemblyLineType.CodeDisassemblyLineType, None, None, contents)
			lines.append(line)

			total_read += length

		self.binary_text.setLines(lines)

	def show_raw_disassembly(self, raw):
		if raw != self.is_raw_disassembly:
			self.splitter.replaceWidget(0, self.disasm_widget if raw else self.bv_widget)
			self.is_raw_disassembly = raw

	def refresh_raw_disassembly(self):
		if not self.debug_state.connected:
			# Can't navigate to remote addr when disconnected
			return

		if self.is_raw_disassembly:
			self.load_raw_disassembly(self.getCurrentOffset())
Beispiel #14
0
class SettingsTree(QTreeWidget):
    def __init__(self, parent=None):
        super(SettingsTree, self).__init__(parent)

        self._type_checker = TypeChecker()
        self.setItemDelegate(VariantDelegate(self._type_checker, self))

        self.setHeaderLabels(("Setting", "Type", "Value"))
        self.header().setSectionResizeMode(0, QHeaderView.Stretch)
        self.header().setSectionResizeMode(2, QHeaderView.Stretch)

        self.settings = None
        self.refresh_timer = QTimer()
        self.refresh_timer.setInterval(2000)
        self.auto_refresh = False

        self.group_icon = QIcon()
        style = self.style()
        self.group_icon.addPixmap(style.standardPixmap(QStyle.SP_DirClosedIcon),
                                  QIcon.Normal, QIcon.Off)
        self.group_icon.addPixmap(style.standardPixmap(QStyle.SP_DirOpenIcon),
                                  QIcon.Normal, QIcon.On)
        self.key_icon = QIcon()
        self.key_icon.addPixmap(style.standardPixmap(QStyle.SP_FileIcon))

        self.refresh_timer.timeout.connect(self.maybe_refresh)

    def set_settings_object(self, settings):
        self.settings = settings
        self.clear()

        if self.settings is not None:
            self.settings.setParent(self)
            self.refresh()
            if self.auto_refresh:
                self.refresh_timer.start()
        else:
            self.refresh_timer.stop()

    def sizeHint(self):
        return QSize(800, 600)

    def set_auto_refresh(self, autoRefresh):
        self.auto_refresh = autoRefresh

        if self.settings is not None:
            if self.auto_refresh:
                self.maybe_refresh()
                self.refresh_timer.start()
            else:
                self.refresh_timer.stop()

    def set_fallbacks_enabled(self, enabled):
        if self.settings is not None:
            self.settings.setFallbacksEnabled(enabled)
            self.refresh()

    def maybe_refresh(self):
        if self.state() != QAbstractItemView.EditingState:
            self.refresh()

    def refresh(self):
        if self.settings is None:
            return

        # The signal might not be connected.
        try:
            self.itemChanged.disconnect(self.update_setting)
        except:
            pass

        self.settings.sync()
        self.update_child_items(None)

        self.itemChanged.connect(self.update_setting)

    def event(self, event):
        if event.type() == QEvent.WindowActivate:
            if self.isActiveWindow() and self.auto_refresh:
                self.maybe_refresh()

        return super(SettingsTree, self).event(event)

    def update_setting(self, item):
        key = item.text(0)
        ancestor = item.parent()

        while ancestor:
            key = ancestor.text(0) + '/' + key
            ancestor = ancestor.parent()

        d = item.data(2, Qt.UserRole)
        self.settings.setValue(key, item.data(2, Qt.UserRole))

        if self.auto_refresh:
            self.refresh()

    def update_child_items(self, parent):
        divider_index = 0

        for group in self.settings.childGroups():
            child_index = self.find_child(parent, group, divider_index)
            if child_index != -1:
                child = self.child_at(parent, child_index)
                child.setText(1, '')
                child.setText(2, '')
                child.setData(2, Qt.UserRole, None)
                self.move_item_forward(parent, child_index, divider_index)
            else:
                child = self.create_item(group, parent, divider_index)

            child.setIcon(0, self.group_icon)
            divider_index += 1

            self.settings.beginGroup(group)
            self.update_child_items(child)
            self.settings.endGroup()

        for key in self.settings.childKeys():
            child_index = self.find_child(parent, key, 0)
            if child_index == -1 or child_index >= divider_index:
                if child_index != -1:
                    child = self.child_at(parent, child_index)
                    for i in range(child.childCount()):
                        self.delete_item(child, i)
                    self.move_item_forward(parent, child_index, divider_index)
                else:
                    child = self.create_item(key, parent, divider_index)
                child.setIcon(0, self.key_icon)
                divider_index += 1
            else:
                child = self.child_at(parent, child_index)

            value = self.settings.value(key)
            if value is None:
                child.setText(1, 'Invalid')
            else:
                # Try to convert to type unless a QByteArray is received
                if isinstance(value, str):
                    value_type = self._type_checker.type_from_text(value)
                    if value_type:
                        value = self.settings.value(key, type=value_type)
                child.setText(1, value.__class__.__name__)
            child.setText(2, VariantDelegate.displayText(value))
            child.setData(2, Qt.UserRole, value)

        while divider_index < self.child_count(parent):
            self.delete_item(parent, divider_index)

    def create_item(self, text, parent, index):
        after = None

        if index != 0:
            after = self.child_at(parent, index - 1)

        if parent is not None:
            item = QTreeWidgetItem(parent, after)
        else:
            item = QTreeWidgetItem(self, after)

        item.setText(0, text)
        item.setFlags(item.flags() | Qt.ItemIsEditable)
        return item

    def delete_item(self, parent, index):
        if parent is not None:
            item = parent.takeChild(index)
        else:
            item = self.takeTopLevelItem(index)
        del item

    def child_at(self, parent, index):
        if parent is not None:
            return parent.child(index)
        else:
            return self.topLevelItem(index)

    def child_count(self, parent):
        if parent is not None:
            return parent.childCount()
        else:
            return self.topLevelItemCount()

    def find_child(self, parent, text, startIndex):
        for i in range(self.child_count(parent)):
            if self.child_at(parent, i).text(0) == text:
                return i
        return -1

    def move_item_forward(self, parent, oldIndex, newIndex):
        for int in range(oldIndex - newIndex):
            self.delete_item(parent, newIndex)
Beispiel #15
0
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.art_scene = QGraphicsScene(self)
        self.art_scene.addText('Open an image file.')
        self.ui.art_view.setScene(self.art_scene)
        self.symbols_scene = QGraphicsScene(self)
        self.ui.symbols_view.setScene(self.symbols_scene)
        self.ui.action_exit.triggered.connect(self.close)
        self.ui.action_open_art.triggered.connect(self.open_image)
        self.ui.action_open_words.triggered.connect(self.open_words)
        self.ui.action_save.triggered.connect(self.save_pdf)
        self.ui.action_save_png.triggered.connect(self.save_png)
        self.ui.action_shuffle.triggered.connect(self.shuffle)
        self.ui.action_sort.triggered.connect(self.sort)
        self.ui.rows.valueChanged.connect(self.on_options_changed)
        self.ui.columns.valueChanged.connect(self.on_options_changed)
        self.ui.word_clues_radio.toggled.connect(self.on_options_changed)
        self.ui.symbol_clues_radio.toggled.connect(self.on_options_changed)

        self.word_layout = QGridLayout(self.ui.word_content)
        self.ui.word_scroll.setWidgetResizable(True)
        self.word_labels: typing.Dict[str, QLabel] = {}
        self.word_shuffler = WordShuffler([])

        self.clues = None

        self.pixmap = self.scaled_pixmap = self.mini_pixmap = None
        self.sliced_pixmap_item: typing.Optional[QGraphicsPixmapItem] = None
        self.sliced_image: typing.Optional[QImage] = None
        self.selection_grid: typing.Optional[SelectionGrid] = None
        self.cells = []
        self.art_shuffler: typing.Optional[ArtShuffler] = None
        self.symbols_source_pixmap_item: typing.Optional[
            QGraphicsPixmapItem] = None
        self.symbols_pixmap_item: typing.Optional[QGraphicsPixmapItem] = None
        self.symbols_image: typing.Optional[QImage] = None
        self.symbols_shuffler: typing.Optional[ArtShuffler] = None
        self.selected_row: typing.Optional[int] = None
        self.selected_column: typing.Optional[int] = None
        self.settings = QSettings()
        self.image_path: typing.Optional[str] = self.settings.value(
            'image_path')
        self.words_path: typing.Optional[str] = self.settings.value(
            'words_path')

        self.dirty_letters = set()
        self.timer = QTimer()
        self.timer.setInterval(500)
        self.timer.setSingleShot(True)
        # noinspection PyUnresolvedReferences
        self.timer.timeout.connect(self.on_dirty)

        self.row_count = self.column_count = 0
        self.clue_type = ClueType.words
        self.ui.rows.setValue(self.settings.value('row_count', 6, int))
        self.ui.columns.setValue(self.settings.value('column_count', 4, int))
        clue_type_name = self.settings.value('clue_type', ClueType.words.name)
        try:
            clue_type = ClueType[clue_type_name]
        except KeyError:
            clue_type = ClueType.words
        if clue_type == ClueType.words:
            self.ui.word_clues_radio.setChecked(True)
        else:
            self.ui.symbol_clues_radio.setChecked(True)
        self.row_clues: typing.List[QPixmap] = []
        self.column_clues: typing.List[QPixmap] = []
        self.on_options_changed()

    def on_dirty(self):
        for letter in self.dirty_letters:
            self.word_labels[letter].setText(
                self.word_shuffler.make_display(letter))
            self.settings.setValue(f'word_{letter}',
                                   self.word_shuffler[letter])
        if self.dirty_letters:
            self.clues = self.word_shuffler.make_clues()
            self.art_shuffler.clues = dict(self.clues)
            self.dirty_letters.clear()
            self.on_selection_moved()

        if self.pixmap is not None:
            x, y, width, height = self.get_selected_fraction()
            self.settings.setValue('x', x)
            self.settings.setValue('y', y)
            self.settings.setValue('width', width)
            self.settings.setValue('height', height)

        new_rows = self.ui.rows.value()
        new_columns = self.ui.columns.value()
        if self.ui.word_clues_radio.isChecked():
            new_clue_type = ClueType.words
        else:
            new_clue_type = ClueType.symbols
        if (new_rows, new_columns,
                new_clue_type) == (self.row_count, self.column_count,
                                   self.clue_type):
            return
        self.settings.setValue('row_count', new_rows)
        self.settings.setValue('column_count', new_columns)
        self.settings.setValue('clue_type', new_clue_type.name)
        self.row_count, self.column_count = new_rows, new_columns
        self.clue_type = new_clue_type

        word_count = (self.row_count * self.column_count)
        while self.word_layout.count():
            layout_item = self.word_layout.takeAt(0)
            layout_item.widget().deleteLater()
        self.word_labels.clear()

        self.row_clues.clear()
        self.column_clues.clear()
        if self.image_path is not None:
            self.load_image(self.image_path)

        if self.words_path is not None:
            self.load_words(self.words_path)

        letters = [chr(65 + i) for i in range(word_count)]
        if self.word_shuffler.needs_blank:
            letters.insert(0, '')
        word_fields = {}
        for i, letter in enumerate(letters):
            word_field = QLineEdit()
            self.word_layout.addWidget(word_field, i, 0)
            # noinspection PyUnresolvedReferences
            word_field.textEdited.connect(partial(self.on_word_edited, letter))

            word_label = QLabel()
            self.word_layout.addWidget(word_label, i, 1)
            self.word_labels[letter] = word_label
            word_fields[letter] = word_field
        for i, letter in enumerate(letters):
            word = self.settings.value(f'word_{letter}', '')
            self.word_shuffler[letter] = word
            self.dirty_letters.add(letter)
            word_fields[letter].setText(word)

    def on_options_changed(self, *_):
        self.timer.start()

    def shuffle(self):
        self.clues = self.word_shuffler.make_clues()
        if self.art_shuffler is not None:
            self.art_shuffler.shuffle()
            self.on_selection_moved()

    def sort(self):
        if self.art_shuffler is not None:
            self.art_shuffler.sort()
            self.on_selection_moved()

    def open_words(self):
        word_filter = 'Text files (*.txt)'
        if self.words_path is None:
            words_folder = None
        else:
            words_folder = str(Path(self.words_path).parent)
        file_name, _ = QFileDialog.getOpenFileName(self,
                                                   "Open a words file.",
                                                   dir=words_folder,
                                                   filter=word_filter)
        if not file_name:
            return
        self.settings.setValue('words_path', file_name)
        self.load_words(file_name)

    def load_words(self, words_path):
        with open(words_path) as f:
            choice = 0
            if choice == 0:
                self.word_shuffler = WordShuffler(f)
            else:
                self.word_shuffler = WordStripper(f)

    def open_image(self):
        formats = QImageReader.supportedImageFormats()
        patterns = (f'*.{fmt.data().decode()}' for fmt in formats)
        image_filter = f'Images ({" ".join(patterns)})'
        if self.image_path is None:
            image_folder = None
        else:
            image_folder = str(Path(self.image_path).parent)
        file_name, _ = QFileDialog.getOpenFileName(self,
                                                   "Open an image file.",
                                                   dir=image_folder,
                                                   filter=image_filter)
        if not file_name:
            return
        self.settings.setValue('image_path', file_name)
        self.load_image(file_name)

    def load_image(self, image_path):
        self.pixmap = QPixmap(image_path)
        if self.pixmap.isNull():
            self.pixmap = None
        self.image_path = image_path
        self.scale_image()

    def scale_image(self):
        if self.pixmap is None:
            return

        if self.selection_grid is None:
            x = self.settings.value('x', 0.0, float)
            y = self.settings.value('y', 0.0, float)
            width = self.settings.value('width', 1.0, float)
            height = self.settings.value('height', 1.0, float)
        else:
            x, y, width, height = self.get_selected_fraction()
        self.art_scene.clear()
        self.cells.clear()
        view_size = self.ui.art_view.maximumViewportSize()
        if view_size.width() == 0:
            return
        self.art_scene.setSceneRect(0, 0, view_size.width(),
                                    view_size.height())
        display_size = QSize(view_size.width() * 0.99 / 2,
                             view_size.height() * 0.99)
        self.scaled_pixmap = self.pixmap.scaled(
            display_size, aspectMode=Qt.AspectRatioMode.KeepAspectRatio)
        self.art_scene.addPixmap(self.scaled_pixmap)
        scaled_size = self.scaled_pixmap.size()
        self.selection_grid = SelectionGrid(scaled_size.width() * x,
                                            scaled_size.height() * y,
                                            scaled_size.width() * width,
                                            scaled_size.height() * height,
                                            row_count=self.row_count,
                                            column_count=self.column_count)
        self.selection_grid.on_moved = self.on_selection_moved
        self.art_scene.addItem(self.selection_grid)
        self.sliced_image = QImage(display_size,
                                   QImage.Format.Format_ARGB32_Premultiplied)
        self.check_clues()
        self.art_shuffler = ArtShuffler(self.selection_grid.row_count,
                                        self.selection_grid.column_count,
                                        self.sliced_image,
                                        QRect(0, 0, display_size.width(),
                                              display_size.height()),
                                        clues=self.clues,
                                        row_clues=self.row_clues,
                                        column_clues=self.column_clues)
        self.sliced_pixmap_item = self.art_scene.addPixmap(
            QPixmap.fromImage(self.sliced_image))
        self.sliced_pixmap_item.setPos(display_size.width(), 0)

        self.symbols_scene.clear()
        self.symbols_source_pixmap_item = self.symbols_scene.addPixmap(
            self.scaled_pixmap)
        self.symbols_image = QImage(display_size,
                                    QImage.Format.Format_ARGB32_Premultiplied)
        if self.symbols_shuffler is not None:
            selected_row = self.symbols_shuffler.selected_row
            selected_column = self.symbols_shuffler.selected_column
        else:
            selected_row = 0
            selected_column = None
        self.symbols_shuffler = ArtShuffler(self.selection_grid.row_count,
                                            self.selection_grid.column_count,
                                            self.symbols_image,
                                            QRect(0, 0, display_size.width(),
                                                  display_size.height()),
                                            row_clues=self.row_clues,
                                            column_clues=self.column_clues)
        self.symbols_shuffler.selected_row = selected_row
        self.symbols_shuffler.selected_column = selected_column
        self.symbols_pixmap_item = ClickablePixmapItem(
            QPixmap.fromImage(self.symbols_image))
        self.symbols_pixmap_item.on_click = self.on_symbols_clicked
        self.symbols_scene.addItem(self.symbols_pixmap_item)

        self.symbols_pixmap_item.setPos(display_size.width(), 0)

        self.on_selection_moved()

    def on_symbols_clicked(self, event: QGraphicsSceneMouseEvent):
        self.symbols_scene.clearSelection()
        self.symbols_shuffler.select_clue(event.pos().toPoint())
        self.on_selection_moved()

    def on_word_edited(self, letter, word):
        self.word_shuffler[letter] = word
        self.dirty_letters.add(letter)
        self.timer.start()

    def get_selected_fraction(self):
        selection_rect = self.selection_grid.rect()
        selection_pos = self.selection_grid.pos()
        size = self.scaled_pixmap.size()
        x = (selection_pos.x() + selection_rect.x()) / size.width()
        width = selection_rect.width() / size.width()
        y = (selection_pos.y() + selection_rect.y()) / size.height()
        height = selection_rect.height() / size.height()
        return x, y, width, height

    def on_selection_moved(self):
        selected_pixmap = self.get_selected_pixmap()
        self.art_shuffler.draw(selected_pixmap)
        self.sliced_pixmap_item.setPixmap(QPixmap.fromImage(self.sliced_image))

        selected_pixmap = self.get_selected_pixmap()
        cell_width = (selected_pixmap.width() /
                      self.selection_grid.column_count)
        cell_height = (selected_pixmap.height() /
                       self.selection_grid.row_count)
        self.row_clues.clear()
        self.column_clues.clear()
        for i in range(self.selection_grid.row_count):
            clue_image = selected_pixmap.copy(0, i * cell_height, cell_width,
                                              cell_height)
            self.row_clues.append(clue_image)
        for j in range(self.selection_grid.column_count):
            clue_image = selected_pixmap.copy(j * cell_width, 0, cell_width,
                                              cell_height)
            self.column_clues.append(clue_image)
        self.symbols_shuffler.row_clues = self.row_clues
        self.symbols_shuffler.column_clues = self.column_clues

        self.symbols_shuffler.draw_grid(selected_pixmap)
        self.symbols_pixmap_item.setPixmap(
            QPixmap.fromImage(self.symbols_image))
        self.timer.start()

    def get_selected_pixmap(self) -> QPixmap:
        x, y, width, height = self.get_selected_fraction()
        original_size = self.pixmap.size()
        selected_pixmap = self.pixmap.copy(x * original_size.width(),
                                           y * original_size.height(),
                                           width * original_size.width(),
                                           height * original_size.height())
        return selected_pixmap

    def resizeEvent(self, event: QResizeEvent):
        super().resizeEvent(event)
        self.scale_image()

    def save_pdf(self):
        pdf_folder = self.settings.value('pdf_folder')
        file_name, _ = QFileDialog.getSaveFileName(self,
                                                   "Save a PDF file.",
                                                   dir=pdf_folder,
                                                   filter='Documents (*.pdf)')
        if not file_name:
            return
        self.settings.setValue('pdf_folder', os.path.dirname(file_name))
        writer = QPdfWriter(file_name)
        writer.setPageSize(QPageSize(QPageSize.Letter))
        writer.setTitle('Sliced Art Puzzle')
        writer.setCreator('Don Kirkby')
        self.paint_puzzle(writer)

    def save_png(self):
        pdf_folder = self.settings.value('pdf_folder')
        file_name, _ = QFileDialog.getSaveFileName(self,
                                                   "Save an image file.",
                                                   dir=pdf_folder,
                                                   filter='Images (*.png)')
        if not file_name:
            return
        writer = QPixmap(1000, 2000)
        self.paint_puzzle(writer)
        writer.save(file_name)
        self.settings.setValue('pdf_folder', os.path.dirname(file_name))

    def paint_puzzle(self, writer: QPaintDevice):
        self.check_clues()
        painter = QPainter(writer)
        try:
            print_shuffler = ArtShuffler(self.art_shuffler.rows,
                                         self.art_shuffler.cols,
                                         writer,
                                         QRect(0, 0, writer.width(),
                                               round(writer.height() / 2)),
                                         clues=self.clues,
                                         row_clues=self.row_clues,
                                         column_clues=self.column_clues)
            print_shuffler.cells = self.art_shuffler.cells[:]
            print_shuffler.is_shuffled = self.art_shuffler.is_shuffled
            selected_pixmap = self.get_selected_pixmap()
            print_shuffler.draw(selected_pixmap, painter)

            print_shuffler.rect.moveTop(writer.height() / 2)
            print_shuffler.draw_grid(selected_pixmap, painter)
        finally:
            painter.end()

    def check_clues(self):
        if self.clue_type == ClueType.words:
            if self.clues is None:
                self.clues = self.word_shuffler.make_clues()
            self.row_clues.clear()
            self.column_clues.clear()
        else:
            self.clues = None
Beispiel #16
0
class AlarmClock(QWidget):

    started = Signal()
    aborted = Signal()
    finished = Signal()

    def __init__(self, parent=None):

        super().__init__(parent)
        self.alarm = QTime()
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.update_timer = QTimer(self)
        self.update_timer.setInterval(1000)

        self.current_label = QLabel('00:00:00')
        font = QFont()
        font.setPointSize(50)
        self.current_label.setFont(font)
        self.time_edit = QTimeEdit(self)
        self.time_edit.setDisplayFormat('H:mm')
        self.set_around_btn = QPushButton('Set around time', self)

        self.set_ui()
        self.set_connection()
        self.reset()

        self.update_timer.start()
        self.update_label()
        self.set_around_time()

    def set_ui(self):
        self.vlayout = QVBoxLayout(self)
        self.vlayout.addWidget(self.current_label)
        self.vlayout.addWidget(self.time_edit)
        self.vlayout.addWidget(self.set_around_btn)

    def set_connection(self):
        self.timer.timeout.connect(self.stop)
        self.update_timer.timeout.connect(self.update_label)
        self.set_around_btn.clicked.connect(self.set_around_time)

    def set_around_time(self):
        current = QTime.currentTime()
        if current.minute() < 29:
            self.time_edit.setTime(QTime(current.hour(), 30))
        else:
            self.time_edit.setTime(QTime(current.hour() + 1, 0))

    def update_label(self):
        current = QTime.currentTime()
        self.current_label.setText('{hour:02}:{min:02}:{sec:02}'.format(
            hour=current.hour(), min=current.minute(), sec=current.second()))

    def start(self):
        self.time_edit.setEnabled(False)
        self.alarm = self.time_edit.time()
        self.timer.start(QTime.currentTime().msecsTo(self.alarm))
        self.started.emit()

    def abort(self):
        self.reset()
        self.aborted.emit()

    def stop(self):
        self.reset()
        self.finished.emit()

    def reset(self):
        self.timer.stop()
        self.time_edit.setEnabled(True)

    def get_notify_message(self):
        return ''

    @property
    def name(self):
        return 'Alarm Clock'