commit e84f2bec81485c76c8e2ed43986343953fa7f139 Author: 283375 Date: Wed Sep 13 03:40:02 2023 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e62763 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +__debug*.py +/projects +/.vscode + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/project.py b/project.py new file mode 100644 index 0000000..69d6072 --- /dev/null +++ b/project.py @@ -0,0 +1,211 @@ +import importlib +import json +import logging +import os +import re +import time +from copy import deepcopy +from functools import cached_property +from pathlib import Path +from typing import Any +from hashlib import md5 +from io import BytesIO + +import cv2 + +PROJECTS_ROOT_PATH = Path("projects") +ACCEPT_EXTS = [".jpg", ".png"] + + +class Project: + path: Path + + def __init__(self, path: Path): + self.path = path + self._tagValueDict = {} + with open(self.path / "project.json", "r", encoding="utf-8") as jf: + projectJson = json.loads(jf.read()) + self._tagValueDict: dict[str, Any] = projectJson["tagValueMap"] + self.name = projectJson.get("name", self.path.name) + self._tags = list(self._tagValueDict.keys()) + self._values = list(self._tagValueDict.values()) + + def __repr__(self): + return f"Project(path={repr(self.path)})" + + @cached_property + def tags(self): + return deepcopy(self._tags) + + @cached_property + def values(self): + return deepcopy(self.values) + + @cached_property + def tagValueMap(self): + return deepcopy(self._tagValueDict) + + @cached_property + def tagsReExp(self): + tagsDivided = "|".join(str(tag) for tag in self.tags) + return re.compile(f"^({tagsDivided})\\^") + + @cached_property + def sourcesPath(self): + return self.path / "sources" + + @cached_property + def samplesPath(self): + return self.path / "samples" + + @cached_property + def samplesUnclassifiedPath(self): + return self.samplesPath / "unclassified" + + @cached_property + def samplesClassifiedPath(self): + return self.samplesPath / "classified" + + @cached_property + def samplesIgnoredPath(self): + return self.samplesPath / "ignored" + + def createFolders(self): + folders = [ + self.sourcesPath, + self.samplesClassifiedPath, + self.samplesUnclassifiedPath, + self.samplesIgnoredPath, + ] + + for folder in folders: + folder.mkdir(parents=True, exist_ok=True) + + def listPathFiles(self, path: Path, acceptSuffixes: list[str] = ACCEPT_EXTS): + return [p for p in path.glob("**/*") if p.suffix in acceptSuffixes] + + @property + def sources(self): + return self.listPathFiles(self.sourcesPath) + + @property + def samples(self): + return self.listPathFiles(self.samplesPath) + + @property + def samplesUnclassified(self): + return self.listPathFiles(self.samplesUnclassifiedPath) + + @property + def samplesClassified(self): + return self.listPathFiles(self.samplesClassifiedPath) + + @property + def samplesIgnored(self): + return self.listPathFiles(self.samplesIgnoredPath) + + def samplesByTag(self, tag: str): + if tag not in self.tags: + raise ValueError(f'Unknown tag "{tag}"') + + samples = self.samples + return [p for p in samples if p.stem.startswith(f"{tag}^")] + + def extractYield(self): + cwdPath = Path(os.getcwd()) + importParts = [ + *self.path.resolve().relative_to(cwdPath.resolve()).parts, + "extract", + ] + importName = ".".join(importParts) + projectExtractModule = importlib.import_module(importName) + getSamples = projectExtractModule.getSamples + assert callable(getSamples) + + extractLogger = logging.getLogger( + f"extract-{self.name}-{int(time.time() * 1000)}" + ) + + extractLogger.info("Reading existing samples MD5...") + # existingSamplesMd5 = [ + # self.getSampleOriginalFileName(sample).split(".")[0] for sample in samples + # ] + existingSamplesMd5 = [] + for sample in self.samples: + with open(sample, "rb") as sf: + existingSamplesMd5.append(md5(sf.read()).hexdigest()) + + sources = self.sources + sourcesNum = len(sources) + for i, source in enumerate(sources): + try: + extractLogger.info(f"Extracting {source.resolve()}") + samples = getSamples(source) + for sample in samples: + success, sampleBuffer = cv2.imencode(".jpg", sample) + if not success: + extractLogger.warning( + f"cv2 cannot encode {sampleMd5} from {source.name}, skipping" + ) + continue + + sampleMd5 = md5(sampleBuffer).hexdigest() + if sampleMd5 in existingSamplesMd5: + extractLogger.debug(f"{sampleMd5} from {source.name} skipped") + continue + + extractLogger.info(f"{sampleMd5} <- {source.name}") + sampleSavePath = self.samplesUnclassifiedPath / f"{sampleMd5}.jpg" + with open(sampleSavePath, "wb") as sf: + sf.write(sampleBuffer) + existingSamplesMd5.append(sampleMd5) + except Exception: + extractLogger.exception(f"Error extracting {source.resolve()}") + finally: + yield (source, i, sourcesNum) + + def extract(self): + list(self.extractYield()) + + def getSampleOriginalFileName(self, sample: Path): + return self.tagsReExp.sub("", sample.name) + + def classify(self, sample: Path, tag: str): + if tag not in self.tags: + raise ValueError(f'Unknown tag "{tag}"') + + originalFileName = self.getSampleOriginalFileName(sample) + classifiedFileName = f"{tag}^{originalFileName}" + return sample.rename(self.samplesClassifiedPath / classifiedFileName) + + def unclassify(self, sample: Path): + originalFileName = self.getSampleOriginalFileName(sample) + return sample.rename(self.samplesUnclassifiedPath / originalFileName) + + def ignore(self, sample: Path): + originalFileName = self.getSampleOriginalFileName(sample) + return sample.rename(self.samplesIgnoredPath / originalFileName) + + +class Projects: + def __init__(self, rootFolderPath=PROJECTS_ROOT_PATH): + self.rootFolderPath = rootFolderPath + self.projects: list[Project] = [] + self.detectProjects() + + def detectProjects(self): + self.projects.clear() + + folders = [p for p in self.rootFolderPath.iterdir() if p.is_dir()] + for folder in folders: + if not (folder / "project.json").exists(): + continue + project = Project(folder) + if not ( + project.sourcesPath.exists() + and project.samplesClassifiedPath.exists() + and project.samplesUnclassifiedPath.exists() + and project.samplesIgnoredPath.exists() + ): + continue + self.projects.append(project) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ca5f507 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.black] +force-exclude = ''' +( + .*_rc.py + | .*_ui.py +) +''' + +[tool.isort] +profile = "black" +extend_skip_glob = ["*_rc.py", "*_ui.py"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..41e5ac7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tqdm==4.66.1 +PySide6==6.5.2 diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/components/logItemDetails.ui b/ui/components/logItemDetails.ui new file mode 100644 index 0000000..00c6095 --- /dev/null +++ b/ui/components/logItemDetails.ui @@ -0,0 +1,19 @@ + + + LogItemDetails + + + + 0 + 0 + 603 + 469 + + + + LogItemDetails + + + + + diff --git a/ui/components/logItemDetails_ui.py b/ui/components/logItemDetails_ui.py new file mode 100644 index 0000000..1d813a6 --- /dev/null +++ b/ui/components/logItemDetails_ui.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'logItemDetails.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QSizePolicy, QWidget) + +class Ui_LogItemDetails(object): + def setupUi(self, LogItemDetails): + if not LogItemDetails.objectName(): + LogItemDetails.setObjectName(u"LogItemDetails") + LogItemDetails.resize(603, 469) + LogItemDetails.setWindowTitle(u"LogItemDetails") + + self.retranslateUi(LogItemDetails) + + QMetaObject.connectSlotsByName(LogItemDetails) + # setupUi + + def retranslateUi(self, LogItemDetails): + pass + # retranslateUi + diff --git a/ui/components/logViewer.ui b/ui/components/logViewer.ui new file mode 100644 index 0000000..ee015b3 --- /dev/null +++ b/ui/components/logViewer.ui @@ -0,0 +1,94 @@ + + + LogViewer + + + + 0 + 0 + 597 + 443 + + + + LogViewer + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Group + + + + + + + + + + INFO + + + + + + + 5 + + + 1 + + + 1 + + + 2 + + + true + + + Qt::Horizontal + + + QSlider::TicksBelow + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 25 + 10 + + + + + + + + + + + + + + + diff --git a/ui/components/logViewer_ui.py b/ui/components/logViewer_ui.py new file mode 100644 index 0000000..8119622 --- /dev/null +++ b/ui/components/logViewer_ui.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'logViewer.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout, + QHeaderView, QLabel, QSizePolicy, QSlider, + QSpacerItem, QTreeView, QVBoxLayout, QWidget) + +class Ui_LogViewer(object): + def setupUi(self, LogViewer): + if not LogViewer.objectName(): + LogViewer.setObjectName(u"LogViewer") + LogViewer.resize(597, 443) + LogViewer.setWindowTitle(u"LogViewer") + self.verticalLayout = QVBoxLayout(LogViewer) + self.verticalLayout.setObjectName(u"verticalLayout") + self.frame = QFrame(LogViewer) + self.frame.setObjectName(u"frame") + self.frame.setFrameShape(QFrame.StyledPanel) + self.frame.setFrameShadow(QFrame.Raised) + self.gridLayout = QGridLayout(self.frame) + self.gridLayout.setObjectName(u"gridLayout") + self.label = QLabel(self.frame) + self.label.setObjectName(u"label") + + self.gridLayout.addWidget(self.label, 0, 3, 1, 1) + + self.groupComboBox = QComboBox(self.frame) + self.groupComboBox.setObjectName(u"groupComboBox") + + self.gridLayout.addWidget(self.groupComboBox, 0, 4, 1, 1) + + self.levelLabel = QLabel(self.frame) + self.levelLabel.setObjectName(u"levelLabel") + + self.gridLayout.addWidget(self.levelLabel, 0, 0, 1, 1) + + self.levelSlider = QSlider(self.frame) + self.levelSlider.setObjectName(u"levelSlider") + self.levelSlider.setMaximum(5) + self.levelSlider.setSingleStep(1) + self.levelSlider.setPageStep(1) + self.levelSlider.setValue(2) + self.levelSlider.setTracking(True) + self.levelSlider.setOrientation(Qt.Horizontal) + self.levelSlider.setTickPosition(QSlider.TicksBelow) + + self.gridLayout.addWidget(self.levelSlider, 0, 1, 1, 1) + + self.horizontalSpacer = QSpacerItem(25, 10, QSizePolicy.Preferred, QSizePolicy.Minimum) + + self.gridLayout.addItem(self.horizontalSpacer, 0, 2, 1, 1) + + self.gridLayout.setColumnStretch(1, 1) + self.gridLayout.setColumnStretch(4, 1) + + self.verticalLayout.addWidget(self.frame) + + self.logTreeView = QTreeView(LogViewer) + self.logTreeView.setObjectName(u"logTreeView") + + self.verticalLayout.addWidget(self.logTreeView) + + + self.retranslateUi(LogViewer) + + QMetaObject.connectSlotsByName(LogViewer) + # setupUi + + def retranslateUi(self, LogViewer): + self.label.setText(QCoreApplication.translate("LogViewer", u"Group", None)) + self.levelLabel.setText(QCoreApplication.translate("LogViewer", u"INFO", None)) + pass + # retranslateUi + diff --git a/ui/components/projectEntry.py b/ui/components/projectEntry.py new file mode 100644 index 0000000..0105a9f --- /dev/null +++ b/ui/components/projectEntry.py @@ -0,0 +1,33 @@ +from PySide6.QtWidgets import QWidget + +from project import Project + +from .projectEntry_ui import Ui_ProjectEntry + + +class ProjectEntry(Ui_ProjectEntry, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + self.project = None + + def setProject(self, project: Project): + self.project = project + self.updateLabels() + + def updateLabels(self): + if not self.project: + self.projectNameLabel.setText("-") + self.projectDescriptionLabel.setText("-") + return + + self.projectNameLabel.setText(self.project.name) + self.projectDescriptionLabel.setText( + "
".join( + [ + str(self.project.path.resolve()), + f"{len(self.project.sources)} sources", + f"{len(self.project.samples)} samples ({len(self.project.samplesUnclassified)} unclassified)", + ] + ) + ) diff --git a/ui/components/projectEntry.ui b/ui/components/projectEntry.ui new file mode 100644 index 0000000..2edb353 --- /dev/null +++ b/ui/components/projectEntry.ui @@ -0,0 +1,116 @@ + + + ProjectEntry + + + + 0 + 0 + 605 + 488 + + + + projectEntry + + + + + + 0 + + + + Manage + + + + + + Extract + + + + + + + + 14 + true + + + + - + + + + + + + ... + + + + + + + - + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Classify + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + SamplesListWidget + QListWidget +
ui.components.samplesListWidget
+
+
+ + +
diff --git a/ui/components/projectEntry_ui.py b/ui/components/projectEntry_ui.py new file mode 100644 index 0000000..78bb438 --- /dev/null +++ b/ui/components/projectEntry_ui.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'projectEntry.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QGridLayout, QHBoxLayout, QLabel, + QListWidget, QListWidgetItem, QPushButton, QSizePolicy, + QSpacerItem, QTabWidget, QVBoxLayout, QWidget) + +from ui.components.samplesListWidget import SamplesListWidget + +class Ui_ProjectEntry(object): + def setupUi(self, ProjectEntry): + if not ProjectEntry.objectName(): + ProjectEntry.setObjectName(u"ProjectEntry") + ProjectEntry.resize(605, 488) + self.verticalLayout = QVBoxLayout(ProjectEntry) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tabWidget = QTabWidget(ProjectEntry) + self.tabWidget.setObjectName(u"tabWidget") + self.tabManage = QWidget() + self.tabManage.setObjectName(u"tabManage") + self.gridLayout = QGridLayout(self.tabManage) + self.gridLayout.setObjectName(u"gridLayout") + self.pushButton = QPushButton(self.tabManage) + self.pushButton.setObjectName(u"pushButton") + + self.gridLayout.addWidget(self.pushButton, 2, 0, 1, 1) + + self.projectNameLabel = QLabel(self.tabManage) + self.projectNameLabel.setObjectName(u"projectNameLabel") + font = QFont() + font.setPointSize(14) + font.setBold(True) + self.projectNameLabel.setFont(font) + + self.gridLayout.addWidget(self.projectNameLabel, 0, 0, 1, 2) + + self.pushButton_2 = QPushButton(self.tabManage) + self.pushButton_2.setObjectName(u"pushButton_2") + + self.gridLayout.addWidget(self.pushButton_2, 2, 1, 1, 1) + + self.projectDescriptionLabel = QLabel(self.tabManage) + self.projectDescriptionLabel.setObjectName(u"projectDescriptionLabel") + + self.gridLayout.addWidget(self.projectDescriptionLabel, 1, 0, 1, 2) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.gridLayout.addItem(self.verticalSpacer, 3, 0, 1, 2) + + self.tabWidget.addTab(self.tabManage, "") + self.tabClassify = QWidget() + self.tabClassify.setObjectName(u"tabClassify") + self.horizontalLayout = QHBoxLayout(self.tabClassify) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.unclassifiedListWidget = SamplesListWidget(self.tabClassify) + self.unclassifiedListWidget.setObjectName(u"unclassifiedListWidget") + + self.horizontalLayout.addWidget(self.unclassifiedListWidget) + + self.verticalLayout_2 = QVBoxLayout() + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.tagsListWidget = QListWidget(self.tabClassify) + self.tagsListWidget.setObjectName(u"tagsListWidget") + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tagsListWidget.sizePolicy().hasHeightForWidth()) + self.tagsListWidget.setSizePolicy(sizePolicy) + + self.verticalLayout_2.addWidget(self.tagsListWidget) + + self.classfiedListWidget = SamplesListWidget(self.tabClassify) + self.classfiedListWidget.setObjectName(u"classfiedListWidget") + + self.verticalLayout_2.addWidget(self.classfiedListWidget) + + + self.horizontalLayout.addLayout(self.verticalLayout_2) + + self.tabWidget.addTab(self.tabClassify, "") + + self.verticalLayout.addWidget(self.tabWidget) + + + self.retranslateUi(ProjectEntry) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(ProjectEntry) + # setupUi + + def retranslateUi(self, ProjectEntry): + ProjectEntry.setWindowTitle(QCoreApplication.translate("ProjectEntry", u"projectEntry", None)) + self.pushButton.setText(QCoreApplication.translate("ProjectEntry", u"Extract", None)) + self.projectNameLabel.setText(QCoreApplication.translate("ProjectEntry", u"-", None)) + self.pushButton_2.setText(QCoreApplication.translate("ProjectEntry", u"...", None)) + self.projectDescriptionLabel.setText(QCoreApplication.translate("ProjectEntry", u"-", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabManage), QCoreApplication.translate("ProjectEntry", u"Manage", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabClassify), QCoreApplication.translate("ProjectEntry", u"Classify", None)) + # retranslateUi + diff --git a/ui/components/samplesListWidget.py b/ui/components/samplesListWidget.py new file mode 100644 index 0000000..03e4209 --- /dev/null +++ b/ui/components/samplesListWidget.py @@ -0,0 +1,6 @@ +from PySide6.QtWidgets import QListWidget + + +class SamplesListWidget(QListWidget): + def __init__(self, parent=None): + super().__init__(parent) diff --git a/ui/mainWindow.py b/ui/mainWindow.py new file mode 100644 index 0000000..57de834 --- /dev/null +++ b/ui/mainWindow.py @@ -0,0 +1,43 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QListWidgetItem, QMainWindow, QSizePolicy, QWidget + +from .mainWindow_ui import Ui_MainWindow +from .tabs.tabProjects import TabProjects + + +class MainWindow(Ui_MainWindow, QMainWindow): + TabWidgetTypeRole = Qt.ItemDataRole.UserRole + 10 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.tabsListWidget.itemClicked.connect(self.setItemSelected) + self.tabsListWidget.currentItemChanged.connect(self.switchTab) + + projectsItem = QListWidgetItem("Projects", self.tabsListWidget) + projectsItem.setData(self.TabWidgetTypeRole, TabProjects) + self.tabsListWidget.addItem(projectsItem) + + self.tabsListWidget.setMaximumWidth( + self.tabsListWidget.sizeHintForColumn(0) + 10 + ) + + self.tabsListWidget.setCurrentIndex(self.tabsListWidget.model().index(0, 0)) + + def setItemSelected(self, item: QListWidgetItem): + item.setSelected(True) + + def switchTab(self): + item = self.tabsListWidget.currentItem() + + tabWidgetType = item.data(self.TabWidgetTypeRole) + tabWidget: QWidget = tabWidgetType(self) + # tabWidget.setSizePolicy( + # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred + # ) + + self.centralWidget().layout().removeWidget(self.tabWidget) + self.tabWidget.deleteLater() + self.centralWidget().layout().addWidget(tabWidget) + self.tabWidget = tabWidget diff --git a/ui/mainWindow.ui b/ui/mainWindow.ui new file mode 100644 index 0000000..017c3af --- /dev/null +++ b/ui/mainWindow.ui @@ -0,0 +1,43 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + diff --git a/ui/mainWindow_ui.py b/ui/mainWindow_ui.py new file mode 100644 index 0000000..fbc5ef5 --- /dev/null +++ b/ui/mainWindow_ui.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'mainWindow.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QListWidget, QListWidgetItem, + QMainWindow, QSizePolicy, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.horizontalLayout = QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.tabsListWidget = QListWidget(self.centralwidget) + self.tabsListWidget.setObjectName(u"tabsListWidget") + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tabsListWidget.sizePolicy().hasHeightForWidth()) + self.tabsListWidget.setSizePolicy(sizePolicy) + + self.horizontalLayout.addWidget(self.tabsListWidget) + + self.tabWidget = QWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth()) + self.tabWidget.setSizePolicy(sizePolicy1) + + self.horizontalLayout.addWidget(self.tabWidget) + + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) + # retranslateUi + diff --git a/ui/tabs/tabProjects.py b/ui/tabs/tabProjects.py new file mode 100644 index 0000000..2de9b49 --- /dev/null +++ b/ui/tabs/tabProjects.py @@ -0,0 +1,53 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QListWidgetItem, QWidget + +from project import Project, Projects + +from ..components.projectEntry import ProjectEntry +from .tabProjects_ui import Ui_TabProjects + + +class ProjectListWidgetItem(QListWidgetItem): + ProjectRole = Qt.ItemDataRole.UserRole + + def __init__(self, project: Project, parent=None): + super().__init__(parent, QListWidgetItem.ItemType.Type) + self.setData(Qt.ItemDataRole.DisplayRole, project.name) + self.setData(self.ProjectRole, project) + + +class TabProjects(Ui_TabProjects, QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + + self.projectListWidget.itemClicked.connect( + self.projectListWidget.setCurrentItem + ) + self.projectListWidget.currentItemChanged.connect(self.switchProject) + + self.detectProjects() + + def detectProjects(self): + ps = Projects() + ps.detectProjects() + projects = ps.projects + for project in projects: + item = ProjectListWidgetItem(project, self.projectListWidget) + self.projectListWidget.addItem(item) + + self.projectListWidget.setMaximumWidth( + self.projectListWidget.sizeHintForColumn(0) + 10 + ) + + def switchProject(self): + item = self.projectListWidget.currentItem() + project: Project = item.data(ProjectListWidgetItem.ProjectRole) + + projectEntry = ProjectEntry(self) + projectEntry.setProject(project) + + self.layout().removeWidget(self.projectEntry) + self.projectEntry.deleteLater() + self.layout().addWidget(projectEntry) + self.projectEntry = projectEntry diff --git a/ui/tabs/tabProjects.ui b/ui/tabs/tabProjects.ui new file mode 100644 index 0000000..9224fbe --- /dev/null +++ b/ui/tabs/tabProjects.ui @@ -0,0 +1,41 @@ + + + TabProjects + + + + 0 + 0 + 701 + 582 + + + + TabProjects + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + diff --git a/ui/tabs/tabProjects_ui.py b/ui/tabs/tabProjects_ui.py new file mode 100644 index 0000000..153be87 --- /dev/null +++ b/ui/tabs/tabProjects_ui.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tabProjects.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QListWidget, QListWidgetItem, + QSizePolicy, QWidget) + +class Ui_TabProjects(object): + def setupUi(self, TabProjects): + if not TabProjects.objectName(): + TabProjects.setObjectName(u"TabProjects") + TabProjects.resize(701, 582) + TabProjects.setWindowTitle(u"TabProjects") + self.horizontalLayout = QHBoxLayout(TabProjects) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.projectListWidget = QListWidget(TabProjects) + self.projectListWidget.setObjectName(u"projectListWidget") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.projectListWidget.sizePolicy().hasHeightForWidth()) + self.projectListWidget.setSizePolicy(sizePolicy) + + self.horizontalLayout.addWidget(self.projectListWidget) + + self.projectEntry = QWidget(TabProjects) + self.projectEntry.setObjectName(u"projectEntry") + sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.projectEntry.sizePolicy().hasHeightForWidth()) + self.projectEntry.setSizePolicy(sizePolicy1) + + self.horizontalLayout.addWidget(self.projectEntry) + + + self.retranslateUi(TabProjects) + + QMetaObject.connectSlotsByName(TabProjects) + # setupUi + + def retranslateUi(self, TabProjects): + pass + # retranslateUi + diff --git a/uiIndex.py b/uiIndex.py new file mode 100644 index 0000000..dfabef9 --- /dev/null +++ b/uiIndex.py @@ -0,0 +1,11 @@ +import sys + +from PySide6.QtWidgets import QApplication + +from ui.mainWindow import MainWindow + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec())