diff --git a/README.md b/README.md index a830a95..77a007a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # PixelBash -a GTK + Python simple paint application. \ No newline at end of file +A simple GTK + Python based paint application. + +# Images +![1 Default view starting out. ](images/pic1.png) +![2 Blank canvas. ](images/pic2.png) +![3 Blank canvas being edited. ](images/pic3.png) +![3 Image being edited. ](images/pic3.png) \ No newline at end of file diff --git a/images/pic1.png b/images/pic1.png new file mode 100644 index 0000000..6fef2f0 Binary files /dev/null and b/images/pic1.png differ diff --git a/images/pic2.png b/images/pic2.png new file mode 100644 index 0000000..9f5d21d Binary files /dev/null and b/images/pic2.png differ diff --git a/images/pic3.png b/images/pic3.png new file mode 100644 index 0000000..380071f Binary files /dev/null and b/images/pic3.png differ diff --git a/images/pic4.png b/images/pic4.png new file mode 100644 index 0000000..58881f2 Binary files /dev/null and b/images/pic4.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2aafb68 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/src/__builtins__.py b/src/__builtins__.py new file mode 100644 index 0000000..02595aa --- /dev/null +++ b/src/__builtins__.py @@ -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() + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..954fbae --- /dev/null +++ b/src/__main__.py @@ -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() diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..f3a3fb5 --- /dev/null +++ b/src/app.py @@ -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() \ No newline at end of file diff --git a/src/data/__init__.py b/src/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/data/cbindings/__init__.py b/src/data/cbindings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/data/cbindings/ctypes_cdll_example_not_working/compile.sh b/src/data/cbindings/ctypes_cdll_example_not_working/compile.sh new file mode 100755 index 0000000..c5ca4a8 --- /dev/null +++ b/src/data/cbindings/ctypes_cdll_example_not_working/compile.sh @@ -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 "$@"; diff --git a/src/data/cbindings/ctypes_cdll_example_not_working/example.py b/src/data/cbindings/ctypes_cdll_example_not_working/example.py new file mode 100644 index 0000000..49b66c5 --- /dev/null +++ b/src/data/cbindings/ctypes_cdll_example_not_working/example.py @@ -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() \ No newline at end of file diff --git a/src/data/cbindings/ctypes_cdll_example_not_working/pixels_to_cairo_surface.c b/src/data/cbindings/ctypes_cdll_example_not_working/pixels_to_cairo_surface.c new file mode 100644 index 0000000..9466505 --- /dev/null +++ b/src/data/cbindings/ctypes_cdll_example_not_working/pixels_to_cairo_surface.c @@ -0,0 +1,38 @@ +#include + +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; +} diff --git a/src/data/cbindings/pixbuf2cairo.py b/src/data/cbindings/pixbuf2cairo.py new file mode 100644 index 0000000..71095fe --- /dev/null +++ b/src/data/cbindings/pixbuf2cairo.py @@ -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__() diff --git a/src/data/cbindings/python_package_works/compile.sh b/src/data/cbindings/python_package_works/compile.sh new file mode 100755 index 0000000..cebb6b4 --- /dev/null +++ b/src/data/cbindings/python_package_works/compile.sh @@ -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 "$@"; diff --git a/src/data/cbindings/python_package_works/pixbuf2cairo.c b/src/data/cbindings/python_package_works/pixbuf2cairo.c new file mode 100644 index 0000000..81813b0 --- /dev/null +++ b/src/data/cbindings/python_package_works/pixbuf2cairo.c @@ -0,0 +1,80 @@ +#include +#include +#include +#include + +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); +} diff --git a/src/data/cbindings/python_package_works/setup.py b/src/data/cbindings/python_package_works/setup.py new file mode 100644 index 0000000..e18466c --- /dev/null +++ b/src/data/cbindings/python_package_works/setup.py @@ -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] +) diff --git a/src/data/event.py b/src/data/event.py new file mode 100644 index 0000000..7256402 --- /dev/null +++ b/src/data/event.py @@ -0,0 +1,11 @@ +# Python imports + +# Lib imports +import cairo + +# Application imports + + +class Event: + def __init__(self): + super(Event, self).__init__() \ No newline at end of file diff --git a/src/data/mouse_buttons.py b/src/data/mouse_buttons.py new file mode 100644 index 0000000..4fb89e8 --- /dev/null +++ b/src/data/mouse_buttons.py @@ -0,0 +1,11 @@ +# Python imports + +# Lib imports + +# Application imports + + +class MouseButton: + LEFT_BUTTON = 1 + MIDDLE_BUTTON = 2 + RIGHT_BUTTON = 3 diff --git a/src/data/point.py b/src/data/point.py new file mode 100644 index 0000000..9843cd9 --- /dev/null +++ b/src/data/point.py @@ -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 \ No newline at end of file diff --git a/src/data/points.py b/src/data/points.py new file mode 100644 index 0000000..7b0a747 --- /dev/null +++ b/src/data/points.py @@ -0,0 +1,10 @@ +# Python imports + +# Lib imports + +# Application imports + + +class Points(list): + def __init__(self): + super(Points, self).__init__() \ No newline at end of file diff --git a/src/libs/__init__.py b/src/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/libs/event_collection.py b/src/libs/event_collection.py new file mode 100644 index 0000000..f0ff24c --- /dev/null +++ b/src/libs/event_collection.py @@ -0,0 +1,10 @@ +# Python imports + +# Lib imports + +# Application imports + + +class EventCollection(list): + def __init__(self): + super(EventCollection, self).__init__() \ No newline at end of file diff --git a/src/libs/event_system.py b/src/libs/event_system.py new file mode 100644 index 0000000..cd6975f --- /dev/null +++ b/src/libs/event_system.py @@ -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 diff --git a/src/libs/history_manager.py b/src/libs/history_manager.py new file mode 100644 index 0000000..410846d --- /dev/null +++ b/src/libs/history_manager.py @@ -0,0 +1,10 @@ +# Python imports + +# Lib imports + +# Application imports + + +class HistoryManager(list): + def __init__(self): + super(HistoryManager, self).__init__() \ No newline at end of file diff --git a/src/libs/singleton.py b/src/libs/singleton.py new file mode 100644 index 0000000..23b7191 --- /dev/null +++ b/src/libs/singleton.py @@ -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 diff --git a/src/libs/surface_manager.py b/src/libs/surface_manager.py new file mode 100644 index 0000000..5572663 --- /dev/null +++ b/src/libs/surface_manager.py @@ -0,0 +1,10 @@ +# Python imports + +# Lib imports + +# Application imports + + +class SurfaceManager(list): + def __init__(self): + super(SurfaceManager, self).__init__() \ No newline at end of file diff --git a/src/widgets/__init__.py b/src/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/widgets/brushes/__init__.py b/src/widgets/brushes/__init__.py new file mode 100644 index 0000000..4134dfc --- /dev/null +++ b/src/widgets/brushes/__init__.py @@ -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 \ No newline at end of file diff --git a/src/widgets/brushes/arrow.py b/src/widgets/brushes/arrow.py new file mode 100644 index 0000000..0ace15d --- /dev/null +++ b/src/widgets/brushes/arrow.py @@ -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() diff --git a/src/widgets/brushes/brush_base.py b/src/widgets/brushes/brush_base.py new file mode 100644 index 0000000..9fd8773 --- /dev/null +++ b/src/widgets/brushes/brush_base.py @@ -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...") \ No newline at end of file diff --git a/src/widgets/brushes/circle.py b/src/widgets/brushes/circle.py new file mode 100644 index 0000000..03b93ca --- /dev/null +++ b/src/widgets/brushes/circle.py @@ -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() \ No newline at end of file diff --git a/src/widgets/brushes/erase.py b/src/widgets/brushes/erase.py new file mode 100644 index 0000000..706bfb7 --- /dev/null +++ b/src/widgets/brushes/erase.py @@ -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) diff --git a/src/widgets/brushes/grid.py b/src/widgets/brushes/grid.py new file mode 100644 index 0000000..a6cb9cb --- /dev/null +++ b/src/widgets/brushes/grid.py @@ -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() diff --git a/src/widgets/brushes/line.py b/src/widgets/brushes/line.py new file mode 100644 index 0000000..a0be9f8 --- /dev/null +++ b/src/widgets/brushes/line.py @@ -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) diff --git a/src/widgets/brushes/square.py b/src/widgets/brushes/square.py new file mode 100644 index 0000000..713422a --- /dev/null +++ b/src/widgets/brushes/square.py @@ -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() \ No newline at end of file diff --git a/src/widgets/button_box.py b/src/widgets/button_box.py new file mode 100644 index 0000000..03018f8 --- /dev/null +++ b/src/widgets/button_box.py @@ -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,)) + diff --git a/src/widgets/containers/__init__.py b/src/widgets/containers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/widgets/containers/container.py b/src/widgets/containers/container.py new file mode 100644 index 0000000..c3e45cc --- /dev/null +++ b/src/widgets/containers/container.py @@ -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() ) \ No newline at end of file diff --git a/src/widgets/controls/__init__.py b/src/widgets/controls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/widgets/controls/open_file_button.py b/src/widgets/controls/open_file_button.py new file mode 100644 index 0000000..b3c6bbf --- /dev/null +++ b/src/widgets/controls/open_file_button.py @@ -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 diff --git a/src/widgets/controls/save_as_button.py b/src/widgets/controls/save_as_button.py new file mode 100644 index 0000000..bb7962c --- /dev/null +++ b/src/widgets/controls/save_as_button.py @@ -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 diff --git a/src/widgets/draw_area.py b/src/widgets/draw_area.py new file mode 100644 index 0000000..c001da1 --- /dev/null +++ b/src/widgets/draw_area.py @@ -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 overly 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 overly 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() diff --git a/src/widgets/image_type_dialog.py b/src/widgets/image_type_dialog.py new file mode 100644 index 0000000..83aa89b --- /dev/null +++ b/src/widgets/image_type_dialog.py @@ -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 diff --git a/src/widgets/surface.py b/src/widgets/surface.py new file mode 100644 index 0000000..777d22a --- /dev/null +++ b/src/widgets/surface.py @@ -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) diff --git a/src/window.py b/src/window.py new file mode 100644 index 0000000..638a162 --- /dev/null +++ b/src/window.py @@ -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() \ No newline at end of file