Initial push
This commit is contained in:
16
README.md
16
README.md
@@ -1,3 +1,15 @@
|
||||
# PixelBash
|
||||
`# PixelBash
|
||||
|
||||
a GTK + Python simple paint application.
|
||||
A simple GTK + Python based paint application.
|
||||
|
||||
# Keyboard Shortcuts
|
||||
* (undo) ctrl+z
|
||||
* (redo) ctrl+y
|
||||
* (save as) ctrl+s
|
||||
* (new surface) ctrl+n
|
||||
|
||||
# Images
|
||||

|
||||

|
||||

|
||||

|
BIN
images/pic1.png
Normal file
BIN
images/pic1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
images/pic2.png
Normal file
BIN
images/pic2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
BIN
images/pic3.png
Normal file
BIN
images/pic3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
BIN
images/pic4.png
Normal file
BIN
images/pic4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 410 KiB |
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
PyGObject==3.40.1
|
||||
pycairo==1.28.0
|
||||
pygobject-stubs --no-cache-dir --config-settings=config=Gtk3,Gdk3,Soup2
|
||||
setproctitle==1.2.2
|
37
src/__builtins__.py
Normal file
37
src/__builtins__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Python imports
|
||||
import builtins
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.event_system import EventSystem
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = False)
|
||||
thread.start()
|
||||
return thread
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = True)
|
||||
thread.start()
|
||||
return thread
|
||||
return wrapper
|
||||
|
||||
|
||||
# NOTE: Just reminding myself we can add to builtins two different ways...
|
||||
# __builtins__.update({"event_system": Builtins()})
|
||||
builtins.APP_NAME = "PixelBash"
|
||||
|
||||
builtins.threaded = threaded_wrapper
|
||||
builtins.daemon_threaded = daemon_threaded_wrapper
|
||||
builtins.get_class = lambda x: globals()[x]
|
||||
|
||||
builtins.event_system = EventSystem()
|
||||
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
33
src/__main__.py
Normal file
33
src/__main__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Python imports
|
||||
import argparse
|
||||
import faulthandler
|
||||
import traceback
|
||||
from setproctitle import setproctitle
|
||||
|
||||
import tracemalloc
|
||||
tracemalloc.start()
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from __builtins__ import *
|
||||
from app import Application
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
setproctitle(f"{APP_NAME}")
|
||||
faulthandler.enable()
|
||||
|
||||
app = Application()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
quit()
|
16
src/app.py
Normal file
16
src/app.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from window import Window
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self):
|
||||
super(Application, self).__init__()
|
||||
|
||||
|
||||
def run(self):
|
||||
win = Window()
|
||||
win.start()
|
0
src/data/__init__.py
Normal file
0
src/data/__init__.py
Normal file
0
src/data/cbindings/__init__.py
Normal file
0
src/data/cbindings/__init__.py
Normal file
15
src/data/cbindings/ctypes_cdll_example_not_working/compile.sh
Executable file
15
src/data/cbindings/ctypes_cdll_example_not_working/compile.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# . CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
function main() {
|
||||
cd "$(dirname "")"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
gcc -shared -o pixels_to_cairo_surface.so -fPIC pixels_to_cairo_surface.c
|
||||
}
|
||||
main "$@";
|
@@ -0,0 +1,71 @@
|
||||
# Python imports
|
||||
import ctypes
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
def _pixels_to_cairo_surface(
|
||||
pixbuf: GdkPixbuf.Pixbuf,
|
||||
width: int,
|
||||
height: int
|
||||
) -> cairo.ImageSurface:
|
||||
rowstride = pixbuf.get_rowstride()
|
||||
has_alpha = pixbuf.get_has_alpha()
|
||||
n_channels = pixbuf.get_n_channels()
|
||||
pixels = memoryview(pixbuf.get_pixels())
|
||||
|
||||
cairo_stride = width * 4
|
||||
cairo_data = bytearray(height * cairo_stride)
|
||||
|
||||
lib = ctypes.cdll.LoadLibrary(".pixels_to_cairo_surface.so")
|
||||
IntArray = ctypes.c_int * len(pixels)
|
||||
pixel_buffer = IntArray(*pixels)
|
||||
lib.pixels_to_cairo_surface.restype = ctypes.POINTER(ctypes.c_uint * (height * cairo_stride))
|
||||
lib.pixels_to_cairo_surface.argtypes = [
|
||||
ctypes.c_int,
|
||||
ctypes.c_bool,
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.POINTER(ctypes.c_int)
|
||||
]
|
||||
|
||||
_cairo_data = lib.pixels_to_cairo_surface(
|
||||
rowstride,
|
||||
has_alpha,
|
||||
n_channels,
|
||||
width,
|
||||
height,
|
||||
pixel_buffer
|
||||
)
|
||||
|
||||
cairo_data = _cairo_data.contents
|
||||
|
||||
return cairo.ImageSurface.create_for_data(
|
||||
cairo_data,
|
||||
cairo.FORMAT_ARGB32,
|
||||
width,
|
||||
height,
|
||||
cairo_stride
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
img_path = ""
|
||||
pixbuf = Gtk.Image.new_from_file(img_path).get_pixbuf()
|
||||
width = pixbuf.get_width()
|
||||
height = pixbuf.get_height()
|
||||
_pixels_to_cairo_surface(pixbuf, width, height)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -0,0 +1,38 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
unsigned int *pixels_to_cairo_surface(
|
||||
const int rowstride,
|
||||
const bool has_alpha,
|
||||
const int n_channels,
|
||||
const int width,
|
||||
const int height,
|
||||
const int* pixels
|
||||
) {
|
||||
int cairo_stride = width * 4;
|
||||
unsigned int* cairo_data = malloc(sizeof(unsigned int) * height * cairo_stride);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int p_offset = y * rowstride + x * n_channels;
|
||||
int c_offset = y * cairo_stride + x * 4;
|
||||
|
||||
int r = pixels[p_offset];
|
||||
int g = pixels[p_offset + 1];
|
||||
int b = pixels[p_offset + 2];
|
||||
int a = (has_alpha) ? pixels[p_offset + 3] : 255;
|
||||
|
||||
// Premultiply alpha (important for correct rendering in Cairo)
|
||||
int r_premul = (r * a) / 255;
|
||||
int g_premul = (g * a) / 255;
|
||||
int b_premul = (b * a) / 255;
|
||||
|
||||
// Cairo expects data in BGRA (ARGB32 native endianness)
|
||||
cairo_data[c_offset + 0] = b_premul;
|
||||
cairo_data[c_offset + 1] = g_premul;
|
||||
cairo_data[c_offset + 2] = r_premul;
|
||||
cairo_data[c_offset + 3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
return cairo_data;
|
||||
}
|
9
src/data/cbindings/pixbuf2cairo.py
Normal file
9
src/data/cbindings/pixbuf2cairo.py
Normal file
@@ -0,0 +1,9 @@
|
||||
def __bootstrap__():
|
||||
global __bootstrap__, __loader__, __file__
|
||||
import sys, pkg_resources, importlib.util
|
||||
__file__ = pkg_resources.resource_filename(__name__, 'pixbuf2cairo.cpython-313-x86_64-linux-gnu.so')
|
||||
__loader__ = None; del __bootstrap__, __loader__
|
||||
spec = importlib.util.spec_from_file_location(__name__,__file__)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
__bootstrap__()
|
15
src/data/cbindings/python_package_works/compile.sh
Executable file
15
src/data/cbindings/python_package_works/compile.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# . CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
function main() {
|
||||
cd "$(dirname "")"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
python3 setup.py build && python3 setup.py install --user
|
||||
}
|
||||
main "$@";
|
80
src/data/cbindings/python_package_works/pixbuf2cairo.c
Normal file
80
src/data/cbindings/python_package_works/pixbuf2cairo.c
Normal file
@@ -0,0 +1,80 @@
|
||||
#include <Python.h>
|
||||
#include <cairo.h>
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static PyObject* pixbuf_to_cairo_data(PyObject* self, PyObject* args) {
|
||||
PyObject *py_pixbuf;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &py_pixbuf)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GdkPixbuf *pixbuf = (GdkPixbuf *) PyLong_AsVoidPtr(py_pixbuf);
|
||||
if (!GDK_IS_PIXBUF(pixbuf)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid GdkPixbuf pointer.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int width = gdk_pixbuf_get_width(pixbuf);
|
||||
int height = gdk_pixbuf_get_height(pixbuf);
|
||||
int rowstride = gdk_pixbuf_get_rowstride(pixbuf);
|
||||
int n_channels = gdk_pixbuf_get_n_channels(pixbuf);
|
||||
gboolean has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
|
||||
const guchar *pixels = gdk_pixbuf_get_pixels(pixbuf);
|
||||
|
||||
int cairo_stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
|
||||
Py_ssize_t buffer_size = height * cairo_stride;
|
||||
|
||||
guchar *cairo_data = malloc(buffer_size);
|
||||
if (!cairo_data) {
|
||||
PyErr_NoMemory();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const guchar *src = pixels + y * rowstride;
|
||||
guchar *dst = cairo_data + y * cairo_stride;
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const guchar *p = src + x * n_channels;
|
||||
guchar *q = dst + x * 4;
|
||||
|
||||
guchar r = p[0];
|
||||
guchar g = p[1];
|
||||
guchar b = p[2];
|
||||
guchar a = 255;
|
||||
|
||||
if (has_alpha) {
|
||||
a = p[3];
|
||||
r = (r * a) / 255;
|
||||
g = (g * a) / 255;
|
||||
b = (b * a) / 255;
|
||||
}
|
||||
|
||||
q[0] = b;
|
||||
q[1] = g;
|
||||
q[2] = r;
|
||||
q[3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
return PyBytes_FromStringAndSize((const char *) cairo_data, buffer_size);
|
||||
}
|
||||
|
||||
static PyMethodDef Methods[] = {
|
||||
{"pixbuf_to_cairo_data", pixbuf_to_cairo_data, METH_VARARGS, "Convert GdkPixbuf* to compatible Cairo ImageSurface data format."},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
static struct PyModuleDef moduledef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"pixbuf2cairo",
|
||||
NULL,
|
||||
-1,
|
||||
Methods
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC PyInit_pixbuf2cairo(void) {
|
||||
return PyModule_Create(&moduledef);
|
||||
}
|
31
src/data/cbindings/python_package_works/setup.py
Normal file
31
src/data/cbindings/python_package_works/setup.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from setuptools import setup, Extension
|
||||
import gi
|
||||
gi.require_version("GdkPixbuf", "2.0")
|
||||
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import GObject
|
||||
|
||||
pkg_config_args = [
|
||||
"--cflags", "--libs",
|
||||
"gdk-pixbuf-2.0",
|
||||
"cairo"
|
||||
]
|
||||
|
||||
from subprocess import check_output
|
||||
|
||||
def get_pkgconfig_flags(flag_type):
|
||||
return check_output(["pkg-config", flag_type] + pkg_config_args).decode().split()
|
||||
|
||||
ext = Extension(
|
||||
"pixbuf2cairo",
|
||||
sources=["pixbuf2cairo.c"],
|
||||
include_dirs=[],
|
||||
extra_compile_args=get_pkgconfig_flags("--cflags"),
|
||||
extra_link_args=get_pkgconfig_flags("--libs")
|
||||
)
|
||||
|
||||
setup(
|
||||
name="pixbuf2cairo",
|
||||
version="0.1",
|
||||
ext_modules=[ext]
|
||||
)
|
11
src/data/event.py
Normal file
11
src/data/event.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self):
|
||||
super(Event, self).__init__()
|
11
src/data/mouse_buttons.py
Normal file
11
src/data/mouse_buttons.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class MouseButton:
|
||||
LEFT_BUTTON = 1
|
||||
MIDDLE_BUTTON = 2
|
||||
RIGHT_BUTTON = 3
|
13
src/data/point.py
Normal file
13
src/data/point.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self, x = 0, y = 0):
|
||||
super(Point, self).__init__()
|
||||
|
||||
self.x: int = x
|
||||
self.y: int = y
|
10
src/data/points.py
Normal file
10
src/data/points.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class Points(list):
|
||||
def __init__(self):
|
||||
super(Points, self).__init__()
|
0
src/libs/__init__.py
Normal file
0
src/libs/__init__.py
Normal file
10
src/libs/event_collection.py
Normal file
10
src/libs/event_collection.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class EventCollection(list):
|
||||
def __init__(self):
|
||||
super(EventCollection, self).__init__()
|
73
src/libs/event_system.py
Normal file
73
src/libs/event_system.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Python imports
|
||||
from collections import defaultdict
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
class EventSystem(Singleton):
|
||||
""" Create event system. """
|
||||
|
||||
def __init__(self):
|
||||
self.subscribers = defaultdict(list)
|
||||
self._is_paused = False
|
||||
|
||||
self._subscribe_to_events()
|
||||
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
self.subscribe("pause_event_processing", self._pause_processing_events)
|
||||
self.subscribe("resume_event_processing", self._resume_processing_events)
|
||||
|
||||
def _pause_processing_events(self):
|
||||
self._is_paused = True
|
||||
|
||||
def _resume_processing_events(self):
|
||||
self._is_paused = False
|
||||
|
||||
def subscribe(self, event_type, fn):
|
||||
self.subscribers[event_type].append(fn)
|
||||
|
||||
def unsubscribe(self, event_type, fn):
|
||||
self.subscribers[event_type].remove(fn)
|
||||
|
||||
def unsubscribe_all(self, event_type):
|
||||
self.subscribers.pop(event_type, None)
|
||||
|
||||
def emit(self, event_type, data = None):
|
||||
if self._is_paused and event_type != "resume_event_processing":
|
||||
return
|
||||
|
||||
if event_type in self.subscribers:
|
||||
for fn in self.subscribers[event_type]:
|
||||
if data:
|
||||
if hasattr(data, '__iter__') and not type(data) is str:
|
||||
fn(*data)
|
||||
else:
|
||||
fn(data)
|
||||
else:
|
||||
fn()
|
||||
|
||||
def emit_and_await(self, event_type, data = None):
|
||||
if self._is_paused and event_type != "resume_event_processing":
|
||||
return
|
||||
|
||||
""" NOTE: Should be used when signal has only one listener and vis-a-vis """
|
||||
if event_type in self.subscribers:
|
||||
response = None
|
||||
for fn in self.subscribers[event_type]:
|
||||
if data:
|
||||
if hasattr(data, '__iter__') and not type(data) is str:
|
||||
response = fn(*data)
|
||||
else:
|
||||
response = fn(data)
|
||||
else:
|
||||
response = fn()
|
||||
|
||||
if not response in (None, ''):
|
||||
break
|
||||
|
||||
return response
|
10
src/libs/history_manager.py
Normal file
10
src/libs/history_manager.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class HistoryManager(list):
|
||||
def __init__(self):
|
||||
super(HistoryManager, self).__init__()
|
24
src/libs/singleton.py
Normal file
24
src/libs/singleton.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class SingletonError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Singleton:
|
||||
ccount = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
obj = super(Singleton, cls).__new__(cls)
|
||||
cls.ccount += 1
|
||||
|
||||
if cls.ccount == 2:
|
||||
raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...")
|
||||
|
||||
return obj
|
10
src/libs/surface_manager.py
Normal file
10
src/libs/surface_manager.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class SurfaceManager(list):
|
||||
def __init__(self):
|
||||
super(SurfaceManager, self).__init__()
|
0
src/widgets/__init__.py
Normal file
0
src/widgets/__init__.py
Normal file
7
src/widgets/brushes/__init__.py
Normal file
7
src/widgets/brushes/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .brush_base import BrushBase
|
||||
from .grid import Grid
|
||||
from .arrow import Arrow
|
||||
from .line import Line
|
||||
from .circle import Circle
|
||||
from .square import Square
|
||||
from .erase import Erase
|
45
src/widgets/brushes/arrow.py
Normal file
45
src/widgets/brushes/arrow.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
import traceback
|
||||
|
||||
# Application imports
|
||||
from data.point import Point
|
||||
from data.points import Points
|
||||
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Arrow(BrushBase):
|
||||
def __init__(self):
|
||||
super(Arrow, self).__init__()
|
||||
|
||||
self.points: Points = Points()
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve = None):
|
||||
if len(self.points) < 2:
|
||||
self.points.append( Point(eve.x, eve.y) )
|
||||
return
|
||||
|
||||
self.points[1] = Point(eve.x, eve.y)
|
||||
self.is_valid = True
|
||||
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
|
||||
brush.set_line_width(self.size)
|
||||
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
|
||||
brush.set_source_rgba(*self.color)
|
||||
brush.set_operator(cairo.OPERATOR_OVER);
|
||||
|
||||
brush.move_to(self.points[0].x, self.points[0].y)
|
||||
brush.line_to(self.points[1].x, self.points[1].y)
|
||||
brush.stroke()
|
27
src/widgets/brushes/brush_base.py
Normal file
27
src/widgets/brushes/brush_base.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
from data.event import Event
|
||||
|
||||
|
||||
class UnboundException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class BrushBase(Event):
|
||||
def __init__(self):
|
||||
super(BrushBase, self).__init__()
|
||||
|
||||
self.is_valid: bool = False
|
||||
self.size: int = 12
|
||||
self.color: [] = [0.0, 0.0, 0.0, 1.0]
|
||||
|
||||
|
||||
def update(self, eve):
|
||||
raise UnboundException("Method hasn't been overriden...")
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
raise UnboundException("Method hasn't been overriden...")
|
37
src/widgets/brushes/circle.py
Normal file
37
src/widgets/brushes/circle.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
from data.point import Point
|
||||
from data.points import Points
|
||||
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Circle(BrushBase):
|
||||
def __init__(self):
|
||||
super(Circle, self).__init__()
|
||||
|
||||
self.points: Points = Points()
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve):
|
||||
self.points.append( Point(eve.x, eve.y) )
|
||||
self.is_valid = True
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
brush.set_source_rgba(*self.color)
|
||||
brush.set_operator(cairo.OPERATOR_OVER);
|
||||
|
||||
for point in self.points:
|
||||
brush.move_to(point.x, point.y)
|
||||
brush.arc(point.x, point.y, self.size, 0, 2 * 3.14)
|
||||
brush.fill()
|
42
src/widgets/brushes/erase.py
Normal file
42
src/widgets/brushes/erase.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
from data.point import Point
|
||||
from data.points import Points
|
||||
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Erase(BrushBase):
|
||||
def __init__(self):
|
||||
super(Erase, self).__init__()
|
||||
|
||||
self.points: Points = Points()
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve):
|
||||
self.points.append( Point(eve.x, eve.y) )
|
||||
self.is_valid = True
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
if len(self.points) < 2: return
|
||||
|
||||
brush.set_line_width(self.size)
|
||||
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
|
||||
brush.set_source_rgba(*self.color)
|
||||
brush.set_operator(cairo.OPERATOR_CLEAR);
|
||||
|
||||
brush.move_to(self.points[0].x, self.points[0].y)
|
||||
for point in self.points[1 : -1]:
|
||||
brush.line_to(point.x, point.y)
|
||||
brush.stroke()
|
||||
brush.move_to(point.x, point.y)
|
46
src/widgets/brushes/grid.py
Normal file
46
src/widgets/brushes/grid.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Grid(BrushBase):
|
||||
def __init__(self):
|
||||
super(Grid, self).__init__()
|
||||
|
||||
self.width = 800
|
||||
self.height = 800
|
||||
self.grid_spacing = 12 # px
|
||||
self.color = [0.2, 0.2, 0.2, 0.64] # light gray
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve):
|
||||
self.is_valid = True
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
brush.set_source_rgba(*self.color)
|
||||
|
||||
# Draw vertical lines
|
||||
x = 0
|
||||
while x < self.width:
|
||||
brush.move_to(x, 0)
|
||||
brush.line_to(x, self.height)
|
||||
x += self.grid_spacing
|
||||
|
||||
# Draw horizontal lines
|
||||
y = 0
|
||||
while y < self.height:
|
||||
brush.move_to(0, y)
|
||||
brush.line_to(self.width, y)
|
||||
y += self.grid_spacing
|
||||
|
||||
brush.stroke()
|
43
src/widgets/brushes/line.py
Normal file
43
src/widgets/brushes/line.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
import traceback
|
||||
|
||||
# Application imports
|
||||
from data.point import Point
|
||||
from data.points import Points
|
||||
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Line(BrushBase):
|
||||
def __init__(self):
|
||||
super(Line, self).__init__()
|
||||
|
||||
self.points: Points = Points()
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve = None):
|
||||
self.points.append( Point(eve.x, eve.y) )
|
||||
if len(self.points) < 2: return
|
||||
self.is_valid = True
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
|
||||
brush.set_line_width(self.size)
|
||||
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
|
||||
brush.set_source_rgba(*self.color)
|
||||
brush.set_operator(cairo.OPERATOR_OVER);
|
||||
|
||||
brush.move_to(self.points[0].x, self.points[0].y)
|
||||
for point in self.points[1 : -1]:
|
||||
brush.line_to(point.x, point.y)
|
||||
brush.stroke()
|
||||
brush.move_to(point.x, point.y)
|
43
src/widgets/brushes/square.py
Normal file
43
src/widgets/brushes/square.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
# Application imports
|
||||
from data.point import Point
|
||||
from data.points import Points
|
||||
|
||||
from . import BrushBase
|
||||
|
||||
|
||||
class Square(BrushBase):
|
||||
def __init__(self):
|
||||
super(Square, self).__init__()
|
||||
|
||||
self.points: Points = Points()
|
||||
|
||||
def __copy__(self):
|
||||
copy = type(self)()
|
||||
copy.color = self.color
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def update(self, eve):
|
||||
if len(self.points) < 2:
|
||||
self.points.append( Point(eve.x, eve.y) )
|
||||
return
|
||||
|
||||
self.points[1] = Point(eve.x, eve.y)
|
||||
self.is_valid = True
|
||||
|
||||
def process(self, brush: cairo.Context):
|
||||
x1, y1 = self.points[0].x, self.points[0].y
|
||||
x2, y2 = self.points[1].x, self.points[1].y
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
|
||||
brush.set_source_rgba(*self.color)
|
||||
brush.set_operator(cairo.OPERATOR_OVER);
|
||||
brush.rectangle(x1, y1, w, h)
|
||||
brush.fill()
|
103
src/widgets/button_box.py
Normal file
103
src/widgets/button_box.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
from .controls.save_as_button import SaveAsButton
|
||||
|
||||
|
||||
class ButtonBox(Gtk.Box):
|
||||
def __init__(self):
|
||||
super(ButtonBox, self).__init__()
|
||||
|
||||
self.active_btn = None
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
|
||||
def _load_widgets(self):
|
||||
arrow_btn = Gtk.ToggleButton(label = "Arrow")
|
||||
line_btn = Gtk.ToggleButton(label = "Line")
|
||||
circle_btn = Gtk.ToggleButton(label = "Circle")
|
||||
square_btn = Gtk.ToggleButton(label = "Square")
|
||||
erase_btn = Gtk.ToggleButton(label = "Erase")
|
||||
# save_btn = Gtk.Button(label = "Save")
|
||||
save_btn = SaveAsButton()
|
||||
color_btn = Gtk.ColorButton()
|
||||
size_btn = Gtk.SpinButton()
|
||||
|
||||
self.active_btn = line_btn
|
||||
line_btn.set_active(True)
|
||||
|
||||
color_btn.set_title("Pick a Color")
|
||||
color_btn.set_use_alpha(True)
|
||||
color_btn.set_rgba( Gdk.RGBA(0.0, 0.0, 0.0, 1.0) )
|
||||
|
||||
size_btn.set_numeric(True)
|
||||
size_btn.set_digits(0)
|
||||
size_btn.set_snap_to_ticks(True)
|
||||
size_btn.set_increments(1.0, 10.0)
|
||||
size_btn.set_range(1.0, 100.0)
|
||||
size_btn.set_value(12.0)
|
||||
|
||||
color_btn.connect("color-set", self.set_brush_color)
|
||||
size_btn.connect("value-changed", self.set_brush_size)
|
||||
|
||||
self.add(arrow_btn)
|
||||
self.add(line_btn)
|
||||
self.add(circle_btn)
|
||||
self.add(square_btn)
|
||||
self.add(color_btn)
|
||||
self.add(size_btn)
|
||||
self.add(erase_btn)
|
||||
self.add(save_btn)
|
||||
|
||||
save_btn.connect("clicked", self.save_image)
|
||||
# Note: For buttons and mapping assignment. Spin and Color bttn will
|
||||
# conveniently throw errors OR not be set regarding get_label and NOT map.
|
||||
for child in self.get_children():
|
||||
child.set_hexpand(True)
|
||||
try:
|
||||
if child.get_label():
|
||||
child.set_can_focus(False)
|
||||
child.connect("released", self.set_brush_mode, child.get_label())
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
def set_brush_color(self, widget: any):
|
||||
event_system.emit("set-brush-color", (widget.get_rgba(),))
|
||||
|
||||
def set_brush_size(self, widget: any):
|
||||
event_system.emit("set-brush-size", (widget.get_value(),))
|
||||
|
||||
def set_brush_mode(self, widget: any, mode: str):
|
||||
if mode in ["Save As"]: return
|
||||
|
||||
self.active_btn.set_active(False)
|
||||
widget.set_active(True)
|
||||
self.active_btn = widget
|
||||
|
||||
event_system.emit("set-brush-mode", (mode,))
|
||||
|
||||
def save_image(self, widget: any):
|
||||
fpath = event_system.emit_and_await("save-as")
|
||||
if not fpath: return
|
||||
event_system.emit("save-image", (fpath,))
|
||||
|
0
src/widgets/containers/__init__.py
Normal file
0
src/widgets/containers/__init__.py
Normal file
57
src/widgets/containers/container.py
Normal file
57
src/widgets/containers/container.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from widgets.button_box import ButtonBox
|
||||
from widgets.draw_area import DrawArea
|
||||
|
||||
|
||||
class ScrollWindow(Gtk.ScrolledWindow):
|
||||
def __init__(self):
|
||||
super(ScrollWindow, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_hexpand(True)
|
||||
self.set_vexpand(True)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
view_port = Gtk.Viewport()
|
||||
|
||||
view_port.add( DrawArea() )
|
||||
self.add(view_port)
|
||||
|
||||
class Container(Gtk.Box):
|
||||
def __init__(self):
|
||||
super(Container, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _load_widgets(self):
|
||||
self.add( ButtonBox() )
|
||||
self.add( ScrollWindow() )
|
0
src/widgets/controls/__init__.py
Normal file
0
src/widgets/controls/__init__.py
Normal file
78
src/widgets/controls/open_file_button.py
Normal file
78
src/widgets/controls/open_file_button.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class OpenFileButton(Gtk.Button):
|
||||
"""docstring for OpenFileButton."""
|
||||
|
||||
def __init__(self):
|
||||
super(OpenFileButton, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_label("Open Image...")
|
||||
self.set_image( Gtk.Image.new_from_icon_name("gtk-open", 4) )
|
||||
self.set_always_show_image(True)
|
||||
self.set_image_position(1) # Left - 0, Right = 1
|
||||
self.set_hexpand(False)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("open-file", self._open_file)
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
def _open_file(self, widget = None, eve = None, gfile = None):
|
||||
start_dir = None
|
||||
|
||||
if gfile and gfile.query_exists():
|
||||
start_dir = gfile.get_parent()
|
||||
|
||||
chooser = Gtk.FileChooserDialog("Open Image...", None,
|
||||
Gtk.FileChooserAction.OPEN,
|
||||
(
|
||||
Gtk.STOCK_CANCEL,
|
||||
Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OPEN,
|
||||
Gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
|
||||
chooser.set_select_multiple(False)
|
||||
|
||||
try:
|
||||
folder = widget.get_current_file().get_parent() if not start_dir else start_dir
|
||||
chooser.set_current_folder( folder.get_path() )
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
response = chooser.run()
|
||||
if not response == Gtk.ResponseType.OK:
|
||||
print("response")
|
||||
chooser.destroy()
|
||||
return _gfiles
|
||||
|
||||
filename = chooser.get_filename()
|
||||
if not filename:
|
||||
chooser.destroy()
|
||||
return _gfile
|
||||
|
||||
chooser.destroy()
|
||||
return filename
|
68
src/widgets/controls/save_as_button.py
Normal file
68
src/widgets/controls/save_as_button.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class SaveAsButton(Gtk.Button):
|
||||
def __init__(self):
|
||||
super(SaveAsButton, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_label("Save As")
|
||||
self.set_image( Gtk.Image.new_from_icon_name("gtk-save-as", 4) )
|
||||
self.set_always_show_image(True)
|
||||
self.set_image_position(1) # Left - 0, Right = 1
|
||||
self.set_hexpand(False)
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("save-as", self._save_as)
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
def _save_as(self, widget = None, eve = None, gfile = None):
|
||||
start_dir = None
|
||||
_gfile = None
|
||||
|
||||
chooser = Gtk.FileChooserDialog("Save File As...", None,
|
||||
Gtk.FileChooserAction.SAVE,
|
||||
(
|
||||
Gtk.STOCK_CANCEL,
|
||||
Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE_AS,
|
||||
Gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
|
||||
response = chooser.run()
|
||||
if not response == Gtk.ResponseType.OK:
|
||||
chooser.destroy()
|
||||
return _gfile
|
||||
|
||||
file = chooser.get_filename()
|
||||
if not file:
|
||||
chooser.destroy()
|
||||
return _gfile
|
||||
|
||||
path = file if os.path.isabs(file) else os.path.abspath(file)
|
||||
|
||||
print(f"File To Save As: {path}")
|
||||
chooser.destroy()
|
||||
return path
|
225
src/widgets/draw_area.py
Normal file
225
src/widgets/draw_area.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# Python imports
|
||||
from copy import copy, deepcopy
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
from libs.surface_manager import SurfaceManager
|
||||
from libs.event_collection import EventCollection
|
||||
|
||||
from data.mouse_buttons import MouseButton
|
||||
|
||||
from .surface import Surface
|
||||
|
||||
from . import brushes
|
||||
|
||||
|
||||
class DrawArea(Gtk.DrawingArea):
|
||||
def __init__(self):
|
||||
super(DrawArea, self).__init__()
|
||||
|
||||
self.background_surface: Surface = None
|
||||
self.primary_surface: Surface = None
|
||||
self.intermediate_surface: Surface = None
|
||||
self.surface_manager: SurfaceManager = SurfaceManager()
|
||||
self.event_collection: EventCollection = EventCollection()
|
||||
|
||||
self.is_drawing: bool = False
|
||||
self.brush_mode: str = "Line"
|
||||
self.grid_brush: BrushBase = getattr(brushes, "Grid")()
|
||||
self.brush: BrushBase = getattr(brushes, self.brush_mode)()
|
||||
self.brush_color = self.brush.color
|
||||
self.brush_size = self.brush.size
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
self._set_new_surface()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
margin_px = 24
|
||||
self.set_margin_top(margin_px)
|
||||
self.set_margin_left(margin_px)
|
||||
self.set_margin_right(margin_px)
|
||||
self.set_margin_end(margin_px)
|
||||
|
||||
def _setup_signals(self):
|
||||
self.set_can_focus(True)
|
||||
self.set_size_request(800, 600)
|
||||
|
||||
self.add_events(Gdk.EventMask.KEY_PRESS_MASK)
|
||||
self.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
|
||||
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
|
||||
self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
|
||||
self.add_events(Gdk.EventMask.BUTTON1_MOTION_MASK)
|
||||
|
||||
self.connect("key-release-event", self._key_release_event)
|
||||
self.connect("button-press-event", self._button_press_event)
|
||||
self.connect("button-release-event", self._button_release_event)
|
||||
self.connect("motion-notify-event", self._motion_notify_event)
|
||||
self.connect("draw", self._draw)
|
||||
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("save-image", self._save_image)
|
||||
event_system.subscribe("set-brush-color", self._set_brush_color)
|
||||
event_system.subscribe("set-brush-size", self._set_brush_size)
|
||||
event_system.subscribe("set-brush-mode", self._set_brush_mode)
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
def _set_new_surface(self):
|
||||
size_w, \
|
||||
size_h, \
|
||||
image_surface = event_system.emit_and_await("get-image-size")
|
||||
|
||||
self.grid_brush.width = size_w
|
||||
self.grid_brush.height = size_h
|
||||
self.background_surface = Surface(size_w, size_h)
|
||||
self.primary_surface = Surface(size_w, size_h)
|
||||
self.intermediate_surface = Surface(size_w, size_h)
|
||||
|
||||
if image_surface:
|
||||
self.primary_surface.create_new_area(image_surface)
|
||||
self.primary_surface.set_image_data(image_surface.get_data())
|
||||
|
||||
self.event_collection.clear()
|
||||
self.surface_manager.clear()
|
||||
|
||||
self.grid_brush.update(None)
|
||||
self.set_size_request(size_w, size_h)
|
||||
self.background_surface.update(self.grid_brush)
|
||||
self.surface_manager.append( self.primary_surface )
|
||||
|
||||
def _save_image(self, fpath: str):
|
||||
self.primary_surface.area.write_to_png(fpath)
|
||||
|
||||
def _do_save_image(self):
|
||||
fpath = event_system.emit_and_await("save-as")
|
||||
if not fpath: return
|
||||
self._save_image(fpath)
|
||||
|
||||
def _set_brush_color(self, color: Gdk.RGBA):
|
||||
self.brush = getattr(brushes, self.brush_mode)()
|
||||
self.brush_color = [color.red, color.green, color.blue, color.alpha]
|
||||
self.brush.color = self.brush_color
|
||||
|
||||
def _set_brush_size(self, size: int):
|
||||
self.brush_size = size
|
||||
self.brush = getattr(brushes, self.brush_mode)()
|
||||
self.brush.size = self.brush_size
|
||||
|
||||
def _set_brush_mode(self, mode: str):
|
||||
self.brush_mode = mode
|
||||
self.brush = getattr(brushes, self.brush_mode)()
|
||||
self.brush.color = self.brush_color
|
||||
self.brush.size = self.brush_size
|
||||
|
||||
def _button_press_event(self, widget, eve):
|
||||
if not self.has_focus():
|
||||
self.grab_focus()
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_PRESS and eve.button == MouseButton.LEFT_BUTTON:
|
||||
self.event_collection.clear()
|
||||
|
||||
self.is_drawing = True
|
||||
self._set_brush_mode(self.brush_mode)
|
||||
self.intermediate_surface.update(self.brush)
|
||||
self.brush.update(eve)
|
||||
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_PRESS and eve.button == MouseButton.RIGHT_BUTTON:
|
||||
...
|
||||
|
||||
def _button_release_event(self, widget, eve):
|
||||
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.LEFT_BUTTON:
|
||||
self.is_drawing = False
|
||||
self.intermediate_surface.history_manager.clear()
|
||||
|
||||
if not self.brush.is_valid: return
|
||||
|
||||
self.primary_surface.update(self.brush)
|
||||
self.queue_draw()
|
||||
|
||||
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON:
|
||||
...
|
||||
|
||||
def _key_release_event(self, widget, eve):
|
||||
keyname = Gdk.keyval_name(eve.keyval)
|
||||
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
|
||||
is_control = True if modifiers & Gdk.ModifierType.CONTROL_MASK else False
|
||||
is_shift = True if modifiers & Gdk.ModifierType.SHIFT_MASK else False
|
||||
|
||||
if is_control:
|
||||
if keyname == "z":
|
||||
if len(self.primary_surface.history_manager) == 0: return
|
||||
self.event_collection.append( self.primary_surface.history_manager.pop() )
|
||||
elif keyname == "y":
|
||||
if len(self.event_collection) == 0: return
|
||||
self.primary_surface.history_manager.append( self.event_collection.pop() )
|
||||
elif keyname == "n":
|
||||
self._set_new_surface()
|
||||
elif keyname == "s":
|
||||
self._do_save_image()
|
||||
|
||||
self.queue_draw()
|
||||
|
||||
|
||||
def _motion_notify_event(self, area, eve):
|
||||
if not self.is_drawing: return
|
||||
self.brush.update(eve)
|
||||
self.queue_draw()
|
||||
|
||||
def _draw(self, area, brush: cairo.Context):
|
||||
self._draw_background(area, brush)
|
||||
self._draw_surfaces(area, brush)
|
||||
self._draw_overlay(area, brush)
|
||||
|
||||
return False
|
||||
|
||||
def _draw_background(self, area, brush: cairo.Context):
|
||||
# Note: While drawing, only overlay needs to re-calculate its stack.
|
||||
# This can just copy what exists than re-calculating the surface.
|
||||
if self.is_drawing:
|
||||
brush.set_source_surface(self.background_surface.area, 0.0, 0.0)
|
||||
brush.paint()
|
||||
return
|
||||
|
||||
brush.set_source_surface(self.background_surface.area, 0.0, 0.0)
|
||||
self.background_surface.draw()
|
||||
brush.paint()
|
||||
|
||||
def _draw_surfaces(self, area, brush: cairo.Context):
|
||||
# Note: While drawing, only overlay needs to re-calculate its stack.
|
||||
# This can just copy what exists than re-calculating each surface.
|
||||
if self.is_drawing:
|
||||
for surface in self.surface_manager:
|
||||
brush.set_source_surface(surface.area, 0.0, 0.0)
|
||||
brush.paint()
|
||||
|
||||
return
|
||||
|
||||
for surface in self.surface_manager:
|
||||
brush.set_source_surface(surface.area, 0.0, 0.0)
|
||||
surface.draw()
|
||||
brush.paint()
|
||||
|
||||
def _draw_overlay(self, area, brush: cairo.Context):
|
||||
# Note: When NOT drawing, no overlay data should exist nor be processed...
|
||||
if not self.is_drawing:
|
||||
self.intermediate_surface.clear_surface()
|
||||
return
|
||||
|
||||
brush.set_source_surface(self.intermediate_surface.area, 0.0, 0.0)
|
||||
self.intermediate_surface.draw()
|
||||
brush.paint()
|
135
src/widgets/image_type_dialog.py
Normal file
135
src/widgets/image_type_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# Python imports
|
||||
import ctypes
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
# Application imports
|
||||
from .controls.open_file_button import OpenFileButton
|
||||
from data.cbindings import pixbuf2cairo
|
||||
|
||||
|
||||
class ImageTypeDialog(Gtk.Dialog):
|
||||
def __init__(self):
|
||||
super(ImageTypeDialog, self).__init__()
|
||||
|
||||
self.img_path = None
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_size_request(420, 320)
|
||||
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("get-image-size", self._get_image_size)
|
||||
|
||||
def _load_widgets(self):
|
||||
vbox = self.get_content_area()
|
||||
hbox1 = Gtk.Box()
|
||||
hbox2 = Gtk.Box()
|
||||
head_lbl = Gtk.Label("Image Size")
|
||||
size_w_lbl = Gtk.Label("Width:")
|
||||
size_h_lbl = Gtk.Label("Height:")
|
||||
open_file_btn = OpenFileButton()
|
||||
|
||||
self.size_w_btn = Gtk.SpinButton()
|
||||
self.size_h_btn = Gtk.SpinButton()
|
||||
|
||||
self.size_w_btn.set_numeric(True)
|
||||
self.size_w_btn.set_digits(0)
|
||||
self.size_w_btn.set_snap_to_ticks(True)
|
||||
self.size_w_btn.set_increments(1.0, 100.0)
|
||||
self.size_w_btn.set_range(32.0, 10000.0)
|
||||
self.size_w_btn.set_value(800.0)
|
||||
|
||||
self.size_h_btn.set_numeric(True)
|
||||
self.size_h_btn.set_digits(0)
|
||||
self.size_h_btn.set_snap_to_ticks(True)
|
||||
self.size_h_btn.set_increments(1.0, 100.0)
|
||||
self.size_h_btn.set_range(32.0, 10000.0)
|
||||
self.size_h_btn.set_value(600.0)
|
||||
|
||||
hbox1.set_margin_top(24)
|
||||
head_lbl.set_hexpand(True)
|
||||
size_w_lbl.set_hexpand(True)
|
||||
size_h_lbl.set_hexpand(True)
|
||||
open_file_btn.set_hexpand(True)
|
||||
open_file_btn.set_margin_top(24)
|
||||
open_file_btn.set_margin_bottom(24)
|
||||
open_file_btn.set_margin_left(24)
|
||||
open_file_btn.set_margin_right(24)
|
||||
self.size_w_btn.set_margin_right(24)
|
||||
self.size_h_btn.set_margin_right(24)
|
||||
self.size_w_btn.set_hexpand(True)
|
||||
self.size_h_btn.set_hexpand(True)
|
||||
|
||||
open_file_btn.connect("button-release-event", self._open_files)
|
||||
|
||||
vbox.add(head_lbl)
|
||||
hbox1.add(size_w_lbl)
|
||||
hbox1.add(self.size_w_btn)
|
||||
hbox2.add(size_h_lbl)
|
||||
hbox2.add(self.size_h_btn)
|
||||
vbox.add(hbox1)
|
||||
vbox.add(hbox2)
|
||||
vbox.add(open_file_btn)
|
||||
|
||||
self.add_button("Continue", Gtk.ResponseType.OK)
|
||||
|
||||
vbox.show_all()
|
||||
|
||||
def _get_image_size(self):
|
||||
self.img_path = None
|
||||
|
||||
response = self.run()
|
||||
if response == -4 or response == Gtk.ResponseType.OK:
|
||||
self.hide()
|
||||
|
||||
surface = None
|
||||
width, height = self.size_w_btn.get_value_as_int(), self.size_h_btn.get_value_as_int()
|
||||
if self.img_path:
|
||||
pixbuf = Gtk.Image.new_from_file(self.img_path).get_pixbuf()
|
||||
width = pixbuf.get_width()
|
||||
height = pixbuf.get_height()
|
||||
surface = self._pixels_to_cairo_surface(pixbuf, width, height)
|
||||
|
||||
return width, height, surface
|
||||
|
||||
def _open_files(self, widget, eve):
|
||||
self.img_path = event_system.emit_and_await("open-file")
|
||||
|
||||
|
||||
def _pixels_to_cairo_surface(
|
||||
self,
|
||||
pixbuf: GdkPixbuf.Pixbuf,
|
||||
width: int,
|
||||
height: int
|
||||
) -> cairo.ImageSurface:
|
||||
pixbuf_ptr = int(hash(pixbuf)) # WARNING: not stable across runs
|
||||
cairo_data = pixbuf2cairo.pixbuf_to_cairo_data(pixbuf_ptr)
|
||||
cairo_stride = width * 4
|
||||
|
||||
return cairo.ImageSurface.create_for_data(
|
||||
bytearray(cairo_data),
|
||||
cairo.FORMAT_ARGB32,
|
||||
width,
|
||||
height,
|
||||
cairo_stride
|
||||
)
|
||||
|
||||
return surface
|
53
src/widgets/surface.py
Normal file
53
src/widgets/surface.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import cairo
|
||||
|
||||
|
||||
# Application imports
|
||||
from libs.history_manager import HistoryManager
|
||||
|
||||
from data.event import Event
|
||||
|
||||
|
||||
class Surface:
|
||||
def __init__(self, w = 1, h = 1):
|
||||
super(Surface, self).__init__()
|
||||
|
||||
self.image_data = None
|
||||
|
||||
self.history_manager: HistoryManager = HistoryManager()
|
||||
self.create_new_area(None, w, h)
|
||||
|
||||
|
||||
# Note: https://martimm.github.io/gnome-gtk3/content-docs/tutorial/Cairo/drawing-model.html
|
||||
def create_new_area(self, surface: cairo.ImageSurface = None, w: int = None, h: int = None):
|
||||
if surface:
|
||||
self.area = surface
|
||||
self.brush = cairo.Context(surface)
|
||||
return
|
||||
|
||||
self.area: cairo.ImageSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
|
||||
self.brush: cairo.Context = cairo.Context(self.area)
|
||||
|
||||
def set_image_data(self, data: any):
|
||||
self.image_data = bytearray(data)
|
||||
|
||||
def update(self, event: Event):
|
||||
self.history_manager.append(event)
|
||||
|
||||
def draw(self):
|
||||
if not self.image_data:
|
||||
self.clear_surface()
|
||||
elif self.image_data:
|
||||
dest_data = self.area.get_data()
|
||||
dest_data[:len(self.image_data)] = self.image_data[:]
|
||||
|
||||
for event in self.history_manager:
|
||||
if not event.is_valid: continue
|
||||
event.process(self.brush)
|
||||
|
||||
def clear_surface(self):
|
||||
self.brush.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.brush.paint()
|
||||
self.brush.set_operator(cairo.OPERATOR_OVER)
|
59
src/window.py
Normal file
59
src/window.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Python imports
|
||||
import signal
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from widgets.containers.container import Container
|
||||
from widgets.image_type_dialog import ImageTypeDialog
|
||||
|
||||
|
||||
class Window(Gtk.ApplicationWindow):
|
||||
def __init__(self):
|
||||
super(Window, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
_min_width = 720
|
||||
_min_height = 480
|
||||
_width = 1200
|
||||
_height = 800
|
||||
|
||||
self.set_title(f"{APP_NAME}")
|
||||
self.set_gravity(5) # 5 = CENTER
|
||||
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
|
||||
self.set_size_request(_min_width, _min_height)
|
||||
self.set_default_size(_width, _height)
|
||||
|
||||
# self.set_interactive_debugging(True)
|
||||
|
||||
def _setup_signals(self):
|
||||
self.connect("delete-event", self.stop)
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.stop)
|
||||
|
||||
def _load_widgets(self):
|
||||
image_type_dialog = ImageTypeDialog()
|
||||
|
||||
image_type_dialog.set_attached_to(self)
|
||||
|
||||
self.add( Container() )
|
||||
|
||||
def start(self):
|
||||
Gtk.main()
|
||||
|
||||
def stop(self, widget = None, eve = None):
|
||||
size = self.get_size()
|
||||
pos = self.get_position()
|
||||
|
||||
Gtk.main_quit()
|
Reference in New Issue
Block a user