├── 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://discord.gg/cdS6GxMKrE)
8 |
9 |
10 |
11 |
12 |
13 |
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 | 
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 |
--------------------------------------------------------------------------------