KikkaAI il y a 9 mois
Parent
commit
97966c72c1

+ 75 - 0
.gitignore

@@ -0,0 +1,75 @@
+.cache
+
+*.py[co]
+
+# C extensions
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+#lib
+lib64
+__pycache__
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+
+# Translations
+*.mo
+*.qm
+**/~$translation_en.xlsx
+Tools/i18n/translation.pro
+CustomTranslations/*
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+tmp/
+log/
+*.log
+_site
+apps
+_build/
+Kikka/ios/certificate_config.py
+htmlcov/
+cover/
+.qt_for_python/
+
+.idea/
+.DS_Store
+
+PrivatePlugins
+ExtPlugins
+Temp/
+Log/
+.sunshine/
+error.txt
+venv/
+.venv/
+RecentFiles/
+Playground/
+.Config
+Projects/Local/
+
+# vs
+.vs/
+*.pyproj
+*.sln
+# vscode
+.vscode/
+*.code-workspace

+ 183 - 0
Kikka/KikkaAction.py

@@ -0,0 +1,183 @@
+import os
+import json
+import elevate
+import subprocess
+
+from PySide6.QtCore import Qt, QFileInfo
+
+import Kikka
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+from Kikka.KikkaMemory import Storageable, KikkaMemory
+
+
+class Action:
+    def __init__(self, data):
+        self.name = data[0]
+        self.type = data[1]
+        self.path = data[2]
+        self.icon = data[3]
+        self.param = data[4]
+        self.work_dir = data[5]
+        self.run_as_admin = data[6]
+        self.open_as_private = data[7]
+        self.color = data[8]
+        self.shortcut = data[9]
+        self.remark = data[10]
+    
+    def toList(self):
+        return [
+            self.name,
+            self.type,
+            self.path,
+            self.icon,
+            self.param,
+            self.work_dir,
+            self.run_as_admin,
+            self.open_as_private,
+            self.color,
+            self.shortcut,
+            self.remark
+        ]
+
+
+class SubCategory(Storageable):
+    def __init__(self, name, type):
+        self.name = name
+        self.type = type
+        self.actions = []
+    
+    def __str__(self):
+        return "SubCategory(%s) %s" % (self.name, self.type)
+    
+    def toDict(self):
+        return {
+            'name': self.name,
+            'type': self.type,
+            'actions': [a.toList() for a in self.actions]
+        }
+    
+
+class MainCategory(Storageable):
+    def __init__(self, name):
+        self.name = name
+        self.sub_categories = {}
+    
+    def __getitem__(self, item):
+        return self.sub_categories[item]
+        
+    def __str__(self):
+        return "MainCategory(%s)" % self.name
+        
+    def toDict(self):
+        return {
+            'name': self.name,
+            'sub_categories': [s.toDict() for _, s in self.sub_categories.items()]
+        }
+
+
+@singleton
+class KikkaAction():
+    
+    def __init__(self):
+        self._data = None
+    
+    @property
+    def data(self):
+        return self._data
+        
+    def load(self):
+        data = {}
+        action_data = Kikka.memory.getAction()
+        for col, main_category_data in enumerate(action_data.values()):
+            main_name = main_category_data['name']
+            main_category = MainCategory(main_name)
+            data[col] = main_category
+            
+            for row, sub_category_data in enumerate(main_category_data['sub_categories']):
+                sub_name = sub_category_data['name']
+                sub_category = SubCategory(sub_name, sub_category_data['type'])
+                main_category.sub_categories[row] = sub_category
+                
+                actions = sub_category_data['actions']
+                for action_data in actions:
+                    action = Action(action_data)
+                    sub_category.actions.append(action)
+        self._data = data
+        return data
+    
+    def save(self):
+        Kikka.memory.updateAction(self._data)
+
+    @staticmethod
+    def createAction(name='',
+                     type='',
+                     path='',
+                     icon='',
+                     param='',
+                     work_dir='',
+                     run_as_admin=False,
+                     open_as_private=False,
+                     color='',
+                     shortcut='',
+                     remark=''):
+        data = [name, type, path, icon, param, work_dir, run_as_admin, open_as_private, color, shortcut, remark]
+        action = Action(data)
+        return action
+        
+    def addAction(self, action, column, row):
+        self._data[column].sub_categories[row].actions.append(action)
+        self.save()
+    
+    # region Action Operation
+    
+    def actionOperation(self, operation, param):
+        if operation == EActionOperation.RunAction:
+            self._runAction(param['action'])
+        elif operation == EActionOperation.NewAction:
+            self._newAction(param['filepath'], param['column'], param['row'])
+    
+    def _runAction(self, action, run_as_admin=False, run_for_new_console=False):
+        run_as_admin = run_as_admin or action.run_as_admin
+        
+        if run_as_admin:
+            elevate.elevate()
+        
+        cmd = action.path + " " + action.param
+        work_dir = action.work_dir
+        try:
+            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=work_dir, shell=True)
+            if proc.returncode != 0:
+                print(proc.stderr)
+        except Exception as e:
+            print(e)
+        pass
+    
+    def _newAction(self, filepath, column, row):
+        file_info = QFileInfo(filepath)
+        ext = file_info.suffix().lower()
+        if ext in ["exe", "bat", "cmd", "ps1", "sh"]:
+            # executable
+            action = self.createAction(
+                type="executable",
+                name=file_info.baseName(),
+                path=file_info.filePath(),
+                work_dir=file_info.path(),
+            )
+            KikkaAction().addAction(action, column, row)
+        pass
+    
+    # endregion
+        
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    

+ 8 - 0
Kikka/KikkaConfig.py

@@ -0,0 +1,8 @@
+import os
+
+from Kikka.Utils.Singleton import singleton
+
+@singleton
+class KikkaConfig:
+    def __init__(self):
+        pass

+ 40 - 0
Kikka/KikkaConst.py

@@ -0,0 +1,40 @@
+import os
+from enum import Enum, auto
+
+def _getHomePath():
+    workDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
+    return os.path.normpath( os.path.abspath(workDir))
+
+HOME_PATH = _getHomePath()
+CACHE_PATH = os.path.join(HOME_PATH, ".cache")
+RESOURCES_PATH = os.path.join(HOME_PATH, "Resources")
+
+# memory
+MEMORY_FILE = os.path.join(HOME_PATH, "memory.json")
+
+# icon
+CACHE_ICON_PATH = os.path.join(CACHE_PATH, "icons")
+ICON_CACHE_FILE = os.path.join(CACHE_ICON_PATH, "icon_cache.json")
+
+# theme
+DEFAULT_THEME = "Default"
+THEME_PATH = os.path.join(RESOURCES_PATH, "Theme")
+DEFAULT_THEME_PATH = os.path.join(THEME_PATH, DEFAULT_THEME)
+
+
+class EActionOperation(Enum):
+    RunAction = auto()
+    NewAction = auto()
+
+
+class EShellOperation(Enum):
+    MouseMove = auto()
+    MousePress = auto()
+    MouseRelease = auto()
+
+
+
+
+
+
+

+ 105 - 0
Kikka/KikkaCore.py

@@ -0,0 +1,105 @@
+import os
+import sys
+import json
+
+from PySide6.QtWidgets import QApplication
+
+import Kikka
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+from Kikka.KikkaMemory import KikkaMemory
+from Kikka.KikkaConfig import KikkaConfig
+from Kikka.KikkaStyle import KikkaStyle
+from Kikka.Widgets.MainWindow import MainWindow
+from Kikka.KikkaAction import KikkaAction
+from Kikka.Widgets.Shell.KikkaShell import KikkaShell
+
+
+@singleton
+class KikkaCore():
+    def __init__(self):
+        self._memory = KikkaMemory()
+        self._config = KikkaConfig()
+        self._style = KikkaStyle()
+        self._action = KikkaAction()
+        self._app = None
+        self._mainWindow = None
+        self._shell = None
+        
+    # region property
+    @property
+    def memory(self):
+        return self._memory
+    
+    @property
+    def config(self):
+        return self._config
+    
+    @property
+    def style(self):
+        return self._style
+    
+    @property
+    def app(self):
+        return self._app
+    
+    @property
+    def action(self):
+        return self._action
+    
+    @property
+    def mainWindow(self):
+        return self._mainWindow
+    
+    @property
+    def shell(self):
+        return self._shell
+    
+    
+    # endregion
+    
+    # region init
+    
+    def init(self):
+        self.memory.awake()
+        self.action.load()
+
+        self._app = QApplication(sys.argv)
+        Kikka.app = self.app
+        
+        self.style.switchTheme(DEFAULT_THEME)
+        
+        self._mainWindow = MainWindow()
+        Kikka.mainWindow = self.mainWindow
+        self.mainWindow.initData(self.action.data)
+        self.mainWindow.setPage(0, 0)
+        
+        self._shell = KikkaShell()
+        self.shell.loadShell({"shell_type": "Image"})
+        Kikka.shell = self.shell
+
+        self.connect_slot()
+
+    def connect_slot(self):
+        self.mainWindow.SIGNAL_OPERATION.connect(KikkaAction().actionOperation)
+        self.mainWindow.SIGNAL_OPERATION.connect(self.onMainWindowOperation)
+    
+    def initialized(self):
+        self.mainWindow.show()
+        self.shell.show()
+    
+    # endregion
+
+    def onMainWindowOperation(self, operation, param):
+        pass
+
+    
+
+
+
+
+
+
+
+
+

+ 44 - 0
Kikka/KikkaMemory.py

@@ -0,0 +1,44 @@
+import os
+import json
+
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+
+class Storageable():
+    def toDict(self):
+        raise NotImplementedError
+
+
+@singleton
+class KikkaMemory():
+    def __init__(self):
+        self._memory = None
+
+    def awake(self):
+        if os.path.exists(MEMORY_FILE):
+            with open(MEMORY_FILE, "r", encoding="utf8") as fp:
+                self._memory = json.load(fp)
+    
+    def storage(self):
+        def data2dict(obj):
+            if isinstance(obj, Storageable):
+                return obj.toDict()
+        
+        json_str = json.dumps(self._memory, default=data2dict, ensure_ascii=False, indent=4)
+        with open(MEMORY_FILE, mode="w", newline="", encoding="utf8") as fp:
+            fp.write(json_str)
+    
+    def getAction(self):
+        return self._memory.get("action", {})
+    
+    def updateAction(self, action_data):
+        self._memory["action"] = action_data
+        self.storage()
+    
+    def getConfig(self):
+        return self._memory.get("config", {})
+    
+    def updateConfig(self, config_data):
+        self._memory["config"] = config_data
+        self.storage()

+ 66 - 0
Kikka/KikkaStyle.py

@@ -0,0 +1,66 @@
+import os
+import logging
+from PySide6.QtGui import QIcon, QImage
+
+import Kikka
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+def getDefaultImage():
+    return QImage(os.path.join(RESOURCES_PATH, "default.png"))
+
+@singleton
+class KikkaStyle:
+    
+    def __init__(self):
+        self._theme = None
+    
+    def getThemePath(self):
+        return os.path.join(THEME_PATH, self._theme)
+    
+    def switchTheme(self, theme=DEFAULT_THEME):
+        theme_path = os.path.join(THEME_PATH, theme)
+        theme_file = os.path.join(THEME_PATH, theme, "style_sheet.qss")
+        if not os.path.exists(theme_file):
+            # load default theme
+            theme = DEFAULT_THEME
+            theme_file = os.path.join(THEME_PATH, theme, "style_sheet.qss")
+        
+        with open(theme_file, "r", encoding="utf8") as fp:
+            content = fp.read()
+        
+        self._theme = theme
+        Kikka.app.setStyleSheet(content)
+        
+        file = os.path.join(theme_path, "menu_background.png")
+        if os.path.exists(file):
+            self.bg_image = QImage(file)
+        else:
+            self.bg_image = getDefaultImage()
+            logging.warning("Menu background image NOT found: %s" % file)
+
+        file = os.path.join(theme_path, "menu_foreground.png")
+        if os.path.exists(file):
+            self.fg_image = QImage(file)
+        else:
+            self.fg_image = getDefaultImage()
+            logging.warning("Menu foreground image NOT found: %s" % file)
+
+        file = os.path.join(theme_path, "menu_sidebar.png")
+        if os.path.exists(file):
+            self.side_image = QImage(file)
+        else:
+            self.side_image = getDefaultImage()
+            logging.warning("Menu sidebar image NOT found: %s" % file)
+        
+    
+    def getMenuBGImage(self):
+        return self.bg_image
+    
+    def getMenuFGImage(self):
+        return self.fg_image
+    
+    def getMenuSideImage(self):
+        return self.side_image
+    
+    

+ 100 - 0
Kikka/Utils/IconHelper.py

@@ -0,0 +1,100 @@
+import os
+import sys
+import json
+import hashlib
+from icoextract import IconExtractor
+
+from PySide6.QtCore import Qt, QFileInfo, QSize
+from PySide6.QtGui import QPixmap, QIcon, QImage
+from PySide6.QtWidgets import QFileIconProvider
+
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+
+@singleton
+class IconHelper():
+    
+    def __init__(self):
+        self._cache = {}
+        self._icons = {}
+        self.loadCache()
+        
+    def loadCache(self):
+        os.makedirs(CACHE_ICON_PATH, exist_ok=True)
+        if os.path.exists(ICON_CACHE_FILE):
+            with open(ICON_CACHE_FILE, 'r') as fp:
+                self._cache = json.load(fp)
+            
+    def saveCache(self):
+        with open(ICON_CACHE_FILE, 'w') as fp:
+             json.dump(self._cache, fp)
+    
+    def hash(self, text):
+        return hashlib.sha256(text.encode('utf-8')).hexdigest()
+    
+    def getIconByHash(self, hash):
+        if hash in self._cache.values():
+            if hash in self._icons:
+                return self._icons[hash]
+            else:
+                icon = QIcon(os.path.join(CACHE_ICON_PATH, hash + ".png"))
+                self._icons[hash] = icon
+                return icon
+        return None
+        
+    def getIconByFile(self, filepath):
+        hash = self.hash(filepath)
+        if filepath in self._cache:
+            if hash in self._icons:
+                return self._icons[hash]
+            else:
+                icon_path = os.path.join(CACHE_ICON_PATH, hash + ".png")
+                if os.path.exists(icon_path):
+                    icon = QIcon(icon_path)
+                    self._icons[hash] = icon
+                    return icon
+                
+        file = QFileInfo(filepath)
+        if not file.exists():
+            return None
+        
+        icon = None
+        if file.suffix().lower() in ["exe", "dll", "mun"]:
+            temp_icon = os.path.join(CACHE_ICON_PATH, "_temp.ico")
+            extractor = IconExtractor(file.filePath())
+            extractor.export_icon(temp_icon)
+            icon = QIcon(temp_icon)
+            available_sizes = icon.availableSizes()
+            if not available_sizes:
+                return None
+        else:
+            p = QFileIconProvider()
+            icon = p.icon(file)
+        
+        if icon is None or icon.isNull():
+            return None
+            
+        # for size in available_sizes:
+        #     pixmap = icon.pixmap(size)
+        #     name = "%dx%s.png" % (size.width(), size.height())
+        #     pixmap.save(os.path.join(CacheDir, name))
+        
+        actualSize = icon.actualSize(QSize(72, 72))
+        pixmap = icon.pixmap(actualSize)
+        pixmap.save(os.path.join(CACHE_ICON_PATH, hash + ".png"))
+        self._cache[filepath] = hash
+        return self.getIconByHash(hash)
+    
+
+if __name__ == "__main__":
+    from PySide6.QtWidgets import QApplication
+    app = QApplication(sys.argv)
+    
+    file = r"C:\example.exe"
+    # file = C:\example.svg"
+    # file = C:\example.lnk"
+    icon = IconHelper().getIconByFile(file)
+    pixmap = icon.pixmap(64, 64)
+    pixmap.save("icon.png")
+

+ 9 - 0
Kikka/Utils/Singleton.py

@@ -0,0 +1,9 @@
+
+
+def singleton(cls):
+    instances = {}
+    def get_instance(*args, **kwargs):
+        if cls not in instances:
+            instances[cls] = cls(*args, **kwargs)
+        return instances[cls]
+    return get_instance

+ 0 - 0
Kikka/Utils/__init__.py


+ 141 - 0
Kikka/Widgets/IconPanel.py

@@ -0,0 +1,141 @@
+import os
+
+from PySide6.QtCore import Qt, QSortFilterProxyModel, QSize, Signal
+from PySide6.QtWidgets import QTableView, QStyledItemDelegate, QHeaderView
+from PySide6.QtGui import QStandardItemModel, QStandardItem, QIcon
+
+from Kikka.KikkaConst import *
+from Kikka.Utils.IconHelper import IconHelper
+
+
+class IconPanelItem(QStandardItem):
+    pass
+    
+    
+class IconPanelModel(QStandardItemModel):
+    def __init__(self):
+        super().__init__()
+
+    def flags(self, index):
+        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsDragEnabled
+
+
+class IconPanelProxyModel(QSortFilterProxyModel):
+    pass
+
+
+class IconPanelStyledItemDelegate(QStyledItemDelegate):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+
+    def paint(self, painter, option, index):
+        icon = index.data(Qt.ItemDataRole.DecorationRole)
+        if icon:
+            rect = option.rect
+            icon.paint(painter, rect, Qt.AlignmentFlag.AlignCenter)
+
+    def sizeHint(self, option, index):
+        return QSize(64, 64)
+
+
+class IconPanel(QTableView):
+    SIGNAL_DROP = Signal(str)
+    SIGNAL_DOUBLE_CLICK = Signal(object)
+    
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self.setAcceptDrops(True)
+        self.initGUI()
+        
+        self.emptyIcon = QIcon(os.path.join(RESOURCES_PATH, "icon1.svg"))
+        self.doubleClicked.connect(self.onDoubleClick)
+
+    def initGUI(self):
+        self.setDragDropMode(QTableView.DragDropMode.InternalMove)
+        self.setSelectionMode(QTableView.SelectionMode.SingleSelection)
+        # self.setSelectionBehavior(QAbstractItemView.SelectItems)
+        # self.setGridStyle(Qt.DotLine)
+        self.setShowGrid(False)
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        # self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        self.verticalHeader().setVisible(False)
+        self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
+        self.horizontalHeader().setVisible(False)
+        self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
+
+    def setData(self, actions):
+        self._model = IconPanelModel()
+        self._items = []
+        self._model.setColumnCount(4)
+        for i, action in enumerate(actions):
+            item = IconPanelItem()
+            item.setText(action.name)
+            
+            icon_file = action.path
+            icon = IconHelper().getIconByFile(icon_file)
+            if icon is None:
+                icon = self.emptyIcon
+            item.setIcon(icon)
+            item.setData(action)
+            self._items.append(item)
+
+        self._proxyModel = IconPanelProxyModel()
+        self._proxyModel.setSourceModel(self._model)
+
+        self.setModel(self._proxyModel)
+        self.setItemDelegate(IconPanelStyledItemDelegate(self))
+        self.updateView()
+        IconHelper().saveCache()
+
+    def updateView(self):
+        columnCount = max(self.width() // 64, 3)
+        rowCount = max(len(self._items) // 64, 3)
+        self.setIconSize(QSize(64, 64))
+        self.horizontalHeader().setDefaultSectionSize(72)
+        self.verticalHeader().setDefaultSectionSize(72)
+
+        self._model.clear()
+        self._model.setColumnCount(columnCount)
+        for i in range(columnCount):
+            self.setColumnWidth(i, 64)
+        for i in range(rowCount):
+            self.setRowHeight(i, 64)
+
+        for i, item in enumerate(self._items):
+            self._model.setItem(i // columnCount, i % columnCount, item)
+        
+    def resizeEvent(self, event):
+        super().resizeEvent(event)
+        self.updateView()
+
+    def dragEnterEvent(self, event):
+        mimeData = event.mimeData()
+        if mimeData.hasUrls():
+            event.acceptProposedAction()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        mimeData = event.mimeData()
+        if mimeData.hasUrls():
+            event.acceptProposedAction()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        mimeData = event.mimeData()
+        if mimeData.hasUrls():
+            for url in mimeData.urls():
+                file_path = url.toLocalFile()
+                self.SIGNAL_DROP.emit(file_path)
+        event.acceptProposedAction()
+
+    def onDoubleClick(self, index):
+        if index.isValid():
+            item = self._model.item(index.row(), index.column())
+            action = item.data()
+            self.SIGNAL_DOUBLE_CLICK.emit(action)
+    
+    def mousePressEvent(self, event):
+        return super().mousePressEvent(event)
+    

+ 622 - 0
Kikka/Widgets/KMenu.py

@@ -0,0 +1,622 @@
+import os
+import logging
+
+from PySide6.QtCore import QRect, QSize, Qt, QRectF, QPoint, QEvent, Property
+from PySide6.QtWidgets import QMenu, QStyle, QStyleOptionMenuItem, QStyleOption, QWidget, QApplication
+from PySide6.QtWidgets import QToolTip
+from PySide6.QtGui import QIcon, QImage, QPainter, QFont, QPalette, QColor, QKeySequence, QAction, QActionGroup
+
+
+import Kikka
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+
+
+class KMenu(QMenu):
+    
+    def __init__(self, title='', parent=None):
+        QMenu.__init__(self, title, parent)
+        self._parent = parent
+        self._aRect = {}
+        self._bg_image = None
+        self._fg_image = None
+        self._side_image = None
+        self._hover_color = None
+
+        self.installEventFilter(self)
+        self.setMouseTracking(True)
+        self.setSeparatorsCollapsible(False)
+        
+    def getHoverColor(self):
+        return self._hover_color
+
+    def setHoverColor(self, color):
+        if isinstance(color, QColor):
+            self._hover_color = color
+            
+    hoverColor = Property(QColor, fget=getHoverColor, fset=setHoverColor)
+
+    def addMenuItem(self, text, callbackfunc=None, iconfilepath=None, group=None):
+        if iconfilepath is None:
+            act = QAction(text, self._parent)
+        elif os.path.exists(iconfilepath):
+            act = QAction(QIcon(iconfilepath), text, self._parent)
+        else:
+            logging.info("fail to add menu item")
+            return
+
+        if callbackfunc is not None:
+            act.triggered.connect(callbackfunc)
+
+        if group is None:
+            self.addAction(act)
+        else:
+            self.addAction(group.addAction(act))
+
+        return act
+
+    def addSubMenu(self, menu):
+        act = self.addMenu(menu)
+        return act
+
+    def getSubMenu(self, menuName):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() == menuName:
+                return act.menu()
+        return None
+
+    def getAction(self, actionName):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() == actionName:
+                return act
+        return None
+
+    def getActionByData(self, data):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.data() == data:
+                return act
+        return None
+
+    def checkAction(self, actionName, isChecked):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() != actionName:
+                continue
+
+            act.setChecked(isChecked)
+        pass
+
+    def setPosition(self, pos):
+        rect = QApplication.instance().primaryScreen().geometry()
+        w = rect.width()
+        h = rect.height()
+        if pos.y() + self.height() > h: pos.setY(h - self.height())
+        if pos.y() < 0: pos.setY(0)
+
+        if pos.x() + self.width() > w: pos.setX(w - self.width())
+        if pos.x() < 0: pos.setX(0)
+        self.move(pos)
+
+    def updateActionRect(self):
+        """
+        void QMenuPrivate::updateActionRects(const QRect &screen) const
+        https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/da/d61/class_q_menu_private.html#acf93cda3ebe88b1234dc519c5f1b0f5d
+        """
+        self._aRect = {}
+        topmargin = 0
+        leftmargin = 0
+        rightmargin = 0
+
+        # qmenu.cpp Line 259:
+        # init
+        max_column_width = 0
+        dh = self.height()
+        y = 0
+        style = self.style()
+        opt = QStyleOption()
+        opt.initFrom(self)
+        hmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuHMargin, opt, self)
+        vmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuVMargin, opt, self)
+        icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
+        fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
+        deskFw = style.pixelMetric(QStyle.PixelMetric.PM_MenuDesktopFrameWidth, opt, self)
+        tearoffHeight = style.pixelMetric(QStyle.PixelMetric.PM_MenuTearoffHeight, opt, self) if self.isTearOffEnabled() else 0
+
+        # for compatibility now - will have to refactor this away
+        tabWidth = 0
+        maxIconWidth = 0
+        hasCheckableItems = False
+        # ncols = 1
+        # sloppyAction = 0
+
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.isSeparator() or act.isVisible() is False:
+                continue
+            # ..and some members
+            hasCheckableItems |= act.isCheckable()
+            ic = act.icon()
+            if ic.isNull() is False:
+                maxIconWidth = max(maxIconWidth, icone + 4)
+
+        # qmenu.cpp Line 291:
+        # calculate size
+        qfm = self.fontMetrics()
+        previousWasSeparator = True  # this is true to allow removing the leading separators
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+
+            if act.isVisible() is False \
+            or (self.separatorsCollapsible() and previousWasSeparator and act.isSeparator()):
+                # we continue, this action will get an empty QRect
+                self._aRect[i] = QRect()
+                continue
+
+            previousWasSeparator = act.isSeparator()
+
+            # let the style modify the above size..
+            opt = QStyleOptionMenuItem()
+            self.initStyleOption(opt, act)
+            fm = opt.fontMetrics
+
+            sz = QSize()
+            # sz = self.sizeHint().expandedTo(self.minimumSize()).expandedTo(self.minimumSizeHint()).boundedTo(self.maximumSize())
+            # calc what I think the size is..
+            if act.isSeparator():
+                sz = QSize(2, 2)
+            else:
+                s = act.text()
+                if '\t' in s:
+                    t = s.index('\t')
+                    act.setText(s[t + 1:])
+                    tabWidth = max(int(tabWidth), qfm.boundingRect(s[t + 1:]).width())
+                else:
+                    seq = act.shortcut()
+                    if seq.isEmpty() is False:
+                        tabWidth = max(int(tabWidth), qfm.boundingRect(seq.toString()).width())
+
+                sz.setWidth(fm.boundingRect(QRect(), Qt.TextFlag.TextSingleLine | Qt.TextFlag.TextShowMnemonic, s).width())
+                sz.setHeight(fm.height())
+
+                if not act.icon().isNull():
+                    is_sz = QSize(icone, icone)
+                    if is_sz.height() > sz.height():
+                        sz.setHeight(is_sz.height())
+
+            sz = style.sizeFromContents(QStyle.ContentsType.CT_MenuItem, opt, sz, self)
+
+            if sz.isEmpty() is False:
+                max_column_width = max(max_column_width, sz.width())
+                # wrapping
+                if y + sz.height() + vmargin > dh - deskFw * 2:
+                    # ncols += 1
+                    y = vmargin
+                y += sz.height()
+                # update the item
+                self._aRect[i] = QRect(0, 0, sz.width(), sz.height())
+        pass  # exit for
+
+        max_column_width += tabWidth  # finally add in the tab width
+        sfcMargin = style.sizeFromContents(QStyle.ContentsType.CT_Menu, opt, QSize(0, 0), self).width()
+        min_column_width = self.minimumWidth() - (sfcMargin + leftmargin + rightmargin + 2 * (fw + hmargin))
+        max_column_width = max(min_column_width, max_column_width)
+
+        # qmenu.cpp Line 259:
+        # calculate position
+        base_y = vmargin + fw + topmargin + tearoffHeight
+        x = hmargin + fw + leftmargin
+        y = base_y
+
+        for i in range(len(self.actions())):
+            if self._aRect[i].isNull():
+                continue
+            if y + self._aRect[i].height() > dh - deskFw * 2:
+                x += max_column_width + hmargin
+                y = base_y
+
+            self._aRect[i].translate(x, y)  # move
+            self._aRect[i].setWidth(max_column_width)  # uniform width
+            y += self._aRect[i].height()
+
+        # update menu size
+        s = self.sizeHint()
+        self.resize(s)
+    
+    def getPenColor(self, opt):
+        # if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
+        #     color = self.separator_color
+
+        if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
+            if self._hover_color:
+                color = self._hover_color
+            else:
+                color = opt.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText)
+        elif not (opt.state & QStyle.StateFlag.State_Enabled):
+            color = opt.palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text)
+        else:
+            color = opt.palette.color(QPalette.ColorGroup.Normal, QPalette.ColorRole.Text)
+        return color
+    
+    def drawControl(self, p, opt, arect, icon):
+        """
+        due to overrides the "paintEvent" method, so we must repaint all menu item by self.
+        luckly, we have qt source code to reference.
+        
+        File: qtbase\src\widgets\styles\qstylesheetstyle.cpp
+        Function: void drawControl (ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w=0) const
+        """
+        
+        # Line 3891: case CE_MenuItem:
+        style = self.style()
+        p.setPen(self.getPenColor(opt))
+
+        # Line 3932: draw icon and checked sign
+        checkable = opt.checkType != QStyleOptionMenuItem.CheckType.NotCheckable
+        checked = opt.checked if checkable else False
+        if opt.icon.isNull() is False:  # has custom icon
+            dis = not (opt.state & QStyle.StateFlag.State_Enabled)
+            active = opt.state & QStyle.StateFlag.State_Selected
+            mode = QIcon.Mode.Disabled if dis else QIcon.Mode.Normal
+            if active != 0 and not dis:
+                mode = QIcon.Mode.Active
+
+            fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
+            icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
+            iconRect = QRectF(arect.x() - fw, arect.y(), self._side_image.width(), arect.height())
+            if checked:
+                pixmap = icon.pixmap(QSize(icone, icone), mode, QIcon.State.On)
+            else:
+                pixmap = icon.pixmap(QSize(icone, icone), mode)
+
+            pixw = pixmap.width()
+            pixh = pixmap.height()
+            pmr = QRectF(0, 0, pixw, pixh)
+            pmr.moveCenter(iconRect.center())
+
+            if checked:
+                p.drawRect(QRectF(pmr.x() - 1, pmr.y() - 1, pixw + 2, pixh + 2))
+            p.drawPixmap(pmr.topLeft(), pixmap)
+
+        elif checkable and checked:  # draw default checked sign
+            opt.rect = QRect(0, arect.y(), self._side_image.width(), arect.height())
+            opt.palette.setColor(QPalette.ColorRole.Text, self.getPenColor(opt))
+            style.drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opt, p, self)
+
+        # Line 3952: draw menu text
+        p.setFont(opt.font)
+        text_flag = Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextShowMnemonic | Qt.TextFlag.TextDontClip | Qt.TextFlag.TextSingleLine
+        tr = QRect(arect)
+        s = opt.text
+        if '\t' in s:
+            ss = s[s.index('\t') + 1:]
+            fontwidth = opt.fontMetrics.boundingRect(ss).width()
+            tr.moveLeft(opt.rect.right() - fontwidth)
+            tr = QStyle.visualRect(opt.direction, opt.rect, tr)
+            p.drawText(tr, text_flag, ss)
+        tr.moveLeft(self._side_image.width() + arect.x())
+        tr = QStyle.visualRect(opt.direction, opt.rect, tr)
+        p.drawText(tr, text_flag, s)
+
+        # Line 3973: draw sub menu arrow
+        if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.SubMenu:
+            arrowW = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorWidth, opt, self)
+            arrowH = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorHeight, opt, self)
+            arrowRect = QRect(0, 0, arrowW, arrowH)
+            arrowRect.moveBottomRight(arect.bottomRight())
+            arrow = QStyle.PrimitiveElement.PE_IndicatorArrowLeft if opt.direction == Qt.LayoutDirection.RightToLeft else QStyle.PrimitiveElement.PE_IndicatorArrowRight
+
+            opt.rect = arrowRect
+            opt.palette.setColor(QPalette.ColorRole.ButtonText, self.getPenColor(opt))
+            style.drawPrimitive(arrow, opt, p, self)
+        pass
+
+    def paintEvent(self, event):
+        # init
+        self._bg_image = Kikka.style.getMenuBGImage()
+        self._fg_image = Kikka.style.getMenuFGImage()
+        self._side_image = Kikka.style.getMenuSideImage()
+        self.updateActionRect()
+        p = QPainter(self)
+
+        # draw background
+        p.fillRect(QRect(QPoint(), self.size()), self._side_image.pixelColor(0, 0))
+        vertical = False
+        y = self.height()
+        while y > 0:
+            yy = y - self._bg_image.height()
+            p.drawImage(0, yy, self._side_image.mirrored(False, vertical))
+            x = self._side_image.width()
+            while x < self.width():
+                p.drawImage(x, yy, self._bg_image.mirrored(False, vertical))
+                x += self._bg_image.width()
+                p.drawImage(x, yy, self._bg_image.mirrored(True, vertical))
+                x += self._bg_image.width() + 1
+            y -= self._bg_image.height()
+            vertical = not vertical
+
+        # draw item
+        actioncount = len(self.actions())
+        for i in range(actioncount):
+            act = self.actions()[i]
+            arect = QRect(self._aRect[i])
+            if event.rect().intersects(arect) is False:
+                continue
+
+            opt = QStyleOptionMenuItem()
+            self.initStyleOption(opt, act)
+            opt.rect = arect
+            if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
+                # Selected Item, draw foreground image
+                p.setClipping(True)
+                p.setClipRect(arect.x() + self._side_image.width(), arect.y(), self.width() - self._side_image.width(),
+                              arect.height())
+
+                p.fillRect(QRect(QPoint(), self.size()), self._fg_image.pixelColor(0, 0))
+                vertical = False
+                y = self.height()
+                while y > 0:
+                    x = self._side_image.width()
+                    while x < self.width():
+                        yy = y - self._fg_image.height()
+                        p.drawImage(x, yy, self._fg_image.mirrored(False, vertical))
+                        x += self._fg_image.width()
+                        p.drawImage(x, yy, self._fg_image.mirrored(True, vertical))
+                        x += self._fg_image.width() + 1
+                    y -= self._fg_image.height()
+                    vertical = not vertical
+                p.setClipping(False)
+
+            if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
+                # Separator
+                # p.setPen(opt.palette.color(QPalette.ColorRole.Text))
+                y = int(arect.y() + arect.height() / 2)
+                p.drawLine(self._side_image.width(), y, arect.width(), y)
+            else:
+                # MenuItem
+                self.drawControl(p, opt, arect, act.icon())
+        pass  # exit for
+
+    def eventFilter(self, obj, event):
+        if obj == self:
+            if event.type() == QEvent.Type.WindowDeactivate:
+                self.Hide()
+            elif event.type() == QEvent.Type.ToolTip:
+                act = self.activeAction()
+                if act != 0 and act.toolTip() != act.text():
+                    QToolTip.showText(event.globalPos(), act.toolTip())
+                else:
+                    QToolTip.hideText()
+        return False
+
+
+def _createTestMenu(parent=None):
+    # test callback function
+    def _test_callback(index=0, title=''):
+        logging.info("MainMenu_callback: click [%d] %s" % (index, title))
+
+    def _test_Exit(testmenu):
+        # from kikka_app import KikkaApp
+        callbackfunc = lambda: print("Exit!!")
+        testmenu.addMenuItem("Exit", callbackfunc)
+
+    def _test_MenuItemState(testmenu):
+        menu = KMenu("MenuItem State", testmenu)
+        c = 16
+        for i in range(c):
+            text = str("%s-item%d" % (menu.title(), i))
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            act = menu.addMenuItem(text, callbackfunc)
+
+            if i >= c / 2:
+                act.setDisabled(True)
+                act.setText("%s-disable" % act.text())
+            if i % 8 >= c / 4:
+                act.setIcon(icon)
+                act.setText("%s-icon" % act.text())
+            if i % 4 >= c / 8:
+                act.setCheckable(True)
+                act.setText("%s-ckeckable" % act.text())
+            if i % 2 >= c / 16:
+                act.setChecked(True)
+                act.setText("%s-checked" % act.text())
+        testmenu.addSubMenu(menu)
+
+    def _test_Shortcut(testmenu):
+        menu = KMenu("Shortcut", testmenu)
+
+        c = 4
+        for i in range(c):
+            text = str("%s-item" % (str(chr(ord('A') + i))))
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            act = menu.addMenuItem(text, callbackfunc)
+
+            if i == 0:
+                act.setShortcut(QKeySequence("Ctrl+T"))
+                act.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
+                act.setShortcutVisibleInContextMenu(True)
+        testmenu.addSubMenu(menu)
+        pass
+
+    def _test_StatusTip(testmenu):
+        pass
+
+    def _test_Separator(testmenu):
+        menu = KMenu("Separator", testmenu)
+        menu.addSeparator()
+        c = 5
+        for i in range(c):
+            text = str("%s-item%d" % (menu.title(), i))
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            menu.addMenuItem(text, callbackfunc)
+            for j in range(i + 2): menu.addSeparator()
+        testmenu.addSubMenu(menu)
+
+    def _test_MultipleItem(testmenu):
+        menu = KMenu("Multiple item", testmenu)
+        for i in range(100):
+            text = str("%s-item%d" % (menu.title(), i))
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            menu.addMenuItem(text, callbackfunc)
+        testmenu.addSubMenu(menu)
+
+    def _test_LongTextItem(testmenu):
+        menu = KMenu("Long text item", testmenu)
+        for i in range(5):
+            text = str("%s-item%d " % (menu.title(), i)) * 20
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            menu.addMenuItem(text, callbackfunc)
+        testmenu.addSubMenu(menu)
+
+    def _test_LargeMenu(testmenu):
+        menu = KMenu("Large menu", testmenu)
+        for i in range(60):
+            text = str("%s-item%d " % (menu.title(), i)) * 10
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            menu.addMenuItem(text, callbackfunc)
+            if i % 5 == 0: menu.addSeparator()
+        testmenu.addSubMenu(menu)
+
+    def _test_LimitTest(testmenu):
+        menu = KMenu("LimitTest", testmenu)
+        _test_LargeMenu(menu)
+        _test_MultipleItem(menu)
+        _test_LongTextItem(menu)
+        testmenu.addSubMenu(menu)
+
+    def _test_Submenu(testmenu):
+        menu = KMenu("Submenu", testmenu)
+        testmenu.addSubMenu(menu)
+
+        submenu = KMenu("submenu1", menu)
+        menu.addSubMenu(submenu)
+        m = submenu
+        for i in range(8):
+            next = KMenu("submenu%d" % (i + 2), testmenu)
+            m.addSubMenu(next)
+            m = next
+
+        submenu = KMenu("submenu2", menu)
+        menu.addSubMenu(submenu)
+        m = submenu
+        for i in range(8):
+            for j in range(10):
+                text = str("%s-item%d" % (m.title(), j))
+                callbackfunc = lambda checked, a=j, b=text: _test_callback(a, b)
+                m.addMenuItem(text, callbackfunc)
+            next = KMenu("submenu%d" % (i + 2), testmenu)
+            m.addSubMenu(next)
+            m = next
+
+        submenu = KMenu("SubMenu State", testmenu)
+        c = 16
+        for i in range(c):
+            text = str("%s-%d" % (submenu.title(), i))
+            m = KMenu(text, submenu)
+            act = submenu.addSubMenu(m)
+            callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+            act.triggered.connect(callbackfunc)
+            if i >= c / 2:
+                act.setDisabled(True)
+                act.setText("%s-disable" % act.text())
+            if i % 8 >= c / 4:
+                act.setIcon(icon)
+                act.setText("%s-icon" % act.text())
+            if i % 4 >= c / 8:
+                act.setCheckable(True)
+                act.setText("%s-ckeckable" % act.text())
+            if i % 2 >= c / 16:
+                act.setChecked(True)
+                act.setText("%s-checked" % act.text())
+            submenu.addSubMenu(m)
+        menu.addSubMenu(submenu)
+
+    def _test_ImageTest(testmenu):
+        imagetestmenu = KMenu("ImageTest", testmenu)
+        testmenu.addSubMenu(imagetestmenu)
+
+        menu = KMenu("MenuImage-normal", imagetestmenu)
+        for i in range(32):
+            text = " " * 54
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-bit", imagetestmenu)
+        menu.addMenuItem('')
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-small", imagetestmenu)
+        for i in range(10):
+            text = " " * 30
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-long", imagetestmenu)
+        for i in range(64):
+            text = " " * 54
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-long2", imagetestmenu)
+        for i in range(32):
+            text = " " * 30
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-large", imagetestmenu)
+        for i in range(64):
+            text = " " * 300
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+        menu = KMenu("MenuImage-verylarge", imagetestmenu)
+        for i in range(100):
+            text = " " * 600
+            menu.addMenuItem(text)
+        imagetestmenu.addSubMenu(menu)
+
+    if parent is None:
+        parent = QWidget(f=Qt.WindowType.Dialog)
+    icon = QIcon(r"icon.ico")
+    menu_test = KMenu("TestMenu", parent)
+
+    _test_Exit(menu_test)
+    menu_test.addSeparator()
+    _test_MenuItemState(menu_test)
+    _test_Shortcut(menu_test)
+    _test_StatusTip(menu_test)
+    _test_Separator(menu_test)
+    _test_LimitTest(menu_test)
+    _test_Submenu(menu_test)
+    menu_test.addSeparator()
+    _test_ImageTest(menu_test)
+    menu_test.addSeparator()
+    _test_Exit(menu_test)
+
+    return menu_test
+
+
+if __name__ == '__main__':
+
+    menu = _createTestMenu()
+    menu.setPosition(QPoint(100, 100))
+    menu.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 877 - 0
Kikka/Widgets/KikkaMenu.py

@@ -0,0 +1,877 @@
+import os
+import logging
+
+from PySide6.QtCore import QRect, QSize, Qt, QRectF, QPoint, QEvent
+from PySide6.QtWidgets import QMenu, QStyle, QStyleOptionMenuItem, QStyleOption, QWidget, QApplication
+from PySide6.QtWidgets import QToolTip
+from PySide6.QtGui import QIcon, QImage, QPainter, QFont, QPalette, QColor, QKeySequence, QAction, QActionGroup
+
+
+# import kikka
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+
+@singleton
+class KikkaMenu:
+    _instance = None
+    isDebug = False
+
+    @staticmethod
+    def getMenu(ghost_id, soul_id):
+        pass
+        # ghost = kikka.core.getGhost(ghost_id)
+        # if ghost is not None:
+        #     return ghost.getSoul(soul_id).getMenu()
+        # else:
+        #     logging.warning('menu lost')
+        #     return None
+
+    @staticmethod
+    def setAppMenu(menu):
+        # QApplication.instance().trayIcon.setContextMenu(None)
+        # QApplication.instance().trayIcon.setContextMenu(menu)
+        pass
+
+    @staticmethod
+    def createSoulMainMenu(ghost):
+        return
+        # import kikka
+        #
+        # parent = QWidget(f=Qt.WindowType.Dialog)
+        # mainmenu = KMenu(parent, ghost.ID, "Main")
+        #
+        # # shell list
+        # menu = KMenu(mainmenu, ghost.ID, "Shells")
+        # group1 = QActionGroup(parent)
+        # for i in range(kikka.shell.getShellCount()):
+        #     shell = kikka.shell.getShellByIndex(i)
+        #     callbackfunc = lambda checked, name=shell.name: ghost.changeShell(name)
+        #     act = menu.addMenuItem(shell.unicode_name, callbackfunc, None, group1)
+        #     act.setData(shell.name)
+        #     act.setCheckable(True)
+        #     act.setToolTip(shell.description)
+        # menu.setToolTipsVisible(True)
+        # mainmenu.addSubMenu(menu)
+        #
+        # # clothes list
+        # menu = KMenu(mainmenu, ghost.ID, "Clothes")
+        # menu.setEnabled(False)
+        # mainmenu.addSubMenu(menu)
+        #
+        # # balloon list
+        # menu = KMenu(mainmenu, ghost.ID, "Balloons")
+        # group2 = QActionGroup(parent)
+        # for i in range(kikka.balloon.getBalloonCount()):
+        #     balloon = kikka.balloon.getBalloonByIndex(i)
+        #     callbackfunc = lambda checked, name=balloon.name: ghost.setBalloon(name)
+        #     act = menu.addMenuItem(balloon.unicode_name, callbackfunc, None, group2)
+        #     act.setCheckable(True)
+        # mainmenu.addSubMenu(menu)
+        #
+        # optionmenu = KikkaMenu.createOptionMenu(parent, ghost)
+        # mainmenu.addSubMenu(optionmenu)
+        #
+        # # debug option
+        # if kikka.core.isDebug is True:
+        #
+        #     def callbackfunction1():
+        #         kikka.core.isDebug = not kikka.core.isDebug
+        #         kikka.core.repaintAllGhost()
+        #
+        #     def callbackfunction2():
+        #         kikka.shell.isDebug = not kikka.shell.isDebug
+        #         kikka.core.repaintAllGhost()
+        #
+        #     menu = KMenu(mainmenu, ghost.ID, "Debug")
+        #
+        #     act = menu.addMenuItem("Show ghost data", callbackfunction1)
+        #     act.setCheckable(True)
+        #     act.setChecked(kikka.core.isDebug is True)
+        #
+        #     act = menu.addMenuItem("Show shell frame", callbackfunction2)
+        #     act.setCheckable(True)
+        #     act.setChecked(kikka.shell.isDebug is True)
+        #
+        #     menu.addSubMenu(KMenu(menu, ghost.ID, "TestSurface"))
+        #     menu.addSubMenu(KikkaMenu.createTestMenu(menu))
+        #
+        #     mainmenu.addSeparator()
+        #     mainmenu.addSubMenu(menu)
+        # pass
+        #
+        # from kikka_app import KikkaApp
+        # callbackfunc = lambda: KikkaApp.this().exitApp()
+        # mainmenu.addSeparator()
+        # mainmenu.addMenuItem("Exit", callbackfunc)
+        # return mainmenu
+
+    @staticmethod
+    def createSoulDefaultMenu(ghost):
+        import kikka
+
+        parent = QWidget(f=Qt.WindowType.Dialog)
+        mainmenu = KMenu(parent, ghost.ID, "Main")
+
+        # shell list
+        menu = KMenu(mainmenu, ghost.ID, "Shells")
+        group1 = QActionGroup(parent)
+        for i in range(kikka.shell.getShellCount()):
+            shell = kikka.shell.getShellByIndex(i)
+            callbackfunc = lambda checked, name=shell.name: ghost.changeShell(name)
+            act = menu.addMenuItem(shell.unicode_name, callbackfunc, None, group1)
+            act.setData(shell.name)
+            act.setCheckable(True)
+        mainmenu.addSubMenu(menu)
+
+        # clothes list
+        menu = KMenu(mainmenu, ghost.ID, "Clothes")
+        menu.setEnabled(False)
+        mainmenu.addSubMenu(menu)
+
+        from kikka_app import KikkaApp
+        callbackfunc = lambda: KikkaApp.this().exitApp()
+        mainmenu.addMenuItem("Exit", callbackfunc)
+
+        return mainmenu
+
+    @staticmethod
+    def createOptionMenu(parent, ghost):
+        optionmenu = KMenu(parent, ghost.ID, "Option")
+
+        callbackfunc1 = lambda checked: ghost.resetWindowsPosition(True, False)
+        optionmenu.addMenuItem("Reset Shell Position", callbackfunc1)
+
+        callbackfunc2 = lambda checked, g=ghost: g.setIsLockOnTaskbar(checked)
+        act = optionmenu.addMenuItem("Lock on taskbar", callbackfunc2)
+        act.setCheckable(True)
+        act.setChecked(ghost.getIsLockOnTaskbar())
+
+        return optionmenu
+
+    # ###########################################################################
+    @staticmethod
+    def createTestMenu(parent=None):
+        # test callback function
+        def _test_callback(index=0, title=''):
+            logging.info("MainMenu_callback: click [%d] %s" % (index, title))
+
+        def _test_Exit(testmenu):
+            # from kikka_app import KikkaApp
+            callbackfunc = lambda: print("Exit!!")
+            testmenu.addMenuItem("Exit", callbackfunc)
+
+        def _test_MenuItemState(testmenu):
+            menu = KMenu(testmenu, 0, "MenuItem State")
+            c = 16
+            for i in range(c):
+                text = str("%s-item%d" % (menu.title(), i))
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                act = menu.addMenuItem(text, callbackfunc)
+
+                if i >= c / 2:
+                    act.setDisabled(True)
+                    act.setText("%s-disable" % act.text())
+                if i % 8 >= c / 4:
+                    act.setIcon(icon)
+                    act.setText("%s-icon" % act.text())
+                if i % 4 >= c / 8:
+                    act.setCheckable(True)
+                    act.setText("%s-ckeckable" % act.text())
+                if i % 2 >= c / 16:
+                    act.setChecked(True)
+                    act.setText("%s-checked" % act.text())
+            testmenu.addSubMenu(menu)
+
+        def _test_Shortcut(testmenu):
+            menu = KMenu(testmenu, 0, "Shortcut")
+
+            c = 4
+            for i in range(c):
+                text = str("%s-item" % (str(chr(ord('A') + i))))
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                act = menu.addMenuItem(text, callbackfunc)
+
+                if i == 0:
+                    act.setShortcut(QKeySequence("Ctrl+T"))
+                    act.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
+                    act.setShortcutVisibleInContextMenu(True)
+            testmenu.addSubMenu(menu)
+            pass
+
+        def _test_StatusTip(testmenu):
+            pass
+
+        def _test_Separator(testmenu):
+            menu = KMenu(testmenu, 0, "Separator")
+            menu.addSeparator()
+            c = 5
+            for i in range(c):
+                text = str("%s-item%d" % (menu.title(), i))
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                menu.addMenuItem(text, callbackfunc)
+                for j in range(i + 2): menu.addSeparator()
+            testmenu.addSubMenu(menu)
+
+        def _test_MultipleItem(testmenu):
+            menu = KMenu(testmenu, 0, "Multiple item")
+            for i in range(100):
+                text = str("%s-item%d" % (menu.title(), i))
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                menu.addMenuItem(text, callbackfunc)
+            testmenu.addSubMenu(menu)
+
+        def _test_LongTextItem(testmenu):
+            menu = KMenu(testmenu, 0, "Long text item")
+            for i in range(5):
+                text = str("%s-item%d " % (menu.title(), i)) * 20
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                menu.addMenuItem(text, callbackfunc)
+            testmenu.addSubMenu(menu)
+
+        def _test_LargeMenu(testmenu):
+            menu = KMenu(testmenu, 0, "Large menu")
+            for i in range(60):
+                text = str("%s-item%d " % (menu.title(), i)) * 10
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                menu.addMenuItem(text, callbackfunc)
+                if i % 5 == 0: menu.addSeparator()
+            testmenu.addSubMenu(menu)
+
+        def _test_LimitTest(testmenu):
+            menu = KMenu(testmenu, 0, "LimitTest")
+            _test_LargeMenu(menu)
+            _test_MultipleItem(menu)
+            _test_LongTextItem(menu)
+            testmenu.addSubMenu(menu)
+
+        def _test_Submenu(testmenu):
+            menu = KMenu(testmenu, 0, "Submenu")
+            testmenu.addSubMenu(menu)
+
+            submenu = KMenu(menu, 0, "submenu1")
+            menu.addSubMenu(submenu)
+            m = submenu
+            for i in range(8):
+                next = KMenu(testmenu, 0, "submenu%d" % (i + 2))
+                m.addSubMenu(next)
+                m = next
+
+            submenu = KMenu(menu, 0, "submenu2")
+            menu.addSubMenu(submenu)
+            m = submenu
+            for i in range(8):
+                for j in range(10):
+                    text = str("%s-item%d" % (m.title(), j))
+                    callbackfunc = lambda checked, a=j, b=text: _test_callback(a, b)
+                    m.addMenuItem(text, callbackfunc)
+                next = KMenu(testmenu, 0, "submenu%d" % (i + 2))
+                m.addSubMenu(next)
+                m = next
+
+            submenu = KMenu(testmenu, 0, "SubMenu State")
+            c = 16
+            for i in range(c):
+                text = str("%s-%d" % (submenu.title(), i))
+                m = KMenu(submenu, 0, text)
+                act = submenu.addSubMenu(m)
+                callbackfunc = lambda checked, a=i, b=text: _test_callback(a, b)
+                act.triggered.connect(callbackfunc)
+                if i >= c / 2:
+                    act.setDisabled(True)
+                    act.setText("%s-disable" % act.text())
+                if i % 8 >= c / 4:
+                    act.setIcon(icon)
+                    act.setText("%s-icon" % act.text())
+                if i % 4 >= c / 8:
+                    act.setCheckable(True)
+                    act.setText("%s-ckeckable" % act.text())
+                if i % 2 >= c / 16:
+                    act.setChecked(True)
+                    act.setText("%s-checked" % act.text())
+                submenu.addSubMenu(m)
+            menu.addSubMenu(submenu)
+
+        def _test_ImageTest(testmenu):
+            imagetestmenu = KMenu(testmenu, 0, "ImageTest")
+            testmenu.addSubMenu(imagetestmenu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-normal")
+            for i in range(32):
+                text = " " * 54
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-bit")
+            menu.addMenuItem('')
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-small")
+            for i in range(10):
+                text = " " * 30
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-long")
+            for i in range(64):
+                text = " " * 54
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-long2")
+            for i in range(32):
+                text = " " * 30
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-large")
+            for i in range(64):
+                text = " " * 300
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+            menu = KMenu(imagetestmenu, 0, "MenuImage-verylarge")
+            for i in range(100):
+                text = " " * 600
+                menu.addMenuItem(text)
+            imagetestmenu.addSubMenu(menu)
+
+        if parent is None:
+            parent = QWidget(f=Qt.WindowType.Dialog)
+        icon = QIcon(r"icon.ico")
+        menu_test = KMenu(parent, 0, "TestMenu")
+
+        _test_Exit(menu_test)
+        menu_test.addSeparator()
+        _test_MenuItemState(menu_test)
+        _test_Shortcut(menu_test)
+        _test_StatusTip(menu_test)
+        _test_Separator(menu_test)
+        _test_LimitTest(menu_test)
+        _test_Submenu(menu_test)
+        menu_test.addSeparator()
+        _test_ImageTest(menu_test)
+        menu_test.addSeparator()
+        _test_Exit(menu_test)
+
+        return menu_test
+
+    @staticmethod
+    def updateTestSurface(menu, ghost, curSurface=-1):
+        if kikka.core.isDebug is False or menu is None:
+            return
+
+        debugmenu = None
+        for act in menu.actions():
+            if act.text() == 'Debug':
+                debugmenu = act.menu()
+                break
+
+        if debugmenu is None:
+            return
+
+        sufacemenu = None
+        for act in debugmenu.actions():
+            if act.text() == 'TestSurface':
+                sufacemenu = act.menu()
+                break
+
+        if sufacemenu is None:
+            return
+
+        sufacemenu.clear()
+        surfacelist = ghost.getShell().getSurfaceNameList()
+        group = QActionGroup(sufacemenu.parent())
+        for surfaceID, item in surfacelist.items():
+            callbackfunc = lambda checked, faceID=surfaceID: ghost.getSoul(0).setSurface(faceID)
+            name = "%3d - %s(%s)" % (surfaceID, item[0], item[1])
+            act = sufacemenu.addMenuItem(name, callbackfunc, None, group)
+            act.setCheckable(True)
+            if surfaceID == curSurface:
+                act.setChecked(True)
+        pass
+
+
+def getDefaultImage():
+    return QImage(os.path.join(RESOURCES_PATH, "shell.png"))
+
+
+def getMenuStyle():
+    shellMenuStyle = ShellMenuStyle()
+    shellMenuStyle.background_image = os.path.join(RESOURCES_PATH, "menu_background.png")
+    shellMenuStyle.foreground_image = os.path.join(RESOURCES_PATH, "menu_foreground.png")
+    shellMenuStyle.sidebar_image = os.path.join(RESOURCES_PATH, "menu_sidebar.png")
+    return MenuStyle(shellMenuStyle)
+
+class ShellMenuStyle:
+    def __init__(self):
+        self.hidden = False
+
+        self.font_family = ''
+        self.font_size = -1
+
+        self.background_image = ''
+        self.background_font_color = [-1, -1, -1]
+        self.background_alignment = 'lefttop'
+
+        self.foreground_image = ''
+        self.foreground_font_color = [-1, -1, -1]
+        self.foreground_alignment = 'lefttop'
+
+        self.disable_font_color = [-1, -1, -1]
+        self.separator_color = [-1, -1, -1]
+
+        self.sidebar_image = ''
+        self.sidebar_alignment = 'lefttop'
+
+
+class MenuStyle:
+    def __init__(self, shellMenu):
+        # image
+        if os.path.exists(shellMenu.background_image):
+            self.bg_image = QImage(shellMenu.background_image)
+        else:
+            self.bg_image = getDefaultImage()
+            logging.warning("Menu background image NOT found: %s" % shellMenu.background_image)
+
+        if os.path.exists(shellMenu.foreground_image):
+            self.fg_image = QImage(shellMenu.foreground_image)
+        else:
+            self.fg_image = getDefaultImage()
+            logging.warning("Menu foreground image NOT found: %s" % shellMenu.foreground_image)
+
+        if os.path.exists(shellMenu.sidebar_image):
+            self.side_image = QImage(shellMenu.sidebar_image)
+        else:
+            self.side_image = getDefaultImage()
+            logging.warning("Menu sidebar image NOT found: %s" % shellMenu.sidebar_image)
+
+        # font and color
+        if shellMenu.font_family != '':
+            self.font = QFont(shellMenu.font_family, shellMenu.font_size)
+        else:
+            self.font = None
+
+        if -1 in shellMenu.background_font_color:
+            self.bg_font_color = None
+        else:
+            self.bg_font_color = QColor(shellMenu.background_font_color[0],
+                                        shellMenu.background_font_color[1],
+                                        shellMenu.background_font_color[2])
+
+        if -1 in shellMenu.foreground_font_color:
+            self.fg_font_color = None
+        else:
+            self.fg_font_color = QColor(shellMenu.foreground_font_color[0],
+                                        shellMenu.foreground_font_color[1],
+                                        shellMenu.foreground_font_color[2])
+
+        if -1 in shellMenu.disable_font_color:
+            self.disable_font_color = None
+        else:
+            self.disable_font_color = QColor(shellMenu.disable_font_color[0],
+                                             shellMenu.disable_font_color[1],
+                                             shellMenu.disable_font_color[2])
+
+        if -1 in shellMenu.separator_color:
+            self.separator_color = None
+        else:
+            self.separator_color = QColor(shellMenu.separator_color[0],
+                                          shellMenu.separator_color[1],
+                                          shellMenu.separator_color[2])
+
+        # others
+        self.hidden = shellMenu.hidden
+        self.background_alignment = shellMenu.background_alignment
+        self.foreground_alignment = shellMenu.foreground_alignment
+        self.sidebar_alignment = shellMenu.sidebar_alignment
+        pass
+
+    def getPenColor(self, opt):
+        if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
+            color = self.separator_color
+        elif opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
+            color = self.fg_font_color
+        elif not (opt.state & QStyle.StateFlag.State_Enabled):
+            color = self.disable_font_color
+        else:
+            color = self.bg_font_color
+
+        if color is None:
+            color = opt.palette.color(QPalette.ColorRole.Text)
+        return color
+
+
+class KMenu(QMenu):
+    def __init__(self, parent, gid, title=''):
+        QMenu.__init__(self, title, parent)
+
+        self.gid = gid
+        self._parent = parent
+        self._aRect = {}
+        self._bg_image = None
+        self._fg_image = None
+        self._side_image = None
+
+        self.installEventFilter(self)
+        self.setMouseTracking(True)
+        self.setStyleSheet("QMenu { menu-scrollable: 1; }")
+        self.setSeparatorsCollapsible(False)
+
+    def addMenuItem(self, text, callbackfunc=None, iconfilepath=None, group=None):
+        if iconfilepath is None:
+            act = QAction(text, self._parent)
+        elif os.path.exists(iconfilepath):
+            act = QAction(QIcon(iconfilepath), text, self._parent)
+        else:
+            logging.info("fail to add menu item")
+            return
+
+        if callbackfunc is not None:
+            act.triggered.connect(callbackfunc)
+
+        if group is None:
+            self.addAction(act)
+        else:
+            self.addAction(group.addAction(act))
+
+        self.confirmMenuSize(act)
+        return act
+
+    def addSubMenu(self, menu):
+        act = self.addMenu(menu)
+        self.confirmMenuSize(act, menu.title())
+        return act
+
+    def getSubMenu(self, menuName):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() == menuName:
+                return act.menu()
+        return None
+
+    def getAction(self, actionName):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() == actionName:
+                return act
+        return None
+
+    def getActionByData(self, data):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.data() == data:
+                return act
+        return None
+
+    def checkAction(self, actionName, isChecked):
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.text() != actionName:
+                continue
+
+            act.setChecked(isChecked)
+        pass
+
+    def confirmMenuSize(self, item, text=''):
+        # s = self.sizeHint()
+        # w, h = kikka.helper.getScreenResolution()
+
+        # if text == '':
+        #     text = item.text()
+        # if KikkaMenu.isDebug and s.height() > h:
+        #     logging.warning("the Menu_Height out of Screen_Height, too many menu item when add: %s" % text)
+        # if KikkaMenu.isDebug and s.width() > w:
+        #     logging.warning("the Menu_Width out of Screen_Width, too menu item text too long when add: %s" % text)
+        return
+
+    def setPosition(self, pos):
+        rect = QApplication.instance().primaryScreen().geometry()
+        w = rect.width()
+        h = rect.height()
+        if pos.y() + self.height() > h: pos.setY(h - self.height())
+        if pos.y() < 0: pos.setY(0)
+
+        if pos.x() + self.width() > w: pos.setX(w - self.width())
+        if pos.x() < 0: pos.setX(0)
+        self.move(pos)
+
+    def updateActionRect(self):
+        """
+        void QMenuPrivate::updateActionRects(const QRect &screen) const
+        https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/da/d61/class_q_menu_private.html#acf93cda3ebe88b1234dc519c5f1b0f5d
+        """
+        self._aRect = {}
+        topmargin = 0
+        leftmargin = 0
+        rightmargin = 0
+
+        # qmenu.cpp Line 259:
+        # init
+        max_column_width = 0
+        dh = self.height()
+        y = 0
+        style = self.style()
+        opt = QStyleOption()
+        opt.initFrom(self)
+        hmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuHMargin, opt, self)
+        vmargin = style.pixelMetric(QStyle.PixelMetric.PM_MenuVMargin, opt, self)
+        icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
+        fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
+        deskFw = style.pixelMetric(QStyle.PixelMetric.PM_MenuDesktopFrameWidth, opt, self)
+        tearoffHeight = style.pixelMetric(QStyle.PixelMetric.PM_MenuTearoffHeight, opt, self) if self.isTearOffEnabled() else 0
+
+        # for compatibility now - will have to refactor this away
+        tabWidth = 0
+        maxIconWidth = 0
+        hasCheckableItems = False
+        # ncols = 1
+        # sloppyAction = 0
+
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+            if act.isSeparator() or act.isVisible() is False:
+                continue
+            # ..and some members
+            hasCheckableItems |= act.isCheckable()
+            ic = act.icon()
+            if ic.isNull() is False:
+                maxIconWidth = max(maxIconWidth, icone + 4)
+
+        # qmenu.cpp Line 291:
+        # calculate size
+        qfm = self.fontMetrics()
+        previousWasSeparator = True  # this is true to allow removing the leading separators
+        for i in range(len(self.actions())):
+            act = self.actions()[i]
+
+            if act.isVisible() is False \
+            or (self.separatorsCollapsible() and previousWasSeparator and act.isSeparator()):
+                # we continue, this action will get an empty QRect
+                self._aRect[i] = QRect()
+                continue
+
+            previousWasSeparator = act.isSeparator()
+
+            # let the style modify the above size..
+            opt = QStyleOptionMenuItem()
+            self.initStyleOption(opt, act)
+            fm = opt.fontMetrics
+
+            sz = QSize()
+            # sz = self.sizeHint().expandedTo(self.minimumSize()).expandedTo(self.minimumSizeHint()).boundedTo(self.maximumSize())
+            # calc what I think the size is..
+            if act.isSeparator():
+                sz = QSize(2, 2)
+            else:
+                s = act.text()
+                if '\t' in s:
+                    t = s.index('\t')
+                    act.setText(s[t + 1:])
+                    tabWidth = max(int(tabWidth), qfm.width(s[t + 1:]))
+                else:
+                    seq = act.shortcut()
+                    if seq.isEmpty() is False:
+                        tabWidth = max(int(tabWidth), qfm.boundingRect(seq.toString()).width())
+
+                sz.setWidth(fm.boundingRect(QRect(), Qt.TextFlag.TextSingleLine | Qt.TextFlag.TextShowMnemonic, s).width())
+                sz.setHeight(fm.height())
+
+                if not act.icon().isNull():
+                    is_sz = QSize(icone, icone)
+                    if is_sz.height() > sz.height():
+                        sz.setHeight(is_sz.height())
+
+            sz = style.sizeFromContents(QStyle.ContentsType.CT_MenuItem, opt, sz, self)
+
+            if sz.isEmpty() is False:
+                max_column_width = max(max_column_width, sz.width())
+                # wrapping
+                if y + sz.height() + vmargin > dh - deskFw * 2:
+                    # ncols += 1
+                    y = vmargin
+                y += sz.height()
+                # update the item
+                self._aRect[i] = QRect(0, 0, sz.width(), sz.height())
+        pass  # exit for
+
+        max_column_width += tabWidth  # finally add in the tab width
+        sfcMargin = style.sizeFromContents(QStyle.ContentsType.CT_Menu, opt, QSize(0, 0), self).width()
+        min_column_width = self.minimumWidth() - (sfcMargin + leftmargin + rightmargin + 2 * (fw + hmargin))
+        max_column_width = max(min_column_width, max_column_width)
+
+        # qmenu.cpp Line 259:
+        # calculate position
+        base_y = vmargin + fw + topmargin + tearoffHeight
+        x = hmargin + fw + leftmargin
+        y = base_y
+
+        for i in range(len(self.actions())):
+            if self._aRect[i].isNull():
+                continue
+            if y + self._aRect[i].height() > dh - deskFw * 2:
+                x += max_column_width + hmargin
+                y = base_y
+
+            self._aRect[i].translate(x, y)  # move
+            self._aRect[i].setWidth(max_column_width)  # uniform width
+            y += self._aRect[i].height()
+
+        # update menu size
+        s = self.sizeHint()
+        self.resize(s)
+
+    def drawControl(self, p, opt, arect, icon, menustyle):
+        """
+        due to overrides the "paintEvent" method, so we must repaint all menu item by self.
+        luckly, we have qt source code to reference.
+
+        void drawControl (ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w=0) const
+        https://cep.xray.aps.anl.gov/software/qt4-x11-4.8.6-browser/df/d91/class_q_style_sheet_style.html#ab92c0e0406eae9a15bc126b67f88c110
+        Line 3533: element = CE_MenuItem
+        """
+
+        style = self.style()
+        p.setPen(menustyle.getPenColor(opt))
+
+        # Line 3566: draw icon and checked sign
+        checkable = opt.checkType != QStyleOptionMenuItem.CheckType.NotCheckable
+        checked = opt.checked if checkable else False
+        if opt.icon.isNull() is False:  # has custom icon
+            dis = not (opt.state & QStyle.StateFlag.State_Enabled)
+            active = opt.state & QStyle.StateFlag.State_Selected
+            mode = QIcon.Mode.Disabled if dis else QIcon.Mode.Normal
+            if active != 0 and not dis: mode = QIcon.Mode.Active
+
+            fw = style.pixelMetric(QStyle.PixelMetric.PM_MenuPanelWidth, opt, self)
+            icone = style.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, opt, self)
+            iconRect = QRectF(arect.x() - fw, arect.y(), self._side_image.width(), arect.height())
+            if checked:
+                pixmap = icon.pixmap(QSize(icone, icone), mode, QIcon.State.On)
+            else:
+                pixmap = icon.pixmap(QSize(icone, icone), mode)
+
+            pixw = pixmap.width()
+            pixh = pixmap.height()
+            pmr = QRectF(0, 0, pixw, pixh)
+            pmr.moveCenter(iconRect.center())
+
+            if checked: p.drawRect(QRectF(pmr.x() - 1, pmr.y() - 1, pixw + 2, pixh + 2))
+            p.drawPixmap(pmr.topLeft(), pixmap)
+
+        elif checkable and checked:  # draw default checked sign
+            opt.rect = QRect(0, arect.y(), self._side_image.width(), arect.height())
+            opt.palette.setColor(QPalette.ColorRole.Text, menustyle.getPenColor(opt))
+            style.drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opt, p, self)
+
+        # Line 3604: draw emnu text
+        font = menustyle.font
+        if font is not None:
+            p.setFont(font)
+        else:
+            p.setFont(opt.font)
+        text_flag = Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextShowMnemonic | Qt.TextFlag.TextDontClip | Qt.TextFlag.TextSingleLine
+
+        tr = QRect(arect)
+        s = opt.text
+        if '\t' in s:
+            ss = s[s.index('\t') + 1:]
+            fontwidth = opt.fontMetrics.width(ss)
+            tr.moveLeft(opt.rect.right() - fontwidth)
+            tr = QStyle.visualRect(opt.direction, opt.rect, tr)
+            p.drawText(tr, text_flag, ss)
+        tr.moveLeft(self._side_image.width() + arect.x())
+        tr = QStyle.visualRect(opt.direction, opt.rect, tr)
+        p.drawText(tr, text_flag, s)
+
+        # Line 3622: draw sub menu arrow
+        if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.SubMenu:
+            arrowW = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorWidth, opt, self)
+            arrowH = style.pixelMetric(QStyle.PixelMetric.PM_IndicatorHeight, opt, self)
+            arrowRect = QRect(0, 0, arrowW, arrowH)
+            arrowRect.moveBottomRight(arect.bottomRight())
+            arrow = QStyle.PrimitiveElement.PE_IndicatorArrowLeft if opt.direction == Qt.LayoutDirection.RightToLeft else QStyle.PrimitiveElement.PE_IndicatorArrowRight
+
+            opt.rect = arrowRect
+            opt.palette.setColor(QPalette.ColorRole.ButtonText, menustyle.getPenColor(opt))
+            style.drawPrimitive(arrow, opt, p, self)
+        pass
+
+    def paintEvent(self, event):
+        # init
+        menustyle = getMenuStyle()
+        self._bg_image = menustyle.bg_image
+        self._fg_image = menustyle.fg_image
+        self._side_image = menustyle.side_image
+        self.updateActionRect()
+        p = QPainter(self)
+
+        # draw background
+        p.fillRect(QRect(QPoint(), self.size()), self._side_image.pixelColor(0, 0))
+        vertical = False
+        y = self.height()
+        while y > 0:
+            yy = y - self._bg_image.height()
+            p.drawImage(0, yy, self._side_image.mirrored(False, vertical))
+            x = self._side_image.width()
+            while x < self.width():
+                p.drawImage(x, yy, self._bg_image.mirrored(False, vertical))
+                x += self._bg_image.width()
+                p.drawImage(x, yy, self._bg_image.mirrored(True, vertical))
+                x += self._bg_image.width() + 1
+            y -= self._bg_image.height()
+            vertical = not vertical
+
+        # draw item
+        actioncount = len(self.actions())
+        for i in range(actioncount):
+            act = self.actions()[i]
+            arect = QRect(self._aRect[i])
+            if event.rect().intersects(arect) is False:
+                continue
+
+            opt = QStyleOptionMenuItem()
+            self.initStyleOption(opt, act)
+            opt.rect = arect
+            if opt.state & QStyle.StateFlag.State_Selected and opt.state & QStyle.StateFlag.State_Enabled:
+                # Selected Item, draw foreground image
+                p.setClipping(True)
+                p.setClipRect(arect.x() + self._side_image.width(), arect.y(), self.width() - self._side_image.width(),
+                              arect.height())
+
+                p.fillRect(QRect(QPoint(), self.size()), self._fg_image.pixelColor(0, 0))
+                vertical = False
+                y = self.height()
+                while y > 0:
+                    x = self._side_image.width()
+                    while x < self.width():
+                        yy = y - self._fg_image.height()
+                        p.drawImage(x, yy, self._fg_image.mirrored(False, vertical))
+                        x += self._fg_image.width()
+                        p.drawImage(x, yy, self._fg_image.mirrored(True, vertical))
+                        x += self._fg_image.width() + 1
+                    y -= self._fg_image.height()
+                    vertical = not vertical
+                p.setClipping(False)
+
+            if opt.menuItemType == QStyleOptionMenuItem.MenuItemType.Separator:
+                # Separator
+                p.setPen(menustyle.getPenColor(opt))
+                y = int(arect.y() + arect.height() / 2)
+                p.drawLine(self._side_image.width(), y, arect.width(), y)
+            else:
+                # MenuItem
+                self.drawControl(p, opt, arect, act.icon(), menustyle)
+        pass  # exit for
+
+    def eventFilter(self, obj, event):
+        if obj == self:
+            if event.type() == QEvent.Type.WindowDeactivate:
+                self.Hide()
+            elif event.type() == QEvent.Type.ToolTip:
+                act = self.activeAction()
+                if act != 0 and act.toolTip() != act.text():
+                    QToolTip.showText(event.globalPos(), act.toolTip())
+                else:
+                    QToolTip.hideText()
+        return False

+ 215 - 0
Kikka/Widgets/MainWindow.py

@@ -0,0 +1,215 @@
+import os.path
+
+from PySide6.QtCore import Qt, QFileInfo, Signal, QFile, QPoint
+from PySide6.QtGui import QColor, QIcon
+from PySide6.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QTabBar, QListWidget, QLabel, QSplitter, QToolButton
+from qframelesswindow import FramelessWindow, TitleBarButton, TitleBarBase
+
+import Kikka
+from Kikka.KikkaConst import *
+from .IconPanel import IconPanel
+from Kikka.Widgets.KMenu import KMenu
+
+class CustomTitleBar(TitleBarBase):
+    """ Custom title bar """
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        self.minBtn.hide()
+        self.maxBtn.hide()
+        self.closeBtn.hide()
+        
+        self.settingsButton = QToolButton()
+        self.settingsButton.resize(18, 18)
+        self.settingsButton.setObjectName("SettingButton")
+        self.settingsButton.clicked.connect(self.onSettingsButtonClicked)
+        self.settingsButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+        self.settingsButton.setIcon(QIcon(os.path.join(Kikka.style.getThemePath(), "settings.svg")))
+        
+        self.closeButton = QToolButton()
+        self.closeButton.resize(18, 18)
+        self.closeButton.setObjectName("CloseButton")
+        self.closeButton.clicked.connect(self.closeBtn.clicked)
+        self.closeButton.setIcon(QIcon(os.path.join(Kikka.style.getThemePath(), "close.svg")))
+
+        self.iconLabel = QLabel(self)
+        self.iconLabel.setObjectName("IconLabel")
+        self.iconLabel.setPixmap(QIcon(os.path.join(Kikka.style.getThemePath(), "title.svg")).pixmap(26, 26))
+        
+        self.titleLabel = QLabel(self)
+        self.titleLabel.setObjectName("TitleLabel")
+        self.titleLabel.setText("Kikka")
+        self.titleLabel.adjustSize()
+        
+        self.hBoxLayout = QHBoxLayout(self)
+        self.hBoxLayout.setSpacing(0)
+        self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
+        self.hBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
+        self.hBoxLayout.addWidget(self.iconLabel)
+        self.hBoxLayout.addWidget(self.titleLabel)
+        self.hBoxLayout.addStretch(1)
+        self.hBoxLayout.addWidget(self.settingsButton)
+        self.hBoxLayout.addWidget(self.closeButton)
+
+    def mouseDoubleClickEvent(self, event):
+        pass
+    
+    def onSettingsButtonClicked(self):
+        # menu = KMenu(self, 0, "TTTitle")
+        # menu.setPosition(QPoint(100, 100))
+        # menu.show()
+        
+        from Kikka.Widgets.KMenu import _createTestMenu
+        menu = _createTestMenu()
+        menu.setObjectName("ContextMenu")
+        menu.setPosition(QPoint(100, 100))
+        menu.show()
+    
+    
+class PagePanel(QWidget):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self._updating = False
+        self.currentColumnIndex = -1
+        self.currentRowIndex = -1
+        self.data = None
+        self.initGUI()
+
+    def initGUI(self):
+        self.iconPanel = IconPanel()
+
+        self.columnTab = QTabBar()
+        self.columnTab.setContentsMargins(0, 0, 0, 0)
+        self.columnTab.setDrawBase(True)
+        self.columnTab.setExpanding(False)
+        self.columnTab.setDocumentMode(True)
+        self.columnTab.setElideMode(Qt.TextElideMode.ElideNone)
+        self.columnTab.tabBarClicked.connect(self.onTabClicked)
+
+        self.rowList = QListWidget()
+        self.rowList.itemClicked.connect(self.onListClicked)
+        
+        self.splitter = QSplitter()
+        self.splitter.setChildrenCollapsible(False)
+        self.splitter.addWidget(self.rowList)
+        self.splitter.addWidget(self.iconPanel)
+        self.splitter.setContentsMargins(0, 0, 0, 0)
+        self.splitter.setStretchFactor(0, 1)
+        self.splitter.setStretchFactor(1, 5)
+
+        layout2 = QVBoxLayout()
+        layout2.setSpacing(0)
+        layout2.setContentsMargins(0, 0, 0, 0)
+        layout2.addWidget(self.columnTab)
+        layout2.addWidget(self.splitter)
+        self.setLayout(layout2)
+    
+    def initData(self, data):
+        self._updating = True
+        self.data = data
+
+        # init column
+        sub = len(data) - self.columnTab.count()
+        while sub > 0:
+            self.columnTab.addTab("")
+            sub -= 1
+        while sub < 0:
+            self.columnTab.setTabVisible(self.columnTab.count() - sub, False)
+            sub += 1
+
+        for i, category in data.items():
+            self.columnTab.setTabText(i, category.name)
+
+        self._updating = False
+
+    def setPage(self, columnIndex, rowIndex):
+        if columnIndex < 0 or columnIndex >= self.columnTab.count(): return
+        if rowIndex < 0: return
+
+        self._updating = True
+
+        self.rowList.clear()
+        rowData = self.data[columnIndex]
+
+        for i, subCategory in rowData.sub_categories.items():
+            self.rowList.addItem(subCategory.name)
+
+        self.columnTab.setCurrentIndex(columnIndex)
+        self.rowList.setCurrentRow(rowIndex)
+        panelData = rowData[rowIndex]
+        self.iconPanel.setData(panelData.actions)
+        
+        self.currentColumnIndex = columnIndex
+        self.currentRowIndex = rowIndex
+        self._updating = False
+
+    def onTabClicked(self, columnIndex):
+        self.setPage(columnIndex, 0)
+    
+    def onListClicked(self, item):
+        index = self.rowList.indexFromItem(item)
+        self.currentRowIndex = index.row()
+        panelData = self.data[self.currentColumnIndex][self.currentRowIndex]
+        self.iconPanel.setData(panelData.actions)
+        pass
+    
+
+
+class MainWindow(FramelessWindow):
+    SIGNAL_OPERATION = Signal(EActionOperation, dict)  # operation, param
+    
+    def __init__(self, parent=None):
+        super().__init__(parent=parent)
+        self.initGUI()
+        self.connectSlot()
+
+    def initGUI(self):
+        self.setWindowTitle("Acrylic Window")
+
+        self.setTitleBar(CustomTitleBar(self))
+        self.panel = PagePanel(self)
+
+        layout = QVBoxLayout()
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setSpacing(0)
+        layout.addSpacing(self.titleBar.height())
+        layout.addWidget(self.panel)
+        self.setLayout(layout)
+    
+    def initData(self, data):
+        self.panel.initData(data)
+
+    def connectSlot(self):
+        self.panel.iconPanel.SIGNAL_DROP.connect(self.onDropFileEnter)
+        self.panel.iconPanel.SIGNAL_DOUBLE_CLICK.connect(self.onDoubleClickIcon)
+        
+    def setPage(self, columnIndex, rowIndex):
+        self.panel.setPage(columnIndex, rowIndex)
+    
+    def getCurrentPage(self):
+        return (self.panel.currentRowIndex, self.panel.currentColumnIndex)
+    
+    def onDropFileEnter(self, filepath):
+        param = {
+            'filepath': filepath,
+            'column': self.panel.currentColumnIndex,
+            'row': self.panel.currentRowIndex,
+        }
+        self.SIGNAL_OPERATION.emit(EActionOperation.NewAction, param)
+        self.setPage(self.panel.currentColumnIndex, self.panel.currentRowIndex)
+    
+    def onDoubleClickIcon(self, action):
+        param = {
+            'action': action,
+        }
+        self.SIGNAL_OPERATION.emit(EActionOperation.RunAction, param)
+
+
+    
+    
+    
+    
+    
+    
+    
+    

+ 60 - 0
Kikka/Widgets/Shell/ImageShellWindow.py

@@ -0,0 +1,60 @@
+import os
+
+from PySide6.QtCore import Qt, Signal, QPoint, QSize
+from PySide6.QtWidgets import QWidget
+from PySide6.QtGui import QPixmap, QPainter, QImage
+
+from Kikka.KikkaConst import *
+from .KikkaShell import ShellBase
+
+
+class ImageShellWindow(ShellBase):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self._size = QSize(512, 512)
+        self._cavan_image = None
+        self._pixmap = None
+        
+    def loadData(self, data):
+        self._size = QSize(512, 512)
+        img = QImage(os.path.join(RESOURCES_PATH, "shell.png"))
+        self.setImage(img)
+        self.repaint()
+    
+    def onKikkaOperation(self, operation, param):
+        pass
+
+    def setImage(self, image):
+        # if isDebug:
+        #     image = self.debugDraw(image)
+        
+        self._cavan_image = QImage(self._size, QImage.Format.Format_ARGB32_Premultiplied)
+        painter = QPainter(self._cavan_image)
+        painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
+        painter.fillRect(self._cavan_image.rect(), Qt.GlobalColor.transparent)
+        painter.end()
+        del painter
+        
+        painter = QPainter(self._cavan_image)
+        painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
+        painter.drawImage(QPoint(), image)
+        painter.end()
+        del painter
+
+        # self._cavan_image.save("shell_image.png")
+        
+        pixmap = QPixmap().fromImage(image, Qt.ImageConversionFlag.AutoColor)
+        self._pixmap = pixmap
+
+        self.setFixedSize(self._pixmap.size())
+        self.setMask(self._pixmap.mask())
+        self.repaint()
+
+    def paintEvent(self, event):
+        if self._pixmap is None:
+            return
+
+        painter = QPainter(self)
+        painter.drawPixmap(QPoint(), self._pixmap)
+
+

+ 124 - 0
Kikka/Widgets/Shell/KikkaShell.py

@@ -0,0 +1,124 @@
+import asyncio
+
+from PySide6.QtCore import Qt, Signal, QPoint
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QSplashScreen
+from qframelesswindow import FramelessWindow
+
+from Kikka.KikkaConst import *
+from Kikka.Utils.Singleton import singleton
+
+@singleton
+class KikkaShell():
+    
+    def __init__(self):
+        self.init()
+        self.shellWindow = None
+    
+    def init(self):
+        pass
+    
+    def loadShell(self, data):
+        
+        async def _async_load_start(data):
+            future = asyncio.ensure_future(async_load(data))
+            future.add_done_callback(_loaded)
+            await asyncio.gather(future)
+        
+        async def async_load(data):
+            await asyncio.sleep(1)
+            shell_type = data.get("shell_type")
+            if shell_type is None:
+                return None
+            elif shell_type == "Image":
+                from .ImageShellWindow import ImageShellWindow
+                shellWindow = ImageShellWindow()
+                shellWindow.loadData(data)
+                return shellWindow
+            return None
+            
+        def _loaded(future):
+            shellWindow = future.result()
+            self.shellWindow = shellWindow
+            self.show()
+
+        asyncio.run(_async_load_start(data))
+    
+    def onKikkaOperation(self, operation, param):
+        pass
+    
+    def switchShell(self, new_shell):
+        if self.shellWindow is not None:
+            self.shellWindow.deleteLater()
+        self.shellWindow = new_shell
+
+    def show(self):
+        self.shellWindow.show()
+
+
+class ShellBase(QSplashScreen):
+    SIGNAL_SHELL_OPERATION = Signal(EShellOperation, dict)
+    
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self._isMoving = False
+        self.init()
+    
+    def init(self):
+        self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
+        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
+        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
+        self.setMouseTracking(True)
+        self.setAcceptDrops(True)
+    
+    def loadData(self, data):
+        raise NotImplemented
+
+    def onKikkaOperation(self, operation, param):
+        raise NotImplemented
+
+    def mousePressEvent(self, event):
+        self._movepos = event.globalPos() - self.pos()
+        if event.buttons() == Qt.MouseButton.LeftButton:
+            self._isMoving = True
+            event.accept()
+    
+    def mouseMoveEvent(self, event):
+        # self._mouseLogging("mouseMoveEvent", event.buttons(), event.globalPos().x(), event.globalPos().y())
+        self._mousepos = event.pos()
+        if self._isMoving and event.buttons() == Qt.MouseButton.LeftButton:
+            self.move(event.globalPos() - self._movepos)
+            event.accept()
+        else:
+            self._isMoving = False
+
+    def mouseReleaseEvent(self, event):
+        self._isMoving = False
+    
+    def mouseDoubleClickEvent(self, event):
+        if event.buttons() == Qt.MouseButton.LeftButton:
+            self._isMoving = False
+    
+    def wheelEvent(self, event):
+        pass
+
+    def dragEnterEvent(self, event):
+        event.accept()
+    
+    def dropEvent(self, event):
+        urls = event.mimeData().urls()
+    
+    def move(self, *__args):
+        if len(__args) == 1 and isinstance(__args[0], QPoint):
+            x = __args[0].x()
+            y = __args[0].y()
+        elif len(__args) == 2 and isinstance(__args[0], int) and isinstance(__args[1], int):
+            x = __args[0]
+            y = __args[1]
+        else:
+            super().move(*__args)
+            return
+
+        super().move(x, y)
+    
+    
+    

+ 0 - 0
Kikka/Widgets/Shell/__init__.py


+ 0 - 0
Kikka/Widgets/__init__.py


+ 11 - 0
Kikka/__init__.py

@@ -0,0 +1,11 @@
+from Kikka.KikkaCore import KikkaCore
+
+core = KikkaCore()            #
+memory = core.memory          # data save and load
+config = core.config          # option and setting
+style = core.style            # style
+action = core.action          # main window action
+app = core.app                # app
+main_window = core.mainWindow # main window
+shell = core.shell            # shell windows manager
+

+ 16 - 0
Resources/Theme/Default/action_default.svg

@@ -0,0 +1,16 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <defs>
+    <style>.canvas{fill: none; opacity: 0;}.light-defaultgrey-10{fill: #e0ffff; opacity: 0.1;}.light-defaultgrey{fill: #e0ffff; opacity: 1;}.cls-1{opacity:0.75;}</style>
+  </defs>
+  <title>IconLightBug</title>
+  <g id="canvas" class="canvas">
+    <path class="canvas" d="M16,16H0V0H16Z" />
+  </g>
+  <g id="level-1">
+    <g class="cls-1">
+      <path class="light-defaultgrey-10" d="M12.5,4.5v10H2.5V1.5h7Z" />
+      <path class="light-defaultgrey" d="M12.854,4.146l-3-3L9.5,1h-7L2,1.5v13l.5.5h10l.5-.5V4.5ZM3,14V2H9V5h3v9Z" />
+    </g>
+    <path class="light-defaultgrey" d="M8,6v5H7V6Zm.25,6.5a.75.75,0,1,1-.75-.75A.75.75,0,0,1,8.25,12.5Z" />
+  </g>
+</svg>

+ 35 - 0
Resources/Theme/Default/descript.txt

@@ -0,0 +1,35 @@
+charset,UTF-8
+craftman,8
+craftmanw,8
+type,shell
+id,_Default_2
+name,Default2
+mode,material
+
+sakura.balloon.offsetx,50
+sakura.balloon.offsety,50
+kero.balloon.offsetx,0
+kero.balloon.offsety,20
+
+menu.background.bitmap.filename,background.png
+menu.foreground.bitmap.filename,foreground.png
+menu.sidebar.bitmap.filename,sidebar.png
+menu.background.font.color.r,235
+menu.background.font.color.g,235
+menu.background.font.color.b,255
+menu.foreground.font.color.r,100
+menu.foreground.font.color.g,100
+menu.foreground.font.color.b,120
+menu.disable.font.color.r,200
+menu.disable.font.color.g,184
+menu.disable.font.color.b,213
+menu.separator.color.r,200
+menu.separator.color.g,184
+menu.separator.color.b,213
+menu.background.alignment,lefttop
+menu.foreground.alignment,lefttop
+menu.sidebar.alignment,top
+
+shiori.logo.filename,ai.png
+shiori.logo.alignment,leftbottom
+shiori.logo.drawmethod,1

+ 12 - 0
Resources/Theme/Default/icon_close.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <defs>
+    <style>.canvas{fill: none; opacity: 0;}.light-defaultgrey{fill: #999; opacity: 1;}</style>
+  </defs>
+  <title>IconLightClose</title>
+  <g id="canvas">
+    <path class="canvas" d="M16,16H0V0H16Z" />
+  </g>
+  <g id="level-1">
+    <path class="light-defaultgrey" d="M8.207,7.5l5.147,5.146-.708.708L7.5,8.207,2.354,13.354l-.708-.708L6.793,7.5,1.646,2.354l.708-.708L7.5,6.793l5.146-5.147.708.708Z" />
+  </g>
+</svg>

+ 14 - 0
Resources/Theme/Default/icon_setting.svg

@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <defs>
+    <style>.canvas{fill: none; opacity: 0;}.light-defaultgrey-10{fill: #666666; opacity: 0.1;}.light-defaultgrey{fill: #666666; opacity: 1;}</style>
+  </defs>
+  <title>IconLightSettings</title>
+  <g id="canvas" class="canvas">
+    <path class="canvas" d="M16,16H0V0H16Z" />
+  </g>
+  <g id="level-1">
+    <path class="light-defaultgrey-10" d="M14.5,9V7L12,6.5l-.111-.268L13.3,4.111,11.889,2.7,9.768,4.111,9.5,4,9,1.5H7L6.5,4l-.268.111L4.111,2.7,2.7,4.111,4.111,6.232,4,6.5,1.5,7V9L4,9.5l.111.268L2.7,11.889,4.111,13.3l2.121-1.414L6.5,12,7,14.5H9L9.5,12l.268-.111L11.889,13.3,13.3,11.889,11.889,9.768,12,9.5ZM8,11a3,3,0,1,1,3-3A3,3,0,0,1,8,11Z" />
+    <path class="light-defaultgrey" d="M8,11a3,3,0,1,1,3-3A3,3,0,0,1,8,11ZM8,6a2,2,0,1,0,2,2A2,2,0,0,0,8,6Z" />
+    <path class="light-defaultgrey" d="M15,6.59l-2.426-.485,1.373-2.057L11.952,2.053,9.9,3.426,9.41,1H6.59L6.105,3.426,4.048,2.053l-1.995,2L3.426,6.105,1,6.59V9.41L3.426,9.9,2.053,11.952l2,1.995,2.057-1.373L6.59,15H9.41L9.9,12.574l2.057,1.373,1.995-1.995L12.574,9.9,15,9.41Zm-1,2-2.359.472-.317.76,1.336,2-.835.835-2-1.336-.76.317L8.59,14H7.41l-.472-2.359-.76-.317-2,1.336-.835-.835,1.336-2-.317-.76L2,8.59V7.41l2.359-.472.317-.76-1.336-2,.835-.835,2,1.336.76-.317L7.41,2H8.59l.472,2.359.76.317,2-1.336.835.835-1.336,2,.317.76L14,7.41Z" />
+  </g>
+</svg>

+ 32 - 0
Resources/Theme/Default/icon_title.svg

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 width="800px" height="800px" viewBox="0 0 512 512"  xml:space="preserve">
+<style type="text/css">
+<![CDATA[
+	.st0{fill:#ffb400;}
+]]>
+</style>
+<g>
+	<path class="st0" d="M360.102,240.012l10.156-10.266c0,0,15.609-13.406,33.406-7.328c30.984,10.578,66.781-0.875,91.609-25.734
+		c7.063-7.063,15.641-21.234,15.641-21.234c0.984-1.344,1.328-3.047,0.922-4.672l-1.922-7.906c-0.359-1.484-1.313-2.75-2.625-3.531
+		c-1.313-0.766-2.891-0.969-4.344-0.547l-60.984,16.969c-2.266,0.625-4.688-0.219-6.063-2.109l-28.015-38.594
+		c-0.859-1.172-1.219-2.641-1.016-4.063l5.641-41c0.297-2.234,1.891-4.047,4.063-4.656l64.406-17.922
+		c2.906-0.813,4.672-3.813,3.953-6.766l-2.547-10.359c-0.344-1.469-1.281-2.719-2.563-3.5c0,0-5.047-3.344-8.719-5.234
+		c-36.578-18.891-82.64-13.031-113.312,17.656c-22.656,22.656-31.531,53.688-27.375,83.156c3.203,22.656,1.703,34.703-8.078,45.047
+		c-0.891,0.922-3.703,3.734-8.047,8L360.102,240.012z"/>
+	<path class="st0" d="M211.383,295.418C143.024,361.652,68.461,433.715,68.461,433.715c-2.547,2.438-4,5.797-4.047,9.313
+		c-0.047,3.5,1.344,6.891,3.813,9.375l31.938,31.938c2.5,2.484,5.875,3.859,9.391,3.813c3.516-0.031,6.859-1.5,9.281-4.031
+		l139.328-140.953L211.383,295.418z"/>
+	<path class="st0" d="M501.43,451.371c2.484-2.484,3.859-5.859,3.813-9.375c-0.031-3.516-1.5-6.859-4.031-9.297L227.415,166.246
+		l-43.953,43.969L450.805,483.09c2.438,2.547,5.781,4,9.297,4.047s6.891-1.344,9.391-3.828L501.43,451.371z"/>
+	<path class="st0" d="M254.196,32.621c-32.969-12.859-86.281-14.719-117.156,16.141c-24.313,24.313-59.875,59.891-59.875,59.891
+		c-12.672,12.656-0.906,25.219-10.266,34.563c-9.359,9.359-24.313,0-32.734,8.422L3.29,182.527c-4.391,4.375-4.391,11.5,0,15.891
+		l43.016,43.016c4.391,4.391,11.516,4.391,15.906,0l30.875-30.875c8.438-8.422-0.938-23.375,8.438-32.719
+		c12.609-12.625,26.375-10.484,34.328-2.547l15.891,15.891l17.219,4.531l43.953-43.953l-5.063-16.688
+		c-14.016-14.031-16.016-30.266-7.234-39.047c13.594-13.594,36.047-33.234,57.078-41.656
+		C271.102,49.012,267.055,35.668,254.196,32.621z M194.571,103.48c-0.063,0.047,5.859-7.281,5.969-7.375L194.571,103.48z"/>
+</g>
+</svg>

BIN
Resources/Theme/Default/menu_background.png


BIN
Resources/Theme/Default/menu_foreground.png


BIN
Resources/Theme/Default/menu_sidebar.png


BIN
Resources/Theme/Default/shell_0.png


+ 136 - 0
Resources/Theme/Default/style_sheet.qss

@@ -0,0 +1,136 @@
+
+MainWindow {
+    background-color: #1E1E1E;
+}
+
+
+CustomTitleBar {
+    margin-bottom: 15px;
+}
+CustomTitleBar QLabel#IconLabel {
+    min-width: 28px;
+    min-height: 28px;
+    background-repeat: no-repeat;
+}
+CustomTitleBar QLabel#TitleLabel {
+    font: "黑体";
+    font-size: 28px;
+    font-weight: bold;
+    color: #e0ffff;
+}
+CustomTitleBar QToolButton {
+    min-width: 20px;
+    max-width: 20px;
+    min-height: 18px;
+    max-height: 18px;
+    border: none;
+    background-color: transparent;
+}
+CustomTitleBar QToolButton#SettingButton {
+    background-color: transparent;
+}
+CustomTitleBar QToolButton#CloseButton {
+    background-color: #921010;
+}
+CustomTitleBar QToolButton#CloseButton:hover {
+    background-color: #F10000;
+}
+CustomTitleBar QToolButton#CloseButton:pressed {
+    background-color: #BF0000;
+}
+
+PagePanel {
+    border: none;
+}
+
+PagePanel QTabBar {
+    background-color: transparent;
+    border-bottom: 1px solid #e0ffff;
+}
+
+PagePanel QTabBar::tab {
+    width: 60px;
+    height: 30px;
+    font-weight: bold;
+    color: #e0ffff;
+    background-color: #0085ff;
+    border-bottom: 1px solid #e0ffff;
+}
+PagePanel QTabBar::tab:selected {
+    background-color: #006fff;
+}
+PagePanel QTabBar::tab:hover {
+    background-color: #69b4ff;
+}
+PagePanel QTabBar::tab:pressed {
+    background-color: #006fff;
+}
+
+
+
+PagePanel QSplitter {
+    border: none;
+}
+PagePanel QSplitter::handle {
+    width: 1px;
+}
+PagePanel QSplitter::handle:horizontal {
+    width: 1px;
+}
+
+
+
+PagePanel QListWidget {
+
+    min-width: 60px;
+    max-width: 300px;
+
+    color: #e0ffff;
+    background-color: transparent;
+    outline: 0;
+    border: none;
+}
+PagePanel QListWidget::item {
+    border: none;
+    height: 30px;
+}
+PagePanel QListWidget::item:hover{
+    background-color: #69b4ff;
+}
+PagePanel QListWidget::item:selected {
+    border: none;
+}
+PagePanel QListWidget::item:selected:!active,
+PagePanel QListWidget::item:selected:active {
+    color: #e0ffff;
+    border: none;
+    background-color: #006fff;
+}
+
+
+
+IconPanel {
+    border: none;
+    background-color: transparent;
+}
+
+
+KMenu {
+    menu-scrollable: 1;
+    color: #DDDDDD;
+    Qproperty-hoverColor: #FFFFFF;
+}
+KMenu:disabled {
+    color: #666666;
+}
+
+
+
+
+
+
+
+
+
+
+

BIN
Resources/default.png


+ 240 - 0
category.json

@@ -0,0 +1,240 @@
+{
+    "0": {
+        "name": "常用",
+        "sub_categories": [
+            {
+                "name": "常用",
+                "type": "panel",
+                "actions": [
+                    [
+                        "SpaceSniffer",
+                        "exe",
+                        "C:\\Software\\Tools\\SpaceSniffer\\SpaceSniffer.exe",
+                        "",
+                        " -p \"666\\6\" -m q,j",
+                        "",
+                        "false",
+                        "",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "FastCopy",
+                        "exe",
+                        "C:\\Software\\Tools\\FastCopy\\FastCopy.exe",
+                        "",
+                        "",
+                        "",
+                        "false",
+                        "",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "启动",
+                        "dir",
+                        "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp",
+                        "",
+                        "",
+                        "",
+                        "false",
+                        "",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "土豆兄弟(Brotato)",
+                        "link",
+                        "com.epicgames.launcher://apps/3b391953ce4b42bb9a28a2fee249154b%3A518490da62794d0a8f3695cbe2fcf0a6%3Ae8bbb84be35640cda646233152ff3428?action=launch&silent=true",
+                        "D:\\Game\\EPic\\Epic%20Games\\Brotato\\Brotato.exe",
+                        "",
+                        "",
+                        "false",
+                        "false",
+                        "#FFFFFF",
+                        "",
+                        "3b391953ce4b42bb9a28a2fee249154b%3A518490da62794d0a8f3695cbe2fcf0a6%3Ae8bbb84be35640cda646233152ff3428.url"
+                    ],
+                    [
+                        "baidu",
+                        "link",
+                        "https://www.baidu.com/",
+                        "",
+                        " -a \"6666\"",
+                        "",
+                        "false",
+                        "true",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "我的电脑",
+                        "shell",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "true",
+                        "",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "回收站",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "false",
+                        "",
+                        "#FF0000",
+                        "",
+                        "hello, alice"
+                    ],
+                    [
+                        "组策略",
+                        "",
+                        "",
+                        "%windir%\\system32\\shell32.dll,1",
+                        "",
+                        "",
+                        "false",
+                        "",
+                        "#FFFFFF",
+                        "",
+                        ""
+                    ],
+                    [
+                        "Note",
+                        "note",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "",
+                        "hello, alice"
+                    ]
+                ]
+            },
+            {
+                "name": "下载",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "播放器",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "Adobe",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "脚本",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "Test",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "便签",
+                "type": "note",
+                "actions": []
+            },
+            {
+                "name": "全部",
+                "type": "all",
+                "actions": []
+            }
+        ]
+    },
+    "1": {
+        "name": "工作",
+        "sub_categories": [
+            {
+                "name": "IDE",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "git",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "editor",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "cmd",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "美术工具",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "内部工具",
+                "type": "panel",
+                "actions": []
+            }
+        ]
+    },
+    "2": {
+        "name": "工具",
+        "sub_categories": [
+            {
+                "name": "小工具",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "扫描检测",
+                "type": "panel",
+                "actions": []
+            },
+            {
+                "name": "系统设置",
+                "type": "panel",
+                "actions": []
+            }
+        ]
+    },
+    "3": {
+        "name": "游戏",
+        "sub_categories": [
+            {
+                "name": "新标签",
+                "type": "panel",
+                "actions": []
+            }
+        ]
+    },
+    "4": {
+        "name": "工具",
+        "sub_categories": [
+            {
+                "name": "新标签",
+                "type": "panel",
+                "actions": []
+            }
+        ]
+    }
+}

+ 11 - 0
main.py

@@ -0,0 +1,11 @@
+import sys
+
+def awake():
+    import Kikka
+    Kikka.core.init()
+    Kikka.core.initialized()
+    sys.exit(Kikka.core.app.exec())
+
+
+if __name__ == '__main__':
+    awake()

+ 272 - 0
memory.json

@@ -0,0 +1,272 @@
+{
+    "action": {
+        "0": {
+            "name": "常用",
+            "sub_categories": [
+                {
+                    "name": "常用",
+                    "type": "panel",
+                    "actions": [
+                        [
+                            "SpaceSniffer",
+                            "exe",
+                            "C:\\Software\\Tools\\SpaceSniffer\\SpaceSniffer.exe",
+                            "",
+                            " -p \"666\\6\" -m q,j",
+                            "",
+                            "false",
+                            "",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "FastCopy",
+                            "exe",
+                            "C:\\Software\\Tools\\FastCopy\\FastCopy.exe",
+                            "",
+                            "",
+                            "",
+                            "false",
+                            "",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "启动",
+                            "dir",
+                            "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp",
+                            "",
+                            "",
+                            "",
+                            "false",
+                            "",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "土豆兄弟(Brotato)",
+                            "link",
+                            "com.epicgames.launcher://apps/3b391953ce4b42bb9a28a2fee249154b%3A518490da62794d0a8f3695cbe2fcf0a6%3Ae8bbb84be35640cda646233152ff3428?action=launch&silent=true",
+                            "D:\\Game\\EPic\\Epic%20Games\\Brotato\\Brotato.exe",
+                            "",
+                            "",
+                            "false",
+                            "false",
+                            "#FFFFFF",
+                            "",
+                            "3b391953ce4b42bb9a28a2fee249154b%3A518490da62794d0a8f3695cbe2fcf0a6%3Ae8bbb84be35640cda646233152ff3428.url"
+                        ],
+                        [
+                            "baidu",
+                            "link",
+                            "https://www.baidu.com/",
+                            "",
+                            " -a \"6666\"",
+                            "",
+                            "false",
+                            "true",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "我的电脑",
+                            "shell",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "true",
+                            "",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "回收站",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "false",
+                            "",
+                            "#FF0000",
+                            "",
+                            "hello, alice"
+                        ],
+                        [
+                            "组策略",
+                            "",
+                            "",
+                            "%windir%\\system32\\shell32.dll,1",
+                            "",
+                            "",
+                            "false",
+                            "",
+                            "#FFFFFF",
+                            "",
+                            ""
+                        ],
+                        [
+                            "Note",
+                            "note",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "",
+                            "hello, alice"
+                        ]
+                    ]
+                },
+                {
+                    "name": "下载",
+                    "type": "panel",
+                    "actions": [
+                        [
+                            "SpaceSniffer",
+                            "executable",
+                            "D:/_MyProject/KikkaShortcut/.cache/icons/SpaceSniffer.exe",
+                            "",
+                            "",
+                            "D:/_MyProject/KikkaShortcut/.cache/icons",
+                            false,
+                            false,
+                            "",
+                            "",
+                            ""
+                        ],
+                        [
+                            "SpaceSniffer",
+                            "executable",
+                            "D:/_MyProject/KikkaShortcut/.cache/icons/SpaceSniffer.exe",
+                            "",
+                            "",
+                            "D:/_MyProject/KikkaShortcut/.cache/icons",
+                            false,
+                            false,
+                            "",
+                            "",
+                            ""
+                        ]
+                    ]
+                },
+                {
+                    "name": "播放器",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "Adobe",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "脚本",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "Test",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "便签",
+                    "type": "note",
+                    "actions": []
+                },
+                {
+                    "name": "全部",
+                    "type": "all",
+                    "actions": []
+                }
+            ]
+        },
+        "1": {
+            "name": "工作",
+            "sub_categories": [
+                {
+                    "name": "IDE",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "git",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "editor",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "cmd",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "美术工具",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "内部工具",
+                    "type": "panel",
+                    "actions": []
+                }
+            ]
+        },
+        "2": {
+            "name": "工具",
+            "sub_categories": [
+                {
+                    "name": "小工具",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "扫描检测",
+                    "type": "panel",
+                    "actions": []
+                },
+                {
+                    "name": "系统设置",
+                    "type": "panel",
+                    "actions": []
+                }
+            ]
+        },
+        "3": {
+            "name": "游戏",
+            "sub_categories": [
+                {
+                    "name": "新标签",
+                    "type": "panel",
+                    "actions": []
+                }
+            ]
+        },
+        "4": {
+            "name": "工具",
+            "sub_categories": [
+                {
+                    "name": "新标签",
+                    "type": "panel",
+                    "actions": []
+                }
+            ]
+        }
+    },
+    "config": {
+        "theme": "Default"
+    }
+}

+ 9 - 0
requirement.txt

@@ -0,0 +1,9 @@
+icoextract
+PySide6
+PySideSix-Frameless-Window
+PyInstaller
+elevate
+
+
+
+

+ 19 - 0
todolist.md

@@ -0,0 +1,19 @@
+
+
+
+
+
+- 任务栏图标
+- 图标下文字
+
+
+
+
+
+
+
+
+
+
+
+