~nora/qt-sandbox

12714664a6d34aef26a1a9897578657d378b1ac9 — nora 2 months ago b8e5fa5
Add tasks application
A tasks/app.spec => tasks/app.spec +45 -0
@@ 0,0 1,45 @@
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[('resources', 'resources/')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='Tasks',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['resources/icon.ico'],
)

A tasks/main.py => tasks/main.py +14 -0
@@ 0,0 1,14 @@
from pathlib import Path
from PySide6.QtWidgets import QApplication

from package.main_window import MainWindow

SOURCE_FILE = Path(__file__).resolve()
SOURCE_DIR = SOURCE_FILE.parent
DATA_DIR = SOURCE_DIR / "resources"

if __name__ == "__main__":
    app = QApplication()
    win = MainWindow(DATA_DIR)
    win.show()
    app.exec()

A tasks/package/api/task.py => tasks/package/api/task.py +63 -0
@@ 0,0 1,63 @@
from pathlib import Path
import json
import logging

logging.basicConfig(level=logging.DEBUG)

TASKS_DIR = Path.home() / ".tasks"
TASKS_FILE = TASKS_DIR / "tasks.json"


def get_tasks():
    if TASKS_FILE.exists():
        with open(TASKS_FILE, "r") as f:
            return json.load(f)
    return {}


def add_task(name):
    tasks = get_tasks()
    if name in tasks.keys():
        logging.error("Une tâche avec le même nom existe déjà")
        return False

    tasks[name] = False
    _write_tasks(tasks)
    return True


def del_task(name):
    tasks = get_tasks()
    if name not in tasks.keys():
        logging.error("La tâche n'existe pas")
        return False

    del tasks[name]
    _write_tasks(tasks)
    return True


def set_task_status(name, done=True):
    tasks = get_tasks()
    if name not in tasks.keys():
        logging.error("La tâche n'existe pas")
        return False

    tasks[name] = done
    _write_tasks(tasks)
    return True


def _write_tasks(tasks):
    if not TASKS_DIR.exists():
        TASKS_DIR.mkdir()

    with open(TASKS_FILE, "w") as f:
        json.dump(tasks, f, indent=4)
        logging.info("Les tâches ont bien été mises à jour")


if __name__ == "__main__":
    add_task("Task 1")
    set_task_status("Task 1")
    del_task("Task 1")

A tasks/package/main_window.py => tasks/package/main_window.py +144 -0
@@ 0,0 1,144 @@
from pathlib import Path
from PySide6 import QtWidgets, QtGui, QtCore

import package.api.task


COLORS = {False: (235, 64, 52), True: (160, 237, 83)}


class TaskItem(QtWidgets.QListWidgetItem):
    def __init__(self, name, done, list_widget):
        super().__init__(name)

        self.listWidget = list_widget
        self.name = name
        self.done = done

        self.listWidget.addItem(self)
        self.set_background_color()

    def toggle_state(self):
        self.done = not self.done
        package.api.task.set_task_status(self.name, self.done)
        self.set_background_color()

    def set_background_color(self):
        color = COLORS.get(self.done)
        self.setBackground(QtGui.QColor(*color))
        color_str = ", ".join(map(str, color))
        stylesheet = f"""
        QListView::item:selected {{
            background: rgb({color_str});
            color: rgb(0, 0, 0);
        }}"""
        self.listWidget.setStyleSheet(stylesheet)


class MainWindow(QtWidgets.QWidget):
    def __init__(self, app_dir):
        super().__init__()
        self.app_dir = app_dir
        self.setup_ui()
        self.get_tasks()

    def setup_ui(self):
        self.create_widgets()
        self.create_layouts()
        self.modify_widgets()
        self.add_widgets_to_layouts()
        self.setup_connections()

    def create_widgets(self):
        self.lw_tasks = QtWidgets.QListWidget()
        self.btn_add = QtWidgets.QPushButton()
        self.btn_clean = QtWidgets.QPushButton()
        self.btn_quit = QtWidgets.QPushButton()

        self.tray = QtWidgets.QSystemTrayIcon()

    def modify_widgets(self):
        self.setWindowTitle("Tasks")
        self.setWindowIcon(QtGui.QIcon(str(self.app_dir / "icon.ico")))
        self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)

        stylesheet = f"""\
            QPushButton {{
                border: none;
            }}
            QListView {{
                border: none;
            }}
            QListView::item {{
                height: 50px;
            }}"""
        self.setStyleSheet(stylesheet)

        self.main_layout.setContentsMargins(0, 0, 0, 0)
        self.main_layout.setSpacing(0)

        self.btn_add.setIcon(QtGui.QIcon(str(self.app_dir / "add.svg")))
        self.btn_clean.setIcon(QtGui.QIcon(str(self.app_dir / "clean.svg")))
        self.btn_quit.setIcon(QtGui.QIcon(str(self.app_dir / "close.svg")))

        self.btn_add.setFixedSize(36, 36)
        self.btn_clean.setFixedSize(36, 36)
        self.btn_quit.setFixedSize(36, 36)

        self.lw_tasks.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.lw_tasks.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        self.tray.setIcon(QtGui.QIcon(str(self.app_dir / "icon.png")))
        self.tray.setVisible(True)

    def create_layouts(self):
        self.main_layout = QtWidgets.QVBoxLayout(self)
        self.layout_buttons = QtWidgets.QHBoxLayout()

    def add_widgets_to_layouts(self):
        self.main_layout.addWidget(self.lw_tasks)
        self.main_layout.addLayout(self.layout_buttons)

        self.layout_buttons.addWidget(self.btn_add)
        self.layout_buttons.addStretch()
        self.layout_buttons.addWidget(self.btn_clean)
        self.layout_buttons.addWidget(self.btn_quit)

    def setup_connections(self):
        self.lw_tasks.itemClicked.connect(lambda lw_item: lw_item.toggle_state())
        self.btn_add.clicked.connect(self.add_tasks)
        self.btn_clean.clicked.connect(self.clean_tasks)
        self.btn_quit.clicked.connect(self.close)
        self.tray.activated.connect(self.tray_clicked)

    def add_tasks(self):
        task_name, success = QtWidgets.QInputDialog.getText(self,
                                                            "Ajouter une tâche",
                                                            "Nom de la tâche :")
        if success and task_name:
            package.api.task.add_task(task_name)
            self.get_tasks()

    def get_tasks(self):
        self.lw_tasks.clear()
        tasks = package.api.task.get_tasks()
        for task_name, done in tasks.items():
            TaskItem(name=task_name, done=done, list_widget=self.lw_tasks)

    def clean_tasks(self):
        for i in range(self.lw_tasks.count()):
            lw_item = self.lw_tasks.item(i)
            if lw_item.done:
                package.api.task.del_task(lw_item.name)
        self.get_tasks()

    def tray_clicked(self):
        if self.isHidden():
            self.showNormal()
        else:
            self.hide()

    def center_under_tray(self):
        tray_x = self.tray.geometry().x()
        w, h = self.sizeHint().toTuple()
        self.move(tray_x - (w / 2), 25)

A tasks/resources/add.svg => tasks/resources/add.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
\ No newline at end of file

A tasks/resources/clean.svg => tasks/resources/clean.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3v10zM14 5h-3l-1-1H6L5 5H2v2h12z"/><path fill="none" d="M0 0h24v24H0z"/></svg>
\ No newline at end of file

A tasks/resources/close.svg => tasks/resources/close.svg +1 -0
@@ 0,0 1,1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"/></svg>
\ No newline at end of file

A tasks/resources/icon.ico => tasks/resources/icon.ico +0 -0
A tasks/resources/icon.png => tasks/resources/icon.png +0 -0