├── src ├── addons │ ├── colorpicker │ │ ├── __init__.py │ │ ├── vcolorpicker │ │ │ ├── __main__.py │ │ │ ├── ui │ │ │ │ ├── exit.ico │ │ │ │ ├── img.qrc │ │ │ │ └── img_rc.py │ │ │ ├── __init__.py │ │ │ ├── img.py │ │ │ └── vcolorpicker.py │ │ ├── icons │ │ │ └── copy-icon.svg │ │ └── colorpicker.py │ ├── notes │ │ ├── icon.png │ │ ├── notes.qss │ │ ├── notes_save.py │ │ └── notes.py │ ├── Settings │ │ ├── icon.png │ │ ├── structure.py │ │ └── Settings.py │ ├── shortcuts │ │ ├── icon.png │ │ ├── shortcuts.py │ │ └── dialog.py │ ├── youtube_downloader │ │ └── icon.png │ └── DertoEarlyTests │ │ ├── TURNOFF_SteamDiscord.pyw │ │ ├── ttf2woff.pyw │ │ ├── 100to1image.pyw │ │ └── ss_overlay.pyw ├── ui │ ├── base_window │ │ ├── __init__.py │ │ ├── tab_widget.py │ │ ├── base_window.py │ │ └── title_bar_layer.py │ ├── icons │ │ ├── icon.png │ │ ├── Ellipse 15.png │ │ ├── Ellipse 16.png │ │ ├── red_button.png │ │ ├── edit_button.png │ │ ├── green_button.png │ │ ├── yellow_buton.png │ │ ├── yellow_button.png │ │ ├── FlowBuddy_Icon.png │ │ ├── red_button_long.png │ │ ├── edit_button_hover.png │ │ ├── green_button_hover.png │ │ ├── green_button_long.png │ │ ├── red_button_hover.png │ │ ├── yellow_button_long.png │ │ ├── yellow_button_hover.png │ │ ├── default_launcher_icon.png │ │ ├── green_button_hover_long.png │ │ ├── red_button_hover_long.png │ │ └── yellow_button_hover_long.png │ ├── fonts │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Medium.ttf │ │ ├── Montserrat-Regular.ttf │ │ └── Montserrat-SemiBold.ttf │ ├── __init__.py │ ├── entry_box.py │ ├── utils.py │ ├── dialog.py │ ├── logo.py │ ├── custom_button.py │ ├── tooltip.py │ └── launcher_design.drawio ├── utils │ ├── __init__.py │ ├── buttons.py │ ├── colors.py │ ├── signal.py │ └── hot_keys.py ├── temporary_settings_launcher.py ├── main.py ├── settings.py ├── FileSystem.py ├── SaveFile.py ├── addon.py └── launcher.py ├── .gitattributes ├── requirements_dev.txt ├── tests ├── stand_alone_dialog.py ├── test_shortcuts.py ├── run_notes.py ├── test_components.py ├── test_notes.py ├── tab_window.py ├── test.py └── test_SaveFile.py ├── requirements.txt ├── TODO.md ├── .github └── workflows │ └── formatter.yml ├── .gitignore └── README.md /src/addons/colorpicker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/base_window/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_window import BaseWindow, TabsWindow -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .hot_keys import HotKeys 2 | from .signal import Signal -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/ui/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/icon.png -------------------------------------------------------------------------------- /src/addons/notes/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/addons/notes/icon.png -------------------------------------------------------------------------------- /src/ui/icons/Ellipse 15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/Ellipse 15.png -------------------------------------------------------------------------------- /src/ui/icons/Ellipse 16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/Ellipse 16.png -------------------------------------------------------------------------------- /src/ui/icons/red_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/red_button.png -------------------------------------------------------------------------------- /src/addons/Settings/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/addons/Settings/icon.png -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/__main__.py: -------------------------------------------------------------------------------- 1 | from vcolorpicker import getColor 2 | 3 | print(getColor()) 4 | -------------------------------------------------------------------------------- /src/addons/shortcuts/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/addons/shortcuts/icon.png -------------------------------------------------------------------------------- /src/ui/icons/edit_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/edit_button.png -------------------------------------------------------------------------------- /src/ui/icons/green_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/green_button.png -------------------------------------------------------------------------------- /src/ui/icons/yellow_buton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/yellow_buton.png -------------------------------------------------------------------------------- /src/ui/icons/yellow_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/yellow_button.png -------------------------------------------------------------------------------- /src/ui/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /src/ui/icons/FlowBuddy_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/FlowBuddy_Icon.png -------------------------------------------------------------------------------- /src/ui/icons/red_button_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/red_button_long.png -------------------------------------------------------------------------------- /src/ui/fonts/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/fonts/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /src/ui/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /src/ui/icons/edit_button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/edit_button_hover.png -------------------------------------------------------------------------------- /src/ui/icons/green_button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/green_button_hover.png -------------------------------------------------------------------------------- /src/ui/icons/green_button_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/green_button_long.png -------------------------------------------------------------------------------- /src/ui/icons/red_button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/red_button_hover.png -------------------------------------------------------------------------------- /src/ui/icons/yellow_button_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/yellow_button_long.png -------------------------------------------------------------------------------- /src/ui/fonts/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/fonts/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /src/ui/icons/yellow_button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/yellow_button_hover.png -------------------------------------------------------------------------------- /src/addons/youtube_downloader/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/addons/youtube_downloader/icon.png -------------------------------------------------------------------------------- /src/ui/icons/default_launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/default_launcher_icon.png -------------------------------------------------------------------------------- /src/ui/icons/green_button_hover_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/green_button_hover_long.png -------------------------------------------------------------------------------- /src/ui/icons/red_button_hover_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/red_button_hover_long.png -------------------------------------------------------------------------------- /src/ui/icons/yellow_button_hover_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/ui/icons/yellow_button_hover_long.png -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | black # formatter 2 | ruff # linter 3 | pytest # for unittesting 4 | python-dotenv # for environment variables 5 | 6 | -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/ui/exit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derto42/FlowBuddy/HEAD/src/addons/colorpicker/vcolorpicker/ui/exit.ico -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/ui/img.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | exit.ico 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/stand_alone_dialog.py: -------------------------------------------------------------------------------- 1 | import test_components 2 | 3 | from ui.dialog import ConfirmationDialog 4 | 5 | req=ConfirmationDialog("This is a test") 6 | x=req.exec() -------------------------------------------------------------------------------- /tests/test_shortcuts.py: -------------------------------------------------------------------------------- 1 | import test_components 2 | module=test_components.load_addon("shortcuts") 3 | test_components.activate_addon("shortcuts") 4 | test_components.application.exec() -------------------------------------------------------------------------------- /tests/run_notes.py: -------------------------------------------------------------------------------- 1 | import test_components 2 | 3 | module = test_components.load_addon("notes") 4 | 5 | test_components.activate_addon("notes") 6 | 7 | test_components.application.exec() 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | keyboard==0.13.5 3 | pynput 4 | pyqt5==5.15.9 5 | pyqt5-qt5==5.15.2 6 | pyqt5-sip==12.12.1 7 | requests==2.31.0 8 | pytube>=15.0.0 # for youtube downloads 9 | -------------------------------------------------------------------------------- /src/temporary_settings_launcher.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget, QApplication 2 | 3 | from ui.settings.ui import SettingsUI 4 | 5 | app = QApplication([]) 6 | win = SettingsUI() 7 | win.show() 8 | 9 | app.exec() -------------------------------------------------------------------------------- /src/addons/notes/notes.qss: -------------------------------------------------------------------------------- 1 | QTextEdit{ 2 | background-color:lightgrey; 3 | border-radius: 10; 4 | } 5 | 6 | QTabWidget{ 7 | font-weight: 600; 8 | border-radius: 8px; 9 | padding: 5px 15px; 10 | margin-top: 10px; 11 | outline: 0px; 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all the UI elements.""" 2 | from .base_window import BaseWindow, TabsWindow 3 | from .custom_button import RedButton, GrnButton, YelButton, TextButton 4 | from .dialog import BaseDialog, ConfirmationDialog, ACCEPTED, REJECTED 5 | from .entry_box import Entry 6 | from . import utils 7 | import settings -------------------------------------------------------------------------------- /src/addons/DertoEarlyTests/TURNOFF_SteamDiscord.pyw: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | # Use the taskkill command to force quit all Steam processes 4 | subprocess.run(["taskkill", "/F", "/IM", "Steam.exe"]) 5 | 6 | # Use the taskkill command to force quit all Discord processes 7 | subprocess.run(["taskkill", "/F", "/IM", "Discord.exe"]) 8 | 9 | 10 | # Exit the script 11 | exit() 12 | -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | vcolorpicker 3 | 4 | Simply let a user pick a color using a visual selector. 5 | """ 6 | 7 | __version__ = "1.4.0" 8 | __author__ = 'nlfmt' 9 | 10 | from .vcolorpicker import ColorPicker 11 | from .vcolorpicker import hsv2rgb, hsv2hex, rgb2hsv, rgb2hex, hex2rgb, hex2hsv 12 | from .vcolorpicker import getColor, useAlpha, useLightTheme 13 | 14 | -------------------------------------------------------------------------------- /src/addons/Settings/structure.py: -------------------------------------------------------------------------------- 1 | # keys 2 | KEY = "key" 3 | TYPE = "type" 4 | SETTING_TYPE = "setting_type" 5 | OPTIONS = "options" 6 | DEFAULT = "default" 7 | 8 | 9 | # args 10 | UPDATE = "update" 11 | 12 | # types 13 | ENTRY = "entry" 14 | SWITCH = "switch" 15 | SPIN = "spin" 16 | DROPDOWN = "dropdown" 17 | 18 | 19 | STRUCTURE = { 20 | "UI Group": { 21 | "UI Scale": { 22 | KEY: "ui_scale", 23 | TYPE: float, 24 | SETTING_TYPE: SPIN, 25 | OPTIONS: [UPDATE], 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/ui/base_window/tab_widget.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from PyQt5 import QtGui 4 | from PyQt5.QtCore import QObject 5 | from PyQt5.QtWidgets import QVBoxLayout, QTabWidget, QWidget 6 | 7 | 8 | class TabWidget(QTabWidget): 9 | def __init__(self, parent: QWidget | None = None) -> None: 10 | super().__init__(parent) 11 | 12 | self.tabBar().setVisible(False) # hiding the tabs bar 13 | 14 | 15 | # making the Tab Widget hidden 16 | def paintEvent(self, paint_event) -> None: 17 | return 18 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Project 2 | 3 | Project Description 4 | 5 | [TODO.md spec & Kanban Board](https://bit.ly/3fCwKfM) 6 | 7 | ### Todo 8 | 9 | - [ ] hover and hold show name and info on the button 10 | - [ ] Increase spacing between Groups 11 | - [ ] Add a delete confirmation 12 | - [ ] Make the position get saved in the data.json 13 | - [ ] Change the Add_Group button in Normal mode to create a new window in the overlay 14 | - [ ] Add Shortcut Buttons for tools 15 | 16 | ### In Progress 17 | 18 | - [ ] Pick name: 19 | - -BookmarkBuddy 20 | - -FlowHub 21 | - -FlowBuddy 22 | - -BookmarkHub 23 | - -Flowee 24 | - [ ] Logo + GitHub Design 25 | 26 | ### Done ✓ 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ui/entry_box.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QLineEdit, QWidget 2 | from settings import CORNER_RADIUS, UI_SCALE 3 | from .utils import get_font 4 | 5 | 6 | ENTRY_BOX_STYLE = f""" 7 | background-color: #DADADA; 8 | border-radius: {CORNER_RADIUS * UI_SCALE}px; 9 | padding-left: {int((27 - 4) * UI_SCALE)}px; 10 | padding-right: {int((27 - 4) * UI_SCALE)}px; 11 | """ 12 | 13 | 14 | class Entry(QLineEdit): 15 | def __init__(self, parent: QWidget = None, place_holder: str = "Text") -> None: 16 | super().__init__(parent) 17 | self.setPlaceholderText(place_holder) 18 | self.setFixedSize(int(200 * UI_SCALE), int(40 * UI_SCALE)) 19 | self.setFont(get_font(size=int(16 * UI_SCALE))) 20 | self.setStyleSheet("color: #282828") 21 | self.setStyleSheet(ENTRY_BOX_STYLE) 22 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QtGui import QIcon 4 | from PyQt5.QtWidgets import QApplication, QMenu, QSystemTrayIcon 5 | 6 | import FileSystem as FS 7 | from addon import AddOnBase, load_addons, add_ons 8 | from launcher import LowerWidget 9 | 10 | 11 | def main(): 12 | global widgets 13 | app = QApplication(sys.argv) 14 | 15 | tray_icon = AddOnBase.system_tray_icon = QSystemTrayIcon(QIcon(FS.icon("icon.png"))) 16 | tray_icon.setToolTip("FlowBuddy") 17 | tray_icon.show() 18 | 19 | menu = QMenu() 20 | quit_action = menu.addAction("Quit") 21 | quit_action.triggered.connect(app.quit) 22 | tray_icon.setContextMenu(menu) 23 | 24 | load_addons() 25 | 26 | widgets=LowerWidget(add_ons) 27 | 28 | sys.exit(app.exec_()) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /src/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains settings values of our application. 3 | """ 4 | 5 | from typing import Any 6 | from importlib import reload 7 | 8 | import SaveFile as Data 9 | 10 | 11 | # Function to retrieve settings from the SaveFile module. 12 | def _get_setting(setting_name: str) -> tuple[Any, bool]: 13 | try: 14 | return Data.get_setting(setting_name), True 15 | except Data.NotFoundException: 16 | return None, False 17 | 18 | 19 | 20 | CORNER_RADIUS = 12 21 | STROKE_WIDTH = 2 22 | 23 | # Assign the retrieved value if it is found; otherwise, assign the default value. 24 | UI_SCALE: float = _load[0] if (_load:=_get_setting("ui_scale"))[1] and isinstance(_load[0], (int, float)) else 1.0 25 | 26 | 27 | 28 | 29 | from PyQt5.QtCore import QSize, QPoint 30 | 31 | def apply_ui_scale(value: int | float | QSize | QPoint) -> int | float | QSize | QPoint: 32 | scaled_value = value * UI_SCALE 33 | return type(value)(scaled_value) -------------------------------------------------------------------------------- /src/addons/DertoEarlyTests/ttf2woff.pyw: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog 3 | from fontTools.ttLib.woff2 import compress 4 | import os 5 | 6 | # create a Tkinter root window (it will not be displayed) 7 | root = tk.Tk() 8 | root.withdraw() 9 | 10 | # open a file dialog to select the input font file 11 | input_file_path = filedialog.askopenfilename( 12 | title="Select the input font file", 13 | filetypes=[("Font files", "*.otf *.ttf"), ("All files", "*.*")] 14 | ) 15 | 16 | if not input_file_path: 17 | print("No input file selected. Exiting...") 18 | exit() 19 | 20 | # determine the output file path based on the input file path 21 | input_file_dir, input_file_name = os.path.split(input_file_path) 22 | input_file_name_base, input_file_extension = os.path.splitext(input_file_name) 23 | output_file_path = os.path.join(input_file_dir, input_file_name_base + ".woff2") 24 | 25 | # compress the font 26 | compress(input_file_path, output_file_path) 27 | 28 | print(f"Font compressed and saved as {output_file_path}") 29 | -------------------------------------------------------------------------------- /src/utils/buttons.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Callable 2 | 3 | from PyQt5.QtCore import Qt, QSize 4 | from PyQt5.QtGui import QIcon 5 | from PyQt5.QtWidgets import QPushButton 6 | 7 | from src.utils.colors import replace_color 8 | 9 | 10 | def create_button(icon: str, size: Tuple[int, int], location: Tuple[int, int], style: str, action: Callable, icon_size: Tuple[int, int] = None) -> QPushButton: 11 | """Create a button from defined parameters.""" 12 | button = QPushButton() 13 | button.setIcon(QIcon(f'../icons/{icon}')) 14 | button.setFixedSize(*size) 15 | button.setCursor(Qt.PointingHandCursor) 16 | button.setStyleSheet(style) 17 | button.move(*location) 18 | 19 | # Change the color of the button when hovered 20 | button.enterEvent = lambda event: button.setStyleSheet(replace_color(style, "light")) 21 | button.leaveEvent = lambda event: button.setStyleSheet(replace_color(style, "dark")) 22 | 23 | if icon_size: 24 | button.setIconSize(QSize(*icon_size)) 25 | button.clicked.connect(action) 26 | 27 | return button 28 | -------------------------------------------------------------------------------- /tests/test_components.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | import os 3 | import sys 4 | from types import ModuleType 5 | import inspect 6 | 7 | from PyQt5.QtWidgets import QApplication 8 | 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src")) 11 | 12 | import addon 13 | 14 | application = QApplication([]) 15 | 16 | def load_addon(name: str) -> ModuleType: 17 | module_name = f"addons.{name}.{name}" 18 | addon.add_on_paths[module_name] = f"addons/{name}/{name}" 19 | addon.currently_loading_module = module_name 20 | print(f"Addon name: {name}\nAddon path: {addon.add_on_paths[module_name]}\nModule: {module_name}") 21 | print(f"Importing '{module_name}' module.") 22 | module = import_module(f"addons.{name}.{name}") 23 | print("Import complete.") 24 | addon.currently_loading_module = None 25 | addon.add_ons[module_name] = module 26 | return module 27 | 28 | def activate_addon(name: str) -> None: 29 | module_name = f"addons.{name}.{name}" 30 | func = addon.AddOnBase(module_name).activate 31 | print(f"Activating addon {name}.") 32 | func() -------------------------------------------------------------------------------- /.github/workflows/formatter.yml: -------------------------------------------------------------------------------- 1 | name: Format Python code with black and push changes 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | format_and_push: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v2 10 | 11 | - name: Set up Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.9 15 | 16 | - name: Install black 17 | run: pip install black 18 | 19 | - name: Format code with black 20 | run: black . 21 | 22 | - name: Check for changes 23 | id: git-check 24 | run: | 25 | git diff --quiet || echo "Changes detected" 26 | echo "changes=$(git diff --quiet || echo "true")" >> $GITHUB_ENV 27 | 28 | - name: Configure Git 29 | if: env.changes == 'true' 30 | run: | 31 | git config --global user.email "actions@github.com" 32 | git config --global user.name "GitHub Actions" 33 | 34 | - name: Commit and push changes 35 | if: env.changes == 'true' 36 | run: | 37 | git add -A 38 | git commit -m "Auto-format code with black" 39 | git push 40 | -------------------------------------------------------------------------------- /tests/test_notes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from add_ons.notes import JottingDownWindow, NoteTab 3 | from PyQt5.QtCore import Qt 4 | from ui.dialog import ConfirmationDialog 5 | 6 | 7 | 8 | 9 | def test_delete_tab(qtbot, tmpdir): 10 | # Create a temporary directory for testing 11 | temp_dir = tmpdir.mkdir("addons/notes") 12 | 13 | # Create a test file in the temporary directory 14 | test_file = temp_dir.join("test.txt") 15 | test_file.write("Test content") 16 | 17 | # Create the JottingDownWindow instance 18 | window = JottingDownWindow() 19 | window.notes_folder = str(temp_dir) 20 | window.tab_widget.addTab(NoteTab(str(test_file)), "test.txt") 21 | 22 | # Add the window to the QtBot for interaction 23 | qtbot.addWidget(window) 24 | 25 | # Mock the QMessageBox.question method to always return QMessageBox.Yes 26 | def mock_question(_, __, ___, ____, default_button): 27 | dialog = ConfirmationDialog("Delete tab test.txt?") 28 | return True 29 | 30 | ConfirmationDialog.question = mock_question 31 | 32 | # Simulate the delete_tab action 33 | with qtbot.waitSignal(window.tab_widget.tabCloseRequested): 34 | # Get the delete button of the tab 35 | delete_button = window.tab_widget.tabBar().tabButton(0, 2) 36 | qtbot.mouseClick(delete_button, Qt.LeftButton) 37 | 38 | # Verify that the tab is removed 39 | assert window.tab_widget.count() == 0 40 | 41 | # Verify that the file is deleted 42 | assert not os.path.exists(str(test_file)) 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/addons/colorpicker/icons/copy-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/colors.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | import re 3 | 4 | 5 | def lighten_color(hex_value: str, amount: int = 0.5) -> str: 6 | """Lighten a hex valued color by a specific amount.""" 7 | r, g, b = tuple(int(hex_value[i:i + 2], 16) for i in (0, 2, 4)) 8 | h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) 9 | l = min(1.0, l + amount) 10 | r, g, b = tuple(round(i * 255) for i in colorsys.hls_to_rgb(h, l, s)) 11 | 12 | return "#{:02x}{:02x}{:02x}".format(r, g, b) 13 | 14 | 15 | def darken_color(hex_value: str, amount: int = 0.5) -> str: 16 | """Darken a hex valued color by a specific amount.""" 17 | r, g, b = tuple(int(hex_value[i:i + 2], 16) for i in (0, 2, 4)) 18 | h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) 19 | l = max(0.0, l - amount) 20 | r, g, b = tuple(round(i * 255) for i in colorsys.hls_to_rgb(h, l, s)) 21 | 22 | return "#{:02x}{:02x}{:02x}".format(r, g, b) 23 | 24 | 25 | def replace_color(style: str, color: str, amount: int = 0.5) -> str: 26 | """ 27 | Replace a color in a stylesheet string with a new color. 28 | You can mention the color as a hex value or as "light" or "dark". 29 | When mentioning the color as "light" or "dark", 30 | you can mention the amount of lightness or darkness from 0 to 1. 31 | """ 32 | pattern = r"background-color:\s*([^\s;}]+)" 33 | match = re.search(pattern, style) 34 | property_name = match[1] 35 | new_color = "#000000" 36 | if color == "light": 37 | new_color = lighten_color(match[2], amount) 38 | elif color == "dark": 39 | new_color = darken_color(match[2], amount) 40 | elif color.startswith("#") and len(color) == 7: 41 | new_color = color 42 | return str(re.sub(pattern, f"{property_name}{new_color}", style)) 43 | -------------------------------------------------------------------------------- /src/FileSystem.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | import sys 4 | 5 | 6 | SAVE_FILE_NAME = "save.json" 7 | ICONS_FOLDER_NAME = "ui/icons" 8 | FONTS_FOLDER_NAME = "ui/fonts" 9 | ADDONS_NAME = "addons" 10 | 11 | PROGRAM_DIR = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | SAVE_FILE = os.path.join(PROGRAM_DIR, SAVE_FILE_NAME) 14 | ICONS_FOLDER = os.path.join(PROGRAM_DIR, ICONS_FOLDER_NAME) 15 | FONTS_FOLDER = os.path.join(PROGRAM_DIR, FONTS_FOLDER_NAME) 16 | ADDONS_FOLDER = os.path.join(PROGRAM_DIR, ADDONS_NAME) 17 | 18 | PLATFORM = sys.platform 19 | 20 | 21 | def abspath(path: str) -> str | None: 22 | """Returns the absolute path. Returns None if the path does not exist.""" 23 | _path = os.path.join(PROGRAM_DIR, path) 24 | return os.path.abspath(_path) if os.path.exists(_path) else None 25 | 26 | 27 | def exists(path: str): 28 | """Returns True if the path exists. Returns False otherwise""" 29 | return os.path.exists(path) 30 | 31 | 32 | def icon(icon_name: str) -> str | None: 33 | """Returns the absolute path of given icon. Returns None if the icon does not exist.""" 34 | path = os.path.join(ICONS_FOLDER, icon_name) 35 | return os.path.abspath(path).replace('\\', '/') if os.path.exists(path) else None 36 | 37 | 38 | def font(font_name: str) -> str | None: 39 | """Returns the absolute path of given font. Returns None if the font does not exist.""" 40 | path = os.path.join(FONTS_FOLDER, font_name) 41 | return os.path.abspath(path).replace('\\', '/') if os.path.exists(path) else None 42 | 43 | 44 | def open_file(file_path: str | None) -> None: 45 | if file_path is not None: 46 | if PLATFORM in ('win32',): 47 | os.startfile(file_path) 48 | elif PLATFORM in ('linux', 'darwin'): 49 | os.system(f'xdg-open {file_path}') 50 | 51 | -------------------------------------------------------------------------------- /src/utils/signal.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Generic, TypeVar 2 | 3 | ArgType = TypeVar("ArgType") 4 | 5 | class Signal(Generic[ArgType]): 6 | """ 7 | A custom signal implementation for implementing the observer pattern. 8 | 9 | This class provides a basic mechanism for creating custom signals 10 | and connecting event handlers to those signals. 11 | 12 | Attributes: 13 | _handlers (list): A list to store event handlers connected to the signal. 14 | 15 | Methods: 16 | connect(handler): Connects an event handler to the signal. 17 | disconnect(handler): Disconnects an event handler from the signal. 18 | emit(*args, **kwargs): Emits the signal, calling all connected event handlers. 19 | """ 20 | 21 | def __init__(self, *args: ArgType, **kwargs: Dict[str, ArgType]): 22 | self._handlers: list[Callable[[ArgType, Dict[str, ArgType]], Any]] = [] 23 | 24 | def __call__(self, *args: ArgType, **kwargs: Dict[str, ArgType]): 25 | self.emit(*args, **kwargs) 26 | 27 | 28 | def connect(self, handler: Callable[[ArgType, Dict[str, ArgType]], Any]): 29 | """ 30 | Connect an event handler to the signal. 31 | 32 | Args: 33 | handler (callable): The event handler function to connect. 34 | """ 35 | self._handlers.append(handler) 36 | 37 | def disconnect(self, handler: Callable[[ArgType, Dict[str, ArgType]], Any]): 38 | """ 39 | Disconnect an event handler from the signal. 40 | 41 | Args: 42 | handler (callable): The event handler function to disconnect. 43 | """ 44 | self._handlers.remove(handler) 45 | 46 | def emit(self, *args: ArgType, **kwargs: Dict[str, ArgType]): 47 | """ 48 | Emit the signal, invoking all connected event handlers. 49 | 50 | Args: 51 | *args: Variable arguments to pass to the event handlers. 52 | **kwargs: Keyword arguments to pass to the event handlers. 53 | """ 54 | for handler in self._handlers: 55 | handler(*args, **kwargs) 56 | -------------------------------------------------------------------------------- /src/addons/notes/notes_save.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | import sys 4 | import json 5 | 6 | 7 | FILE_PATH = os.path.join(os.path.dirname(__file__)) 8 | DATA_NAME = "data" 9 | DATA_FOLDER = os.path.join(FILE_PATH, DATA_NAME) 10 | PLATFORM = sys.platform 11 | CONFIG_FILE = os.path.join(DATA_FOLDER, "config.json") 12 | 13 | 14 | def save_file_data(file_name: str, file_data: str = "") -> None: 15 | file_name+=".txt" 16 | SAVE_FILE = os.path.join(DATA_FOLDER, file_name) 17 | with open(SAVE_FILE, "w") as f: 18 | f.write(file_data) 19 | 20 | 21 | def delete_file_data(file_name: str) -> None: 22 | file_name+=".txt" 23 | SAVE_FILE = os.path.join(DATA_FOLDER, file_name) 24 | if exists(SAVE_FILE): 25 | os.remove(SAVE_FILE) 26 | 27 | 28 | def exists(file_name: str): 29 | """Returns True if the path exists. Returns False otherwise""" 30 | SAVE_FILE = os.path.join(DATA_FOLDER, file_name) 31 | return os.path.exists(SAVE_FILE) 32 | 33 | 34 | def get_file_data(file_name): 35 | file_name+=".txt" 36 | SAVE_FILE = os.path.join(DATA_FOLDER, file_name) 37 | if exists(SAVE_FILE): 38 | with open(SAVE_FILE, "r") as file: 39 | return file.read() 40 | 41 | 42 | if not exists(DATA_FOLDER): 43 | os.makedirs(DATA_FOLDER) 44 | 45 | 46 | def open_file(file_path: str | None) -> None: 47 | if file_path is not None: 48 | if PLATFORM in ("win32",): 49 | os.startfile(file_path) 50 | elif PLATFORM in ("linux", "darwin"): 51 | os.system(f"xdg-open {file_path}") 52 | 53 | 54 | def get_config(): 55 | with open(CONFIG_FILE, "r") as file: 56 | config = json.load(file) 57 | return config 58 | 59 | 60 | def create_config_from_text_files(): 61 | config = { 62 | "files": [file_name for file_name in os.listdir(DATA_FOLDER)], 63 | "last_active": 0, 64 | } 65 | write_config(config) 66 | 67 | 68 | def write_config(config): 69 | with open(CONFIG_FILE, "w") as f: 70 | f.write(json.dumps(config)) 71 | 72 | 73 | # If there are any exceptions related to JSON decoding, file not found, or missing keys, 74 | # a save file is created using the FS module. 75 | try: 76 | with open(CONFIG_FILE) as f: 77 | data = json.load(f) 78 | _, _ = data["files"], data["last_active"] 79 | except (json.JSONDecodeError, FileNotFoundError, KeyError): 80 | create_config_from_text_files() 81 | -------------------------------------------------------------------------------- /tests/tab_window.py: -------------------------------------------------------------------------------- 1 | import test_components 2 | 3 | import os 4 | from PyQt5.QtCore import Qt, QSize 5 | from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QTextEdit 6 | from PyQt5.QtGui import QPainter, QColor, QPaintEvent, QMouseEvent, QTextCursor 7 | 8 | from ui.base_window.title_bar_layer import TitleBarLayer 9 | from ui import BaseWindow, TabsWindow 10 | from ui.utils import get_font 11 | from settings import UI_SCALE 12 | 13 | class NoteTab(QWidget): 14 | def __init__(self, file_path): 15 | super().__init__() 16 | self.file_path = file_path 17 | 18 | # Create QTextEdit within the QWidget 19 | self.text_edit = QTextEdit() 20 | self.text_edit.setFont(get_font(size=16)) 21 | self.text_edit.textChanged.connect(self.save_text_to_file) 22 | self.text_edit.setAcceptRichText(False) 23 | # This is for the tab itself 24 | # Add QTextEdit to layout with padding 25 | layout = QVBoxLayout() 26 | layout.addWidget(self.text_edit) 27 | # Set the margins 28 | layout.setContentsMargins( 29 | int(24 * UI_SCALE), 30 | int(24 * UI_SCALE), 31 | int(22 * UI_SCALE), 32 | int(22 * UI_SCALE), 33 | ) 34 | self.setLayout(layout) 35 | self.load_text_from_file() 36 | 37 | self.setStyleSheet("""QTextEdit{ 38 | background-color:lightgrey; 39 | border-radius: 10; 40 | } """) 41 | 42 | def load_text_from_file(self): 43 | if os.path.exists(self.file_path): 44 | with open(self.file_path, "r") as file: 45 | self.text_edit.setPlainText(file.read()) 46 | self.text_edit.moveCursor(QTextCursor.End) 47 | 48 | def save_text_to_file(self): 49 | with open(self.file_path, "w") as file: 50 | file.write(self.text_edit.toPlainText()) 51 | 52 | def create_new_file(self): 53 | with open(self.file_path, "w") as file: 54 | file.write("") 55 | 56 | app = QApplication([]) 57 | 58 | window = TabsWindow() 59 | notes_1 = window.addTab(NoteTab("notes.txt"), "title1") 60 | window.addTab(NoteTab("notes.txt"), "title2") 61 | window.addTab(p := QPushButton("Button 1", window), "Button") 62 | p.clicked.connect(lambda: window.removeTab(notes_1)) 63 | window.show() 64 | 65 | window.setFixedSize(800, 400) 66 | 67 | window.red_button.clicked.connect(lambda: print("Red button clicked")) 68 | window.add_button.clicked.connect(lambda: print("Add button clicked")) 69 | 70 | app.exec() -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | test_custom_buttons = False 2 | base_window = False 3 | base_dialog = False 4 | group_node = False 5 | main_window = False 6 | logo = True 7 | 8 | 9 | def _custom_buttons_(): 10 | layout.addWidget(RedButton()) 11 | layout.addWidget(YelButton()) 12 | layout.addWidget(GrnButton()) 13 | layout.addWidget(RedButton(button_type="long")) 14 | layout.addWidget(YelButton(button_type="long")) 15 | layout.addWidget(GrnButton(button_type="long")) 16 | layout.addWidget(TextButton()) 17 | layout.addWidget(TextButton(text="Click Me!")) 18 | 19 | def _base_window_(): 20 | window = BaseWindow() 21 | window.setLayout(layout:=QVBoxLayout()) 22 | layout.addWidget(RedButton()) 23 | layout.addWidget(YelButton()) 24 | layout.addWidget(GrnButton()) 25 | layout.addWidget(RedButton(button_type="long")) 26 | layout.addWidget(YelButton(button_type="long")) 27 | layout.addWidget(GrnButton(button_type="long")) 28 | layout.addWidget(TextButton()) 29 | layout.addWidget(TextButton(text="Click Me!")) 30 | window.show() 31 | app.exec() 32 | 33 | def _base_dialog_(): 34 | add_group = GroupDialog() 35 | add_group.exec() 36 | print(gres:=add_group.result()) 37 | 38 | add_task = TaskDialog() 39 | print(tres:=add_task.exec()) 40 | 41 | edit_group = GroupDialog() 42 | edit_group.for_edit(gres) 43 | print(edit_group.exec()) 44 | 45 | edit_task = TaskDialog() 46 | edit_task.for_edit(*tres) 47 | print(edit_task.exec()) 48 | 49 | app.exec() 50 | 51 | def _group_node_(): 52 | win.setStyleSheet("background : '#838383'") 53 | layout.setContentsMargins(0, 0, 0, 0) 54 | layout.setSpacing(0) 55 | layout.addWidget(GroupNode("Group 1")) 56 | layout.addWidget(GroupNode("Group 2")) 57 | layout.addWidget(GroupNode("Group 3")) 58 | 59 | def _main_window_(): 60 | window = MainWindow() 61 | window.show() 62 | app.exec() 63 | 64 | def _logo_(): 65 | window = Buddy() 66 | window.show() 67 | app.exec() 68 | 69 | from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout 70 | from ui.custom_button import * 71 | from ui.base_window import * 72 | from ui.dialog import * 73 | from addons.shortcuts.shortcuts import * 74 | from ui.logo import * 75 | 76 | app = QApplication([]) 77 | win = QWidget() 78 | win.setLayout(layout:=QVBoxLayout()) 79 | 80 | if test_custom_buttons: _custom_buttons_() 81 | if base_window: _base_window_() 82 | if base_dialog: _base_dialog_() 83 | if group_node: _group_node_() 84 | if main_window: _main_window_() 85 | if logo: _logo_() 86 | 87 | win.show() 88 | app.exec() -------------------------------------------------------------------------------- /src/SaveFile.py: -------------------------------------------------------------------------------- 1 | """Manages the save.json files. (if save file not provided, will use default save file.)\n 2 | NOTE: If you want to save settings of addons, 3 | please use apply_setting, get_setting, remove_setting 4 | methods from AddOnBase class instead.""" 5 | 6 | from __future__ import annotations 7 | 8 | import json 9 | from os import path 10 | from typing import Optional, Union 11 | 12 | from FileSystem import abspath, SAVE_FILE 13 | 14 | 15 | JsonType = Union[dict, list, tuple, str, int, float, bool, None] 16 | 17 | 18 | class NotFoundException(Exception): 19 | def __init__(self, name: str): 20 | super().__init__(f"'{name}' not found") 21 | 22 | 23 | def _create_empty_save_file(file_path: str) -> None: 24 | """Creates an empty save file in the file_path directory.""" 25 | with open(f"{file_path}", "w") as f: 26 | json.dump({}, f) 27 | 28 | 29 | def _prepare_save_file(save_file: Optional[str] = None) -> str: 30 | """If the save_file exists, retruns the save_file path. Otherwise, creates a new save_file.""" 31 | 32 | abs_file_path = SAVE_FILE if save_file is None else abspath(save_file) 33 | 34 | if not path.exists(abs_file_path): 35 | _create_empty_save_file(abs_file_path) 36 | 37 | try: 38 | with open(abs_file_path, "r") as f: 39 | _ = json.load(f) 40 | except json.JSONDecodeError: 41 | _create_empty_save_file(abs_file_path) 42 | 43 | return abs_file_path 44 | 45 | 46 | def apply_setting(name: str, value: JsonType, save_file: Optional[str] = None) -> None: 47 | save_file_path = _prepare_save_file(save_file) 48 | 49 | with open(save_file_path, "r") as save_file: 50 | json_data = json.load(save_file) 51 | 52 | json_data[name] = value 53 | 54 | with open(save_file_path, "w") as save_file: 55 | json.dump(json_data, save_file, indent=4) 56 | 57 | 58 | def get_setting(name: str, save_file: Optional[str] = None) -> JsonType: 59 | save_file_path = _prepare_save_file(save_file) 60 | 61 | with open(save_file_path, "r") as save_file: 62 | json_data = json.load(save_file) 63 | 64 | if name in json_data: 65 | return json_data[name] 66 | raise NotFoundException(name) 67 | 68 | 69 | def remove_setting(name: str, save_file: Optional[str] = None) -> None: 70 | save_file_path = _prepare_save_file(save_file) 71 | 72 | with open(save_file_path, "r") as save_file: 73 | json_data = json.load(save_file) 74 | 75 | if name in json_data: 76 | del json_data[name] 77 | with open(save_file_path, "w") as save_file: 78 | json.dump(json_data, save_file, indent=4) 79 | 80 | raise NotFoundException(name) 81 | -------------------------------------------------------------------------------- /src/ui/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional, Union 2 | 3 | from PyQt5.QtGui import QFont, QFontDatabase 4 | 5 | import FileSystem as File 6 | 7 | 8 | DEFAULT_REGULAR = "Montserrat-Regular.ttf" 9 | DEFAULT_MEDIUM = "Montserrat-Medium.ttf" 10 | DEFAULT_SEMI_BOLD = "Montserrat-SemiBold.ttf" 11 | DEFAULT_BOLD = "Montserrat-Bold.ttf" 12 | DEFAULT_FONT_SIZE = 16 13 | 14 | _loaded_fonts = {} 15 | _default_fonts_loaded = False 16 | 17 | MEDIUM = "medium" 18 | SEMIBOLD = "semibold" 19 | BOLD = "bold" 20 | 21 | # Note: Semi Bold is named DemiBold 22 | SHORT_NAME_TO_WEIGHT = { 23 | "Thin": QFont.Thin, 24 | "Extralight": QFont.ExtraLight, 25 | "Light": QFont.Light, 26 | "Regular": QFont.Normal, 27 | "Medium": QFont.Medium, 28 | "Semibold": QFont.DemiBold, 29 | "Bold": QFont.Bold, 30 | "Extrabold": QFont.ExtraBold, 31 | "Black": QFont.Black, 32 | } 33 | 34 | 35 | def get_font(font_name: str = DEFAULT_REGULAR, 36 | size: int = DEFAULT_FONT_SIZE, 37 | weight: Literal["medium", "semibold", "bold", "regular"] = "regular") -> QFont: 38 | 39 | global _default_fonts_loaded 40 | 41 | _size = size 42 | _italic = False 43 | 44 | if font_name == DEFAULT_REGULAR: 45 | if not _default_fonts_loaded: 46 | _loaded_fonts[DEFAULT_MEDIUM] = QFontDatabase.addApplicationFont(File.font(DEFAULT_MEDIUM)) 47 | _loaded_fonts[DEFAULT_BOLD] = QFontDatabase.addApplicationFont(File.font(DEFAULT_BOLD)) 48 | _loaded_fonts[DEFAULT_SEMI_BOLD] = QFontDatabase.addApplicationFont(File.font(DEFAULT_SEMI_BOLD)) 49 | _loaded_fonts[DEFAULT_REGULAR] = QFontDatabase.addApplicationFont(File.font(DEFAULT_REGULAR)) 50 | _default_fonts_loaded = True 51 | if weight == "regular": 52 | font_name = DEFAULT_REGULAR 53 | elif weight == "medium": 54 | font_name = DEFAULT_MEDIUM 55 | elif weight == "semibold": 56 | font_name = DEFAULT_SEMI_BOLD 57 | elif weight == "bold": 58 | font_name = DEFAULT_BOLD 59 | _weight = SHORT_NAME_TO_WEIGHT["Regular"] 60 | 61 | elif font_name not in _loaded_fonts: 62 | # add font to application. 63 | _loaded_fonts[font_name] = QFontDatabase.addApplicationFont(File.font(font_name)) 64 | if isinstance(weight, int): 65 | _weight = weight 66 | elif isinstance(weight,str): 67 | _weight = SHORT_NAME_TO_WEIGHT[weight.title()] 68 | else: 69 | _weight = SHORT_NAME_TO_WEIGHT[weight.title()] 70 | 71 | _family_name = QFontDatabase.applicationFontFamilies(_loaded_fonts[font_name])[0] 72 | 73 | system_fonts = QFontDatabase().families() 74 | if "Montserrat" in system_fonts: 75 | _family_name = "Montserrat" 76 | _weight = SHORT_NAME_TO_WEIGHT[weight.title()] 77 | 78 | return QFont(_family_name, _size, _weight, _italic) -------------------------------------------------------------------------------- /src/ui/dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtWidgets import ( 3 | QWidget, 4 | QDialog, 5 | QVBoxLayout, 6 | QHBoxLayout, 7 | QLabel 8 | ) 9 | from PyQt5.QtGui import QKeyEvent, QShowEvent 10 | 11 | 12 | from settings import UI_SCALE 13 | from .custom_button import RedButton, GrnButton 14 | from .utils import get_font 15 | 16 | 17 | ACCEPTED = QDialog.Accepted 18 | REJECTED = QDialog.Rejected 19 | 20 | 21 | class BaseDialog(QDialog): 22 | def __init__(self, title: str = "Title", 23 | parent: QWidget | None = None,) -> None: 24 | super().__init__(parent = parent) 25 | 26 | self._layout = QVBoxLayout() 27 | self.setLayout(self._layout) 28 | self._layout.setContentsMargins(0, 0, 0, 0) 29 | 30 | self._title = QLabel(title, self) 31 | self._layout.addWidget(self._title) 32 | self._title.setFont(get_font(size=int(24 * UI_SCALE), weight="semibold")) 33 | self._title.setStyleSheet("color: #282828") 34 | self._title.setAlignment(Qt.AlignCenter) 35 | 36 | self._main_layout = QWidget(self) 37 | self._layout.addWidget(self._main_layout) 38 | 39 | self._layout.insertLayout 40 | self._layout.addLayout(button_layout:=QHBoxLayout()) 41 | 42 | button_layout.addStretch() 43 | button_layout.addWidget(reject_button:=RedButton(self, "long")) 44 | button_layout.addSpacing(int(7 * UI_SCALE)) 45 | button_layout.addWidget(accept_button:=GrnButton(self, "long")) 46 | button_layout.addStretch() 47 | accept_button.clicked.connect(lambda : self.accept()) 48 | reject_button.clicked.connect(lambda : self.reject()) 49 | accept_button.setToolTip("Ok") 50 | reject_button.setToolTip("Cancel") 51 | accept_button.setDefault(True) 52 | 53 | self.setLayout = self._main_layout.setLayout 54 | self.layout = self._main_layout.layout 55 | 56 | self.setModal(True) 57 | 58 | # self.setFixedSize(100, 100) 59 | 60 | 61 | def setTitle(self, title: str) -> None: 62 | self._title.setText(title) 63 | 64 | 65 | def keyPressEvent(self, a0: QKeyEvent) -> None: 66 | if a0.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return]: 67 | self.accept() 68 | elif a0.key() is Qt.Key.Key_Escape: 69 | self.reject() 70 | return super().keyPressEvent(a0) 71 | 72 | def showEvent(self, a0: QShowEvent) -> None: 73 | self.adjustSize() 74 | return super().showEvent(a0) 75 | 76 | 77 | class ConfirmationDialog(BaseDialog): 78 | def __init__(self, title: str = "Title", parent: QWidget | None = None) -> None: 79 | super().__init__(title, parent) 80 | 81 | self._title.setFont(get_font(size=int(16 * UI_SCALE))) 82 | self._title.setStyleSheet("color: #282828") 83 | -------------------------------------------------------------------------------- /src/ui/logo.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from PyQt5 import QtCore, QtGui 3 | from PyQt5.QtCore import Qt, QRectF, QVariantAnimation, QEasingCurve, QSize, QTimer, QPoint 4 | from PyQt5.QtWidgets import ( 5 | QWidget, 6 | QVBoxLayout, 7 | QHBoxLayout, 8 | QLabel, 9 | QGraphicsDropShadowEffect, 10 | ) 11 | from PyQt5.QtGui import ( 12 | QColor, 13 | QPainter, 14 | QPainterPath, 15 | QPaintEvent, 16 | QMouseEvent, 17 | QShowEvent 18 | ) 19 | 20 | 21 | from .custom_button import RedButton, GrnButton, Button 22 | from settings import CORNER_RADIUS, UI_SCALE 23 | 24 | 25 | class Buddy(QWidget): 26 | def __init__(self, parent: QWidget | None = None) -> None: 27 | super().__init__(parent, Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 28 | self.setAttribute(Qt.WA_TranslucentBackground) 29 | 30 | self.setLayout(layout:=QVBoxLayout(self)) 31 | layout.setContentsMargins(int(10 * UI_SCALE), int(10 * UI_SCALE), int(10 * UI_SCALE), int(10 * UI_SCALE)) 32 | layout.setSpacing(0) 33 | 34 | layout.addLayout(eye_layout:=QHBoxLayout()) 35 | eye_layout.addWidget(l_button:=RedButton(self)) 36 | eye_layout.addSpacing(int(12 * UI_SCALE)) 37 | eye_layout.addWidget(r_button:=GrnButton(self)) 38 | 39 | layout.addSpacing(int(10 * UI_SCALE)) 40 | 41 | layout.addWidget(smile:=Button(self, custom_size=QSize(int(47 * UI_SCALE), int(18 * UI_SCALE))), alignment=Qt.AlignCenter) 42 | smile.set_icons("edit_button") 43 | 44 | smile.clicked.connect(self.spawn) 45 | 46 | smile.animate = False 47 | l_button.animate = False 48 | r_button.animate = False 49 | 50 | self._spawner = QVariantAnimation() 51 | self._spawner.valueChanged.connect(self.move) 52 | self.easing_curve = QEasingCurve.OutCubic 53 | self.duration = 500 54 | 55 | 56 | def spawn(self) -> None: 57 | pos = self.pos() 58 | self._spawner.setStartValue(pos) 59 | pos.setY(pos.y()-int(100 * UI_SCALE)) 60 | self._spawner.setEndValue(pos) 61 | self._spawner.start() 62 | 63 | 64 | def paintEvent(self, a0: QPaintEvent) -> None: 65 | 66 | painter = QPainter(self) 67 | painter.setRenderHint(QPainter.Antialiasing) 68 | 69 | path = QPainterPath() 70 | path.addRoundedRect(QRectF(self.rect()), CORNER_RADIUS, CORNER_RADIUS) 71 | painter.fillPath(path, QColor("#FFFFFF")) 72 | 73 | return super().paintEvent(a0) 74 | 75 | def mousePressEvent(self, a0: QMouseEvent) -> None: 76 | if a0.button() == Qt.LeftButton: 77 | self._offset = a0.pos() 78 | return super().mousePressEvent(a0) 79 | 80 | def mouseMoveEvent(self, a0: QMouseEvent) -> None: 81 | if self._offset is not None and a0.buttons() == Qt.LeftButton: 82 | self.move(a0.globalPos() - self._offset) 83 | return super().mouseMoveEvent(a0) 84 | 85 | def mouseReleaseEvent(self, a0: QMouseEvent) -> None: 86 | self._offset = None 87 | return super().mouseReleaseEvent(a0) 88 | 89 | def showEvent(self, a0: QShowEvent) -> None: 90 | ret = super().showEvent(a0) 91 | self.spawn() 92 | return ret -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local IDE settings 2 | .idea/ 3 | .vscode/ 4 | Pipfile* 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # save file 161 | /**/save.json 162 | notes/config.json 163 | 164 | # addons order config file 165 | src/addons/order.json 166 | config.json 167 | *.txt 168 | -------------------------------------------------------------------------------- /src/addons/DertoEarlyTests/100to1image.pyw: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import math 3 | import os 4 | import random 5 | import tkinter as tk 6 | from tkinter import filedialog 7 | import svgwrite 8 | import tempfile 9 | import shutil 10 | 11 | # Create a Tkinter root window to open the file dialog 12 | root = tk.Tk() 13 | root.withdraw() 14 | 15 | # Prompt the user to select input images 16 | filetypes = [("Image files", "*.png;*.jpeg;*.jpg;*.svg")] 17 | input_paths = filedialog.askopenfilenames(title="Select input images", filetypes=filetypes) 18 | 19 | # Prompt the user to select the output location and filename 20 | output_path = filedialog.asksaveasfilename(title="Select output location and filename", filetypes=[("PNG files", "*.png"), ("SVG files", "*.svg")], defaultextension=".png") 21 | 22 | # Group the input paths by file type 23 | input_paths_by_type = {} 24 | for path in input_paths: 25 | _, ext = os.path.splitext(path) 26 | if ext in input_paths_by_type: 27 | input_paths_by_type[ext].append(path) 28 | else: 29 | input_paths_by_type[ext] = [path] 30 | 31 | # Set the dimensions of the logos (assumed to be square) 32 | logo_size = 512 33 | 34 | # Set the margin between the logos 35 | margin = 10 36 | 37 | # Calculate the number of rows and columns needed for the PNG/JPEG images 38 | png_jpg_paths = input_paths_by_type.get('.png', []) + input_paths_by_type.get('.jpg', []) + input_paths_by_type.get('.jpeg', []) 39 | num_logos = len(png_jpg_paths) 40 | side_length = math.ceil(math.sqrt(num_logos)) 41 | num_rows = math.ceil(num_logos / side_length) 42 | num_cols = side_length 43 | 44 | # Calculate the size of the final image for the PNG/JPEG images 45 | img_size = logo_size * num_cols + margin * (num_cols + 1), logo_size * num_rows + margin * (num_rows + 1) 46 | final_img = Image.new("RGB", img_size, (255, 255, 255)) 47 | 48 | # Loop through each PNG/JPEG logo and add it to the final image 49 | for i, path in enumerate(png_jpg_paths): 50 | row = i // num_cols 51 | col = i % num_cols 52 | x_offset = margin + (col * (logo_size + margin)) 53 | y_offset = margin + (row * (logo_size + margin)) 54 | logo_img = Image.open(path) 55 | logo_img = logo_img.resize((logo_size, logo_size)) 56 | final_img.paste(logo_img, (x_offset, y_offset)) 57 | 58 | # Save the final PNG/JPEG image to the specified output location 59 | if output_path.endswith('.png'): 60 | final_img.save(output_path) 61 | 62 | # If there are SVG images, create a separate SVG image 63 | svg_paths = input_paths_by_type.get('.svg', []) 64 | if svg_paths: 65 | # Create the SVG image using svgwrite 66 | dwg = svgwrite.Drawing(filename=os.path.splitext(output_path)[0] + '.svg', size=img_size) 67 | for i, path in enumerate(svg_paths): 68 | x = margin + ((i % num_cols) * (logo_size + margin)) 69 | y = margin + ((i // num_cols) * (logo_size + margin)) 70 | svg_elem = svgwrite.image.Image(path, size=(logo_size, logo_size), insert=(x, y)) 71 | dwg.add(svg_elem) 72 | dwg.save() 73 | 74 | # If there are only SVG images, save the SVG file to the specified output location 75 | elif output_path.endswith('.svg'): 76 | svg_paths = input_paths_by_type.get('.svg', []) 77 | if svg_paths: 78 | # Create the SVG image using svgwrite 79 | dwg = svgwrite.Drawing(filename=output_path, size=img_size) 80 | for i, path in enumerate(svg_paths): 81 | x = margin + ((i % num_cols) * (logo_size + margin)) 82 | y = margin + ((i // num_cols) * (logo_size + margin)) 83 | svg_elem = svgwrite.image.Image(path, size=(logo_size, logo_size), insert=(x, y)) 84 | dwg.add(svg_elem) 85 | dwg.save() 86 | 87 | # Print a message indicating where the final image(s) were saved 88 | if svg_paths: 89 | print(f"Final images saved to {output_path} and {os.path.splitext(output_path)[0]}.svg") 90 | else: 91 | print(f"Final image saved to {output_path}") 92 | 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | [![](https://dcbadge.vercel.app/api/server/cdS6GxMKrE)](https://discord.gg/cdS6GxMKrE) 8 | 9 | 10 |

11 | Version 12 | 13 | License: MIT 14 | 15 |

16 |
17 |
18 |
19 | 20 |

21 | FlowBuddy is a user-friendly productivity application. It provides the user with a shortcut management tool as well as other plugins that are still in development. 22 |

23 |

24 | Table of Contents 25 |

26 | 27 | * [Features](#features) 28 | * [Getting Started](#getting-started) 29 | * [Potential Future Features](#potential-future-features) 30 | * [Future Vision](#future-vision) 31 | * [UI Explanation](#ui-explanation) 32 | * [Demo](#demo) 33 | 34 |
35 | 36 | 37 | 38 | ![Animation](https://user-images.githubusercontent.com/125327962/236888361-c172fb52-747e-4f4c-9da0-0ecd4c9d4a12.gif) 39 | 40 | ## Features 41 | 42 | 1. **Manage custom groups for your shortcuts in one place** 43 | 2. **Access all of your important files and websites in one click ** 44 | 3. **Save the window position** 45 | - FlowBuddy saves the overlay layout exactly how you want it 46 | 4. **Runs in the background** 47 | - Open overlay at any time by pressing the Ctrl+` keys 48 | - Quit easily through system tray 49 | 5. **Easily Toggle between Edit and Normal Modes** 50 | 6. **Take notes organised in a smart way** 51 | 52 | ## Getting Started 53 | 54 | To get started with FlowBuddy, follow these steps: 55 | 56 | 1. Install Python 3.10 57 | 2. Run `pip install -r requirements.txt` 58 | 3. Open `main.py` 59 | 4. Press the Ctrl+` keys to access the overlay 60 | 61 | ## Future Vision 62 | 63 | FlowBuddy will become an All-In-One solution that eliminates the need for juggling between different websites/apps/files. Anything you need will be at your fingertips ready to use. 64 | 65 | We are planning on adding template support. With many plugins to chose from, there will be endless options for anyone to setup their working environment exactly how they want. 66 | 67 | It will be template based so that people can share their configurations with others. 68 | This will help people discover new tools easier. 69 | 70 | 71 | Although there are hundreds of apps that can do specific features of FlowBuddy better, our approach is unique. 72 | 73 | We believe that simpicity is at the core of productivity. 74 | 75 | ## Potential Future Features 76 | 77 | 1. **No-Manual Notes** 78 | - select text, right click and send straight to FlowBuddy's Note Tool. 79 | 2. **Shortcut Templates** 80 | - Ability to share current configuration for others to use, will help others find good tools easier. 81 | 3. **Shortcut Templates** 82 | - Create a textbox in FlowBuddy that will quickly search anything we type. 83 | 4. **Work Clock** 84 | - Set up work cycles (Example: 2 hours of work, 15 minutes of break) Keep track of time working on a specific task. 85 | 5. **Easy Reminder Tool** 86 | 6. **Folder Mirror** 87 | - Window in FlowBuddy that will always display a specific folder. For example Downloads 88 | - Set up a project folder Mirror and add smart project restructuring to make sure every project is organized the exact same way 89 | 7. **Clipboard History** 90 | 91 | 92 | 93 | 94 | ## UI Explanation 95 | 96 | **Red buttons** — Exit/Delete 97 | **Green button** — Add Group/Task 98 | **Orange button** — Edit Mode 99 | 100 | ## Demo 101 | 102 | 103 | 104 | https://github.com/derto42/FlowBuddy/assets/125327962/a6f2ebf4-20b3-4566-9d69-141a67c17d6b 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/utils/hot_keys.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | from pynput import keyboard 3 | 4 | 5 | class HotKeys: 6 | _shortcuts_and_callbacks: dict[str, list[Callable, ...]] = {} 7 | _listener = None 8 | 9 | @staticmethod 10 | def add_global_shortcut(shortcut: str, callback: Callable) -> None: 11 | """ 12 | Adds a global shortcut with the specified `shortcut` key combination and `callback` function. 13 | 14 | This method adds a global shortcut that can be triggered by pressing the specified `shortcut` key combination 15 | from anywhere with or without the application. When the shortcut is triggered, the associated `callback` function will be called. 16 | 17 | Args: 18 | shortcut (str): The key combination for the global shortcut, specified in a string format. The format should 19 | follow the keyboard library's syntax for defining keyboard shortcuts. 20 | callback (Callable): The function to be called when the global shortcut is triggered. The function should 21 | be callable and take no arguments. 22 | 23 | Example: 24 | HotKeys.add_global_shortcut('++a', my_callback_function) 25 | 26 | Note: 27 | - The `shortcut` argument should be unique for each global shortcut. If a duplicate `shortcut` is added, the 28 | existing shortcut will be overwritten. 29 | - The `callback` function will be called synchronously when the global shortcut is triggered. It should 30 | execute quickly and avoid any long-running or blocking operations to prevent freezing the application. 31 | """ 32 | 33 | if not isinstance(callback, Callable): 34 | raise TypeError("Callback must be a callable object.") 35 | 36 | if shortcut not in HotKeys._shortcuts_and_callbacks.keys(): 37 | HotKeys._shortcuts_and_callbacks[shortcut] = [] 38 | HotKeys._shortcuts_and_callbacks[shortcut].append(callback) 39 | 40 | if HotKeys._listener is not None and HotKeys._listener.is_alive(): 41 | HotKeys._listener.stop() 42 | 43 | hot_keys = {_shortcut: lambda _shortcut=_shortcut: HotKeys._call_callbacks(_shortcut) for _shortcut in HotKeys._shortcuts_and_callbacks.keys()} 44 | HotKeys._listener = keyboard.GlobalHotKeys(hot_keys) 45 | HotKeys._listener.setName("HotKeys Listener") 46 | HotKeys._listener.start() 47 | HotKeys._listener.wait() 48 | 49 | 50 | @staticmethod 51 | def _call_callbacks(shortcut: str) -> None: 52 | """ 53 | Calls all the callback functions associated with the specified `shortcut`. 54 | 55 | This method retrieves the list of callback functions associated with the given `shortcut` and calls each 56 | function sequentially. The functions should be previously added using the `add_global_shortcut` method. 57 | 58 | Args: 59 | shortcut (str): The key combination of the global shortcut for which the callback functions need to be called. 60 | """ 61 | (func() for func in HotKeys._shortcuts_and_callbacks[shortcut]) 62 | 63 | 64 | @staticmethod 65 | def format_shortcut_string(key: str) -> str: 66 | x = ["alt", "alt_l", "alt_r", "alt_gr", "backspace", "caps_lock", "cmd", "cmd_l", "cmd_r", 67 | "ctrl", "ctrl_l", "ctrl_r", "delete", "down", "end", "enter", "esc", 68 | "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", 69 | "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", 70 | "home", "left", "page_down", "page_up", "right", 71 | "shift", "shift_l", "shift_r", "space", "tab", "up", 72 | "insert", "menu", "num_lock", "print_screen", "scroll_lock",] 73 | return "+".join(f"<{k.lower().strip()}>" if k.lower().strip() in x else k.lower().strip() for k in key.split("+")) 74 | 75 | 76 | 77 | # for test 78 | if __name__ == "__main__": 79 | HotKeys.add_global_shortcut("+k", lambda: print("key 1")) 80 | HotKeys.add_global_shortcut("+k", lambda: print("key 2")) 81 | HotKeys.add_global_shortcut("+k", lambda: print("key 3")) 82 | HotKeys.add_global_shortcut("+l", lambda: print("key 4")) 83 | 84 | HotKeys.format_shortcut_string("Shift + k") 85 | HotKeys.format_shortcut_string("Shift") 86 | HotKeys.format_shortcut_string("k") 87 | 88 | input() 89 | -------------------------------------------------------------------------------- /tests/test_SaveFile.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from addons.shortcuts.shortcuts_save import ( 4 | GroupClass, 5 | TaskAlreadyInGroup, 6 | TaskClass, 7 | get_group_by_id, 8 | get_task_by_id, 9 | is_id_used, 10 | load_groups, 11 | load_tasks, 12 | delete_group_by_id, 13 | delete_task_by_id, 14 | ) 15 | 16 | 17 | class TestGroupClass(unittest.TestCase): 18 | def test_group_attributes(self): 19 | test_group_class_1 = GroupClass("id_Test", None, None) 20 | test_group_class_2 = GroupClass("id_Test", "G_123456", None) 21 | 22 | self.assertEqual(test_group_class_1.group_id, f"G_{id(test_group_class_1)}") 23 | self.assertEqual(str(test_group_class_1), "id_Test") 24 | self.assertEqual(test_group_class_2.group_id, "G_123456") 25 | 26 | def test_group_task_func(self): 27 | test_group_class_1 = GroupClass("id_Test", None, ["T_12345", "T_987654"]) 28 | test_group_class_2 = GroupClass("id_Test", None, ["T_12345", "T_987654"]) 29 | test_group_class_2.remove("T_987654") 30 | 31 | self.assertEqual(test_group_class_2.group_tasks, ["T_12345"]) 32 | 33 | test_group_class_2.append("T_102030") 34 | 35 | self.assertEqual(test_group_class_1.group_tasks, ["T_12345", "T_987654"]) 36 | self.assertEqual(test_group_class_2.group_tasks, ["T_12345", "T_102030"]) 37 | self.assertRaises(TaskAlreadyInGroup, test_group_class_2.append, "T_102030") 38 | 39 | self.assertIsInstance(test_group_class_1.create_task("Test_task", "T_908070"), TaskClass) 40 | 41 | self.assertIn("T_908070", test_group_class_1.group_tasks) 42 | test_group_class_1.delete_task("T_908070") 43 | self.assertNotIn("T_908070", test_group_class_1.group_tasks) 44 | 45 | 46 | class TestTaskClass(unittest.TestCase): 47 | def test_task_attributes(self): 48 | test_task_class_1 = TaskClass( 49 | "attr_test", 50 | button_text="button test", 51 | url="bbc.com, http://stackoverflow.com", 52 | file_path=None, 53 | directory_path=None, 54 | ) 55 | test_task_class_2 = TaskClass( 56 | "attr_test", 57 | task_id="T_123456", 58 | button_text="button test", 59 | url="bbc.com, http://stackoverflow.com", 60 | file_path=None, 61 | directory_path=None, 62 | ) 63 | 64 | self.assertEqual(test_task_class_1.task_id, f"T_{id(test_task_class_1)}") 65 | self.assertEqual(test_task_class_2.task_id, "T_123456") 66 | 67 | self.assertEqual(str(test_task_class_1), "attr_test") 68 | self.assertIsInstance(test_task_class_1.url, list) 69 | 70 | self.assertEqual(test_task_class_1.url[0], "https://www.bbc.com/") 71 | self.assertEqual(test_task_class_1.url[1], "https://stackoverflow.com/") 72 | 73 | test_task_class_2.url = "github.com" 74 | self.assertEqual(test_task_class_2.url[0], "https://github.com/") 75 | 76 | 77 | class TestJsonWrite(unittest.TestCase): 78 | def test_json_data(self): 79 | test_group_class_1 = GroupClass("id_Test", "G_123456", None) 80 | test_task_class_1 = TaskClass( 81 | "Json_test", 82 | task_id="T_102030", 83 | button_text="button test", 84 | url="bbc.com, http://stackoverflow.com", 85 | file_path=None, 86 | directory_path=None, 87 | ) 88 | 89 | test_group_class_2 = get_group_by_id("G_123456") 90 | test_task_class_2 = get_task_by_id("T_102030") 91 | 92 | self.assertIsInstance(test_group_class_2, GroupClass) 93 | self.assertIsInstance(test_task_class_2, TaskClass) 94 | 95 | self.assertEqual(test_group_class_1.group_id, test_group_class_2.group_id) 96 | self.assertEqual(test_group_class_1.group_name, test_group_class_2.group_name) 97 | 98 | self.assertEqual(test_task_class_1.task_id, test_task_class_2.task_id) 99 | self.assertEqual(test_task_class_1.task_name, test_task_class_2.task_name) 100 | 101 | self.assertTrue(is_id_used("T_102030")) 102 | 103 | self.assertIn("G_123456", load_groups()) 104 | self.assertIn("T_102030", load_tasks()) 105 | 106 | delete_group_by_id("G_123456") 107 | delete_task_by_id("T_102030") 108 | 109 | self.assertNotIn("G_123456", load_groups()) 110 | self.assertNotIn("T_102030", load_tasks()) 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /src/addons/notes/notes.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtSignal 2 | from PyQt5.QtWidgets import ( 3 | QTextEdit, 4 | QVBoxLayout, 5 | QWidget, 6 | QInputDialog, 7 | ) 8 | from PyQt5.QtGui import ( 9 | QTextCursor, 10 | QKeySequence, 11 | ) 12 | 13 | from addon import AddOnBase 14 | 15 | from ui import ConfirmationDialog 16 | from settings import UI_SCALE 17 | from ui.utils import get_font 18 | from ui.base_window import TabsWindow 19 | from .notes_save import ( 20 | get_file_data, 21 | save_file_data, 22 | write_config, 23 | get_config, 24 | delete_file_data, 25 | ) 26 | 27 | 28 | class NoteTab(QWidget): 29 | def __init__(self, file_name): 30 | super().__init__() 31 | self.file_name = file_name 32 | self.text_edit = QTextEdit() 33 | self.text_edit.setFont(get_font(size=16)) 34 | self.text_edit.textChanged.connect(self.save_text_to_file) 35 | self.text_edit.setAcceptRichText(False) 36 | self.text_edit.setStyleSheet( 37 | """QTextEdit{ 38 | background-color:lightgrey; 39 | border-radius: 10; 40 | } """ 41 | ) 42 | layout = QVBoxLayout() 43 | layout.addWidget(self.text_edit) 44 | # Set the margins 45 | layout.setContentsMargins( 46 | int(24 * UI_SCALE), 47 | int(24 * UI_SCALE), 48 | int(22 * UI_SCALE), 49 | int(22 * UI_SCALE), 50 | ) 51 | self.setLayout(layout) 52 | self.load_text_from_file() 53 | 54 | def load_text_from_file(self): 55 | file_data = get_file_data(self.file_name) 56 | self.text_edit.setPlainText(file_data) 57 | self.text_edit.moveCursor(QTextCursor.End) 58 | 59 | def save_text_to_file(self): 60 | save_file_data(self.file_name, self.text_edit.toPlainText()) 61 | 62 | 63 | class JottingDownWindow(TabsWindow): 64 | window_toggle_signal = pyqtSignal() 65 | 66 | def __init__(self): 67 | super().__init__() 68 | 69 | self.window_toggle_signal.connect(self.toggle_window) 70 | 71 | self.load_tabs() 72 | self.old_pos = None 73 | self.red_button.clicked.connect(self.closeEvent) 74 | self.add_button.clicked.connect(self.add_new_tab) 75 | self.setFixedSize(840, 400) 76 | 77 | def create_tab(self, file_name): 78 | note_tab = NoteTab(file_name) 79 | self.tab = self.addTab(note_tab, file_name) 80 | self.tab.red_button.clicked.connect(lambda: window.remove_tab(file_name)) 81 | 82 | def load_tabs(self): 83 | for file_name in get_config()["files"]: 84 | self.create_tab(file_name) 85 | if self.count() == 0: 86 | self.create_tab("notes") 87 | 88 | def save_tabs(self): 89 | config = { 90 | "files": [self.tabText(i) for i in range(self.count())], 91 | "last_active": self.currentIndex(), 92 | } 93 | write_config(config) 94 | 95 | def remove_tab(self, tab_text): 96 | tabid = self.get_tab_number_from_text(tab_text) 97 | file_name = self.tabText(tabid) 98 | dialog = ConfirmationDialog(f"Delete tab {file_name}?") 99 | res = dialog.exec() 100 | if not res: 101 | return 102 | self.removeTab(tabid) 103 | delete_file_data(file_name) 104 | self.save_tabs() 105 | 106 | def get_tab_number_from_text(self, tab_text): 107 | for i in range(self.count()): 108 | if self.tabText(i) == tab_text: 109 | return i 110 | return -1 111 | 112 | def add_new_tab(self, file_name=""): 113 | if not file_name: 114 | file_name, ok = QInputDialog.getText( 115 | self, "New Note", "Enter the note name:" 116 | ) 117 | if not ok or not file_name: 118 | return 119 | self.create_tab(file_name) 120 | save_file_data(file_name) 121 | self.save_tabs() 122 | self.setCurrentIndex(len(self) - 1) 123 | 124 | def toggle_window(self) -> None: 125 | if self.isHidden(): 126 | window.show() 127 | window.activateWindow() 128 | if current_widget := self.currentWidget(): 129 | current_widget.setFocus() 130 | else: 131 | window.hide() 132 | 133 | def closeEvent(self, event): 134 | self.save_tabs() 135 | self.hide() 136 | 137 | 138 | window = JottingDownWindow() 139 | 140 | AddOnBase().activate = window.window_toggle_signal.emit 141 | AddOnBase().set_activate_shortcut(QKeySequence("Ctrl+`")) 142 | -------------------------------------------------------------------------------- /src/addons/shortcuts/shortcuts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel 4 | 5 | from addon import AddOnBase 6 | from addons.shortcuts.dialog import GroupDialog, REJECTED 7 | from ui.utils import get_font 8 | 9 | from . import shortcuts_save as Data 10 | 11 | from ui import ( 12 | BaseWindow, 13 | GrnButton, 14 | REJECTED, 15 | ) 16 | 17 | from .nodes import ( 18 | GroupNode, 19 | SubNodeManager, 20 | TaskNode 21 | ) 22 | 23 | from settings import apply_ui_scale as scaled 24 | 25 | 26 | add_on_base = AddOnBase() 27 | add_on_base.set_icon_path("icon.png") 28 | add_on_base.set_name("Shortcuts") 29 | 30 | 31 | class MainWindow(BaseWindow): 32 | def __init__(self) -> None: 33 | super().__init__(hide_title_bar = False) 34 | 35 | self._edit_mode: bool = False 36 | 37 | self.toggle_window = lambda: window.show() if window.isHidden() else window.hide() 38 | 39 | self.setContentsMargins(x := scaled(15), x, x, x) 40 | self.setMinimumSize(scaled(110), scaled(76)) 41 | 42 | self.setLayout(layout := QVBoxLayout()) 43 | layout.addLayout(nodes_layout := QVBoxLayout()) 44 | layout.setContentsMargins(0, 0, 0, 0) 45 | layout.setSpacing(0) 46 | nodes_layout.setContentsMargins(0, 0, 0, 0) 47 | nodes_layout.setSpacing(0) 48 | self._nodes_layout = nodes_layout 49 | 50 | self._group_nodes_manager = SubNodeManager(nodes_layout, self) 51 | 52 | layout.addLayout(add_new_group_layout := QHBoxLayout()) 53 | self._setup_add_new_group_button(add_new_group_layout) 54 | 55 | 56 | for group_id in Data.load_groups(): 57 | group_class = Data.get_group_by_id(group_id) 58 | self._add_group_node(group_class) 59 | 60 | self.yel_button.clicked.connect(self._toggle_edit_mode) 61 | self.red_button.clicked.connect(self.hide) 62 | 63 | 64 | def _setup_add_new_group_button(self, add_new_group_layout: QHBoxLayout): 65 | """Creates 'Add New Group' label and green button and places that on add_new_group_layout.""" 66 | add_new_group_layout.setContentsMargins(0, scaled(25), 0, 0) 67 | add_new_group_layout.setSpacing(0) 68 | 69 | add_group_label = QLabel("Add New Group", self) 70 | add_group_label.setFont(get_font(size=scaled(24), weight="semibold")) 71 | add_group_label.setStyleSheet("color: #ABABAB") 72 | 73 | add_group_button = GrnButton(self, "radial") 74 | add_group_button.clicked.connect(self._on_add_group_button) 75 | add_group_button.setToolTip("Add Group") 76 | 77 | add_new_group_layout.addWidget(add_group_label) 78 | add_new_group_layout.addSpacing(scaled(13)) 79 | add_new_group_layout.addWidget(add_group_button) 80 | add_new_group_layout.addStretch() 81 | 82 | self._add_new_group_label = add_group_label 83 | self._add_new_group_button = add_group_button 84 | self._add_new_group_layout = add_new_group_layout 85 | 86 | self._update_edit_mode() 87 | 88 | def _add_group_node(self, group_class: Data.GroupClass) -> None: 89 | group_node = GroupNode(group_class, self) 90 | self._group_nodes_manager.add_node(group_node) 91 | self._update_edit_mode() 92 | group_node.task_node_departed_signal.connect(self._on_task_node_departed) 93 | 94 | def _on_add_group_button(self) -> None: 95 | dialog = GroupDialog(self) 96 | if dialog.exec() != REJECTED: 97 | res = dialog.result() 98 | self._add_group_node(Data.GroupClass(res)) 99 | 100 | def _update_edit_mode(self) -> None: 101 | self._group_nodes_manager.set_edit_mode(self._edit_mode) 102 | self._add_new_group_label.setHidden(not self._edit_mode) 103 | self._add_new_group_button.setHidden(not self._edit_mode) 104 | self._add_new_group_layout.setContentsMargins(0, scaled(25) if self._edit_mode else 0, 0, 0) 105 | self.adjustSize() 106 | 107 | def _toggle_edit_mode(self) -> None: 108 | self._edit_mode = not self._edit_mode 109 | self._update_edit_mode() 110 | 111 | def _on_task_node_departed(self, task_node: TaskNode) -> None: 112 | print(f"{self._nodes[task_node.task_class.task_id]} is departed from {self._nodes[Data.get_group_id_of_task(task_node.task_class.task_id)]}.") 113 | # XXX: Should be implemented 114 | 115 | 116 | def get_first_node(self) -> GroupNode: 117 | return self.layout().itemAt(0).layout().itemAt(0).widget() 118 | 119 | @property 120 | def _nodes(self) -> dict[str, GroupNode | TaskNode]: 121 | """This property is used to keep all the nodes accessible via its save data id.""" 122 | return {**GroupNode.nodes, **TaskNode.nodes} 123 | 124 | 125 | window = MainWindow() 126 | add_on_base.activate = window.toggle_window -------------------------------------------------------------------------------- /src/ui/custom_button.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Literal 2 | from PyQt5.QtCore import Qt, QSize, QVariantAnimation, QEasingCurve, QEvent 3 | from PyQt5.QtWidgets import QPushButton, QWidget 4 | from PyQt5.QtGui import ( 5 | QPainter, 6 | QColor, 7 | QFontMetrics, 8 | QPaintEvent, 9 | QShowEvent, 10 | ) 11 | 12 | 13 | from FileSystem import icon as icon_path 14 | from .utils import get_font 15 | from settings import CORNER_RADIUS, UI_SCALE 16 | 17 | 18 | BUTTON_SIZE = { 19 | "radial": QSize(int(28 * UI_SCALE), int(28 * UI_SCALE)), 20 | "long": QSize(int(104 * UI_SCALE), int(28 * UI_SCALE)), 21 | } 22 | 23 | 24 | class Button(QPushButton): 25 | def __init__(self, parent: Optional[QWidget] = None, 26 | button_type: Literal["long", "radial"] = "radial", 27 | custom_size: QSize = None): 28 | super().__init__(parent=parent) 29 | 30 | self._size = custom_size if custom_size is not None else BUTTON_SIZE[button_type] 31 | self._button_type = button_type 32 | self.animate = False 33 | 34 | self.setCursor(Qt.PointingHandCursor) 35 | self.setFixedSize(self._size) 36 | self.setIconSize(self._size) 37 | 38 | self.animation = QVariantAnimation() 39 | self.animation.valueChanged.connect(self.set_size) 40 | self.easing_curve = QEasingCurve.OutBack 41 | self.duration = 500 42 | 43 | def set_icons(self, icon_name: str) -> None: 44 | suffix = ("_long" if self._button_type == "long" else "") + ".png" 45 | self.setStyleSheet( 46 | ( 47 | """ 48 | QPushButton { 49 | border: none; 50 | icon: url(%s); 51 | margin: 0px; 52 | padding: 0px; 53 | } 54 | QPushButton:hover { 55 | icon: url(%s); 56 | margin: 0px; 57 | padding: 0px; 58 | } 59 | """ 60 | % ( 61 | icon_path(f"{icon_name}{suffix}"), 62 | icon_path(f"{icon_name}_hover{suffix}"), 63 | ) 64 | ) 65 | ) 66 | 67 | def animate_resize(self, hidden: bool): 68 | if not self.animate: 69 | return 70 | zeero = QSize(1, 1) 71 | target_size = self._size 72 | if hidden: 73 | zeero, target_size = target_size, zeero 74 | self.animation.setStartValue(zeero) 75 | self.animation.setEndValue(target_size) 76 | 77 | self.animation.setEasingCurve(self.easing_curve) 78 | self.animation.setDuration(self.duration) 79 | 80 | self.animation.start() 81 | 82 | def set_size(self, size): 83 | self.setIconSize(size) 84 | 85 | def showEvent(self, a0: QShowEvent) -> None: 86 | self.animate_resize(hidden=False) 87 | return super().showEvent(a0) 88 | 89 | def setHidden(self, hidden: bool) -> None: 90 | self.animate_resize(hidden) 91 | return super().setHidden(hidden) 92 | 93 | 94 | class RedButton(Button): 95 | def __init__(self, parent: Optional[QWidget] = None, 96 | button_type: Literal["long", "radial"] = "radial"): 97 | super().__init__(parent = parent, button_type = button_type) 98 | self.set_icons("red_button") 99 | 100 | 101 | class YelButton(Button): 102 | def __init__(self, parent: Optional[QWidget] = None, 103 | button_type: Literal["long", "radial"] = "radial"): 104 | super().__init__(parent = parent, button_type = button_type) 105 | self.set_icons("yellow_button") 106 | 107 | 108 | class GrnButton(Button): 109 | def __init__(self, parent: Optional[QWidget] = None, 110 | button_type: Literal["long", "radial"] = "radial"): 111 | super().__init__(parent = parent, button_type = button_type) 112 | self.set_icons("green_button") 113 | 114 | 115 | class TextButton(QPushButton): 116 | def __init__(self, parent: Optional[QWidget] = None, 117 | text: str = "Text Button"): 118 | super().__init__(parent, text=text) 119 | self.setCursor(Qt.PointingHandCursor) 120 | self._x_padding = int(35 * UI_SCALE) 121 | self._y_padding = int(7 * UI_SCALE) 122 | self.setFont(get_font(size=int(16 * UI_SCALE))) 123 | self.setStyleSheet("color: #282828") 124 | 125 | def sizeHint(self): 126 | font_metrics = QFontMetrics(self.font()) 127 | text_width = font_metrics.width(self.text()) 128 | text_height = font_metrics.height() 129 | button_width = text_width + self._x_padding * 2 # *2 for Adding padding to both sides 130 | button_height = text_height + self._y_padding * 2 131 | return QSize(button_width, button_height) 132 | 133 | def paintEvent(self, a0: QPaintEvent): 134 | painter = QPainter(self) 135 | painter.setRenderHint(QPainter.Antialiasing) 136 | painter.setPen(Qt.NoPen) 137 | painter.setBrush(QColor("#DADADA" if self.underMouse() else "#ECECEC")) 138 | painter.drawRoundedRect(self.rect(), CORNER_RADIUS * UI_SCALE, CORNER_RADIUS * UI_SCALE) 139 | painter.setPen(self.palette().buttonText().color()) 140 | painter.drawText(self.rect(), Qt.AlignCenter, self.text()) 141 | -------------------------------------------------------------------------------- /src/ui/tooltip.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Literal 3 | from PyQt5 import QtGui 4 | from PyQt5.QtCore import Qt, QRectF, QVariantAnimation, QEasingCurve, QPoint, QSize, QAnimationGroup, QRect, QMetaObject 5 | from PyQt5.QtWidgets import ( 6 | QWidget, 7 | QVBoxLayout, 8 | QHBoxLayout, 9 | QLabel, 10 | QGraphicsDropShadowEffect, 11 | QDialog, 12 | QApplication, 13 | ) 14 | from PyQt5.QtGui import ( 15 | QColor, 16 | QPainter, 17 | QPainterPath, 18 | QPaintEvent, 19 | QMouseEvent, 20 | QFontMetrics, 21 | QCursor, 22 | ) 23 | 24 | 25 | from .utils import get_font 26 | from settings import * 27 | 28 | 29 | class ToolTip(QWidget): 30 | def __init__(self, text: str, parent: QWidget = None): 31 | super().__init__(parent) 32 | 33 | self.setAttribute(Qt.WA_TranslucentBackground, True) 34 | self.setAttribute(Qt.WA_TransparentForMouseEvents, True) 35 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 36 | 37 | self.text = text 38 | 39 | self.setFont(get_font(size=8)) 40 | self.setStyleSheet("color: #282828") 41 | 42 | self.setContentsMargins(20, 20, 20, 20) 43 | 44 | shadow = QGraphicsDropShadowEffect(self) 45 | shadow.setColor(QColor(118, 118, 118, 25)) 46 | shadow.setOffset(0, 4) 47 | shadow.setBlurRadius(14) 48 | self.setGraphicsEffect(shadow) 49 | 50 | self._x_padding = 10 51 | self._y_padding = 8 52 | self._width_animation = QVariantAnimation() 53 | self._width_animation.valueChanged.connect(self.setFixedWidth) 54 | self._width_animation.setDuration(200) 55 | self._width_animation.setEasingCurve(QEasingCurve.OutCubic) 56 | 57 | self._pos_animation = QVariantAnimation() 58 | self._pos_animation.valueChanged.connect(self.move) 59 | self._pos_animation.setDuration(200) 60 | self._pos_animation.setEasingCurve(QEasingCurve.OutCubic) 61 | 62 | self._alpha_animation = QVariantAnimation() 63 | self._alpha_animation.valueChanged.connect(self._set_alpha) 64 | self._alpha_animation.setDuration(200) 65 | 66 | self._hide_connection = None 67 | self._alpha: int = 0 68 | 69 | # self.show() 70 | # self.hide() 71 | 72 | 73 | def _set_alpha(self, alpha: int) -> None: 74 | self._alpha = alpha 75 | 76 | def _animate(self, mode: Literal["show", "hide"]) -> None: 77 | if mode == "show": 78 | self._setup_show_animation() 79 | elif mode == "hide": 80 | self._setup_hide_animation() 81 | self._width_animation.start() 82 | self._pos_animation.start() 83 | self._alpha_animation.start() 84 | 85 | def _setup_show_animation(self): 86 | self._width_animation.setStartValue(self.width()) 87 | self._width_animation.setEndValue(self.sizeHint().width()) 88 | self._pos_animation.setStartValue(self.pos()) 89 | self._pos_animation.setEndValue(self._position - QPoint(self.sizeHint().width() // 2, 0)) 90 | self._alpha_animation.setStartValue(0) 91 | self._alpha_animation.setEndValue(255) 92 | 93 | if self._hide_connection is not None: 94 | self._width_animation.finished.disconnect(self._hide_connection) 95 | self._hide_connection = None 96 | 97 | def _setup_hide_animation(self): 98 | self._width_animation.setStartValue(self.width()) 99 | self._width_animation.setEndValue(1) 100 | self._pos_animation.setStartValue(self.pos()) 101 | self._pos_animation.setEndValue(self._position) 102 | self._alpha_animation.setStartValue(255) 103 | self._alpha_animation.setEndValue(0) 104 | 105 | self._hide_connection: QMetaObject.Connection = self._width_animation.finished.connect(self.hide) 106 | 107 | 108 | def setText(self, text: str) -> None: 109 | self.text = text 110 | 111 | def sizeHint(self) -> QSize: 112 | font_metrics = QFontMetrics(self.font()) 113 | text_width = font_metrics.width(self.text) 114 | text_height = font_metrics.height() 115 | button_width = text_width + self._x_padding * 2 # *2 for Adding padding to both sides 116 | button_height = text_height + self._y_padding * 2 117 | return QSize(button_width, button_height) 118 | 119 | def paintEvent(self, a0: QPaintEvent) -> None: 120 | painter = QPainter(self) 121 | painter.setRenderHint(QPainter.Antialiasing) 122 | painter.setPen(Qt.NoPen) 123 | painter.setBrush(QColor(218, 218, 218, self._alpha)) # rgb(218, 218, 218) 124 | painter.drawRoundedRect(self.rect(), CORNER_RADIUS, CORNER_RADIUS) 125 | text_color = self.palette().buttonText().color() 126 | text_color.setAlpha(self._alpha) 127 | painter.setPen(text_color) 128 | painter.drawText(self.rect(), Qt.AlignCenter, self.text) 129 | return super().paintEvent(a0) 130 | 131 | def _show(self) -> None: 132 | self._position: QPoint = QCursor.pos() + QPoint(0, 15) 133 | self.move(self._position) 134 | self._animate("show") 135 | self.show() 136 | 137 | def _hide(self) -> None: 138 | self._animate("hide") -------------------------------------------------------------------------------- /src/addons/DertoEarlyTests/ss_overlay.pyw: -------------------------------------------------------------------------------- 1 | import sys 2 | import cv2 3 | import numpy as np 4 | from PIL import Image 5 | from PyQt5 import QtWidgets, QtCore, QtGui 6 | 7 | class Overlay(QtWidgets.QLabel): 8 | 9 | def __init__(self, parent=None): 10 | super(Overlay, self).__init__(parent) 11 | print("Overlay initialized") 12 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint) 13 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 14 | 15 | self.close_button = QtWidgets.QPushButton(self) 16 | self.close_button.setText("X") 17 | self.close_button.setStyleSheet("background-color: red") 18 | self.close_button.clicked.connect(self.close) 19 | self.close_button.hide() 20 | 21 | self.remove_bg_button = QtWidgets.QPushButton(self) 22 | self.remove_bg_button.setText("Remove BG") 23 | self.remove_bg_button.setStyleSheet("background-color: blue") 24 | self.remove_bg_button.clicked.connect(self.open_color_picker) 25 | self.remove_bg_button.hide() 26 | 27 | self.m_drag = False 28 | self.m_DragPosition = QtCore.QPoint() 29 | 30 | self.timer = QtCore.QTimer(self) 31 | self.timer.timeout.connect(self.hide_buttons) 32 | self.timer.setSingleShot(True) 33 | 34 | def mousePressEvent(self, QMouseEvent): 35 | if QMouseEvent.button() == QtCore.Qt.LeftButton: 36 | self.m_drag = True 37 | self.m_DragPosition = QMouseEvent.globalPos() - self.pos() 38 | QMouseEvent.accept() 39 | 40 | def mouseMoveEvent(self, QMouseEvent): 41 | if QMouseEvent.buttons() == QtCore.Qt.LeftButton: 42 | self.move(QMouseEvent.globalPos() - self.m_DragPosition) 43 | QMouseEvent.accept() 44 | 45 | def mouseReleaseEvent(self, QMouseEvent): 46 | self.m_drag = False 47 | 48 | def close(self): 49 | super(Overlay, self).close() 50 | 51 | def show_close_button(self): 52 | self.close_button.move(self.width()-25, 5) 53 | self.close_button.show() 54 | print("Close button is visible: ", self.close_button.isVisible()) 55 | 56 | def hide_close_button(self): 57 | self.close_button.hide() 58 | 59 | def show_remove_bg_button(self): 60 | self.remove_bg_button.move(self.width()-25, 50) 61 | self.remove_bg_button.show() 62 | 63 | def hide_remove_bg_button(self): 64 | self.remove_bg_button.hide() 65 | 66 | def enterEvent(self, event): 67 | self.show_close_button() 68 | self.show_remove_bg_button() 69 | self.timer.stop() 70 | 71 | def leaveEvent(self, event): 72 | self.timer.start(550) # Start the timer for 1.5 seconds 73 | 74 | def hide_buttons(self): 75 | self.hide_close_button() 76 | self.hide_remove_bg_button() 77 | 78 | 79 | def open_color_picker(self): 80 | color = QtWidgets.QColorDialog.getColor() 81 | 82 | if color.isValid(): 83 | # Convert QColor to RGB values 84 | color_rgb = color.getRgb()[:3] 85 | self.remove_background(color_rgb) 86 | 87 | def remove_background(self, color_rgb): 88 | screenshot = self.pixmap().toImage() 89 | width = screenshot.width() 90 | height = screenshot.height() 91 | ptr = screenshot.bits() 92 | ptr.setsize(height * width * 4) 93 | arr = np.frombuffer(ptr, np.uint8).reshape((height, width, 4)) 94 | 95 | # Convert np array to PIL Image 96 | img = Image.fromarray(arr) 97 | 98 | # Use Pillow's convert function to set transparency 99 | img = img.convert("RGBA") 100 | 101 | datas = img.getdata() 102 | 103 | newData = [] 104 | for item in datas: 105 | # change all (also shades of the color) white (also shades of grey) pixels to transparent 106 | if item[0] in list(range(color_rgb[0]-15, color_rgb[0]+15)): 107 | newData.append((255, 255, 255, 0)) 108 | else: 109 | newData.append(item) 110 | 111 | img.putdata(newData) 112 | 113 | # Convert PIL Image back to np array 114 | arr = np.array(img) 115 | 116 | qimage = QtGui.QImage(arr.data, width, height, QtGui.QImage.Format_ARGB32) 117 | pixmap = QtGui.QPixmap.fromImage(qimage) 118 | 119 | self.setPixmap(pixmap) 120 | 121 | 122 | 123 | 124 | class ScreenShotOverlay(QtWidgets.QWidget): 125 | 126 | def __init__(self, parent=None): 127 | super(ScreenShotOverlay, self).__init__(parent) 128 | print("ScreenShotOverlay initialized") 129 | self.begin = QtCore.QPoint() 130 | self.end = QtCore.QPoint() 131 | self.screenshot_overlay = None 132 | 133 | 134 | def start(self): 135 | print("Starting ScreenShotOverlay") 136 | self.setWindowOpacity(0.3) 137 | QtWidgets.QApplication.setOverrideCursor( 138 | QtGui.QCursor(QtCore.Qt.CrossCursor)) 139 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | 140 | QtCore.Qt.FramelessWindowHint) 141 | self.setGeometry(QtWidgets.QApplication.desktop().availableGeometry()) 142 | self.showFullScreen() 143 | 144 | def paintEvent(self, event): 145 | qp = QtGui.QPainter(self) 146 | qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3)) 147 | qp.setBrush(QtGui.QColor(128, 128, 255, 128)) 148 | qp.drawRect(QtCore.QRect(self.begin, self.end)) 149 | 150 | def mousePressEvent(self, event): 151 | print("Mouse button pressed") 152 | self.begin = event.pos() 153 | self.end = self.begin 154 | self.update() 155 | 156 | def mouseMoveEvent(self, event): 157 | print("Mouse moved") 158 | self.end = event.pos() 159 | self.update() 160 | 161 | def mouseReleaseEvent(self, event): 162 | print("Mouse button released") 163 | QtWidgets.QApplication.restoreOverrideCursor() 164 | self.hide() 165 | 166 | QtCore.QTimer.singleShot(200, self.take_screenshot) 167 | 168 | def take_screenshot(self): 169 | screenshot = QtWidgets.QApplication.primaryScreen().grabWindow( 170 | QtWidgets.QApplication.desktop().winId(), 171 | min(self.begin.x(), self.end.x()), 172 | min(self.begin.y(), self.end.y()), 173 | abs(self.begin.x() - self.end.x()), 174 | abs(self.begin.y() - self.end.y()) 175 | ) 176 | print("Screenshot taken") 177 | 178 | self.screenshot_overlay = Overlay() 179 | self.screenshot_overlay.setPixmap(screenshot) 180 | self.screenshot_overlay.show() 181 | self.screenshot_overlay.show_close_button() 182 | 183 | print("Overlay is visible: ", self.screenshot_overlay.isVisible()) 184 | 185 | self.close() 186 | 187 | 188 | if __name__ == '__main__': 189 | print("Program started") 190 | app = QtWidgets.QApplication(sys.argv) 191 | overlay = ScreenShotOverlay() 192 | overlay.start() 193 | sys.exit(app.exec_()) 194 | -------------------------------------------------------------------------------- /src/addons/Settings/Settings.py: -------------------------------------------------------------------------------- 1 | from typing import overload 2 | from importlib import reload 3 | import contextlib 4 | from typing import Optional, Tuple, Any 5 | from PyQt5 import QtCore, QtGui 6 | 7 | from PyQt5.QtCore import Qt, pyqtSignal, QRectF 8 | from PyQt5.QtGui import QMouseEvent, QPaintEvent, QPainter, QColor, QCursor, QWheelEvent 9 | from PyQt5.QtWidgets import ( 10 | QHBoxLayout, 11 | QLabel, 12 | QVBoxLayout, 13 | QWidget, 14 | QLayout, 15 | QApplication, 16 | QLineEdit, 17 | QPushButton, 18 | QSpinBox, 19 | QDoubleSpinBox, 20 | ) 21 | 22 | 23 | import SaveFile as Data 24 | from addon import AddOnBase 25 | from ui import BaseWindow, Entry 26 | from settings import UI_SCALE, CORNER_RADIUS 27 | import settings as GlobalSettings # for reloading the values 28 | 29 | from .structure import STRUCTURE, UPDATE, ENTRY, SPIN, KEY, SETTING_TYPE, TYPE, OPTIONS 30 | 31 | 32 | class Button(QPushButton): 33 | def __init__(self, text: str, parent: QWidget | None = None) -> None: 34 | super().__init__(text, parent) 35 | 36 | 37 | class SpinBox(Entry): 38 | def __init__(self, current_value: int | float = 0, step: int | float = 1, 39 | parent: QWidget = None) -> None: 40 | super().__init__(parent, "") 41 | 42 | self._value = current_value 43 | self._step = step 44 | 45 | self.setText(current_value) 46 | 47 | self.setLayout(layout := QVBoxLayout()) 48 | layout.setContentsMargins(self.width() - self.width() // 3, 0, 0, 0) 49 | layout.setSpacing(0) 50 | layout.addWidget(but1 := QPushButton("↑")) 51 | layout.addWidget(but2 := QPushButton("↓")) 52 | but1.setFixedSize(self.width() // 3, self.height() // 2) 53 | but2.setFixedSize(self.width() // 3, self.height() // 2) 54 | but1.clicked.connect(lambda *_: self._value_add()) 55 | but2.clicked.connect(lambda *_: self._value_substract()) 56 | but1.setCursor(Qt.CursorShape.PointingHandCursor) 57 | but2.setCursor(Qt.CursorShape.PointingHandCursor) 58 | 59 | 60 | self.value = self.text 61 | self.setValue = self.setText 62 | self.valueChanged = self.textChanged 63 | self.valueEdited = self.textEdited 64 | 65 | 66 | @overload 67 | def value(self) -> int | float: ... 68 | @overload 69 | def setValue(self, value: int | float) -> None: ... 70 | 71 | 72 | def _value_add(self, step: int | float = None) -> None: 73 | if step is None: step = self._step 74 | self.setValue(self._value + step) 75 | 76 | def _value_substract(self, step: int | float = None) -> None: 77 | if step is None: step = self._step 78 | self.setValue(self._value - step) 79 | 80 | 81 | def text(self) -> int | float: 82 | return self._value 83 | 84 | def setText(self, value: int | float) -> None: 85 | self._value = value 86 | return super().setText(str(value)) 87 | 88 | def mousePressEvent(self, a0: QMouseEvent) -> None: 89 | return super().mousePressEvent(a0) 90 | 91 | def wheelEvent(self, a0: QWheelEvent) -> None: 92 | self._value_add((a0.angleDelta().y() // 120) * self._step) 93 | return super().wheelEvent(a0) 94 | 95 | 96 | class SettingsUI(QWidget): 97 | window_toggle_signal = pyqtSignal() 98 | 99 | def __init__(self, parent: QWidget | None = None) -> None: 100 | super().__init__(parent) 101 | 102 | # temporarily disabled close button. 103 | self.setWindowFlag(QtCore.Qt.WindowType.WindowCloseButtonHint, False) 104 | 105 | self._layouts: dict[str, QVBoxLayout] = {} 106 | 107 | self.setLayout(layout:=QVBoxLayout()) 108 | layout.setContentsMargins(10, 10, 10, 10) 109 | layout.setSpacing(0) 110 | self._layout = layout 111 | 112 | for group_name, settings in STRUCTURE.items(): 113 | self._create_group(group_name) 114 | for setting_name, options in settings.items(): 115 | self._create_setting(group_name, setting_name, options) 116 | 117 | self.window_toggle_signal.connect(lambda: self.show() if self.isHidden() else self.hide()) 118 | 119 | 120 | def _create_group(self, group_name: str) -> None: 121 | self._layout.addLayout(group_layout := QVBoxLayout()) 122 | group_layout.setContentsMargins(0, 0, 0, 0) 123 | group_layout.setSpacing(0) 124 | 125 | group_layout.addWidget((name := QLabel(group_name, self))) 126 | 127 | self._layouts[group_name] = group_layout 128 | 129 | 130 | def _create_setting(self, group_name: str, setting_name: str, options: dict[str, Any]) -> None: 131 | self._layouts[group_name].addLayout(setting_layout := QHBoxLayout()) 132 | setting_layout.setContentsMargins(0, 0, 0, 0) 133 | setting_layout.setSpacing(0) 134 | 135 | setting_layout.addWidget(name := QLabel(setting_name)) 136 | 137 | setting_key: str = options[KEY] 138 | setting_type: str = options[SETTING_TYPE] 139 | value_type: type = options[TYPE] 140 | setting_options: list = options[OPTIONS] 141 | 142 | def get_setting_value(*_, setting_name: str = setting_key) -> Any: 143 | with contextlib.suppress(Data.NotFoundException): 144 | return Data.get_setting(setting_name) 145 | 146 | def set_setting_value(*_, setting_name: str = setting_key, value: Any = None) -> None: 147 | with contextlib.suppress(Data.NotFoundException): 148 | Data.apply_setting(setting_name, value) 149 | reload(GlobalSettings) 150 | 151 | def reset_setting_value(*_, setting_name: str = setting_key, widget: QSpinBox = None) -> None: 152 | with contextlib.suppress(Data.NotFoundException): 153 | Data.remove_setting(setting_name) 154 | if widget is not None: 155 | reload(GlobalSettings) 156 | widget.setValue(UI_SCALE) 157 | 158 | if setting_type == ENTRY: 159 | setting_layout.addWidget(entry := QLineEdit()) 160 | entry.setText(str(get_setting_value())) 161 | entry.textChanged.connect(lambda value: set_setting_value(value=value)) 162 | 163 | elif setting_type == SPIN: 164 | if value_type == int: 165 | spinbox = QSpinBox() 166 | elif value_type == float: 167 | spinbox = QDoubleSpinBox() 168 | 169 | setting_layout.addWidget(spinbox) 170 | spinbox.setValue(value if (value:=get_setting_value()) is not None else UI_SCALE) 171 | spinbox.setRange(0, 10) 172 | spinbox.setSingleStep(0.1) 173 | spinbox.valueChanged.connect(lambda value: set_setting_value(value=value)) 174 | 175 | setting_layout.addWidget(reset_button := QPushButton("Reset")) 176 | reset_button.clicked.connect(lambda _: reset_setting_value(widget=spinbox)) 177 | 178 | 179 | ui_window = SettingsUI() 180 | 181 | addon_base = AddOnBase() 182 | addon_base.activate = ui_window.window_toggle_signal.emit -------------------------------------------------------------------------------- /src/ui/base_window/base_window.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Literal 3 | from PyQt5.QtCore import Qt 4 | from PyQt5.QtWidgets import ( 5 | QGraphicsEffect, 6 | QWidget, 7 | QVBoxLayout, 8 | QGraphicsDropShadowEffect, 9 | ) 10 | from PyQt5.QtGui import ( 11 | QColor, 12 | QPainter, 13 | QIcon, 14 | QResizeEvent, 15 | ) 16 | 17 | from settings import CORNER_RADIUS, apply_ui_scale as scaled 18 | from ui.custom_button import RedButton 19 | 20 | from .title_bar_layer import TabButton, TitleBarLayer 21 | from .tab_widget import TabWidget 22 | 23 | 24 | def add_base_window(widget: QWidget | TabWidget, title_bar: Literal["title", "tab", "hidden"], 25 | parent: QWidget | None = None) -> None: 26 | 27 | if title_bar not in ["title", "tab", "hidden"]: 28 | raise ValueError(f"Invalid title_bar option: '{title_bar}'. title_bar should be 'title' or 'tab'") 29 | 30 | shadow_layer = QWidget(parent, Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 31 | shadow_layer.setAttribute(Qt.WA_TranslucentBackground) 32 | 33 | shadow_layer.setLayout(shadow_layer_layout := QVBoxLayout(shadow_layer)) 34 | shadow_layer_layout.setContentsMargins(x := scaled(50), x, x, x) 35 | shadow_layer_layout.setSpacing(0) 36 | 37 | # create widget for show title bar. 38 | shadow_layer_layout.addWidget(title_bar_layer := TitleBarLayer(title_bar, shadow_layer)) 39 | title_bar_layer.setLayout(title_bar_layer_layout := QVBoxLayout(title_bar_layer)) 40 | title_bar_layer_layout.setContentsMargins(0, 0, 0, 0) 41 | if title_bar == "tab": spacing = scaled(50) 42 | elif title_bar == "title": spacing = scaled(34) 43 | else: spacing = 0 44 | title_bar_layer_layout.addSpacing(scaled(spacing)) 45 | 46 | widget.setParent(title_bar_layer) 47 | title_bar_layer_layout.addWidget(widget) 48 | 49 | # adding shadow that shows behind the main window. 50 | main_window_shadow = QGraphicsDropShadowEffect(title_bar_layer) 51 | main_window_shadow.setColor(QColor(118, 118, 118, 70)) 52 | main_window_shadow.setOffset(0, scaled(10)) 53 | main_window_shadow.setBlurRadius(60) 54 | title_bar_layer.setGraphicsEffect(main_window_shadow) 55 | 56 | # adding shadow that shows in the title bar 57 | title_bar_shadow = QGraphicsDropShadowEffect() 58 | title_bar_shadow.setColor(QColor(118, 118, 118, 25)) 59 | title_bar_shadow.setOffset(0, scaled(-4.33)) 60 | title_bar_shadow.setBlurRadius(scaled(27)) 61 | # the shadow doesn't apply to the title bar if the title_bar is "hidden" 62 | if title_bar != "hidden": 63 | widget.setGraphicsEffect(title_bar_shadow) 64 | 65 | # redirecting some functions to shadow_layer. 66 | widget.show = shadow_layer.show 67 | widget.hide = shadow_layer.hide 68 | widget.isHidden = shadow_layer.isHidden 69 | widget.parent = shadow_layer.parent 70 | widget.setParent = shadow_layer.setParent 71 | 72 | widget.shadow_layer = shadow_layer 73 | widget.title_bar_layer = title_bar_layer 74 | widget.shadow_effect = title_bar_shadow 75 | 76 | 77 | class Buttons: 78 | def __init__(self): 79 | # for linting purposes 80 | self.title_bar_layer: TitleBarLayer 81 | 82 | @property 83 | def red_button(self): 84 | (button := self.title_bar_layer.buttons.red_button).show() 85 | self.title_bar_layer._set_button_position() 86 | return button 87 | 88 | @property 89 | def yel_button(self): 90 | (button := self.title_bar_layer.buttons.yel_button).show() 91 | self.title_bar_layer._set_button_position() 92 | return button 93 | 94 | @property 95 | def grn_button(self): 96 | (button := self.title_bar_layer.buttons.grn_button).show() 97 | self.title_bar_layer._set_button_position() 98 | return button 99 | 100 | 101 | class BaseWindow(QWidget, Buttons): 102 | def __init__(self, hide_title_bar: bool = False, parent: QWidget | None = None) -> None: 103 | super().__init__() 104 | 105 | # fot linting 106 | self.shadow_layer: QWidget 107 | self.title_bar_layer: TitleBarLayer 108 | self.shadow_effect: QGraphicsDropShadowEffect 109 | 110 | add_base_window(self, "hidden" if hide_title_bar else "title", parent) 111 | 112 | 113 | def set_title(self, title: str) -> None: 114 | self.title_bar_layer.set_title(title) 115 | 116 | def title(self) -> str: 117 | return self.title_bar_layer.title() 118 | 119 | 120 | def setGraphicsEffect(self, effect: QGraphicsEffect) -> None: 121 | """NOTE: Shadow effect is already applied to this window.\n 122 | for access the shadow effect, self.shadow_layer.""" 123 | # this function defined here just for add the docstring 124 | return super().setGraphicsEffect(effect) 125 | 126 | def resizeEvent(self, a0: QResizeEvent) -> None: 127 | self.title_bar_layer.adjustSize() 128 | self.shadow_layer.adjustSize() 129 | return super().resizeEvent(a0) 130 | 131 | 132 | class TabsWindow(TabWidget, Buttons): 133 | class TabIndex(int): 134 | """This class created for access the tab_button and tab_button.red_button from the index of the tab.""" 135 | def __new__(cls, index: int, tab_button: TabButton): 136 | cls.tab_button = tab_button 137 | return super().__new__(cls, index) 138 | 139 | @property 140 | def red_button(self) -> RedButton: 141 | (button := self.tab_button.red_button).show() 142 | return button 143 | 144 | def __init__(self, parent: QWidget | None = None) -> None: 145 | super().__init__() 146 | 147 | # fot linting 148 | self.shadow_layer: QWidget 149 | self.title_bar_layer: TitleBarLayer 150 | self.shadow_effect: QGraphicsDropShadowEffect 151 | 152 | add_base_window(self, "tab", parent) 153 | 154 | 155 | @property 156 | def add_button(self): 157 | (button := self.title_bar_layer.add_button).show() 158 | self.title_bar_layer._reset_tab_positions() 159 | return button 160 | 161 | 162 | def setGraphicsEffect(self, effect: QGraphicsEffect) -> None: 163 | """NOTE: Shadow effect is already applied to this window.\n 164 | for access the shadow effect, self.shadow_layer.""" 165 | # this function defined here just for add the docstring 166 | return super().setGraphicsEffect(effect) 167 | 168 | 169 | def addTab(self, widget: QWidget, label: str, icon: QIcon | None = None) -> TabsWindow.TabIndex: 170 | # NOTE: index of QTabWidget is tab_id here 171 | tab_id = super().addTab(widget, label) 172 | tab_button = self.title_bar_layer.add_tab_button(label, tab_id) 173 | tab_button.clicked.connect(self.setCurrentIndex) 174 | self.title_bar_layer.set_tab_focus(self.currentIndex()) 175 | return TabsWindow.TabIndex(tab_id, tab_button) 176 | 177 | def removeTab(self, index: int) -> None: 178 | super().removeTab(index) 179 | self.title_bar_layer.remove_tab_button(index) 180 | self.title_bar_layer.set_tab_focus(self.currentIndex()) 181 | 182 | def setCurrentIndex(self, index: int) -> None: 183 | self.title_bar_layer.set_tab_focus(index) 184 | return super().setCurrentIndex(index) 185 | 186 | 187 | def paintEvent(self, paint_event) -> None: 188 | painter = QPainter(self) 189 | painter.setBrush(QColor("#FFFFFF")) 190 | painter.setPen(Qt.PenStyle.NoPen) 191 | painter.drawRoundedRect(self.rect(), x := scaled(CORNER_RADIUS), x) 192 | painter.end() 193 | 194 | def resizeEvent(self, a0: QResizeEvent) -> None: 195 | self.title_bar_layer.adjustSize() 196 | self.shadow_layer.adjustSize() 197 | return super().resizeEvent(a0) 198 | -------------------------------------------------------------------------------- /src/addons/shortcuts/dialog.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Any, Tuple, Optional, Literal 3 | from PyQt5.QtCore import QEvent, QVariantAnimation,QPropertyAnimation, QEasingCurve, QRect 4 | 5 | from PyQt5.QtGui import QCursor, QResizeEvent 6 | from PyQt5.QtWidgets import ( 7 | QWidget, 8 | QVBoxLayout, 9 | QFileDialog, 10 | QGraphicsOpacityEffect, 11 | ) 12 | from .shortcuts_save import TaskClass 13 | 14 | from ui import BaseDialog, ACCEPTED, REJECTED, TextButton, Entry 15 | 16 | 17 | class FileChooseButton(TextButton): 18 | 19 | class InnerButton(TextButton): 20 | def __init__(self, parent: QWidget, text: str = "Text Button", side: str = Literal["left", "right"]): 21 | super().__init__(parent, text) 22 | 23 | self.side = side 24 | self._parent = parent 25 | 26 | self.exposed_geometry = None 27 | self.hidden_geometry = None 28 | 29 | self.opacity = QGraphicsOpacityEffect() 30 | self.opacity.setOpacity(0.0) 31 | self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity") 32 | self.opacity_anim.setDuration(500) 33 | self.opacity_anim.setEasingCurve(QEasingCurve.Type.OutCubic) 34 | 35 | self.setGraphicsEffect(self.opacity) 36 | 37 | self.animation = QVariantAnimation() 38 | self.animation.setDuration(500) 39 | self.animation.setEasingCurve(QEasingCurve.Type.OutCubic) 40 | self.animation.valueChanged.connect(self.setGeometry) 41 | 42 | 43 | def spawn(self): 44 | self.animation.stop() 45 | self.opacity_anim.stop() 46 | self.animation.setStartValue(self.geometry()) 47 | self.animation.setEndValue(self.exposed_geometry) 48 | self.opacity_anim.setStartValue(self.opacity.opacity()) 49 | self.opacity_anim.setEndValue(0.99) 50 | self.animation.start() 51 | self.opacity_anim.start() 52 | 53 | def kill(self): 54 | self.animation.stop() 55 | self.opacity_anim.stop() 56 | self.animation.setStartValue(self.geometry()) 57 | self.animation.setEndValue(self.hidden_geometry) 58 | self.opacity_anim.setStartValue(self.opacity.opacity()) 59 | self.opacity_anim.setEndValue(0.0) 60 | self.animation.start() 61 | self.opacity_anim.start() 62 | 63 | def define_geometries(self): 64 | geo = self._parent.geometry() 65 | width = geo.width() // 2 66 | height = geo.height() 67 | self.exposed_geometry = QRect(width if self.side == "right" else 0, 0, 68 | width, height) 69 | self.hidden_geometry = QRect(width // 2, 0, width, height) 70 | 71 | self.setGeometry(self.hidden_geometry) 72 | 73 | def resizeEvent(self, a0: QResizeEvent) -> None: 74 | if self.exposed_geometry is None: 75 | self.define_geometries() 76 | return super().resizeEvent(a0) 77 | 78 | 79 | def __init__(self, parent: QWidget | None = None, text: str = "Text Button"): 80 | super().__init__(parent, text) 81 | 82 | self.adjustSize() 83 | 84 | self.file_button = self.InnerButton(self, "File", "left") 85 | self.folder_button = self.InnerButton(self, "Folder", "right") 86 | 87 | def enterEvent(self, a0: QEvent) -> None: 88 | self.file_button.spawn() 89 | self.folder_button.spawn() 90 | return super().enterEvent(a0) 91 | 92 | def leaveEvent(self, a0: QEvent) -> None: 93 | self.file_button.kill() 94 | self.folder_button.kill() 95 | return super().leaveEvent(a0) 96 | 97 | 98 | class GroupDialog(BaseDialog): 99 | def __init__(self, parent: QWidget | None = None) -> None: 100 | super().__init__("New Group", parent) 101 | 102 | self.setLayout(layout:=QVBoxLayout()) 103 | 104 | self._name_entry = Entry(self, "Group Name") 105 | layout.addWidget(self._name_entry) 106 | self._name_entry.setFocus() 107 | 108 | def for_edit(self, name: str): 109 | self.setTitle("Edit Group") 110 | self._name_entry.setText(name) 111 | self._name_entry.setToolTip("Group Name") 112 | 113 | 114 | def result(self) -> str | BaseDialog.DialogCode: 115 | return self._name_entry.text() if super().result() == ACCEPTED else super().result() 116 | 117 | def exec(self) -> Any: 118 | self.adjustSize() 119 | cursor_pos = QCursor.pos() 120 | geo = self.geometry() 121 | self.setGeometry(geo.adjusted(cursor_pos.x() - geo.width()//2, cursor_pos.y() - geo.height()//2, 0, 0)) 122 | 123 | super().exec() 124 | return self.result() 125 | 126 | def exec_(self) -> int: 127 | return self.exec() 128 | 129 | 130 | class TaskDialog(BaseDialog): 131 | def __init__(self, parent: QWidget | None = None) -> None: 132 | super().__init__("New Task", parent) 133 | 134 | self._file_path = None 135 | 136 | self.setLayout(layout:=QVBoxLayout()) 137 | 138 | self._name_entry = Entry(self, "Task Name") 139 | self._button_entry = Entry(self, "Button Text") 140 | self._url_entry = Entry(self, "URL") 141 | file_choose_button = FileChooseButton(self, "Choose File") 142 | file_choose_button.file_button.clicked.connect(lambda: self._choose_file("file")) 143 | file_choose_button.folder_button.clicked.connect(lambda: self._choose_file("folder")) 144 | 145 | self._name_entry.setToolTip("Task Name") 146 | self._button_entry.setToolTip("Button Text") 147 | self._url_entry.setToolTip("URL") 148 | file_choose_button.setToolTip("Choose File") 149 | 150 | layout.addWidget(self._name_entry) 151 | layout.addWidget(self._button_entry) 152 | layout.addWidget(self._url_entry) 153 | layout.addWidget(file_choose_button) 154 | 155 | self._name_entry.setFocus() 156 | 157 | def _choose_file(self, type: Literal["file", "folder"]): 158 | options = QFileDialog.Options() 159 | options |= QFileDialog.ReadOnly 160 | if type == "file": 161 | self._file_path, _ = QFileDialog.getOpenFileName(self, "Choose File", "", 162 | "All Files (*)", options=options) 163 | elif type == "folder": 164 | self._file_path = QFileDialog.getExistingDirectory(self, "Choose Folder", "", options=options) 165 | 166 | def for_edit(self, task_class: TaskClass) -> None: 167 | name, button_text, url, file_path, _ = task_class.get_task_data().values() 168 | self.setTitle("Edit Task") 169 | self._name_entry.setText(name) 170 | self._button_entry.setText(button_text if button_text is not None else "") 171 | self._url_entry.setText(', '.join(url) if url is not None else "") 172 | self._file_path = file_path if file_path is not None else "" 173 | 174 | def result(self) -> Tuple[Optional[str]]: 175 | if (ret:=super().result()) != ACCEPTED: 176 | return ret 177 | namet, buttont = self._name_entry.text(), self._button_entry.text() 178 | urlt, filet = self._url_entry.text(), self._file_path 179 | name = namet 180 | button_text = buttont if buttont else None 181 | url = urlt if urlt else None 182 | file_path = filet if filet else None 183 | return name, button_text, url, file_path 184 | 185 | def exec(self) -> tuple[str, str, str, str]: 186 | self.adjustSize() 187 | cursor_pos = QCursor.pos() 188 | geo = self.geometry() 189 | self.setGeometry(geo.adjusted(cursor_pos.x() - geo.width()//2, cursor_pos.y() - geo.height()//2, 0, 0)) 190 | 191 | super().exec() 192 | return self.result() 193 | 194 | def exec_(self): 195 | return self.exec() 196 | -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/img.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.15.0) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x06\x8b\ 13 | \x00\ 14 | \x01\x08\x3e\x78\x9c\xed\x9d\x4f\x8b\x5c\x45\x14\xc5\xab\xd3\xe2\ 15 | \x84\x68\x18\x57\xee\x62\x3f\x10\x4c\x60\x3e\x44\x2f\x34\xe8\xca\ 16 | \xa0\x4b\x43\x10\x5c\x04\xc1\x31\x20\x68\xa2\xab\x69\xdd\xeb\x17\ 17 | \x10\x14\x43\xc8\x1f\x5d\xb8\x90\x8c\x22\xa8\x0d\x7e\x84\x28\x6a\ 18 | \x70\x31\xe0\x2e\x62\x1c\x51\x71\x84\xc9\x8c\xb7\x5e\x55\xbd\x57\ 19 | \x5d\xce\xa4\xff\x55\xd5\xad\x7a\x75\x7e\xe1\xa6\xbb\x7a\xfa\xbd\ 20 | \x3a\xb7\xce\x79\x45\x77\xcf\x24\x23\x44\x8f\xfe\x8c\x46\x82\xfe\ 21 | \xae\xc4\xa9\xa3\x3d\xf1\xa8\x10\xe2\x14\x15\x3d\x24\x1f\xac\x1f\ 22 | \xaf\xa1\xaf\xfd\x70\x5c\xd4\x05\x00\x00\x00\x00\x00\x00\x00\x00\ 23 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xac\x54\x55\ 24 | \x75\x9c\xea\x65\xaa\xcf\xa8\x7e\xa1\xfa\x97\xea\x6f\xaa\x9f\xa9\ 25 | \x3e\xa4\x7a\x8e\xea\x08\xb7\xce\x98\x50\xbf\x27\xa9\xde\xa0\xba\ 26 | \x4c\xf5\x25\xd5\xa6\x5e\x8b\x57\xa9\x1e\xe3\xd6\xe7\x03\xea\xa3\ 27 | \x47\x75\x81\xea\x57\xaa\xfd\x29\xf5\x1d\xd5\x33\xdc\x9a\x43\x43\ 28 | \x3d\xae\x51\x7d\x31\x65\x2d\xee\x51\xdd\xa0\x1a\x70\xeb\x5d\x14\ 29 | \xd2\xfe\x30\xd5\x27\x33\xf8\x6e\xd7\x1e\xd5\xdb\xdc\xda\x43\x41\ 30 | \xbd\x9d\xa5\xfa\x67\x8e\xf5\xf8\x83\xea\x0c\xb7\xee\x79\xd1\xde\ 31 | \x7f\x3b\xa7\xf7\x76\xbd\xc7\xdd\x83\x6f\xa8\xa7\x37\x17\x5c\x8b\ 32 | \x5d\xaa\x73\xdc\xfa\x67\xc5\x83\xf7\x9d\xcb\xc0\x12\xde\x67\x95\ 33 | \x01\xd2\x78\xcc\x93\xf7\x9d\xc9\x80\x07\xef\xb3\xc8\x00\x69\x3b\ 34 | \x42\x75\xd3\xa3\xf7\xd9\x67\xc0\xa3\xf7\x76\x06\x4e\x73\xf7\x75\ 35 | \x10\xa4\xeb\xb5\x00\xde\x67\x9b\x81\x00\xde\x9b\xfa\xbd\x4a\xec\ 36 | \x7d\x01\xe9\x79\xa4\x52\xaf\x55\x43\xf9\x9f\x55\x06\x02\x7a\x6f\ 37 | \xea\x3a\x77\x8f\x36\xa4\x67\x3d\x70\xbf\xd9\x64\x20\x82\xf7\xb2\ 38 | \xe4\xe7\x03\x27\xb8\x7b\x35\x54\xd3\x3f\xcf\x28\x22\x03\x91\xbc\ 39 | \x37\xb5\xce\xdd\xaf\x81\xb4\xdc\x89\xd8\x77\x92\x19\x88\xec\xbd\ 40 | \xac\x2b\xdc\x3d\x1b\x22\xf7\x9d\x5c\x06\x18\xbc\x97\xf5\x35\x77\ 41 | \xdf\x86\x4a\x7d\x2f\xa7\xc8\x0c\x30\x79\x2f\x6b\xcc\xdd\xbb\x81\ 42 | \xb4\xdc\x66\x5a\x03\xd6\x0c\x30\x7a\x2f\xeb\x2a\x57\xdf\x2e\xa4\ 43 | \xe5\x3a\xe3\x3a\xb0\x64\x80\xd9\x7b\x59\x17\x62\xf7\x7c\x18\xa4\ 44 | \xe5\x79\xe6\xb5\x88\x9a\x81\x04\xbc\x97\xdf\x27\x7d\x3c\x56\xbf\ 45 | \xd3\xa8\xd4\x67\xbf\xb7\x4a\xc8\x40\x02\xde\xcb\xda\x0c\xdd\xe7\ 46 | \xbc\x90\xa6\xa7\x2a\xf5\xf9\x34\xf7\xda\x04\xcb\x40\x22\xde\xcb\ 47 | \xd7\xda\x6b\xa1\x7a\x5c\x06\xd2\x75\x31\x81\xf5\x09\x92\x81\x44\ 48 | \xbc\x97\xfb\xfe\x0b\xbe\x7b\xf3\x09\xe9\x7b\x37\x81\x75\xf2\x9a\ 49 | \x81\x44\xbc\x97\x75\xc9\x57\x4f\x21\xe9\x52\x06\xe0\xfd\x62\x74\ 50 | \x21\x03\xf0\x7e\x39\x72\xce\x00\xbc\xf7\x43\x8e\x19\x80\xf7\x7e\ 51 | \xc9\x29\x03\xf0\x3e\x0c\x39\x64\x00\xde\x87\x25\xe5\x0c\xc0\xfb\ 52 | \x38\xa4\x98\x01\x78\x1f\x97\x94\x32\x00\xef\x79\x48\x28\x03\x29\ 53 | \x54\x51\xde\x1b\x90\x81\x72\xbd\x37\x14\x9e\x81\xa2\xbd\x37\x14\ 54 | \x9a\x01\x78\x6f\x51\x58\x06\xe0\xfd\x01\x14\x92\x01\x78\x7f\x1f\ 55 | \x3a\x9e\x01\x78\x3f\x03\x1d\xcd\x00\xbc\x9f\x83\x8e\x65\x00\xde\ 56 | \x2f\x40\x47\x32\x00\xef\x97\x20\xf3\x0c\xc0\x7b\x0f\x64\x9a\x01\ 57 | \x78\xef\x91\xcc\x32\x00\xef\x03\x90\x49\x06\xe0\x7d\x40\x12\xcf\ 58 | \x00\xbc\x8f\x40\xa2\x19\x80\xf7\x11\x49\x2c\x03\xf0\x3e\x32\x55\ 59 | \x3a\x3f\xb7\x23\x8b\xfd\xff\x21\x29\x89\xc4\xbc\x47\x06\x22\x92\ 60 | \xa8\xf7\xc8\x40\x04\x12\xf7\x1e\x19\x08\x48\x26\xde\x23\x03\x01\ 61 | \xc8\xcc\x7b\x64\xc0\x23\x99\x7a\x8f\x0c\x78\x20\x73\xef\x91\x81\ 62 | \x25\xe8\x88\xf7\xc8\xc0\x02\x74\xcc\x7b\x64\x60\x0e\x3a\xea\x3d\ 63 | \x32\x30\x03\x1d\xf7\x1e\x19\xb8\x0f\x85\x78\x8f\x0c\x1c\x00\xad\ 64 | \xc7\x5b\x09\x78\x82\x0c\x30\x50\xa8\xf7\xc8\x80\x28\xde\xfb\xa2\ 65 | \x33\x00\xef\xcb\xcd\x40\x42\xde\x5f\xaa\xd2\xf9\x39\xa2\x22\x32\ 66 | \x90\x92\xf7\x96\x26\x64\x20\x02\x29\x7a\x6f\x69\x43\x06\x02\x92\ 67 | \xb2\xf7\x96\x46\x64\x20\x00\x39\x78\x6f\x69\x45\x06\x3c\x92\x93\ 68 | \xf7\x96\x66\x64\xc0\x03\x39\x7a\x6f\x69\x47\x06\x96\x20\x67\xef\ 69 | \xad\x1e\x90\x81\x05\xe8\x82\xf7\x56\x2f\xc8\xc0\x1c\x74\xc9\x7b\ 70 | \xab\x27\x64\x60\x06\x48\xdf\xd9\x04\xd6\xc8\xab\xf7\x56\x6f\xa9\ 71 | \x64\xe0\x1d\xdf\xbd\xf9\x80\x74\xad\x55\x7c\xbf\x07\x3c\xa8\xf7\ 72 | \x56\x8f\x29\x64\x40\xfe\x0e\xc0\xa7\x43\xf5\xb8\x28\xa4\x69\x33\ 73 | \x81\xb5\x09\xfe\xef\x70\x13\xc9\xc0\x8f\x54\x0f\x84\xee\x75\x56\ 74 | \x48\xcb\x49\x9d\xcb\x4e\x7b\x6f\xf5\x9b\x42\x06\x9e\x8d\xd5\xef\ 75 | \x34\x48\xcb\xeb\xa5\x78\x6f\xf5\xcc\x9d\x81\xf7\x63\xf7\x7c\x18\ 76 | \xa4\xe5\x6a\x49\xde\x5b\x7d\x73\x66\xe0\x7b\xae\xbe\x5d\x48\xcb\ 77 | \x37\xa5\x79\x6f\xf5\xce\x95\x81\xbb\xdc\xbd\x1b\x48\xcb\x57\x25\ 78 | \x7a\x6f\x60\xca\xc0\x5f\xdc\x7d\x1b\x48\xcb\x47\xa5\x7a\x6f\x60\ 79 | \xc8\xc0\x6d\xee\x9e\x0d\xa4\xe5\x95\x92\xbd\x37\x44\xce\xc0\xc7\ 80 | \xdc\xfd\x1a\x48\xcb\x09\xaa\x7b\x25\x7b\x6f\x88\x98\x81\x17\xb9\ 81 | \x7b\xb5\xa9\xc2\xbf\x07\x48\xde\x7b\x43\x84\x0c\xdc\xa1\x7a\x88\ 82 | \xbb\x4f\x1b\xbd\x07\xdc\x2d\xdd\x7b\x43\xe0\x0c\x9c\xe7\xee\xef\ 83 | \x20\x48\xd7\x69\xaa\xdd\xd2\xbd\x37\x04\xca\xc0\x35\xaa\x1e\x77\ 84 | \x6f\x87\x41\xda\xce\x79\xcc\x40\xb6\xde\x1b\x3c\x67\xe0\x73\xaa\ 85 | \x07\xb9\x7b\x9a\x86\xa7\x0c\x64\xef\xbd\xc1\x53\x06\xa4\xf7\x47\ 86 | \xb9\x7b\x99\x95\x25\x33\xd0\x19\xef\x0d\x4b\x66\x40\x7a\xbf\xc2\ 87 | \xdd\xc3\xbc\x54\xea\xf5\xc0\x6f\x73\xf4\xb9\x53\x25\xfa\xda\xc6\ 88 | \x07\xd4\xdb\xc5\x05\xae\x89\x1b\x39\x7a\x6f\xa8\xd4\xfb\x82\x6b\ 89 | \x33\xf4\x2d\x33\xbe\xc6\xad\x37\x34\xd4\xe3\x93\x54\x3f\xcd\xe0\ 90 | \xbb\x7c\x8f\x77\xbe\x4a\xf8\xb5\xde\x3c\xe8\x1c\xac\x53\x7d\x40\ 91 | \x75\x53\xfb\x2d\x3f\x37\x96\xdf\x3b\x7e\x82\x5b\x5f\x4c\xa8\xdf\ 92 | \x3e\xd5\x19\xaa\xcb\x54\xb7\xf4\x1e\xf9\x67\xa5\x7e\xae\xe3\x53\ 93 | \xaa\x97\xa8\x8e\x71\xeb\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\ 94 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\xb3\x0f\x5c\x46\ 95 | \x03\x75\xbb\xdb\xaf\x6f\xf6\xc4\xaa\x1a\x6f\xab\xc5\xda\x15\x2b\ 96 | \x6a\xbc\x25\x36\xe4\xcd\x8e\x50\xcf\xdb\x1f\x8b\xa1\x7a\x5a\x4f\ 97 | \x9f\x47\x0c\xd4\xd3\xf4\x22\x0b\x75\xa2\xb1\x50\xc7\xed\x09\x75\ 98 | \x22\x1a\x0f\xd5\x79\xd5\x89\x46\x42\x1d\xb7\xd3\x8e\x57\xd5\x79\ 99 | \xd5\x89\x47\xfa\xb8\xad\x76\xdc\xd7\xe7\x31\x63\xf7\x56\xa8\xe9\ 100 | \xdb\xf1\x46\x3d\x7d\x3b\x1e\xd6\xd3\xb7\xe3\x41\x3d\x7d\x3b\x5e\ 101 | \xad\xa7\x6f\xe6\x91\x02\xb6\xcc\x58\xdf\xd1\x0f\xab\x27\xf6\x9a\ 102 | \xc3\xd4\x89\x44\x3d\xbd\xea\x4b\x4e\x24\x36\xe4\xf4\xaa\x6f\x75\ 103 | \x4f\x3d\xaa\xd7\x89\x9e\xb9\xd3\x86\xb3\x3e\xd3\xb6\x91\xa3\x67\ 104 | \x6a\xa6\xd7\x02\x9a\xe9\xb5\x80\x66\x7a\x2d\xa0\x99\x5e\x0b\x68\ 105 | \xa6\xd7\x02\xda\xe9\x95\x00\xfb\xda\x18\xa9\x61\xcf\x8c\xc7\x6a\ 106 | \xdc\x37\xe3\x2d\x35\x5e\x31\xe3\x6d\x35\x5e\x35\xe3\x9d\x89\xe9\ 107 | \x1b\x01\x43\x33\xde\x9b\x9c\xde\x08\x68\x86\x4a\x40\xaf\x1d\x8f\ 108 | \x27\xa6\xd7\x02\x56\xda\xf1\xf6\xc4\xf4\x5a\xc0\xa0\x1d\xef\x4e\ 109 | \x4c\xff\xff\xb1\xfb\x7c\xf7\x7c\xee\x7c\xae\x1e\x57\xaf\xd3\x8f\ 110 | \xdb\xaf\xbb\x1e\xee\x7a\xb9\xeb\xe9\xae\xb7\xeb\x87\xeb\x97\xe3\ 111 | \xa7\xeb\xb7\x9b\x07\x37\x2f\x6e\x9e\xe4\xf4\x76\xde\xea\xbb\x5b\ 112 | \xad\x80\xfa\xa9\x56\x5e\xeb\x53\xb5\x79\x76\xf3\xee\x5e\x0f\xee\ 113 | \xf5\xe2\x5e\x4f\x7a\xa6\x46\x80\xbe\xd3\x08\x18\x8b\xe6\x7a\x3d\ 114 | \xf4\x7a\x1e\x3a\xd7\xfb\x86\xb3\x1f\x68\xd9\x87\xed\x27\xee\x7e\ 115 | \xe3\xee\x47\x13\xfb\xd5\xb8\xdd\xcf\xcc\x78\xa8\xfb\x36\xfb\xc7\ 116 | \x86\x5e\x97\xbe\x3e\xce\xac\xdb\x8a\x3a\xae\xd9\x4f\x57\xd5\x71\ 117 | \xcd\x7e\x3b\x50\xc7\x35\xfb\xb1\x32\xb0\xdd\xaf\x95\xc1\x63\xe3\ 118 | \xab\x95\x48\xa0\xf9\x0f\x4d\xfb\xb6\x32\ 119 | " 120 | 121 | qt_resource_name = b"\ 122 | \x00\x03\ 123 | \x00\x00\x70\x37\ 124 | \x00\x69\ 125 | \x00\x6d\x00\x67\ 126 | \x00\x08\ 127 | \x0f\x07\x42\x1f\ 128 | \x00\x65\ 129 | \x00\x78\x00\x69\x00\x74\x00\x2e\x00\x69\x00\x63\x00\x6f\ 130 | " 131 | 132 | qt_resource_struct_v1 = b"\ 133 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 134 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 135 | \x00\x00\x00\x0c\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ 136 | " 137 | 138 | qt_resource_struct_v2 = b"\ 139 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 140 | \x00\x00\x00\x00\x00\x00\x00\x00\ 141 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 142 | \x00\x00\x00\x00\x00\x00\x00\x00\ 143 | \x00\x00\x00\x0c\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ 144 | \x00\x00\x01\x75\x65\x7f\xd2\x60\ 145 | " 146 | 147 | qt_version = [int(v) for v in QtCore.qVersion().split('.')] 148 | if qt_version < [5, 8, 0]: 149 | rcc_version = 1 150 | qt_resource_struct = qt_resource_struct_v1 151 | else: 152 | rcc_version = 2 153 | qt_resource_struct = qt_resource_struct_v2 154 | 155 | def qInitResources(): 156 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 157 | 158 | def qCleanupResources(): 159 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 160 | 161 | qInitResources() 162 | -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/ui/img_rc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x06\x8b\ 13 | \x00\ 14 | \x01\x08\x3e\x78\x9c\xed\x9d\x4f\x8b\x5c\x45\x14\xc5\xab\xd3\xe2\ 15 | \x84\x68\x18\x57\xee\x62\x3f\x10\x4c\x60\x3e\x44\x2f\x34\xe8\xca\ 16 | \xa0\x4b\x43\x10\x5c\x04\xc1\x31\x20\x68\xa2\xab\x69\xdd\xeb\x17\ 17 | \x10\x14\x43\xc8\x1f\x5d\xb8\x90\x8c\x22\xa8\x0d\x7e\x84\x28\x6a\ 18 | \x70\x31\xe0\x2e\x62\x1c\x51\x71\x84\xc9\x8c\xb7\x5e\x55\xbd\x57\ 19 | \x5d\xce\xa4\xff\x55\xd5\xad\x7a\x75\x7e\xe1\xa6\xbb\x7a\xfa\xbd\ 20 | \x3a\xb7\xce\x79\x45\x77\xcf\x24\x23\x44\x8f\xfe\x8c\x46\x82\xfe\ 21 | \xae\xc4\xa9\xa3\x3d\xf1\xa8\x10\xe2\x14\x15\x3d\x24\x1f\xac\x1f\ 22 | \xaf\xa1\xaf\xfd\x70\x5c\xd4\x05\x00\x00\x00\x00\x00\x00\x00\x00\ 23 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xac\x54\x55\ 24 | \x75\x9c\xea\x65\xaa\xcf\xa8\x7e\xa1\xfa\x97\xea\x6f\xaa\x9f\xa9\ 25 | \x3e\xa4\x7a\x8e\xea\x08\xb7\xce\x98\x50\xbf\x27\xa9\xde\xa0\xba\ 26 | \x4c\xf5\x25\xd5\xa6\x5e\x8b\x57\xa9\x1e\xe3\xd6\xe7\x03\xea\xa3\ 27 | \x47\x75\x81\xea\x57\xaa\xfd\x29\xf5\x1d\xd5\x33\xdc\x9a\x43\x43\ 28 | \x3d\xae\x51\x7d\x31\x65\x2d\xee\x51\xdd\xa0\x1a\x70\xeb\x5d\x14\ 29 | \xd2\xfe\x30\xd5\x27\x33\xf8\x6e\xd7\x1e\xd5\xdb\xdc\xda\x43\x41\ 30 | \xbd\x9d\xa5\xfa\x67\x8e\xf5\xf8\x83\xea\x0c\xb7\xee\x79\xd1\xde\ 31 | \x7f\x3b\xa7\xf7\x76\xbd\xc7\xdd\x83\x6f\xa8\xa7\x37\x17\x5c\x8b\ 32 | \x5d\xaa\x73\xdc\xfa\x67\xc5\x83\xf7\x9d\xcb\xc0\x12\xde\x67\x95\ 33 | \x01\xd2\x78\xcc\x93\xf7\x9d\xc9\x80\x07\xef\xb3\xc8\x00\x69\x3b\ 34 | \x42\x75\xd3\xa3\xf7\xd9\x67\xc0\xa3\xf7\x76\x06\x4e\x73\xf7\x75\ 35 | \x10\xa4\xeb\xb5\x00\xde\x67\x9b\x81\x00\xde\x9b\xfa\xbd\x4a\xec\ 36 | \x7d\x01\xe9\x79\xa4\x52\xaf\x55\x43\xf9\x9f\x55\x06\x02\x7a\x6f\ 37 | \xea\x3a\x77\x8f\x36\xa4\x67\x3d\x70\xbf\xd9\x64\x20\x82\xf7\xb2\ 38 | \xe4\xe7\x03\x27\xb8\x7b\x35\x54\xd3\x3f\xcf\x28\x22\x03\x91\xbc\ 39 | \x37\xb5\xce\xdd\xaf\x81\xb4\xdc\x89\xd8\x77\x92\x19\x88\xec\xbd\ 40 | \xac\x2b\xdc\x3d\x1b\x22\xf7\x9d\x5c\x06\x18\xbc\x97\xf5\x35\x77\ 41 | \xdf\x86\x4a\x7d\x2f\xa7\xc8\x0c\x30\x79\x2f\x6b\xcc\xdd\xbb\x81\ 42 | \xb4\xdc\x66\x5a\x03\xd6\x0c\x30\x7a\x2f\xeb\x2a\x57\xdf\x2e\xa4\ 43 | \xe5\x3a\xe3\x3a\xb0\x64\x80\xd9\x7b\x59\x17\x62\xf7\x7c\x18\xa4\ 44 | \xe5\x79\xe6\xb5\x88\x9a\x81\x04\xbc\x97\xdf\x27\x7d\x3c\x56\xbf\ 45 | \xd3\xa8\xd4\x67\xbf\xb7\x4a\xc8\x40\x02\xde\xcb\xda\x0c\xdd\xe7\ 46 | \xbc\x90\xa6\xa7\x2a\xf5\xf9\x34\xf7\xda\x04\xcb\x40\x22\xde\xcb\ 47 | \xd7\xda\x6b\xa1\x7a\x5c\x06\xd2\x75\x31\x81\xf5\x09\x92\x81\x44\ 48 | \xbc\x97\xfb\xfe\x0b\xbe\x7b\xf3\x09\xe9\x7b\x37\x81\x75\xf2\x9a\ 49 | \x81\x44\xbc\x97\x75\xc9\x57\x4f\x21\xe9\x52\x06\xe0\xfd\x62\x74\ 50 | \x21\x03\xf0\x7e\x39\x72\xce\x00\xbc\xf7\x43\x8e\x19\x80\xf7\x7e\ 51 | \xc9\x29\x03\xf0\x3e\x0c\x39\x64\x00\xde\x87\x25\xe5\x0c\xc0\xfb\ 52 | \x38\xa4\x98\x01\x78\x1f\x97\x94\x32\x00\xef\x79\x48\x28\x03\x29\ 53 | \x54\x51\xde\x1b\x90\x81\x72\xbd\x37\x14\x9e\x81\xa2\xbd\x37\x14\ 54 | \x9a\x01\x78\x6f\x51\x58\x06\xe0\xfd\x01\x14\x92\x01\x78\x7f\x1f\ 55 | \x3a\x9e\x01\x78\x3f\x03\x1d\xcd\x00\xbc\x9f\x83\x8e\x65\x00\xde\ 56 | \x2f\x40\x47\x32\x00\xef\x97\x20\xf3\x0c\xc0\x7b\x0f\x64\x9a\x01\ 57 | \x78\xef\x91\xcc\x32\x00\xef\x03\x90\x49\x06\xe0\x7d\x40\x12\xcf\ 58 | \x00\xbc\x8f\x40\xa2\x19\x80\xf7\x11\x49\x2c\x03\xf0\x3e\x32\x55\ 59 | \x3a\x3f\xb7\x23\x8b\xfd\xff\x21\x29\x89\xc4\xbc\x47\x06\x22\x92\ 60 | \xa8\xf7\xc8\x40\x04\x12\xf7\x1e\x19\x08\x48\x26\xde\x23\x03\x01\ 61 | \xc8\xcc\x7b\x64\xc0\x23\x99\x7a\x8f\x0c\x78\x20\x73\xef\x91\x81\ 62 | \x25\xe8\x88\xf7\xc8\xc0\x02\x74\xcc\x7b\x64\x60\x0e\x3a\xea\x3d\ 63 | \x32\x30\x03\x1d\xf7\x1e\x19\xb8\x0f\x85\x78\x8f\x0c\x1c\x00\xad\ 64 | \xc7\x5b\x09\x78\x82\x0c\x30\x50\xa8\xf7\xc8\x80\x28\xde\xfb\xa2\ 65 | \x33\x00\xef\xcb\xcd\x40\x42\xde\x5f\xaa\xd2\xf9\x39\xa2\x22\x32\ 66 | \x90\x92\xf7\x96\x26\x64\x20\x02\x29\x7a\x6f\x69\x43\x06\x02\x92\ 67 | \xb2\xf7\x96\x46\x64\x20\x00\x39\x78\x6f\x69\x45\x06\x3c\x92\x93\ 68 | \xf7\x96\x66\x64\xc0\x03\x39\x7a\x6f\x69\x47\x06\x96\x20\x67\xef\ 69 | \xad\x1e\x90\x81\x05\xe8\x82\xf7\x56\x2f\xc8\xc0\x1c\x74\xc9\x7b\ 70 | \xab\x27\x64\x60\x06\x48\xdf\xd9\x04\xd6\xc8\xab\xf7\x56\x6f\xa9\ 71 | \x64\xe0\x1d\xdf\xbd\xf9\x80\x74\xad\x55\x7c\xbf\x07\x3c\xa8\xf7\ 72 | \x56\x8f\x29\x64\x40\xfe\x0e\xc0\xa7\x43\xf5\xb8\x28\xa4\x69\x33\ 73 | \x81\xb5\x09\xfe\xef\x70\x13\xc9\xc0\x8f\x54\x0f\x84\xee\x75\x56\ 74 | \x48\xcb\x49\x9d\xcb\x4e\x7b\x6f\xf5\x9b\x42\x06\x9e\x8d\xd5\xef\ 75 | \x34\x48\xcb\xeb\xa5\x78\x6f\xf5\xcc\x9d\x81\xf7\x63\xf7\x7c\x18\ 76 | \xa4\xe5\x6a\x49\xde\x5b\x7d\x73\x66\xe0\x7b\xae\xbe\x5d\x48\xcb\ 77 | \x37\xa5\x79\x6f\xf5\xce\x95\x81\xbb\xdc\xbd\x1b\x48\xcb\x57\x25\ 78 | \x7a\x6f\x60\xca\xc0\x5f\xdc\x7d\x1b\x48\xcb\x47\xa5\x7a\x6f\x60\ 79 | \xc8\xc0\x6d\xee\x9e\x0d\xa4\xe5\x95\x92\xbd\x37\x44\xce\xc0\xc7\ 80 | \xdc\xfd\x1a\x48\xcb\x09\xaa\x7b\x25\x7b\x6f\x88\x98\x81\x17\xb9\ 81 | \x7b\xb5\xa9\xc2\xbf\x07\x48\xde\x7b\x43\x84\x0c\xdc\xa1\x7a\x88\ 82 | \xbb\x4f\x1b\xbd\x07\xdc\x2d\xdd\x7b\x43\xe0\x0c\x9c\xe7\xee\xef\ 83 | \x20\x48\xd7\x69\xaa\xdd\xd2\xbd\x37\x04\xca\xc0\x35\xaa\x1e\x77\ 84 | \x6f\x87\x41\xda\xce\x79\xcc\x40\xb6\xde\x1b\x3c\x67\xe0\x73\xaa\ 85 | \x07\xb9\x7b\x9a\x86\xa7\x0c\x64\xef\xbd\xc1\x53\x06\xa4\xf7\x47\ 86 | \xb9\x7b\x99\x95\x25\x33\xd0\x19\xef\x0d\x4b\x66\x40\x7a\xbf\xc2\ 87 | \xdd\xc3\xbc\x54\xea\xf5\xc0\x6f\x73\xf4\xb9\x53\x25\xfa\xda\xc6\ 88 | \x07\xd4\xdb\xc5\x05\xae\x89\x1b\x39\x7a\x6f\xa8\xd4\xfb\x82\x6b\ 89 | \x33\xf4\x2d\x33\xbe\xc6\xad\x37\x34\xd4\xe3\x93\x54\x3f\xcd\xe0\ 90 | \xbb\x7c\x8f\x77\xbe\x4a\xf8\xb5\xde\x3c\xe8\x1c\xac\x53\x7d\x40\ 91 | \x75\x53\xfb\x2d\x3f\x37\x96\xdf\x3b\x7e\x82\x5b\x5f\x4c\xa8\xdf\ 92 | \x3e\xd5\x19\xaa\xcb\x54\xb7\xf4\x1e\xf9\x67\xa5\x7e\xae\xe3\x53\ 93 | \xaa\x97\xa8\x8e\x71\xeb\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\ 94 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\xb3\x0f\x5c\x46\ 95 | \x03\x75\xbb\xdb\xaf\x6f\xf6\xc4\xaa\x1a\x6f\xab\xc5\xda\x15\x2b\ 96 | \x6a\xbc\x25\x36\xe4\xcd\x8e\x50\xcf\xdb\x1f\x8b\xa1\x7a\x5a\x4f\ 97 | \x9f\x47\x0c\xd4\xd3\xf4\x22\x0b\x75\xa2\xb1\x50\xc7\xed\x09\x75\ 98 | \x22\x1a\x0f\xd5\x79\xd5\x89\x46\x42\x1d\xb7\xd3\x8e\x57\xd5\x79\ 99 | \xd5\x89\x47\xfa\xb8\xad\x76\xdc\xd7\xe7\x31\x63\xf7\x56\xa8\xe9\ 100 | \xdb\xf1\x46\x3d\x7d\x3b\x1e\xd6\xd3\xb7\xe3\x41\x3d\x7d\x3b\x5e\ 101 | \xad\xa7\x6f\xe6\x91\x02\xb6\xcc\x58\xdf\xd1\x0f\xab\x27\xf6\x9a\ 102 | \xc3\xd4\x89\x44\x3d\xbd\xea\x4b\x4e\x24\x36\xe4\xf4\xaa\x6f\x75\ 103 | \x4f\x3d\xaa\xd7\x89\x9e\xb9\xd3\x86\xb3\x3e\xd3\xb6\x91\xa3\x67\ 104 | \x6a\xa6\xd7\x02\x9a\xe9\xb5\x80\x66\x7a\x2d\xa0\x99\x5e\x0b\x68\ 105 | \xa6\xd7\x02\xda\xe9\x95\x00\xfb\xda\x18\xa9\x61\xcf\x8c\xc7\x6a\ 106 | \xdc\x37\xe3\x2d\x35\x5e\x31\xe3\x6d\x35\x5e\x35\xe3\x9d\x89\xe9\ 107 | \x1b\x01\x43\x33\xde\x9b\x9c\xde\x08\x68\x86\x4a\x40\xaf\x1d\x8f\ 108 | \x27\xa6\xd7\x02\x56\xda\xf1\xf6\xc4\xf4\x5a\xc0\xa0\x1d\xef\x4e\ 109 | \x4c\xff\xff\xb1\xfb\x7c\xf7\x7c\xee\x7c\xae\x1e\x57\xaf\xd3\x8f\ 110 | \xdb\xaf\xbb\x1e\xee\x7a\xb9\xeb\xe9\xae\xb7\xeb\x87\xeb\x97\xe3\ 111 | \xa7\xeb\xb7\x9b\x07\x37\x2f\x6e\x9e\xe4\xf4\x76\xde\xea\xbb\x5b\ 112 | \xad\x80\xfa\xa9\x56\x5e\xeb\x53\xb5\x79\x76\xf3\xee\x5e\x0f\xee\ 113 | \xf5\xe2\x5e\x4f\x7a\xa6\x46\x80\xbe\xd3\x08\x18\x8b\xe6\x7a\x3d\ 114 | \xf4\x7a\x1e\x3a\xd7\xfb\x86\xb3\x1f\x68\xd9\x87\xed\x27\xee\x7e\ 115 | \xe3\xee\x47\x13\xfb\xd5\xb8\xdd\xcf\xcc\x78\xa8\xfb\x36\xfb\xc7\ 116 | \x86\x5e\x97\xbe\x3e\xce\xac\xdb\x8a\x3a\xae\xd9\x4f\x57\xd5\x71\ 117 | \xcd\x7e\x3b\x50\xc7\x35\xfb\xb1\x32\xb0\xdd\xaf\x95\xc1\x63\xe3\ 118 | \xab\x95\x48\xa0\xf9\x0f\x4d\xfb\xb6\x32\ 119 | " 120 | 121 | qt_resource_name = b"\ 122 | \x00\x03\ 123 | \x00\x00\x70\x37\ 124 | \x00\x69\ 125 | \x00\x6d\x00\x67\ 126 | \x00\x08\ 127 | \x0f\x07\x42\x1f\ 128 | \x00\x65\ 129 | \x00\x78\x00\x69\x00\x74\x00\x2e\x00\x69\x00\x63\x00\x6f\ 130 | " 131 | 132 | qt_resource_struct_v1 = b"\ 133 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 134 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 135 | \x00\x00\x00\x0c\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ 136 | " 137 | 138 | qt_resource_struct_v2 = b"\ 139 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 140 | \x00\x00\x00\x00\x00\x00\x00\x00\ 141 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 142 | \x00\x00\x00\x00\x00\x00\x00\x00\ 143 | \x00\x00\x00\x0c\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ 144 | \x00\x00\x01\x88\xc0\x36\xaf\xfe\ 145 | " 146 | 147 | qt_version = [int(v) for v in QtCore.qVersion().split('.')] 148 | if qt_version < [5, 8, 0]: 149 | rcc_version = 1 150 | qt_resource_struct = qt_resource_struct_v1 151 | else: 152 | rcc_version = 2 153 | qt_resource_struct = qt_resource_struct_v2 154 | 155 | def qInitResources(): 156 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 157 | 158 | def qCleanupResources(): 159 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 160 | 161 | qInitResources() 162 | -------------------------------------------------------------------------------- /src/addon.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import json 3 | from types import ModuleType 4 | from typing import Callable, Optional 5 | from importlib import import_module 6 | import os 7 | import inspect 8 | 9 | from PyQt5.QtWidgets import QSystemTrayIcon 10 | from PyQt5.QtGui import QKeySequence 11 | 12 | from FileSystem import exists, abspath, icon as get_icon, ADDONS_FOLDER, ADDONS_NAME 13 | from SaveFile import JsonType, NotFoundException, apply_setting, get_setting, remove_setting 14 | from utils import HotKeys 15 | 16 | 17 | add_ons: dict[str, ModuleType] = {} 18 | add_on_paths: dict[str, ModuleType] = {} 19 | 20 | currently_loading_module = None 21 | 22 | 23 | def load_addons() -> None: 24 | """Loads all the modules from the ADDONs folder.""" 25 | global add_ons, add_on_paths, currently_loading_module 26 | 27 | def apply_order(modules_and_paths: dict[str, str]) -> dict[str, str]: 28 | order_file = f"{ADDONS_FOLDER}/order.json" 29 | if not exists(order_file): 30 | default_order_data = { 31 | "_comment": [ 32 | "High priority addons. These addons will be loaded first.", 33 | "Those addons which not specified will be considered as 'medium_priority' and will be loaded after loading hight_priority addons.", 34 | "Low priority addons. These addons will be loaded after loading all the addons.", 35 | "Names are case insensitive." 36 | ], 37 | 38 | 39 | "high_priority": [ 40 | "shortcuts", 41 | "notes", 42 | "youtube_downloader" 43 | ], 44 | 45 | "medium_priority": [], 46 | 47 | "low_priority": [ 48 | "settings" 49 | ] 50 | } 51 | with open(order_file, "w") as f: 52 | json.dump(default_order_data, f, indent=4) 53 | 54 | else: 55 | try: 56 | # apply sorting if possible. 57 | if exists(order_file): 58 | with open(order_file, "r") as f: 59 | order_data = json.load(f) 60 | 61 | high_priorities = (x.lower() for x in order_data["high_priority"]) 62 | medium_priorities = (x.lower() for x in order_data["medium_priority"]) 63 | low_priorities = (x.lower() for x in order_data["low_priority"]) 64 | 65 | modules_name_in_lower = {x.split(".")[-1].lower(): x for x in modules_and_paths} 66 | 67 | priority_addons = [] 68 | for priority in (high_priorities, medium_priorities, low_priorities): 69 | addons = {} 70 | for module in priority: 71 | if module in modules_name_in_lower: 72 | module_name = modules_name_in_lower[module] 73 | addons[module_name] = modules_and_paths.pop(module_name) 74 | priority_addons.append(addons) 75 | 76 | high_priority_addons, medium_priority_addons, low_priority_addons = priority_addons 77 | 78 | rest_addons = modules_and_paths 79 | 80 | return {**high_priority_addons, 81 | **medium_priority_addons, 82 | **rest_addons, 83 | **low_priority_addons} 84 | 85 | except Exception as e: 86 | print(f"Error occurred while applying order for addons.\n{e}") 87 | 88 | return modules_and_paths 89 | 90 | 91 | if exists(ADDONS_FOLDER): 92 | # traverse root directory, and list directories as dirs and files as files 93 | modules_and_paths = {} 94 | 95 | for root, dirs, files in os.walk(ADDONS_FOLDER): 96 | if root != ADDONS_FOLDER: continue 97 | for name in dirs: 98 | dir_path = os.path.join(root, name) 99 | file_path = os.path.join(dir_path, f"{name}.py") 100 | if os.path.isfile(file_path): # If the .py file with same name as directory exists 101 | module_name = f'{ADDONS_NAME}.{name}.{name}' 102 | modules_and_paths[module_name] = file_path 103 | break 104 | 105 | modules_and_paths = apply_order(modules_and_paths) 106 | 107 | for module_name in modules_and_paths: 108 | # Import the module 109 | add_on_paths[module_name] = modules_and_paths[module_name] 110 | currently_loading_module = module_name 111 | AddOnBase() # create a new instance of AddOnBase. 112 | module = import_module(module_name) 113 | currently_loading_module = None 114 | add_ons[module_name] = module 115 | 116 | 117 | 118 | class AddOnBase: 119 | system_tray_icon: QSystemTrayIcon = None # instance of QSystemTrayIcon will be assigned after initializing it 120 | instances: dict[str, AddOnBase] = {} 121 | 122 | def __new__(cls, name: Optional[str] = None): 123 | # returns the instance of currently loading or calling addon module if available. 124 | # if not, returns the AddOnBase instance of module of given addon name. 125 | 126 | if (addon_module:=currently_loading_module) is not None or \ 127 | (addon_module:=AddOnBase._get_calling_module()) is not None: 128 | if name is not None: 129 | print("WARNING: name should not be specified when creating new instace from addon module.", 130 | f"name of this instance is '{addon_module}'.") 131 | 132 | if addon_module in AddOnBase.instances: 133 | return AddOnBase.instances[addon_module] 134 | new_instance = super().__new__(cls) 135 | new_instance._init() 136 | AddOnBase.instances[addon_module] = new_instance 137 | return new_instance 138 | 139 | if name in AddOnBase.instances: 140 | return AddOnBase.instances[name] 141 | else: raise ValueError(f"'{name}' AddOn instance not found.") 142 | 143 | def _init(self): 144 | self.MODULE_NAME = currently_loading_module 145 | self.activate_shortcut = None 146 | 147 | # default name and icon_path 148 | self.name = self.MODULE_NAME.split(".")[-1].replace("_", " ").title() 149 | self.icon_path = "icon.png" 150 | 151 | 152 | @staticmethod 153 | def _get_calling_module() -> str | None: 154 | """Returns the calling module name if calling module is an addon. Otherwise returns None.""" 155 | addon_file = inspect.currentframe().f_back.f_back.f_globals["__file__"] 156 | return next( 157 | ( 158 | module_name 159 | for module_name, path in add_on_paths.items() 160 | if os.path.abspath(path) == os.path.abspath(addon_file) 161 | ), 162 | None, 163 | ) 164 | 165 | 166 | @property 167 | def MODULE(self) -> ModuleType: 168 | return add_ons[self.MODULE_NAME] 169 | 170 | @property 171 | def PATH(self) -> str: 172 | return add_on_paths[self.MODULE_NAME] 173 | 174 | @property 175 | def icon_path(self) -> str: 176 | return self._icon_path 177 | 178 | @icon_path.setter 179 | def icon_path(self, icon_path: str) -> None: 180 | if _icon_path := abspath(f"{os.path.dirname(self.PATH)}/{icon_path}"): 181 | self._icon_path = _icon_path.replace("\\", "/") 182 | # XXX: inform icon not found warning. 183 | else: 184 | self._icon_path = get_icon("default_launcher_icon.png") 185 | 186 | 187 | def activate(self): 188 | """Override this method to call when desktop widget is activated.""" 189 | pass 190 | 191 | def set_activate_shortcut(self, key: QKeySequence) -> None: 192 | """Adds a global shortcut key to call the activate method.""" 193 | self.activate_shortcut: QKeySequence = key 194 | HotKeys.add_global_shortcut(HotKeys.format_shortcut_string(key.toString()), lambda: self.activate()) 195 | 196 | 197 | def set_icon_path(self, icon_path: str) -> None: 198 | """Set the icon path of icon that shows in the launcher. The icon should be in the addon directory.""" 199 | self.icon_path = icon_path 200 | 201 | def set_name(self, name: str) -> None: 202 | """Set custom name for this addon. that shows in the launcher.""" 203 | self.name = name 204 | # XXX: The name must be bound to the conditions. it should be rejected if not. 205 | 206 | 207 | def apply_setting(self, name: str, value: JsonType) -> None: 208 | save_file = os.path.join(os.path.dirname(add_on_paths[self.MODULE_NAME]), "save.json") 209 | return apply_setting(name, value, save_file) 210 | 211 | def get_setting(self, name: str) -> JsonType: 212 | save_file = os.path.join(os.path.dirname(add_on_paths[self.MODULE_NAME]), "save.json") 213 | return get_setting(name, save_file) 214 | 215 | def remove_setting(self, name: str) -> None: 216 | save_file = os.path.join(os.path.dirname(add_on_paths[self.MODULE_NAME]), "save.json") 217 | return remove_setting(name, save_file) 218 | 219 | 220 | @staticmethod 221 | def set_shortcut(key: QKeySequence, function: Callable) -> None: 222 | """Adds a global shortcut""" 223 | HotKeys.add_global_shortcut(HotKeys.format_shortcut_string(key.toString()), function) 224 | 225 | -------------------------------------------------------------------------------- /src/ui/launcher_design.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/ui/base_window/title_bar_layer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Optional, Literal 3 | from PyQt5.QtCore import Qt, QSize, QPoint, pyqtSignal 4 | from PyQt5.QtWidgets import ( 5 | QWidget, 6 | QLabel, 7 | QHBoxLayout, 8 | QGraphicsDropShadowEffect, 9 | ) 10 | from PyQt5.QtGui import ( 11 | QColor, 12 | QPainter, 13 | QPaintEvent, 14 | QMouseEvent, 15 | QPen, 16 | ) 17 | 18 | from settings import apply_ui_scale as scaled, CORNER_RADIUS 19 | from ui.custom_button import RedButton, YelButton, GrnButton 20 | from ui.utils import get_font 21 | 22 | 23 | class Direction(int): 24 | Forward = 1 25 | Backward = -1 26 | 27 | 28 | class TabButton(QWidget): 29 | tab_moved = pyqtSignal(int) 30 | clicked = pyqtSignal(int) 31 | 32 | def __init__(self, tab_id: int, title: str, parent: QWidget | None = None): 33 | super().__init__(parent) 34 | 35 | self._parent = parent 36 | self._offset = None 37 | self._last_postition = None 38 | self.tab_id: int = tab_id 39 | self.focused: bool = False 40 | self.title: str = title 41 | 42 | self.setFont(get_font(size=scaled(16))) 43 | 44 | self._red_button = RedButton(self, "radial") 45 | self._red_button.move(QPoint(self.size().width() - scaled(22 + 10), scaled(8))) 46 | self._red_button.setIconSize(size := scaled(QSize(22, 22))) 47 | self._red_button.setFixedSize(size) 48 | self._red_button.hide() 49 | 50 | self.setFixedSize(self.size()) 51 | 52 | 53 | self.shadow_effect = QGraphicsDropShadowEffect(self) 54 | self.shadow_effect.setOffset(0, scaled(4.3)) 55 | self.shadow_effect.setBlurRadius(16) 56 | 57 | self.setGraphicsEffect(self.shadow_effect) 58 | 59 | self.set_focused(self.focused) # updating the shadow color 60 | 61 | 62 | @property 63 | def red_button(self) -> RedButton: 64 | return self._red_button 65 | 66 | 67 | def set_focused(self, focused: bool) -> None: 68 | self.focused = focused 69 | self.shadow_effect.setColor(QColor(118, 118, 118, 63 if focused else 0)) 70 | if focused: self.raise_() 71 | 72 | def set_title(self, title: str) -> None: 73 | self.title = title 74 | 75 | 76 | @staticmethod 77 | def get_tab_button_position(index: int) -> QPoint: 78 | return QPoint(scaled(20) + (TabButton.size().width() + scaled(16)) * scaled(index), scaled(6)) 79 | 80 | 81 | def paintEvent(self, a0: QPaintEvent) -> None: 82 | painter = QPainter(self) 83 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 84 | painter.setPen(Qt.PenStyle.NoPen) 85 | painter.setBrush(QColor("#FFFFFF")) 86 | painter.drawRoundedRect(0, 0, self.size().width(), self.size().height(), 87 | scaled(12), scaled(12)) 88 | painter.setPen(QPen(self.palette().text().color())) 89 | painter.drawText(self.rect().adjusted(scaled(20), scaled(5), -scaled(52), 0), 90 | Qt.AlignmentFlag.AlignLeft, self.title) 91 | 92 | 93 | def mousePressEvent(self, a0: QMouseEvent) -> None: 94 | if a0.button() == Qt.LeftButton: 95 | self._offset = a0.pos() 96 | self.clicked.emit(self.tab_id) 97 | 98 | def mouseMoveEvent(self, a0: QMouseEvent) -> None: 99 | if self._offset is not None and a0.buttons() == Qt.LeftButton: 100 | new_pos_x = self.mapToParent(a0.pos() - self._offset).x() 101 | 102 | # make sure the new position is within the self._parent widget 103 | if new_pos_x < 0: 104 | new_pos_x = 0 105 | elif new_pos_x + self.width() > self._parent.size().width(): 106 | new_pos_x = self._parent.size().width() - self.width() 107 | 108 | self.tab_moved.emit(self.tab_id) 109 | 110 | self.move(new_pos_x, self.y()) 111 | 112 | def mouseReleaseEvent(self, a0: QMouseEvent) -> None: 113 | self._offset = None 114 | self.tab_moved.emit(self.tab_id) # for update the positions of dragging TabButton 115 | 116 | 117 | @staticmethod 118 | def size() -> QSize: 119 | return scaled(QSize(175, 38)) 120 | 121 | 122 | class Buttons(QWidget): 123 | def __init__(self, parent: QWidget | None = None) -> None: 124 | super().__init__(parent) 125 | 126 | self.setLayout(QHBoxLayout(self)) 127 | self.layout().setContentsMargins(0, 0, 0, 0) 128 | self.layout().setSpacing(scaled(9)) 129 | self.red_button = RedButton(self, "radial") 130 | self.yel_button = YelButton(self, "radial") 131 | self.grn_button = GrnButton(self, "radial") 132 | self.layout().addWidget(self.grn_button) 133 | self.layout().addWidget(self.yel_button) 134 | self.layout().addWidget(self.red_button) 135 | self.red_button.hide() 136 | self.yel_button.hide() 137 | self.grn_button.hide() 138 | 139 | 140 | class TitleBarLayer(QWidget): 141 | def __init__(self, title_bar: Optional[Literal["title", "tab", "hidden"]] = None, 142 | parent: QWidget | None = None) -> None: 143 | super().__init__(parent) 144 | 145 | self.mode = title_bar 146 | self._parent = parent 147 | self._offset_for_drag = None 148 | 149 | if title_bar == "hidden": 150 | return 151 | 152 | self.buttons = Buttons(self) 153 | self._set_button_position() 154 | 155 | if self.mode == "title": 156 | self._init_for_title() 157 | else: 158 | self._init_for_tabs() 159 | 160 | 161 | def _init_for_title(self) -> None: 162 | """initialize title bar for title.""" 163 | self.title_label = QLabel("", self) 164 | self.title_label.move(scaled(20), scaled(5)) 165 | self.title_label.setFont(get_font(size=scaled(16))) 166 | # raise the buttons forward to make be able to click on them 167 | self.buttons.raise_() 168 | 169 | def _init_for_tabs(self) -> None: 170 | """initialize title bar for tabs.""" 171 | self.tabs: dict[int, TabButton] = {} 172 | self.tabs_order: list[int] = [] 173 | self.add_button = GrnButton(self, "radial") 174 | self.add_button.setIconSize(size := scaled(QSize(22, 22))) 175 | self.add_button.setFixedSize(size) 176 | self.add_button.move(scaled(20), scaled(50 if self.mode == "tab" else 34)//2 - self.add_button.height()//2) 177 | self.add_button.hide() 178 | 179 | def _tab_moving(self, tab_id: int): 180 | """Checks if the tab is moved more than a specified amount and changes the order of the tabs.""" 181 | current_order = self.tabs_order.index(tab_id) 182 | changed = self.tabs[tab_id].x() - TabButton.get_tab_button_position(current_order).x() 183 | max_different = TabButton.size().width() * 0.6 184 | 185 | if changed >= max_different: 186 | # if the tab is moved forward more than 0.6 the size of the TabButton 187 | self.move_tab(current_order, current_order + 1) 188 | 189 | elif -changed >= max_different: 190 | # if the tab is moved backward more than 0.6 the size of the TabButton 191 | self.move_tab(current_order, current_order - 1) 192 | 193 | # check if the tab is still moving 194 | if self.tabs[tab_id]._offset is None: 195 | self._reset_tab_positions() 196 | 197 | def _reset_tab_positions(self) -> None: 198 | """Re sets the position of the tab buttons.""" 199 | for index, tab_id in enumerate(self.tabs_order): 200 | tab: TabButton = self.tabs[tab_id] 201 | pos = tab.get_tab_button_position(index) 202 | if tab._offset is None and tab.pos() != pos: # check if the tab is being dragged. 203 | tab.move(pos) 204 | 205 | self._set_add_button_position() 206 | 207 | def _set_button_position(self) -> None: 208 | self.buttons.adjustSize() 209 | self.buttons.move(self.width() - self.buttons.width() - scaled(20), 210 | scaled(50 if self.mode == "tab" else 34)//2 - self.buttons.height()//2) 211 | 212 | def _set_add_button_position(self) -> None: 213 | if not self.add_button.isHidden(): 214 | # applying the x position of green button to the x position of next tab to the last tab. 215 | self.add_button.move(TabButton.get_tab_button_position(len(self.tabs)).x(), self.add_button.y()) 216 | 217 | def set_title(self, title: str) -> None: 218 | self.title_label.setText(title) 219 | 220 | def title(self) -> str: 221 | return self.title_label.text() 222 | 223 | 224 | def add_tab_button(self, title: str, tab_id: int) -> TabButton: 225 | """Adds new tab button to the title bar.""" 226 | 227 | tab_button = TabButton(tab_id, title, self) 228 | tab_button.move(TabButton.get_tab_button_position(tab_id)) 229 | tab_button.show() 230 | self.tabs[tab_id] = tab_button 231 | self.tabs_order.append(tab_id) 232 | tab_button.tab_moved.connect(self._tab_moving) 233 | tab_button.clicked.connect(self.set_tab_focus) 234 | return tab_button 235 | 236 | def remove_tab_button(self, tab_id: int) -> None: 237 | """Removes the tab button from the title bar.""" 238 | tab_button = self.tabs[tab_id] 239 | tab_button.hide() 240 | tab_button.deleteLater() 241 | del self.tabs[tab_id] 242 | del self.tabs_order[tab_id] 243 | self._reset_tab_positions() 244 | 245 | def move_tab(self, current_pos: int, move_to: int) -> None: 246 | """Moves the position of the tab button.""" 247 | self.tabs_order.insert(move_to, self.tabs_order.pop(current_pos)) 248 | self._reset_tab_positions() 249 | 250 | def set_tab_focus(self, tab_id: int) -> None: 251 | if not self.tabs: 252 | return 253 | if self.tabs[tab_id].focused: 254 | return 255 | for _tab_id, _tab_button in self.tabs.items(): 256 | if _tab_id == tab_id: 257 | _tab_button.set_focused(True) 258 | else: 259 | _tab_button.set_focused(False) 260 | 261 | 262 | def paintEvent(self, a0: QPaintEvent) -> None: 263 | painter = QPainter(self) 264 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 265 | painter.setBrush(QColor("#FFFFFF")) 266 | painter.setPen(Qt.PenStyle.NoPen) 267 | painter.drawRoundedRect(self.rect(), x:=scaled(CORNER_RADIUS), x) 268 | 269 | 270 | def mousePressEvent(self, a0: QMouseEvent) -> None: 271 | self._offset_for_drag = a0.pos() 272 | 273 | def mouseMoveEvent(self, a0: QMouseEvent) -> None: 274 | if self._offset_for_drag is None: return 275 | content_margins = self._parent.layout().contentsMargins() 276 | self._parent.move(a0.globalPos() - self._offset_for_drag - 277 | QPoint(content_margins.left(), content_margins.top())) 278 | 279 | def mouseReleaseEvent(self, a0: QMouseEvent) -> None: 280 | self._offset_for_drag = None 281 | 282 | def resizeEvent(self, QResizeEvent) -> None: 283 | if self.mode != "hidden": 284 | self._set_button_position() 285 | return super().resizeEvent(QResizeEvent) 286 | -------------------------------------------------------------------------------- /src/addons/colorpicker/vcolorpicker/vcolorpicker.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------- # 2 | # # 3 | # Modern Color Picker by Tom F. # 4 | # Version 1.4.1 # 5 | # made with Qt Creator & PyQt5 # 6 | # # 7 | # ------------------------------------- # 8 | 9 | import colorsys 10 | from typing import Union 11 | from typing import Optional 12 | 13 | from PyQt5.QtCore import (QPoint, Qt) 14 | from PyQt5.QtGui import QColor 15 | from PyQt5.QtWidgets import ( 16 | QWidget, QApplication, QDialog, QGraphicsDropShadowEffect) 17 | 18 | from .ui_dark import Ui_ColorPicker as Ui_Dark 19 | from .ui_dark_alpha import Ui_ColorPicker as Ui_Dark_Alpha 20 | from .ui_light import Ui_ColorPicker as Ui_Light 21 | from .ui_light_alpha import Ui_ColorPicker as Ui_Light_Alpha 22 | 23 | from .img import * 24 | 25 | 26 | class ColorPicker(QDialog): 27 | 28 | def __init__( 29 | self, lightTheme: bool = True, 30 | useAlpha: bool = True): 31 | """Create a new ColorPicker instance. 32 | 33 | :param lightTheme: If the UI should be light themed. 34 | :param useAlpha: If the ColorPicker should work with alpha values. 35 | """ 36 | 37 | # auto-create QApplication if it doesn't exist yet 38 | self.app = QApplication.instance() 39 | if self.app is None: 40 | self.app = QApplication([]) 41 | 42 | super(ColorPicker, self).__init__() 43 | 44 | self.usingAlpha = useAlpha 45 | self.usingLightTheme = lightTheme 46 | 47 | # Call UI Builder function 48 | if useAlpha: 49 | if lightTheme: 50 | self.ui = Ui_Light_Alpha() 51 | else: 52 | self.ui = Ui_Dark_Alpha() 53 | self.ui.setupUi(self) 54 | else: 55 | if lightTheme: 56 | self.ui = Ui_Light() 57 | else: 58 | self.ui = Ui_Dark() 59 | self.ui.setupUi(self) 60 | 61 | # Make Frameless 62 | self.setWindowFlags(Qt.FramelessWindowHint) 63 | self.setAttribute(Qt.WA_TranslucentBackground) 64 | self.setWindowTitle("Color Picker") 65 | 66 | # Add DropShadow 67 | self.shadow = QGraphicsDropShadowEffect(self) 68 | self.shadow.setBlurRadius(17) 69 | self.shadow.setXOffset(0) 70 | self.shadow.setYOffset(0) 71 | self.shadow.setColor(QColor(0, 0, 0, 150)) 72 | self.ui.drop_shadow_frame.setGraphicsEffect(self.shadow) 73 | 74 | # Connect update functions 75 | self.ui.hue.mouseMoveEvent = self.moveHueSelector 76 | self.ui.red.textEdited.connect(self.rgbChanged) 77 | self.ui.green.textEdited.connect(self.rgbChanged) 78 | self.ui.blue.textEdited.connect(self.rgbChanged) 79 | self.ui.hex.textEdited.connect(self.hexChanged) 80 | if self.usingAlpha: 81 | self.ui.alpha.textEdited.connect(self.alphaChanged) 82 | 83 | # Connect window dragging functions 84 | self.ui.title_bar.mouseMoveEvent = self.moveWindow 85 | self.ui.title_bar.mousePressEvent = self.setDragPos 86 | self.ui.window_title.mouseMoveEvent = self.moveWindow 87 | self.ui.window_title.mousePressEvent = self.setDragPos 88 | 89 | # Connect selector moving function 90 | self.ui.black_overlay.mouseMoveEvent = self.moveSVSelector 91 | self.ui.black_overlay.mousePressEvent = self.moveSVSelector 92 | 93 | # Connect Ok|Cancel Button Box and X Button 94 | # self.ui.buttonBox.accepted.connect(self.accept) 95 | # self.ui.buttonBox.rejected.connect(self.reject) 96 | # self.ui.exit_btn.clicked.connect(self.reject) 97 | self.ui.exit_btn.clicked.connect(self.exit_btn_clicked) 98 | 99 | self.lastcolor = (0, 0, 0) 100 | self.color = (0, 0, 0) 101 | self.alpha = 100 102 | 103 | def exit_btn_clicked(self): 104 | pass 105 | 106 | def getColor(self, lc: tuple = None): 107 | """Open the UI and get a color from the user. 108 | 109 | :param lc: The color to show as previous color. 110 | :return: The selected color. 111 | """ 112 | 113 | if lc is not None and self.usingAlpha: 114 | alpha = lc[3] 115 | lc = lc[:3] 116 | self.setAlpha(alpha) 117 | self.alpha = alpha 118 | if lc is None: 119 | lc = self.lastcolor 120 | else: 121 | self.lastcolor = lc 122 | 123 | self.setRGB(lc) 124 | self.rgbChanged() 125 | r, g, b = lc 126 | self.ui.lastcolor_vis.setStyleSheet( 127 | f"background-color: rgb({r},{g},{b})") 128 | 129 | if self.exec_(): 130 | r, g, b = hsv2rgb(self.color) 131 | self.lastcolor = (r, g, b) 132 | if self.usingAlpha: 133 | return (r, g, b, self.alpha) 134 | return (r, g, b) 135 | 136 | else: 137 | return self.lastcolor 138 | 139 | # Update Functions 140 | def hsvChanged(self): 141 | h, s, v = (100 - self.ui.hue_selector.y() / 1.85, 142 | (self.ui.selector.x() + 6) / 2.0, (194 - self.ui.selector.y()) / 2.0) 143 | r, g, b = hsv2rgb(h, s, v) 144 | self.color = (h, s, v) 145 | self.setRGB((r, g, b)) 146 | self.setHex(hsv2hex(self.color)) 147 | self.ui.color_vis.setStyleSheet(f"background-color: rgb({r},{g},{b})") 148 | self.ui.color_view.setStyleSheet( 149 | f"border-radius: 5px;background-color: qlineargradient(x1:1, x2:0, stop:0 hsl({h}%,100%,50%), stop:1 #fff);") 150 | 151 | def rgbChanged(self): 152 | r, g, b = self.i(self.ui.red.text()), self.i( 153 | self.ui.green.text()), self.i(self.ui.blue.text()) 154 | cr, cg, cb = self.clampRGB((r, g, b)) 155 | 156 | if r != cr or (r == 0 and self.ui.red.hasFocus()): 157 | self.setRGB((cr, cg, cb)) 158 | self.ui.red.selectAll() 159 | if g != cg or (g == 0 and self.ui.green.hasFocus()): 160 | self.setRGB((cr, cg, cb)) 161 | self.ui.green.selectAll() 162 | if b != cb or (b == 0 and self.ui.blue.hasFocus()): 163 | self.setRGB((cr, cg, cb)) 164 | self.ui.blue.selectAll() 165 | 166 | self.color = rgb2hsv(r, g, b) 167 | self.setHSV(self.color) 168 | self.setHex(rgb2hex((r, g, b))) 169 | self.ui.color_vis.setStyleSheet(f"background-color: rgb({r},{g},{b})") 170 | 171 | def hexChanged(self): 172 | hex = self.ui.hex.text() 173 | try: 174 | int(hex, 16) 175 | except ValueError: 176 | hex = "000000" 177 | self.ui.hex.setText("") 178 | r, g, b = hex2rgb(hex) 179 | self.color = hex2hsv(hex) 180 | self.setHSV(self.color) 181 | self.setRGB((r, g, b)) 182 | self.ui.color_vis.setStyleSheet(f"background-color: rgb({r},{g},{b})") 183 | 184 | def alphaChanged(self): 185 | alpha = self.i(self.ui.alpha.text()) 186 | oldalpha = alpha 187 | if alpha < 0: 188 | alpha = 0 189 | if alpha > 100: 190 | alpha = 100 191 | if alpha != oldalpha or alpha == 0: 192 | self.ui.alpha.setText(str(alpha)) 193 | self.ui.alpha.selectAll() 194 | self.alpha = alpha 195 | 196 | # Internal setting functions 197 | def setRGB(self, c): 198 | r, g, b = c 199 | self.ui.red.setText(str(self.i(r))) 200 | self.ui.green.setText(str(self.i(g))) 201 | self.ui.blue.setText(str(self.i(b))) 202 | 203 | def setHSV(self, c): 204 | self.ui.hue_selector.move(7, int((100 - c[0]) * 1.85)) 205 | self.ui.color_view.setStyleSheet( 206 | f"border-radius: 5px;background-color: qlineargradient(x1:1, x2:0, stop:0 hsl({c[0]}%,100%,50%), stop:1 #fff);") 207 | self.ui.selector.move(int(c[1] * 2 - 6), int((200 - c[2] * 2) - 6)) 208 | 209 | def setHex(self, c): 210 | self.ui.hex.setText(c) 211 | 212 | def setAlpha(self, a): 213 | self.ui.alpha.setText(str(a)) 214 | 215 | # Dragging Functions 216 | def setDragPos(self, event): 217 | self.dragPos = event.globalPos() 218 | 219 | def moveWindow(self, event): 220 | # MOVE WINDOW 221 | if event.buttons() == Qt.LeftButton: 222 | self.move(self.pos() + event.globalPos() - self.dragPos) 223 | self.dragPos = event.globalPos() 224 | event.accept() 225 | 226 | def moveSVSelector(self, event): 227 | if event.buttons() == Qt.LeftButton: 228 | pos = event.pos() 229 | if pos.x() < 0: 230 | pos.setX(0) 231 | if pos.y() < 0: 232 | pos.setY(0) 233 | if pos.x() > 200: 234 | pos.setX(200) 235 | if pos.y() > 200: 236 | pos.setY(200) 237 | self.ui.selector.move(pos - QPoint(6, 6)) 238 | self.hsvChanged() 239 | 240 | def moveHueSelector(self, event): 241 | if event.buttons() == Qt.LeftButton: 242 | pos = event.pos().y() - 7 243 | if pos < 0: 244 | pos = 0 245 | if pos > 185: 246 | pos = 185 247 | self.ui.hue_selector.move(QPoint(7, pos)) 248 | self.hsvChanged() 249 | 250 | # Utility 251 | 252 | # Custom int() function, that converts invalid strings to 0 253 | def i(self, text): 254 | try: 255 | return int(text) 256 | except ValueError: 257 | return 0 258 | 259 | # clamp function to remove near-zero values 260 | def clampRGB(self, rgb): 261 | r, g, b = rgb 262 | if r < 0.0001: 263 | r = 0 264 | if g < 0.0001: 265 | g = 0 266 | if b < 0.0001: 267 | b = 0 268 | if r > 255: 269 | r = 255 270 | if g > 255: 271 | g = 255 272 | if b > 255: 273 | b = 255 274 | return r, g, b 275 | 276 | 277 | # Color Utility 278 | def hsv2rgb(h_or_color: Union[tuple, int], s: int = 0, v: int = 0, a: int = None) -> tuple: 279 | """Convert hsv color to rgb color. 280 | 281 | :param h_or_color: The 'hue' value or a color tuple. 282 | :param s: The 'saturation' value. 283 | :param v: The 'value' value. 284 | :param a: The 'alpha' value. 285 | :return: The converted rgb tuple color. 286 | """ 287 | 288 | if type(h_or_color).__name__ == "tuple": 289 | if len(h_or_color) == 4: 290 | h, s, v, a = h_or_color 291 | else: 292 | h, s, v = h_or_color 293 | else: 294 | h = h_or_color 295 | r, g, b = colorsys.hsv_to_rgb(h / 100.0, s / 100.0, v / 100.0) 296 | if a is not None: 297 | return r * 255, g * 255, b * 255, a 298 | return r * 255, g * 255, b * 255 299 | 300 | 301 | def rgb2hsv(r_or_color: Union[tuple, int], g: int = 0, b: int = 0, a: int = None) -> tuple: 302 | """Convert rgb color to hsv color. 303 | 304 | :param r_or_color: The 'red' value or a color tuple. 305 | :param g: The 'green' value. 306 | :param b: The 'blue' value. 307 | :param a: The 'alpha' value. 308 | :return: The converted hsv tuple color. 309 | """ 310 | 311 | if type(r_or_color).__name__ == "tuple": 312 | if len(r_or_color) == 4: 313 | r, g, b, a = r_or_color 314 | else: 315 | r, g, b = r_or_color 316 | else: 317 | r = r_or_color 318 | h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) 319 | if a is not None: 320 | return h * 100, s * 100, v * 100, a 321 | return h * 100, s * 100, v * 100 322 | 323 | 324 | def hex2rgb(hex: str) -> tuple: 325 | """Convert hex color to rgb color. 326 | 327 | :param hex: The hexadecimal string ("xxxxxx"). 328 | :return: The converted rgb tuple color. 329 | """ 330 | 331 | if len(hex) < 6: 332 | hex += "0"*(6-len(hex)) 333 | elif len(hex) > 6: 334 | hex = hex[0:6] 335 | rgb = tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) 336 | return rgb 337 | 338 | 339 | def rgb2hex(r_or_color: Union[tuple, int], g: int = 0, b: int = 0, a: int = 0) -> str: 340 | """Convert rgb color to hex color. 341 | 342 | :param r_or_color: The 'red' value or a color tuple. 343 | :param g: The 'green' value. 344 | :param b: The 'blue' value. 345 | :param a: The 'alpha' value. 346 | :return: The converted hexadecimal color. 347 | """ 348 | 349 | if type(r_or_color).__name__ == "tuple": 350 | r, g, b = r_or_color[:3] 351 | else: 352 | r = r_or_color 353 | hex = '%02x%02x%02x' % (int(r), int(g), int(b)) 354 | return hex 355 | 356 | 357 | def hex2hsv(hex: str) -> tuple: 358 | """Convert hex color to hsv color. 359 | 360 | :param hex: The hexadecimal string ("xxxxxx"). 361 | :return: The converted hsv tuple color. 362 | """ 363 | 364 | return rgb2hsv(hex2rgb(hex)) 365 | 366 | 367 | def hsv2hex(h_or_color: Union[tuple, int], s: int = 0, v: int = 0, a: int = 0) -> str: 368 | """Convert hsv color to hex color. 369 | 370 | :param h_or_color: The 'hue' value or a color tuple. 371 | :param s: The 'saturation' value. 372 | :param v: The 'value' value. 373 | :param a: The 'alpha' value. 374 | :return: The converted hexadecimal color. 375 | """ 376 | 377 | if type(h_or_color).__name__ == "tuple": 378 | h, s, v = h_or_color[:3] 379 | else: 380 | h = h_or_color 381 | return rgb2hex(hsv2rgb(h, s, v)) 382 | 383 | 384 | # toplevel functions 385 | 386 | __instance = None 387 | __lightTheme = False 388 | __useAlpha = False 389 | 390 | 391 | def useAlpha(value=True) -> None: 392 | """Set if the ColorPicker should display an alpha field. 393 | 394 | :param value: True for alpha field, False for no alpha field. Defaults to True 395 | :return: 396 | """ 397 | global __useAlpha 398 | __useAlpha = value 399 | 400 | 401 | def useLightTheme(value=True) -> None: 402 | """Set if the ColorPicker should use the light theme. 403 | 404 | :param value: True for light theme, False for dark theme. Defaults to True 405 | :return: None 406 | """ 407 | 408 | global __lightTheme 409 | __lightTheme = value 410 | 411 | 412 | def getColor(lc: tuple = None) -> tuple: 413 | """Shows the ColorPicker and returns the picked color. 414 | 415 | :param lc: The color to display as previous color. 416 | :return: The picked color. 417 | """ 418 | 419 | global __instance 420 | 421 | if __instance is None: 422 | __instance = ColorPicker(useAlpha=__useAlpha, lightTheme=__lightTheme) 423 | 424 | if __useAlpha != __instance.usingAlpha or __lightTheme != __instance.usingLightTheme: 425 | del __instance 426 | __instance = ColorPicker(useAlpha=__useAlpha, lightTheme=__lightTheme) 427 | 428 | return __instance.getColor(lc) 429 | -------------------------------------------------------------------------------- /src/addons/colorpicker/colorpicker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from typing import Optional 4 | 5 | from pynput import mouse 6 | 7 | from numpy import array 8 | 9 | from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QVBoxLayout, 10 | QGridLayout, QFrame, QSizePolicy, 11 | QSpacerItem, QPushButton) 12 | from PyQt5.QtGui import QCursor, QPainter, QPixmap, QColor, QIcon, QPen, QPainterPath, QImage, QRadialGradient, QBrush 13 | from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRectF 14 | from PyQt5.QtSvg import QSvgWidget 15 | from PIL import ImageGrab, Image 16 | from addon import AddOnBase 17 | 18 | 19 | sys.path.append(os.path.dirname(os.path.dirname( 20 | os.path.dirname(os.path.abspath(__file__))))) 21 | 22 | sys.path.append(os.path.abspath(__file__)) 23 | 24 | from settings import UI_SCALE # pylint: disable=import-error, unused-import 25 | from ui.utils import get_font # pylint: disable=import-error, unused-import 26 | from ui.utils import DEFAULT_BOLD, DEFAULT_REGULAR # pylint: disable=import-error, unused-import 27 | from ui.base_window import BaseWindow # pylint: disable=import-error, unused-import 28 | from ui.custom_button import RedButton, TextButton # pylint: disable=import-error, unused-import 29 | 30 | if __name__ == "__main__": 31 | from vcolorpicker import ColorPicker 32 | else: 33 | from .vcolorpicker import ColorPicker 34 | 35 | 36 | def resize_image(image, target_width, target_height): 37 | qImg = QImage(image) 38 | qImg = qImg.scaled(target_width, target_height, 39 | Qt.IgnoreAspectRatio, Qt.SmoothTransformation) 40 | return qImg 41 | 42 | 43 | def get_pixel_from_position(position) -> str: 44 | """Get pixel color from mouse position 45 | 46 | Returns: 47 | str: Pixel color in hex format 48 | """ 49 | pixel = ImageGrab.grab().load()[position.x(), position.y()] 50 | 51 | # convert pixel color to hex 52 | pixel = f"#{pixel[0]:02x}{pixel[1]:02x}{pixel[2]:02x}" 53 | 54 | return pixel 55 | 56 | 57 | class SelectedColorWidget(QWidget): 58 | def __init__(self, color: str = '#000000'): 59 | super().__init__() 60 | self.color = color 61 | self.clipboard = QApplication.clipboard() 62 | self.layout = QGridLayout() 63 | 64 | self.copy_svg_icon = QSvgWidget(os.path.join(os.path.dirname( 65 | os.path.abspath(__file__)), "icons", "copy-icon.svg")) 66 | self.copy_svg_icon = self.copy_svg_icon.grab() 67 | 68 | self.setLayout(self.layout) 69 | 70 | self.delete_widget_btn = RedButton() 71 | self.delete_widget_btn.setToolTip("Remove selected color") 72 | 73 | # scale button size to 50% of default 74 | self.delete_widget_btn.setIconSize( 75 | self.delete_widget_btn.iconSize() * 0.5) 76 | 77 | self.delete_widget_btn.clicked.connect(self.delete_widget) 78 | 79 | self.color_label_hex = QLabel("Hex Color:") 80 | self.color_label_hex.setFont(get_font(DEFAULT_BOLD, 12)) 81 | self.color_label_hex.setStyleSheet("color: #000000;") 82 | self.color_label_hex.setAlignment(Qt.AlignRight | Qt.AlignVCenter) 83 | 84 | self.color_value_hex = QLabel(self.color) 85 | self.color_value_hex.setFont(get_font(DEFAULT_REGULAR, 10)) 86 | self.color_value_hex.setStyleSheet("color: #000000;") 87 | self.color_value_hex.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) 88 | 89 | self.copy_color_hex_btn = QPushButton() 90 | self.copy_color_hex_btn.setFixedWidth(25) 91 | self.copy_color_hex_btn.setIcon(QIcon(self.copy_svg_icon)) 92 | self.copy_color_hex_btn.setStyleSheet('background: white;') 93 | self.copy_color_hex_btn.setStyleSheet( 94 | "QPushButton:hover { " + f"background-color: {self.color}; " + "}") 95 | self.copy_color_hex_btn.clicked.connect( 96 | lambda: self.clipboard.setText(self.color)) 97 | 98 | self.layout.addWidget(self.color_label_hex, 0, 0, 1, 3) 99 | self.layout.addWidget(self.color_value_hex, 0, 3, 1, 5) 100 | self.layout.addWidget(self.copy_color_hex_btn, 0, 8, 1, 1) 101 | self.layout.addWidget(self.delete_widget_btn, 0, 9, 1, 1) 102 | 103 | self.color_label_rgb = QLabel("RGB Color:") 104 | self.color_label_rgb.setFont(get_font(DEFAULT_BOLD, 12)) 105 | self.color_label_rgb.setStyleSheet("color: #000000;") 106 | self.color_label_rgb.setAlignment(Qt.AlignRight | Qt.AlignVCenter) 107 | 108 | self.color_value_rgb = QLabel(str(QColor(self.color).getRgb()[0:3])) 109 | self.color_value_rgb.setFont(get_font(DEFAULT_REGULAR, 10)) 110 | self.color_value_rgb.setStyleSheet("color: #000000;") 111 | self.color_value_rgb.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) 112 | 113 | self.copy_color_rgb_btn = QPushButton() 114 | self.copy_color_rgb_btn.setFixedWidth(25) 115 | self.copy_color_rgb_btn.setIcon(QIcon(self.copy_svg_icon)) 116 | self.copy_color_rgb_btn.setStyleSheet('background: white;') 117 | 118 | # set color on hover to match color of selected color 119 | self.copy_color_rgb_btn.setStyleSheet( 120 | "QPushButton:hover { " + f"background-color: {self.color}; " + "}") 121 | self.copy_color_rgb_btn.clicked.connect( 122 | lambda: self.clipboard.setText(str(QColor(self.color).getRgb()[0:3]))) 123 | 124 | self.layout.addWidget(self.color_label_rgb, 1, 0, 1, 3) 125 | self.layout.addWidget(self.color_value_rgb, 1, 3, 1, 5) 126 | self.layout.addWidget(self.copy_color_rgb_btn, 1, 8, 1, 1) 127 | 128 | self.color_display_box = QFrame() 129 | self.color_display_box.setStyleSheet( 130 | f"background-color: {self.color};") 131 | self.color_display_box.setMaximumSize(280, 280) 132 | self.color_display_box.setMinimumSize(280, 25) 133 | self.color_display_box.setFrameShape(QFrame.Box) 134 | self.color_display_box.setFrameShape(QFrame.StyledPanel) 135 | 136 | self.color_display_box.setFrameShadow(QFrame.Raised) 137 | self.color_display_box.setLineWidth(2) 138 | 139 | self.layout.addWidget(self.color_display_box, 2, 0, 1, 10) 140 | 141 | # add a spacer to push the color display box to the top 142 | self.layout.addItem(QSpacerItem( 143 | 0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), 3, 0, 1, 10) 144 | 145 | def delete_widget(self): 146 | self.deleteLater() 147 | buddy_color_picker.resize_signal.emit() 148 | 149 | 150 | class ColorPickerWidget(ColorPicker): 151 | def __init__(self) -> None: 152 | super().__init__(True) 153 | 154 | def exit_btn_clicked(self): 155 | self.hide() 156 | buddy_color_picker.add_selected_color_signal.emit( 157 | f"#{self.ui.hex.text()}") 158 | buddy_color_picker.show() 159 | 160 | 161 | class BuddyColorPicker(BaseWindow): 162 | add_selected_color_signal = pyqtSignal(str) 163 | resize_signal = pyqtSignal() 164 | 165 | def __init__(self): 166 | super().__init__() 167 | self.layout = QVBoxLayout() 168 | self.selected_color_widget = None 169 | self.selected_color_widgets = 0 170 | self.added_colors = [] 171 | 172 | self._desktop_color_picker = MagnifierWidget() 173 | self._color_picker = ColorPickerWidget() 174 | 175 | self.setLayout(self.layout) 176 | self.setMaximumHeight(800) 177 | 178 | self.add_selected_color_signal.connect(self.add_selected_color) 179 | self.resize_signal.connect(self.resize_self) 180 | 181 | # self.findChildren(InnerPart)[0].edit_button.hide() 182 | 183 | self.desktop_color_picker = TextButton(text=" Desktop Color Picker ") 184 | self.color_picker = TextButton(text=" Custom Color Picker ") 185 | 186 | self.desktop_color_picker.clicked.connect( 187 | self.start_desktop_color_picker) 188 | self.color_picker.clicked.connect(self.start_color_picker) 189 | 190 | self.layout.addWidget(self.desktop_color_picker) 191 | self.layout.addWidget(self.color_picker) 192 | 193 | self.vertical_spacer = QSpacerItem( 194 | 0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding) 195 | self.layout.addItem(self.vertical_spacer) 196 | 197 | self.animate = True 198 | self.adjustSize() 199 | 200 | def start_desktop_color_picker(self): 201 | self.hide() 202 | self._desktop_color_picker.show() 203 | self._desktop_color_picker.start_color_picker() 204 | 205 | def start_color_picker(self): 206 | self.hide() 207 | self._color_picker.show() 208 | 209 | def on_close_button_clicked(self): 210 | self.hide() 211 | self._desktop_color_picker.hide() 212 | self._color_picker.hide() 213 | 214 | def add_selected_color(self, color: str): 215 | if color not in self.added_colors: 216 | self.added_colors.append(color) 217 | self.selected_color_widget = SelectedColorWidget(color=color) 218 | self.layout.insertWidget( 219 | self.layout.count() - 1, self.selected_color_widget) 220 | 221 | self.selected_color_widgets = len( 222 | self.findChildren(SelectedColorWidget)) 223 | self.setMinimumHeight(300 + 100 * self.selected_color_widgets) 224 | if self.selected_color_widgets > 5: 225 | for ind, selected_color_widget in enumerate(self.findChildren(SelectedColorWidget)): 226 | if ind < self.selected_color_widgets - 5: 227 | selected_color_widget.hide() 228 | 229 | def resize_self(self): 230 | self.setMinimumHeight(100 + 100 * self.selected_color_widgets) 231 | self.setMaximumHeight(200 + 100 * self.selected_color_widgets) 232 | self.adjustSize() 233 | 234 | 235 | class MagnifierWidget(QWidget): 236 | def __init__(self, parent: Optional[QWidget] = None): 237 | super().__init__() 238 | self.parent = parent 239 | self.color = '#000000' 240 | self.track_color = False 241 | self.listener = None 242 | self.initUI() 243 | 244 | def initUI(self): 245 | self.layout = QVBoxLayout(self) 246 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 247 | self.setAttribute(Qt.WA_TranslucentBackground) 248 | 249 | self.label = QLabel(self) 250 | self.label.resize(340, 340) 251 | 252 | self.layout.addWidget(self.label) 253 | 254 | screen_geometry = QApplication.desktop().screenGeometry() 255 | window_width = self.width() 256 | x_pos = screen_geometry.width() - window_width - -200 257 | y_pos = 50 258 | 259 | self.move(x_pos, y_pos) 260 | 261 | self.timer = QTimer() 262 | self.timer.timeout.connect(self.capture) 263 | self.timer.start(60) 264 | 265 | def paintEvent(self, event): 266 | # Override QWidget's paint event to draw a circular mask with smooth edges 267 | path = QPainterPath() 268 | # Adjust the rectangle dimensions 269 | path.addEllipse(QRectF(self.rect()).adjusted(7, 7, -7, -7)) 270 | mask = QPainter(self) 271 | mask.setRenderHint(QPainter.Antialiasing) # Enable anti-aliasing 272 | mask.setClipPath(path) 273 | mask.fillRect(self.rect(), Qt.black) 274 | 275 | def generatePixmapMask(self, diameter): 276 | # Create QPixmap with transparency 277 | mask = QPixmap(diameter, diameter) 278 | mask.fill(Qt.transparent) 279 | 280 | # Create QPainter to draw on the QPixmap 281 | painter = QPainter(mask) 282 | painter.setRenderHint(QPainter.Antialiasing) 283 | 284 | # Create radial gradient 285 | gradient = QRadialGradient( 286 | diameter / 2, diameter / 2, diameter / 2, diameter / 2, diameter / 2) 287 | gradient.setColorAt(0, QColor(0, 0, 0, 255)) 288 | gradient.setColorAt(1, QColor(0, 0, 0, 0)) 289 | 290 | # Set gradient as brush 291 | painter.setBrush(QBrush(gradient)) 292 | painter.setPen(Qt.NoPen) 293 | 294 | # Draw ellipse 295 | painter.drawEllipse(0, 0, diameter, diameter) 296 | painter.end() 297 | 298 | return mask 299 | 300 | def start_color_picker(self): 301 | self.listener = mouse.Listener( 302 | on_click=lambda x, y, button, pressed: self.mousePressEvent(pressed)) 303 | self.listener.start() 304 | self.track_color = True 305 | self.move(50, 50) 306 | self.set_track_color(True) 307 | # self.title_layout.setContentsMargins(20, 10, 0, 10) 308 | 309 | def capture(self): 310 | if self.track_color: 311 | cursor = QCursor.pos() 312 | self.color = get_pixel_from_position(cursor) 313 | x, y = cursor.x() - 8, cursor.y() - 8 314 | x, y = int(x), int(y) 315 | 316 | screen = ImageGrab.grab(bbox=(x, y, x + 17, y + 17)) 317 | screen_resized = screen.resize((340, 340), resample=Image.NEAREST) 318 | 319 | screen_np = array(screen_resized) 320 | 321 | height, width, channel = screen_np.shape 322 | bytesPerLine = 3 * width 323 | qImg = QImage(screen_np.data, width, height, 324 | bytesPerLine, QImage.Format_RGB888) 325 | 326 | pixmap = QPixmap.fromImage(qImg) 327 | 328 | painter = QPainter(pixmap) 329 | pen = QPen(QColor('gray'), 1, Qt.SolidLine) 330 | painter.setPen(pen) 331 | 332 | pixel_size = 340 // 17 333 | 334 | center = 340 // 2 335 | 336 | for i in range(center % pixel_size, 340, pixel_size): 337 | painter.drawLine(i - pixel_size // 2, 0, 338 | i - pixel_size // 2, 340) 339 | painter.drawLine(0, i - pixel_size // 2, 340 | 340, i - pixel_size // 2) 341 | 342 | pen = QPen(QColor('white'), 1, Qt.SolidLine) 343 | painter.setPen(pen) 344 | painter.drawRect(center - pixel_size // 2 - 2, center - 345 | pixel_size // 2 - 2, pixel_size + 3, pixel_size + 3) 346 | 347 | pen = QPen(QColor('black'), 2, Qt.SolidLine) 348 | painter.setPen(pen) 349 | painter.drawRect(center - pixel_size // 2, center - 350 | pixel_size // 2, pixel_size, pixel_size) 351 | 352 | painter.end() 353 | 354 | self.label.setPixmap(pixmap) 355 | 356 | # Apply mask to pixmap 357 | mask = self.generatePixmapMask(pixmap.width()) 358 | pixmap.setMask(mask.mask()) 359 | 360 | # Create QPainterPath 361 | path = QPainterPath() 362 | path.addEllipse(QRectF(0, 0, 0, 0)) 363 | 364 | # Create mask QPainter 365 | mask_painter = QPainter(pixmap) 366 | mask_painter.setRenderHint(QPainter.Antialiasing) 367 | mask_painter.setClipPath(path) 368 | 369 | # Draw pixmap onto itself using the mask painter 370 | mask_painter.drawPixmap(0, 0, pixmap) 371 | mask_painter.end() 372 | 373 | self.label.setPixmap(pixmap) 374 | 375 | def mousePressEvent(self, event): 376 | self.set_track_color(False) 377 | self.listener.stop() 378 | self.hide() 379 | buddy_color_picker.add_selected_color_signal.emit(self.color) 380 | buddy_color_picker.show() 381 | 382 | def set_track_color(self, track_color: bool) -> None: 383 | self.track_color = track_color 384 | 385 | 386 | buddy_color_picker = BuddyColorPicker() 387 | AddOnBase().activate = lambda: buddy_color_picker.show() if buddy_color_picker.isHidden() else buddy_color_picker.hide() 388 | 389 | if __name__ == '__main__': 390 | app = QApplication(sys.argv) 391 | buddy_color_picker = BuddyColorPicker() 392 | buddy_color_picker.show() 393 | 394 | desktop_color_picker = MagnifierWidget() 395 | desktop_color_picker.hide() 396 | 397 | color_picker = ColorPickerWidget() 398 | color_picker.hide() 399 | 400 | sys.exit(app.exec_()) 401 | -------------------------------------------------------------------------------- /src/launcher.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from math import ceil 3 | from types import ModuleType 4 | from typing import Callable, Optional 5 | 6 | from PyQt5.QtCore import ( 7 | Qt, 8 | QPoint, 9 | QRect, 10 | QSize, 11 | pyqtSignal, 12 | ) 13 | from PyQt5.QtWidgets import ( 14 | QApplication, 15 | QMainWindow, 16 | QLabel, 17 | QPushButton, 18 | QWidget, 19 | QHBoxLayout, 20 | ) 21 | from PyQt5.QtGui import ( 22 | QPainter, 23 | QColor, 24 | QBrush, 25 | QKeySequence, 26 | QPaintEvent, 27 | QMouseEvent, 28 | QFontMetrics, 29 | QPixmap, 30 | ) 31 | 32 | from settings import apply_ui_scale as scaled 33 | from ui.utils import get_font 34 | 35 | from FileSystem import icon as get_icon, abspath 36 | from SaveFile import apply_setting, get_setting, remove_setting, NotFoundException 37 | from utils import HotKeys 38 | 39 | from addon import AddOnBase 40 | 41 | 42 | def check_setting(name: str) -> bool: 43 | try: 44 | get_setting(name) 45 | except NotFoundException: 46 | return False 47 | return True 48 | 49 | 50 | class IconButton(QPushButton): 51 | def __init__(self, parent: QWidget, icon_path: str, hover_icon_path: str) -> None: 52 | super().__init__(parent) 53 | 54 | self._icon = icon_path 55 | self._hover_icon = hover_icon_path 56 | 57 | self.setFixedSize(QSize(scaled(100), scaled(100))) 58 | self.setIconSize(QSize(scaled(100), scaled(100))) 59 | 60 | self.setStyleSheet( 61 | ( 62 | """ 63 | QPushButton { 64 | border: none; 65 | icon: url(%s); 66 | margin: 0px; 67 | padding: 0px; 68 | } 69 | QPushButton:hover { 70 | icon: url(%s); 71 | margin: 0px; 72 | padding: 0px; 73 | } 74 | """ 75 | % ( 76 | self._icon, 77 | self._hover_icon, 78 | ) 79 | ) 80 | ) 81 | 82 | 83 | class ShortcutLabel(QWidget): 84 | 85 | class Label(QLabel): 86 | def __init__(self, text: str, parent: Optional[QWidget] = None): 87 | super().__init__(text, parent) 88 | 89 | self.is_plus = text == "+" 90 | 91 | self.setFont(get_font(size=scaled(11), weight="semibold")) 92 | self.setFixedSize(self.sizeHint()) 93 | 94 | def sizeHint(self): 95 | font_metrics = QFontMetrics(self.font()) 96 | text_width = font_metrics.width(self.text()) 97 | text_height = font_metrics.height() 98 | button_width = text_width + 7 * 2 # *2 for Adding padding to both sides 99 | button_height = text_height + 1 * 2 100 | return QSize(scaled(button_width), scaled(button_height)) 101 | 102 | def paintEvent(self, a0: QPaintEvent) -> None: 103 | back_color = QColor(0, 0, 0, 0) if self.is_plus else QColor("#ECECEC") 104 | text_color = QColor("#ECECEC") if self.is_plus else self.palette().buttonText().color() 105 | painter = QPainter(self) 106 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 107 | painter.setPen(Qt.PenStyle.NoPen) 108 | painter.setBrush(back_color) # default color #ECECEC 109 | painter.drawRoundedRect(self.rect(), scaled(5), scaled(5)) 110 | painter.setPen(text_color) 111 | painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.text()) 112 | 113 | 114 | def __init__(self, parent: QWidget, shortcut: QKeySequence): 115 | super().__init__(parent) 116 | 117 | keys = QKeySequence(shortcut[0]).toString().split("+") 118 | self.shortcut_keys = [x.upper() for y in keys for x in (y, "+")][:-1] 119 | 120 | self.setLayout(layout := QHBoxLayout(self)) 121 | layout.setContentsMargins(0, 0, 0, 0) 122 | layout.setSpacing(0) 123 | 124 | layout.addStretch() 125 | 126 | for key in self.shortcut_keys: 127 | label = self.Label(key) 128 | layout.addWidget(label) 129 | 130 | layout.addStretch() 131 | 132 | self.adjustSize() 133 | 134 | 135 | class GroupWidget(QWidget): 136 | animating: int = 0 137 | 138 | def __init__(self, parent: QWidget, index: int, title: str, icon_path: str, hover_icon_path: str, 139 | shortcut: QKeySequence, activate_callback: Callable) -> None: 140 | super().__init__(parent) 141 | 142 | self.index = index 143 | self.setFixedWidth(scaled(100 + 40)) # 40 padding 144 | 145 | self.icon_button = IconButton(self, icon_path, hover_icon_path) 146 | self.icon_button.clicked.connect(activate_callback) 147 | self.icon_button.setGeometry(QRect(scaled(20), 0, self.icon_button.width(), self.icon_button.height())) 148 | 149 | self.title_label = QLabel(title, self) 150 | self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 151 | self.title_label.setWordWrap(True) 152 | self.title_label.setFont(get_font(size = scaled(12), weight="medium")) 153 | self.title_label.setStyleSheet("QLabel { color : #ECECEC }") 154 | self.title_label.setGeometry(QRect(0, scaled(85+11), self.width(), self.title_label.height() + scaled(10))) 155 | 156 | if shortcut is not None: 157 | self.hotkey_label = ShortcutLabel(self, shortcut) 158 | self.hotkey_label.setGeometry(QRect(0, scaled(98+17+17)+self.title_label.sizeHint().height(), 159 | self.width(), 160 | self.hotkey_label.height())) 161 | 162 | self.adjustSize() 163 | self.move(self.get_widget_position(self.index)) 164 | 165 | 166 | @staticmethod 167 | def get_widget_position(index: int) -> QPoint: 168 | """Returns the position of the GroupWidget for main window according to the given index.""" 169 | # 4 is the maximum number of widgets for each line 170 | x = (4 if (_x:=index % 4) == 0 else _x) - 1 171 | y = ceil(index / 4) - 1 172 | position = QPoint(GroupWidget.size().width() * x, (GroupWidget.size().height() + scaled(40)) * y) 173 | return position + QPoint(scaled(20), scaled(40)) # adding left and top padding 174 | 175 | 176 | # setting fixed size of GroupWidget 177 | @staticmethod 178 | def size() -> QSize: 179 | # Note: this widgets exact size is 85, 164. 20 is added to the margins. 180 | # this values don't effect the size of the GroupWidget 181 | return QSize(scaled(100+20+20), scaled(164)) 182 | 183 | 184 | class LowerWidget(QMainWindow): 185 | window_toggle_signal = pyqtSignal() 186 | 187 | def __init__(self, add_ons: dict[str, ModuleType]) -> None: 188 | super().__init__() 189 | 190 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) 191 | self.setAttribute(Qt.WA_TranslucentBackground) 192 | 193 | 194 | self._offset = None 195 | self._moved = False 196 | 197 | self.window_toggle_signal.connect(self.toggle_windows) 198 | 199 | if check_setting("lower_position"): 200 | self.lower_position = QPoint(get_setting("lower_position")[0], get_setting("lower_position")[1]) 201 | else: 202 | desktop = QApplication.desktop() 203 | primary_screen_index = desktop.primaryScreen() 204 | screen = desktop.screen(primary_screen_index) 205 | self.lower_position = QPoint(screen.width() // 2 - self.size().width() // 2, 206 | screen.height() - 60 - self.size().height()) 207 | 208 | self.icon: QPixmap = QPixmap(get_icon("icon.png")).scaled(scaled(35), scaled(35), 209 | Qt.AspectRatioMode.KeepAspectRatio, 210 | Qt.TransformationMode.SmoothTransformation) 211 | 212 | self.icon_label = QLabel(self) 213 | self.icon_label.setPixmap(self.icon) 214 | self.icon_label.setGeometry(scaled(40), scaled(13), scaled(35), scaled(35)) 215 | 216 | self.title_label = QLabel("FlowBuddy", self) 217 | self.title_label.setFont(get_font(size=scaled(16), weight="medium")) 218 | self.title_label.setStyleSheet("QLabel { color : #ECECEC }") 219 | self.title_label.move(scaled(43 + 40), scaled(9 + 5)) 220 | self.title_label.adjustSize() 221 | 222 | self.setFixedSize(self.size()) 223 | 224 | 225 | self.main_window = MainWindow(add_ons) 226 | 227 | hotkey = get_setting("hotkey") if check_setting("hotkey") else "+`" 228 | HotKeys.add_global_shortcut(hotkey, self.window_toggle_signal.emit) 229 | 230 | self.move(self.lower_position) 231 | self.setHidden(get_setting("lower-hidden")) if check_setting("lower-hidden") else self.show() 232 | self.main_window.setHidden(get_setting("upper-hidden")) if check_setting("upper-hidden") else self.show() 233 | 234 | 235 | def toggle_windows(self) -> None: 236 | if self.isHidden(): 237 | for window in self.active_windows: 238 | window.show() 239 | else: 240 | self.active_windows = (x for x in QApplication.allWindows() if x.isVisible()) 241 | for window in self.active_windows: 242 | window.hide() 243 | 244 | 245 | def paintEvent(self, a0: QPaintEvent) -> None: 246 | painter = QPainter(self) 247 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 248 | painter.setPen(Qt.PenStyle.NoPen) 249 | painter.setBrush(QBrush(QColor(0, 0, 0, 178))) 250 | painter.drawRoundedRect(self.rect(), scaled(32), scaled(32)) 251 | 252 | def mousePressEvent(self, a0: QMouseEvent) -> None: 253 | if a0.button() == Qt.MouseButton.LeftButton: 254 | self._offset = a0.pos() 255 | return super().mousePressEvent(a0) 256 | 257 | def mouseMoveEvent(self, a0: QMouseEvent) -> None: 258 | if self._offset is not None and a0.buttons() == Qt.MouseButton.LeftButton: 259 | self.move(a0.globalPos() - self._offset) 260 | self._moved = True 261 | return super().mouseMoveEvent(a0) 262 | 263 | def mouseReleaseEvent(self, a0: QMouseEvent) -> None: 264 | if self._moved: 265 | self.lower_position = self.pos() 266 | apply_setting("lower_position", [self.lower_position.x(), self.lower_position.y()]) 267 | else: 268 | self.main_window.setHidden(not self.main_window.isHidden()) 269 | self._moved = False 270 | self._offset = None 271 | return super().mouseReleaseEvent(a0) 272 | 273 | 274 | def show(self) -> None: 275 | apply_setting("lower-hidden", False) 276 | return super().show() 277 | 278 | def hide(self) -> None: 279 | apply_setting("lower-hidden", True) 280 | return super().hide() 281 | 282 | def setHidden(self, hidden: bool) -> None: 283 | apply_setting("lower-hidden", hidden) 284 | return super().setHidden(hidden) 285 | 286 | 287 | # setting fixed size of LowerWidget 288 | @staticmethod 289 | def size() -> QSize: 290 | # Note: this widgets exact size is 177, 35. 40, 13 is added to the margins. 291 | # this values don't effect the size of the LowerWidget 292 | return QSize(scaled(162+40+40), scaled(35+13+13)) 293 | 294 | 295 | class MainWindow(QWidget): 296 | def __init__(self, add_ons: dict[str, ModuleType], parent: QWidget | None = None) -> None: 297 | super().__init__(parent) 298 | 299 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) 300 | self.setAttribute(Qt.WA_TranslucentBackground) 301 | 302 | self._offset = None 303 | self._moved = False 304 | self.maximized = False 305 | self.widgets: list[GroupWidget] = [] 306 | self.active_windows = [] 307 | 308 | for index, add_on_name in enumerate(add_ons, 1): 309 | self.add_widget(index, add_on_name) 310 | 311 | current_window_size = ws = self.get_window_size() 312 | if check_setting("upper_position"): 313 | self.upper_position = QPoint(get_setting("upper_position")[0], get_setting("upper_position")[1]) 314 | else: 315 | desktop = QApplication.desktop() 316 | primary_screen_index = desktop.primaryScreen() 317 | screen = desktop.screen(primary_screen_index) 318 | self.upper_position = QPoint(screen.width() // 2 - ws.width() // 2, 319 | screen.height() - 150 - ws.height()) 320 | 321 | self.setGeometry(QRect(self.upper_position, current_window_size)) 322 | 323 | 324 | def get_window_size(self) -> QSize: 325 | """Returns the size of the window acording to the GroupWidgets created.""" 326 | len_of_widgets = len(self.widgets) 327 | x = min(len_of_widgets, 4) 328 | y = ceil(len_of_widgets / 4) 329 | window_size = QSize(GroupWidget.size().width() * x, (GroupWidget.size().height() + scaled(40)) * y - scaled(40)) 330 | return window_size + QSize(scaled(20+20), scaled(40+40)) # adding left, right, top and bottom padding 331 | 332 | 333 | def add_widget(self, index: int, add_on_name: str) -> None: 334 | """Adds a new GroupWidget to the main window.""" 335 | 336 | add_on_base_instance = AddOnBase(add_on_name) 337 | 338 | title = add_on_base_instance.name 339 | icon_path = hover_icon_path = add_on_base_instance.icon_path 340 | activate = add_on_base_instance.activate 341 | shortcut = add_on_base_instance.activate_shortcut 342 | 343 | widget = GroupWidget(self, index, title, icon_path, hover_icon_path, shortcut, activate) 344 | self.widgets.append(widget) 345 | 346 | 347 | def toggle_windows(self) -> None: 348 | if self.isHidden(): 349 | for window in self.active_windows: 350 | window.show() 351 | else: 352 | self.active_windows = (x for x in QApplication.allWindows() if x.isVisible()) 353 | for window in self.active_windows: 354 | window.hide() 355 | 356 | 357 | def paintEvent(self, a0: QPaintEvent) -> None: 358 | painter = QPainter(self) 359 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 360 | painter.setPen(Qt.PenStyle.NoPen) 361 | painter.setBrush(QBrush(QColor(0, 0, 0, 178))) 362 | painter.drawRoundedRect(self.rect(), scaled(32), scaled(32)) 363 | 364 | def mousePressEvent(self, a0: QMouseEvent) -> None: 365 | if a0.button() == Qt.MouseButton.LeftButton: 366 | self._offset = a0.pos() 367 | return super().mousePressEvent(a0) 368 | 369 | def mouseMoveEvent(self, a0: QMouseEvent) -> None: 370 | if self._offset is not None and a0.buttons() == Qt.MouseButton.LeftButton: 371 | self.move(a0.globalPos() - self._offset) 372 | self._moved = True 373 | return super().mouseMoveEvent(a0) 374 | 375 | def mouseReleaseEvent(self, a0: QMouseEvent) -> None: 376 | if self._moved: 377 | self.upper_position = self.pos() 378 | apply_setting("upper_position", [self.upper_position.x(), self.upper_position.y()]) 379 | self._moved = False 380 | self._offset = None 381 | return super().mouseReleaseEvent(a0) 382 | 383 | def show(self) -> None: 384 | apply_setting("upper-hidden", False) 385 | return super().show() 386 | 387 | def hide(self) -> None: 388 | apply_setting("upper-hidden", True) 389 | return super().hide() 390 | 391 | def setHidden(self, hidden: bool) -> None: 392 | apply_setting("upper-hidden", hidden) 393 | return super().setHidden(hidden) 394 | --------------------------------------------------------------------------------