Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanNazaruk committed Jan 1, 2023
1 parent 1dfe317 commit 0cf7ace
Show file tree
Hide file tree
Showing 13 changed files with 716 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.idea/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
16 changes: 16 additions & 0 deletions DearPyGui_DragAndDrop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import threading

from . import main as __main
from . import setup as __setup
from .main import DROPEFFECT, KEYSTATE
from .main import DragAndDrop, DragAndDropDataObject
from .main import set_drag_enter, set_drag_over, set_drag_leave, set_drop
from .main import set_drop_effect, get_drop_effect

__version__ = "1.0.0"

def initialize():
if isinstance(__main._DragAndDropForFunctions, type):
__main._DragAndDropForFunctions = __main._DragAndDropForFunctions()

threading.Thread(target=__setup.setup, daemon=True).start()
145 changes: 145 additions & 0 deletions DearPyGui_DragAndDrop/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import annotations

import traceback
from enum import IntEnum
from pathlib import Path
from typing import Type, TypeVar, Dict, Callable, Any, Union, List

import dearpygui.dearpygui as dpg

DragAndDropDataObject = Union[None, str, List[Path]]
SubscriptionTag = TypeVar('SubscriptionTag', bound=int)


class KEYSTATE(IntEnum):
LEFT = 1
RIGHT = 2
SHIFT = 4
CTRL = 8
MIDDLE = 16
ALT = 32


class DROPEFFECT(IntEnum):
NONE = 0
COPY = 1
MOVE = 2


_now_drop_effect: DROPEFFECT = DROPEFFECT.MOVE


def set_drop_effect(effect: DROPEFFECT = DROPEFFECT.NONE):
global _now_drop_effect
_now_drop_effect = effect


def get_drop_effect() -> DROPEFFECT:
return _now_drop_effect


class DragAndDrop():
__subscribers: Dict[SubscriptionTag, Type[DragAndDrop]] = {}
__subscription_tag: SubscriptionTag = None

def DragEnter(self, dataObject: DragAndDropDataObject, keyState: list[KEYSTATE]):
...

def DragOver(self, keyState: list[KEYSTATE]):
...

def DragLeave(self):
...

def Drop(self, dataObject: DragAndDropDataObject, keyState: list[KEYSTATE]):
...

def __init__(self):
if self.__subscription_tag:
self._unsubscribe(self.__subscription_tag)
self.__subscription_tag = self._subscribe(self) # noqa

def __del__(self):
if self.__subscription_tag:
self._unsubscribe(self.__subscription_tag)

@classmethod
def _subscribe(cls, self: Type[DragAndDrop]) -> SubscriptionTag:
subscription_tag = dpg.generate_uuid()
cls.__subscribers[subscription_tag] = self
return subscription_tag

@classmethod
def _unsubscribe(self, subscription_tag: SubscriptionTag):
if subscription_tag in self.__subscribers:
del self.__subscribers[subscription_tag]

@classmethod
def _DragEnter(cls, dataObject, keyState):
for self in cls.__subscribers.values():
try:
self.DragEnter(dataObject, keyState) # noqa
except Exception:
traceback.print_exc()

@classmethod
def _DragOver(cls, keyState):
for self in cls.__subscribers.values():
try:
self.DragOver(keyState) # noqa
except Exception:
traceback.print_exc()

@classmethod
def _DragLeave(cls):
for self in cls.__subscribers.values():
try:
self.DragLeave() # noqa
except Exception:
traceback.print_exc()

@classmethod
def _Drop(cls, dataObject, keyState):
for self in cls.__subscribers.values():
try:
self.Drop(dataObject, keyState) # noqa
except Exception:
traceback.print_exc()


class _DragAndDropForFunctions(DragAndDrop):
def DragEnter(self, dataObject, keyState):
...

def DragOver(self, keyState):
...

def DragLeave(self):
...

def Drop(self, dataObject, keyState):
...


def set_drag_enter(function: Callable[[DragAndDropDataObject, list[KEYSTATE]], Any] = None):
if function is None:
function = lambda *args, **kwargs: ...
_DragAndDropForFunctions.DragEnter = function


def set_drag_over(function: Callable[[list[KEYSTATE]], Any] = None):
if function is None:
function = lambda *args, **kwargs: ...
_DragAndDropForFunctions.DragOver = function


def set_drag_leave(function: Callable[[], Any] = None):
if function is None:
function = lambda *args, **kwargs: ...
_DragAndDropForFunctions.DragLeave = function


def set_drop(function: Callable[[DragAndDropDataObject, list[KEYSTATE]], Any] = None):
if function is None:
function = lambda *args, **kwargs: ...
_DragAndDropForFunctions.Drop = function
64 changes: 64 additions & 0 deletions DearPyGui_DragAndDrop/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import ctypes
import os
import time

import dearpygui.dearpygui as dpg
import pythoncom
import win32gui
from win32com.server.policy import DesignatedWrapPolicy

from . import tools
from .main import DragAndDrop
from .main import get_drop_effect

hwnd = -1


class DropTarget(DesignatedWrapPolicy):
_public_methods_ = ['DragEnter', 'DragOver', 'DragLeave', 'Drop']
_com_interfaces_ = [pythoncom.IID_IDropTarget] # noqa

def __init__(self): # noqa
self._wrap_(self)

def DragEnter(self, dataObject, keyState, point, effect):
ctypes.windll.user32.SetForegroundWindow(hwnd)
DragAndDrop._DragEnter(tools.get_data_from_dataObject(dataObject),
tools.key_state_to_keys_list(keyState))
return 0, get_drop_effect()
# return winerror.S_OK, get_drop_effect()

def DragOver(self, keyState, point, effect):
DragAndDrop._DragOver(tools.key_state_to_keys_list(keyState))
return get_drop_effect()

def DragLeave(self):
DragAndDrop._DragLeave()

def Drop(self, dataObject, keyState, point, effect):
DragAndDrop._Drop(tools.get_data_from_dataObject(dataObject),
tools.key_state_to_keys_list(keyState))


def setup():
global hwnd
if hwnd != -1:
return
hwnd = 0
# Wait for initialization and appearance of the DPG window
while dpg.get_frame_count() == 0:
time.sleep(0.1)

# Get the window hwnd from its own pid
hwnd = tools.get_hwnd_from_pid(os.getpid())

pythoncom.OleInitialize() # noqa
pythoncom.RegisterDragDrop( # noqa
hwnd,
pythoncom.WrapObject( # noqa
DropTarget(),
pythoncom.IID_IDropTarget, # noqa
pythoncom.IID_IDropTarget # noqa
)
)
win32gui.PumpMessages()
87 changes: 87 additions & 0 deletions DearPyGui_DragAndDrop/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from __future__ import annotations

import ctypes.wintypes
from ctypes import wintypes

import pythoncom
import pywintypes
import win32con
from win32comext.shell import shell

from .main import DragAndDropDataObject
from .main import KEYSTATE


def get_data_from_dataObject(dataObject) -> DragAndDropDataObject:
try:
# Drag&Drop files
_format = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL # noqa
sm = dataObject.GetData(_format)
except pywintypes.com_error: # noqa
try:
# Drag&Drop text
_format = win32con.CF_TEXT, None, 1, -1, pythoncom.TYMED_HGLOBAL # noqa
sm = dataObject.GetData(_format)
# returns a string converted from bytes
return sm.data.decode("utf-8")
except pywintypes.com_error: # noqa
# Drag&Drop unknown type
# return nothing
return None

# Drag&Drop files
num_files = shell.DragQueryFile(sm.data_handle, -1)
files = []
for i in range(num_files):
fpath = shell.DragQueryFile(sm.data_handle, i)
files.append(fpath)
# returns a list of file paths
return files


user32 = ctypes.windll.user32
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL,
wintypes.HWND,
wintypes.LPARAM)
user32.EnumWindows.argtypes = [WNDENUMPROC,
wintypes.LPARAM]


def get_hwnd_from_pid(pid: int) -> int | None:
result = None

def callback(hwnd, _):
nonlocal result
lpdw_PID = ctypes.c_ulong()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(lpdw_PID))
hwnd_PID = lpdw_PID.value

if hwnd_PID == pid:
result = hwnd
return False
return True

cb_worker = WNDENUMPROC(callback)
user32.EnumWindows(cb_worker, 0)
return result


def key_state_to_keys_list(keyState: int) -> list[KEYSTATE]:
# https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.drageventargs.keystate?view=windowsdesktop-7.0#remarks
key_state_list = []

# conversion to byte string and reverse it
keyState = '{0:06b}'.format(keyState)[::-1]
if bool(int(keyState[0])):
key_state_list.append(KEYSTATE.LEFT)
if bool(int(keyState[1])):
key_state_list.append(KEYSTATE.RIGHT)
if bool(int(keyState[2])):
key_state_list.append(KEYSTATE.SHIFT)
if bool(int(keyState[3])):
key_state_list.append(KEYSTATE.CTRL)
if bool(int(keyState[4])):
key_state_list.append(KEYSTATE.MIDDLE)
if bool(int(keyState[5])):
key_state_list.append(KEYSTATE.ALT)
return key_state_list
50 changes: 50 additions & 0 deletions Examples/EADME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Examples

You can find several scenarios here, from simple to advanced, for using the library.<br>
Below you can see what they look like and their brief description.

## [Example №1: the simplest use case](example1.py)

Probably the simplest and most used example of using.
Drop the file into the program and our `drop` (change text) function will be called.
![example_1](https://user-images.githubusercontent.com/46572469/210179706-f187b70b-6649-44d4-880b-a6b35a961d08.gif)

## [Example №2: little interactivity](example2.py)

An example with a little interactivity, in the form of a pop-up window that says `Drop!`
(This is "similar" to the popup in Discord when you try to send a file by drag and drop).
This one has more functions, but also seems to be nothing complicated.
In `drag_enter` and `drag_leave` we hide and show the window respectively, and in `drop` we add hiding the window.
![example_2](https://user-images.githubusercontent.com/46572469/210180323-1ab73614-0f35-49e8-b5e1-7c65c332e44b.gif)

## [Example №3: more interactivity!](example3.py)

In this example, you can only drop a file in the popup window.
Added a function `drag_over` which checks the pointing to the window and, depending on the result,
changes the theme of the window and the style of the cursor.
![example_3](https://user-images.githubusercontent.com/46572469/210180650-b91c77d8-19da-4452-ad69-f8caa0201c32.gif)

## [Example №4: keys usage](example4.py)

Let's expand our example again, now the window theme depends on the key presses (CTRL, ALT, SHIFT).
![example_4](https://user-images.githubusercontent.com/46572469/210181492-3a9bca4a-005a-4e5c-b8cf-4a39b041ef27.gif)

## [Example №5: inheriting *DragAndDrop* or how to add an extra function to Drag And Drop](example4.py)

Probably you need a lot of *DragAndDrop* functions when developing some applications,
and only one function can be attached to `set_drop`.
And there is a solution for this, namely to use the class *DragAndDrop*, or rather inherit it.
When `__init__` is called in *DragAndDrop* (`super().__init__()`),
this object (`self`) gets to the Drag-and-Drop functions queue,
where certain functions will be called in the future, for example `{DragAndDrop_oject}.DragOver`.
This example uses only `DragOver` and `Drop`, but also has `DragEnter` and `DragLeave`
which correspond to the functions `.set_drag_enter` and `.set_drag_leave` respectively.
I would also like to add that all functions `.set_*` are called first in the queue
(if you `.initialize()` before the DragAndDrop classes).
And the "function" of setting the cursor style will be called after the queue is finished
(that is, the last called `.set_drop_effect` will be taken).<br>

A little about the example: there are several "areas" in
which you can drop a file and the text in them will change when it `drop`

![example_5](https://user-images.githubusercontent.com/46572469/210182508-d2232d43-0df3-4531-a34b-e773f9000ef9.gif)
25 changes: 25 additions & 0 deletions Examples/example1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import dearpygui.dearpygui as dpg

import DearPyGui_DragAndDrop as dpg_dnd

dpg.create_context()
dpg_dnd.initialize()
dpg.create_viewport(title="Drag and drop example1", width=600, height=600)

with dpg.window() as window:
drop_text = dpg.add_text()
drop_keys = dpg.add_text()
dpg.set_primary_window(window, True)


def drop(data, keys):
dpg.set_value(drop_text, f'{data}')
dpg.set_value(drop_keys, f'{keys}')


dpg_dnd.set_drop(drop)

dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
Loading

0 comments on commit 0cf7ace

Please sign in to comment.