From d5de4decf9eac7e3a431a4a6cef1dfa22e597b6e Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 18:27:38 +0200 Subject: [PATCH 01/22] Modernize TBO to GTK4 --- .gitignore | 32 +- ChangeLog | 12 - HACKING | 37 -- INSTALL | 374 ++----------- Makefile.am | 8 - NEWS | 0 README | 60 +- TODO | 4 - VERSION | 2 +- archlinux/PKGBUILD | 48 +- autogen.sh | 24 - configure.ac | 36 -- data/Makefile.am | 25 - .../net.danigm.tbo.desktop} | 11 +- data/icons/new.svg | 6 + data/icons/redo.svg | 4 + data/icons/undo.svg | 4 + data/icons/zoom-fit.svg | 4 + debian/README | 6 - debian/changelog | 19 +- debian/compat | 1 - debian/control | 38 +- debian/copyright | 68 +-- debian/dirs | 2 - debian/docs | 3 - debian/rules | 8 +- debian/source/format | 1 + debian/tests/control | 3 + debian/tests/smoke | 4 + meson.build | 153 +++++ po/Makevars | 43 -- po/POTFILES | 31 + po/POTFILES.in | 62 -- po/meson.build | 7 + src/Makefile.am | 90 --- src/comic-load.c | 67 ++- src/comic-new-dialog.c | 108 +++- src/comic-open-dialog.c | 55 +- src/comic-saveas-dialog.c | 42 +- src/comic.c | 47 +- src/comic.h | 3 +- src/custom-stock.c | 70 --- src/custom-stock.h | 33 -- src/dnd.c | 215 +++---- src/dnd.h | 24 +- src/doodle-treeview.c | 223 +++++--- src/doodle-treeview.h | 2 +- src/export.c | 256 ++++++--- src/frame.c | 10 +- src/frame.h | 5 +- src/page.c | 2 +- src/tbo-drawing.c | 176 +++--- src/tbo-drawing.h | 8 +- src/tbo-file-dialog.c | 195 +++++++ src/tbo-file-dialog.h | 22 + src/tbo-files.c | 40 +- src/tbo-files.h | 4 +- src/tbo-object-base.c | 2 +- src/tbo-object-base.h | 3 +- src/tbo-object-group.c | 3 +- src/tbo-object-group.h | 3 +- src/tbo-object-pixmap.c | 176 +++++- src/tbo-object-pixmap.h | 9 +- src/tbo-object-svg.c | 161 ++++-- src/tbo-object-svg.h | 8 +- src/tbo-object-text.c | 36 +- src/tbo-object-text.h | 11 +- src/tbo-tool-base.c | 11 +- src/tbo-tool-base.h | 11 +- src/tbo-tool-bubble.c | 8 +- src/tbo-tool-bubble.h | 3 +- src/tbo-tool-doodle.c | 53 +- src/tbo-tool-doodle.h | 4 +- src/tbo-tool-frame.c | 16 +- src/tbo-tool-frame.h | 3 +- src/tbo-tool-selector.c | 309 +++++++--- src/tbo-tool-selector.h | 8 +- src/tbo-tool-text.c | 267 ++++++--- src/tbo-tool-text.h | 4 +- src/tbo-toolbar.c | 521 +++++++++-------- src/tbo-toolbar.h | 42 +- src/tbo-tooltip.c | 10 +- src/tbo-tooltip.h | 2 +- src/tbo-types.h | 16 +- src/tbo-ui-utils.c | 12 +- src/tbo-undo.c | 2 +- src/tbo-undo.h | 4 +- src/tbo-utils.c | 13 + src/tbo-utils.h | 1 + src/tbo-widget.c | 273 +++++++++ src/tbo-widget.h | 38 ++ src/tbo-window.c | 528 ++++++++++++++---- src/tbo-window.h | 30 +- src/tbo.c | 67 ++- src/typestest.c | 81 --- src/ui-menu.c | 492 +++++++++------- src/undotest.c | 84 --- tbo.doap | 19 - test/cairo1.py | 42 -- test/cairosamples/Makefile | 11 - test/cairosamples/togtk.c | 54 -- test/cairosamples/topdf.c | 27 - test/cairosamples/topng.c | 26 - test/cairotest.py | 157 ------ test/ejemplo_2.py | 67 --- test/globo.svg | 65 --- test/rsvgtest.py | 33 -- test/xml/gmarkup.c | 90 --- test/xml/simple.xml | 6 - tests/asset_bounds_check.c | 56 ++ tests/frame_tool_check.c | 48 ++ tests/load_render_check.c | 51 ++ tests/save_roundtrip_check.c | 68 +++ 113 files changed, 3831 insertions(+), 3151 deletions(-) delete mode 100644 ChangeLog delete mode 100644 HACKING delete mode 100644 Makefile.am delete mode 100644 NEWS delete mode 100644 TODO delete mode 100755 autogen.sh delete mode 100644 configure.ac delete mode 100644 data/Makefile.am rename data/{tbo.desktop => applications/net.danigm.tbo.desktop} (50%) create mode 100644 data/icons/new.svg create mode 100644 data/icons/redo.svg create mode 100644 data/icons/undo.svg create mode 100644 data/icons/zoom-fit.svg delete mode 100644 debian/README delete mode 100644 debian/compat delete mode 100644 debian/dirs delete mode 100644 debian/docs create mode 100644 debian/source/format create mode 100644 debian/tests/control create mode 100755 debian/tests/smoke create mode 100644 meson.build delete mode 100644 po/Makevars create mode 100644 po/POTFILES delete mode 100644 po/POTFILES.in create mode 100644 po/meson.build delete mode 100644 src/Makefile.am delete mode 100644 src/custom-stock.c delete mode 100644 src/custom-stock.h create mode 100644 src/tbo-file-dialog.c create mode 100644 src/tbo-file-dialog.h create mode 100644 src/tbo-widget.c create mode 100644 src/tbo-widget.h delete mode 100644 src/typestest.c delete mode 100644 src/undotest.c delete mode 100644 tbo.doap delete mode 100644 test/cairo1.py delete mode 100644 test/cairosamples/Makefile delete mode 100644 test/cairosamples/togtk.c delete mode 100644 test/cairosamples/topdf.c delete mode 100644 test/cairosamples/topng.c delete mode 100644 test/cairotest.py delete mode 100644 test/ejemplo_2.py delete mode 100644 test/globo.svg delete mode 100644 test/rsvgtest.py delete mode 100644 test/xml/gmarkup.c delete mode 100644 test/xml/simple.xml create mode 100644 tests/asset_bounds_check.c create mode 100644 tests/frame_tool_check.c create mode 100644 tests/load_render_check.c create mode 100644 tests/save_roundtrip_check.c diff --git a/.gitignore b/.gitignore index 6b66a75..39fbb97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,6 @@ *.swp *.o *.log -Makefile -Makefile.in *~ -ABOUT-NLS -aclocal.m4 -autom4te.cache/ build/ -config.h -config.h.in -config.status -configure -libtool -m4/ -package/ -po/ChangeLog -po/Makefile.in.in -po/Makevars.template -po/POTFILES -po/Rules-quot -po/boldquot.sed -po/en@boldquot.header -po/en@quot.header -po/es.gmo -po/insert-header.sin -po/quot.sed -po/remove-potcdate.sin -po/stamp-it -po/tbo.pot -po/*.gmo -src/.deps/ -src/tbo -st -stamp-h1 +build-*/ diff --git a/ChangeLog b/ChangeLog deleted file mode 100644 index 354768b..0000000 --- a/ChangeLog +++ /dev/null @@ -1,12 +0,0 @@ -2010-02-11 gettextize - - * m4/gettext.m4: New file, from gettext-0.17. - * m4/iconv.m4: New file, from gettext-0.17. - * m4/lib-ld.m4: New file, from gettext-0.17. - * m4/lib-link.m4: New file, from gettext-0.17. - * m4/lib-prefix.m4: New file, from gettext-0.17. - * m4/nls.m4: New file, from gettext-0.17. - * m4/po.m4: New file, from gettext-0.17. - * m4/progtest.m4: New file, from gettext-0.17. - * Makefile.am (EXTRA_DIST): Add build/config.rpath. - diff --git a/HACKING b/HACKING deleted file mode 100644 index abe2382..0000000 --- a/HACKING +++ /dev/null @@ -1,37 +0,0 @@ -Collaborating -------------- - -If you like that project and you want to collaborate there is some -parts where you can do something that you know. - - * Create a 'doodle library'. - * 'translate' to your mother language. - * 'package' for your favourite distribution. - * If you are a coder take a look to the TODO file. - -Doodle library --------------- - -A doodle library is a set of .svg files with doodles, shapes, or -something that you want to can add in a TBO frame easy. Is easy to -create your own doodle library, it's only a folder with one or more -folder inside, and each folder with one or more .svg files. - -Look for an example the 'foo' or 'bubble' libraries in data/doodle. - -Translations ------------- - -To generate translations file .po exec: - -$ cd po -$ intltool-update es - -'es' is the language code. Then you need to translate the .po file. - ---- -You can send any collaboration to 'AUTHORS' and I will be happy. - -Follow the project development in github: -http://github.com/danigm/tbo fork the project and ask me to merge and -I will be happier. diff --git a/INSTALL b/INSTALL index 7d1c323..99387e7 100644 --- a/INSTALL +++ b/INSTALL @@ -1,365 +1,73 @@ Installation Instructions ************************* -Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005, -2006, 2007, 2008, 2009 Free Software Foundation, Inc. - - Copying and distribution of this file, with or without modification, -are permitted in any medium without royalty provided the copyright -notice and this notice are preserved. This file is offered as-is, -without warranty of any kind. +This project is built with Meson and targets GTK4. Basic Installation ================== - Briefly, the shell commands `./configure; make; make install' should -configure, build, and install this package. The following -more-detailed instructions are generic; see the `README' file for -instructions specific to this package. Some packages provide this -`INSTALL' file but do not implement all of the features documented -below. The lack of an optional feature in a given package is not -necessarily a bug. More recommendations for GNU packages can be found -in *note Makefile Conventions: (standards)Makefile Conventions. - - The `configure' shell script attempts to guess correct values for -various system-dependent variables used during compilation. It uses -those values to create a `Makefile' in each directory of the package. -It may also create one or more `.h' files containing system-dependent -definitions. Finally, it creates a shell script `config.status' that -you can run in the future to recreate the current configuration, and a -file `config.log' containing compiler output (useful mainly for -debugging `configure'). - - It can also use an optional file (typically called `config.cache' -and enabled with `--cache-file=config.cache' or simply `-C') that saves -the results of its tests to speed up reconfiguring. Caching is -disabled by default to prevent problems with accidental use of stale -cache files. - - If you need to do unusual things to compile the package, please try -to figure out how `configure' could check whether to do them, and mail -diffs or instructions to the address given in the `README' so they can -be considered for the next release. If you are using the cache, and at -some point `config.cache' contains results you don't want to keep, you -may remove or edit it. - - The file `configure.ac' (or `configure.in') is used to create -`configure' by a program called `autoconf'. You need `configure.ac' if -you want to change it or regenerate `configure' using a newer version -of `autoconf'. - - The simplest way to compile this package is: - - 1. `cd' to the directory containing the package's source code and type - `./configure' to configure the package for your system. - - Running `configure' might take a while. While running, it prints - some messages telling which features it is checking for. - - 2. Type `make' to compile the package. - - 3. Optionally, type `make check' to run any self-tests that come with - the package, generally using the just-built uninstalled binaries. - - 4. Type `make install' to install the programs and any data files and - documentation. When installing into a prefix owned by root, it is - recommended that the package be configured and built as a regular - user, and only the `make install' phase executed with root - privileges. - - 5. Optionally, type `make installcheck' to repeat any self-tests, but - this time using the binaries in their final installed location. - This target does not install anything. Running this target as a - regular user, particularly if the prior `make install' required - root privileges, verifies that the installation completed - correctly. - - 6. You can remove the program binaries and object files from the - source code directory by typing `make clean'. To also remove the - files that `configure' created (so you can compile the package for - a different kind of computer), type `make distclean'. There is - also a `make maintainer-clean' target, but that is intended mainly - for the package's developers. If you use it, you may have to get - all sorts of other programs in order to regenerate files that came - with the distribution. - - 7. Often, you can also type `make uninstall' to remove the installed - files again. In practice, not all packages have tested that - uninstallation works correctly, even though it is required by the - GNU Coding Standards. - - 8. Some packages, particularly those that use Automake, provide `make - distcheck', which can by used by developers to test that all other - targets like `make install' and `make uninstall' work correctly. - This target is generally not run by end users. - -Compilers and Options -===================== - - Some systems require unusual options for compilation or linking that -the `configure' script does not know about. Run `./configure --help' -for details on some of the pertinent environment variables. - - You can give `configure' initial values for configuration parameters -by setting variables in the command line or in the environment. Here -is an example: - - ./configure CC=c99 CFLAGS=-g LIBS=-lposix - - *Note Defining Variables::, for more details. - -Compiling For Multiple Architectures -==================================== - - You can compile the package for more than one kind of computer at the -same time, by placing the object files for each architecture in their -own directory. To do this, you can use GNU `make'. `cd' to the -directory where you want the object files and executables to go and run -the `configure' script. `configure' automatically checks for the -source code in the directory that `configure' is in and in `..'. This -is known as a "VPATH" build. - - With a non-GNU `make', it is safer to compile the package for one -architecture at a time in the source code directory. After you have -installed the package for one architecture, use `make distclean' before -reconfiguring for another architecture. - - On MacOS X 10.5 and later systems, you can create libraries and -executables that work on multiple system types--known as "fat" or -"universal" binaries--by specifying multiple `-arch' options to the -compiler but only a single `-arch' option to the preprocessor. Like -this: - - ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CPP="gcc -E" CXXCPP="g++ -E" - - This is not guaranteed to produce working output in all cases, you -may have to build one architecture at a time and combine the results -using the `lipo' tool if you have problems. - -Installation Names -================== - - By default, `make install' installs the package's commands under -`/usr/local/bin', include files under `/usr/local/include', etc. You -can specify an installation prefix other than `/usr/local' by giving -`configure' the option `--prefix=PREFIX', where PREFIX must be an -absolute file name. +The simplest way to build and install TBO is: - You can specify separate installation prefixes for -architecture-specific files and architecture-independent files. If you -pass the option `--exec-prefix=PREFIX' to `configure', the package uses -PREFIX as the prefix for installing programs and libraries. -Documentation and other data files still use the regular prefix. + 1. Install the required build dependencies: - In addition, if you use an unusual directory layout you can give -options like `--bindir=DIR' to specify different values for particular -kinds of files. Run `configure --help' for a list of the directories -you can set and what kinds of files go in them. In general, the -default for these options is expressed in terms of `${prefix}', so that -specifying just `--prefix' will affect all of the other directory -specifications that were not explicitly provided. + * meson + * ninja + * pkg-config + * gtk4 + * cairo + * librsvg-2.0 + * gettext (for translations) - The most portable way to affect installation locations is to pass the -correct locations to `configure'; however, many packages provide one or -both of the following shortcuts of passing variable assignments to the -`make install' command line to change installation locations without -having to reconfigure or recompile. + 2. Configure the build directory: - The first method involves providing an override variable for each -affected directory. For example, `make install -prefix=/alternate/directory' will choose an alternate location for all -directory configuration variables that were expressed in terms of -`${prefix}'. Any directories that were specified during `configure', -but not in terms of `${prefix}', must each be overridden at install -time for the entire installation to be relocated. The approach of -makefile variable overrides for each directory variable is required by -the GNU Coding Standards, and ideally causes no recompilation. -However, some platforms have known limitations with the semantics of -shared libraries that end up requiring recompilation when using this -method, particularly noticeable in packages that use GNU Libtool. + meson setup build - The second method involves providing the `DESTDIR' variable. For -example, `make install DESTDIR=/alternate/directory' will prepend -`/alternate/directory' before all installation names. The approach of -`DESTDIR' overrides is not required by the GNU Coding Standards, and -does not work on platforms that have drive letters. On the other hand, -it does better at avoiding recompilation issues, and works well even -when some directory options were not specified in terms of `${prefix}' -at `configure' time. + 3. Compile the project: -Optional Features -================= + meson compile -C build - If the package supports it, you can cause programs to be installed -with an extra prefix or suffix on their names by giving `configure' the -option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + 4. Run the smoke-test suite: - Some packages pay attention to `--enable-FEATURE' options to -`configure', where FEATURE indicates an optional part of the package. -They may also pay attention to `--with-PACKAGE' options, where PACKAGE -is something like `gnu-as' or `x' (for the X Window System). The -`README' should mention any `--enable-' and `--with-' options that the -package recognizes. + meson test -C build --no-rebuild --print-errorlogs --num-processes 1 - For packages that use the X Window System, `configure' can usually -find the X include and library files automatically, but if it doesn't, -you can use the `configure' options `--x-includes=DIR' and -`--x-libraries=DIR' to specify their locations. + 5. Install it locally: - Some packages offer the ability to configure how verbose the -execution of `make' will be. For these packages, running `./configure ---enable-silent-rules' sets the default to minimal output, which can be -overridden with `make V=1'; while running `./configure ---disable-silent-rules' sets the default to verbose, which can be -overridden with `make V=0'. + meson install -C build -Particular systems -================== - - On HP-UX, the default C compiler is not ANSI C compatible. If GNU -CC is not installed, it is recommended to use the following options in -order to use an ANSI C compiler: - - ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" - -and if that doesn't work, install pre-built binaries of GCC for HP-UX. - - On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot -parse its `' header file. The option `-nodtk' can be used as -a workaround. If GNU CC is not installed, it is therefore recommended -to try - - ./configure CC="cc" - -and if that doesn't work, try - - ./configure CC="cc -nodtk" - - On Solaris, don't put `/usr/ucb' early in your `PATH'. This -directory contains several dysfunctional programs; working variants of -these programs are available in `/usr/bin'. So, if you need `/usr/ucb' -in your `PATH', put it _after_ `/usr/bin'. - - On Haiku, software installed for all users goes in `/boot/common', -not `/usr/local'. It is recommended to use the following options: - - ./configure --prefix=/boot/common - -Specifying the System Type +Running Without Installing ========================== - There may be some features `configure' cannot figure out -automatically, but needs to determine by the type of machine the package -will run on. Usually, assuming the package is built to be run on the -_same_ architectures, `configure' can figure that out, but if it prints -a message saying it cannot guess the machine type, give it the -`--build=TYPE' option. TYPE can either be a short name for the system -type, such as `sun4', or a canonical name which has the form: - - CPU-COMPANY-SYSTEM - -where SYSTEM can have one of these forms: - - OS - KERNEL-OS - - See the file `config.sub' for the possible values of each field. If -`config.sub' isn't included in this package, then this package doesn't -need to know the machine type. - - If you are _building_ compiler tools for cross-compiling, you should -use the option `--target=TYPE' to select the type of system they will -produce code for. - - If you want to _use_ a cross compiler, that generates code for a -platform different from the build platform, you should specify the -"host" platform (i.e., that on which the generated programs will -eventually be run) with `--host=TYPE'. - -Sharing Defaults -================ - - If you want to set default values for `configure' scripts to share, -you can create a site shell script called `config.site' that gives -default values for variables like `CC', `cache_file', and `prefix'. -`configure' looks for `PREFIX/share/config.site' if it exists, then -`PREFIX/etc/config.site' if it exists. Or, you can set the -`CONFIG_SITE' environment variable to the location of the site script. -A warning: not all `configure' scripts look for a site script. - -Defining Variables -================== - - Variables not defined in a site shell script can be set in the -environment passed to `configure'. However, some packages may run -configure again during the build, and the customized values of these -variables may be lost. In order to avoid this problem, you should set -them in the `configure' command line, using `VAR=value'. For example: - - ./configure CC=/usr/local2/bin/gcc - -causes the specified `gcc' to be used as the C compiler (unless it is -overridden in the site shell script). - -Unfortunately, this technique does not work for `CONFIG_SHELL' due to -an Autoconf bug. Until the bug is fixed you can use this workaround: - - CONFIG_SHELL=/bin/bash /bin/bash ./configure CONFIG_SHELL=/bin/bash - -`configure' Invocation -====================== +You can run the uninstalled binary directly from the build directory: - `configure' recognizes the following options to control how it -operates. + ./build/tbo -`--help' -`-h' - Print a summary of all of the options to `configure', and exit. +Open the bundled sample comic with: -`--help=short' -`--help=recursive' - Print a summary of the options unique to this package's - `configure', and exit. The `short' variant lists options used - only in the top level, while the `recursive' variant lists options - also present in any nested packages. + ./build/tbo data/tut.tbo -`--version' -`-V' - Print the version of Autoconf used to generate the `configure' - script, and exit. +Translations +============ -`--cache-file=FILE' - Enable the cache: use and save the results of the tests in FILE, - traditionally `config.cache'. FILE defaults to `/dev/null' to - disable caching. +Translations live in the `po/` directory and are built automatically by Meson +when gettext tools are available. -`--config-cache' -`-C' - Alias for `--cache-file=config.cache'. +Packaging +========= -`--quiet' -`--silent' -`-q' - Do not print messages saying which checks are being made. To - suppress all normal output, redirect it to `/dev/null' (any error - messages will still be shown). +The repository currently includes: -`--srcdir=DIR' - Look for the package's source code in directory DIR. Usually - `configure' can determine that directory automatically. + * `archlinux/PKGBUILD` for Arch-based systems + * `debian/` packaging metadata for Debian-based systems -`--prefix=DIR' - Use DIR as the installation prefix. *note Installation Names:: - for more details, including other options available for fine-tuning - the installation locations. +These packaging files are expected to install the application and desktop files +in the standard system locations so that icons, desktop integration and launch +behavior work correctly on a modern Linux desktop. -`--no-create' -`-n' - Run the configure checks, but stop before creating any output - files. +Cleaning +======== -`configure' also accepts some other, not widely useful, options. Run -`configure --help' for more details. +To remove the build directory and rebuild from scratch, simply delete `build/` +and configure it again: + rm -rf build + meson setup build diff --git a/Makefile.am b/Makefile.am deleted file mode 100644 index aeaa0d1..0000000 --- a/Makefile.am +++ /dev/null @@ -1,8 +0,0 @@ -## Process this file with automake to generate Makefile.in -SUBDIRS = src data po - -ACLOCAL_AMFLAGS = -I m4 - -EXTRA_DIST = AUTHORS TODO - -CLEANFILES = *~ diff --git a/NEWS b/NEWS deleted file mode 100644 index e69de29..0000000 diff --git a/README b/README index 521706f..b02aa7d 100644 --- a/README +++ b/README @@ -1,6 +1,59 @@ TBO is an easy and fun program to draw comic and make your presentations funnier. +Building +-------- + +TBO now uses Meson as its build system and GTK4 as its UI toolkit. + +Required tools and libraries: + + * meson + * ninja + * pkg-config + * gtk4 + * cairo + * librsvg-2.0 + * gettext + +Build it from a fresh checkout with: + + meson setup build + meson compile -C build + +Run the application with: + + ./build/tbo + +Open the bundled sample comic with: + + ./build/tbo data/tut.tbo + +Install it locally with: + + meson install -C build + +Run the smoke-test suite with: + + meson test -C build --no-rebuild --print-errorlogs --num-processes 1 + +Translations +------------ + +Translations are stored in `po/` and built automatically by Meson when gettext +tools are available. + +Packaging +--------- + +The repository includes packaging metadata for: + + * Arch Linux: `archlinux/PKGBUILD` + * Debian-based systems: `debian/` + +These files are intended to install the desktop file, icons and locale data in +their standard system locations. + Using TBO --------- @@ -19,8 +72,9 @@ When you run TBO you are in "page view", and here you can: * You can clone a frame pressing ctrl+d * Create new page frames -When you have a frame you can go to "frame view" by double clicking in -the frame with "selector" tool. In frame view you can draw: +When you have a frame you can go to "frame view" by selecting it and +double clicking it or pressing Enter. +In frame view you can draw: * You can add some doodle with doodle-tool, selecting what you want from tool-area and drag&drop it into drawing area. @@ -60,4 +114,4 @@ A .tbo file looks like that: -Read HACKING for information about how you can collaborate. +This repository is self-contained and intended to be cloned, built and run directly with Meson. diff --git a/TODO b/TODO deleted file mode 100644 index b19ec8d..0000000 --- a/TODO +++ /dev/null @@ -1,4 +0,0 @@ -TBO TODO things: ----------------- - - * Fix gtk warnings diff --git a/VERSION b/VERSION index d3827e7..cd5ac03 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0 +2.0 diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index 7389d5f..20118d3 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -1,36 +1,32 @@ -# Maintainer: Daniel Garcia +# Maintainer: Jaime pkgname=tbo-git -pkgver=20110623 +pkgver=r1.0 pkgrel=1 -pkgdesc="Gnome easy and fun comic editor" -arch=('i686' 'x86_64') -url="http://trac.danigm.net/tbo" +pkgdesc="Comic editor built with GTK4" +arch=('x86_64') +url="https://github.com/j4imefoo/TBO" license=('GPL3') -depends=('gtk3' 'cairo' 'librsvg' 'git') -makedepends=('gnome-common git intltool automake gcc') -source=() -md5sums=() +depends=('gtk4' 'cairo' 'librsvg') +makedepends=('git' 'meson' 'ninja' 'pkgconf' 'gettext') +checkdepends=('xorg-server-xvfb') +source=("git+https://github.com/j4imefoo/TBO.git") +sha256sums=('SKIP') -_gitroot="git://git.danigm.net/tbo.git" -_gitname="tbo" +pkgver() { + cd TBO + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} build() { - if [ -d ${srcdir}/${_gitname} ] - then - msg "Updateing local repository..." - cd ${_gitname} - git pull origin master || return 1 - msg "The local files are updated." - else - git clone ${_gitroot} ${_gitname} - fi - msg "git checkout done or server timeout" - msg "Starting make..." + arch-meson TBO build -Dprefix=/usr + meson compile -C build +} - cp -r ${srcdir}/${_gitname} ${srcdir}/${_gitname}-build - cd ${srcdir}/${_gitname}-build +check() { + xvfb-run -a meson test -C build --no-rebuild --print-errorlogs --num-processes 1 +} - ./autogen.sh --prefix=/usr || return 1 - make DESTDIR=${pkgdir} install || return 1 +package() { + meson install -C build --destdir "$pkgdir" } diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index b54dc7e..0000000 --- a/autogen.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# Run this to generate all the initial makefiles, etc. - -srcdir=`dirname $0` -test -z "$srcdir" && srcdir=. - -PKG_NAME="tbo" - -(test -f $srcdir/src/tbo.c) || { - echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" - echo " top-level $PKG_NAME directory" - exit 1 -} - -which gnome-autogen.sh || { - echo "You need to install gnome-common from GNOME SVN and make" - echo "sure the gnome-autogen.sh script is in your \$PATH." - exit 1 -} - -REQUIRED_AUTOMAKE_VERSION=1.9 -REQUIRED_LIBTOOL_VERSION=2.2 -REQUIRED_INTLTOOL_VERSION=0.40.4 -. gnome-autogen.sh diff --git a/configure.ac b/configure.ac deleted file mode 100644 index d6fcc25..0000000 --- a/configure.ac +++ /dev/null @@ -1,36 +0,0 @@ -AC_PREREQ(2.60) -AC_INIT([tbo], [1.0], [dani@danigm.net]) -AC_CONFIG_AUX_DIR([build]) -AM_INIT_AUTOMAKE([1.9.6 -Wall -Werror dist-bzip2]) - -GNOME_COMMON_INIT - -AC_PROG_CC -# Compiling sources with per-target flags requires AM_PROG_CC_C_O -AM_PROG_CC_C_O -AC_PROG_INSTALL -AC_PROG_LIBTOOL - -AM_PATH_GTK_3_0([3.0.0],,AC_MSG_ERROR([Gtk+ 3.0.0 or higher required.])) -PKG_CHECK_MODULES(PACKAGE, "gtk+-3.0 cairo librsvg-2.0") - -# ******************************* -# Internationalization -# ******************************* - -AC_PROG_INTLTOOL -GETTEXT_PACKAGE=tbo -AC_SUBST([GETTEXT_PACKAGE]) -AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"],[Gettext package]) -AM_GLIB_GNU_GETTEXT - -AC_CONFIG_HEADERS([config.h]) -AC_CONFIG_FILES([ - Makefile - src/Makefile - data/Makefile - data/doodle/Makefile - po/Makefile.in -]) - -AC_OUTPUT diff --git a/data/Makefile.am b/data/Makefile.am deleted file mode 100644 index b776b20..0000000 --- a/data/Makefile.am +++ /dev/null @@ -1,25 +0,0 @@ -SUBDIRS = doodle - -tbodir = $(datadir)/tbo -tbo_DATA = tutorial.pdf tut.tbo *.png - -tbouidir = $(datadir)/tbo/ui -tboui_DATA = ui/*.xml - -tboicondir = $(datadir)/tbo/icons -tboicon_DATA = icons/*.svg - -appdir = $(datadir)/applications/ -app_DATA = tbo.desktop - -appicondir = $(datadir)/pixmaps/ -appicon_DATA = tbo.svg - -EXTRA_DIST = \ - *.png \ - ui/*.xml \ - icons/*.svg \ - tbo.desktop \ - tut.tbo \ - tutorial.pdf \ - tbo.svg diff --git a/data/tbo.desktop b/data/applications/net.danigm.tbo.desktop similarity index 50% rename from data/tbo.desktop rename to data/applications/net.danigm.tbo.desktop index 5877041..fbe5d4c 100644 --- a/data/tbo.desktop +++ b/data/applications/net.danigm.tbo.desktop @@ -1,10 +1,11 @@ [Desktop Entry] -Encoding=UTF-8 -Exec=tbo -Icon=tbo Type=Application -Terminal=false Name=TBO -GenericName=TBO +GenericName=Comic Editor +Comment=Create comic strips and illustrated presentations +Exec=tbo %f +Icon=tbo +Terminal=false StartupNotify=true +StartupWMClass=net.danigm.tbo Categories=Graphics;VectorGraphics;GTK; diff --git a/data/icons/new.svg b/data/icons/new.svg new file mode 100644 index 0000000..92d3a95 --- /dev/null +++ b/data/icons/new.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/redo.svg b/data/icons/redo.svg new file mode 100644 index 0000000..36cc9e2 --- /dev/null +++ b/data/icons/redo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/undo.svg b/data/icons/undo.svg new file mode 100644 index 0000000..3783c03 --- /dev/null +++ b/data/icons/undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/zoom-fit.svg b/data/icons/zoom-fit.svg new file mode 100644 index 0000000..65a45d3 --- /dev/null +++ b/data/icons/zoom-fit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/debian/README b/debian/README deleted file mode 100644 index f93b6e0..0000000 --- a/debian/README +++ /dev/null @@ -1,6 +0,0 @@ -The Debian Package tbo ----------------------------- - -Comments regarding the Package - - -- Daniel Garcia Mon, 26 Apr 2010 09:51:33 +0200 diff --git a/debian/changelog b/debian/changelog index e7469f4..babaf21 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,18 +1,5 @@ -tbo (1.0.0) unstable; urgency=low +tbo (1.0-1) unstable; urgency=medium - * New upstream release. + * Repackage project on top of Meson and GTK4. - -- Daniel Garcia Sat, 13 Apr 2013 06:40:04 -0400 - -tbo (0.98~git20100623+2295523-0ubuntu1) lucid; urgency=low - - * New upstream release - * debian/rules: simplified by using cdbs - - -- Roberto C. Morano Wed, 30 Jun 2010 13:46:12 +0200 - -tbo (0.93.001.git.e250) unstable; urgency=low - - * Initial Release. - - -- Daniel Garcia Mon, 26 Apr 2010 09:51:33 +0200 + -- Jaime Fri, 17 Apr 2026 17:00:00 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 45a4fb7..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -8 diff --git a/debian/control b/debian/control index f499307..24e7a21 100644 --- a/debian/control +++ b/debian/control @@ -1,23 +1,25 @@ Source: tbo -Section: gnome +Section: graphics Priority: optional -Maintainer: Daniel Garcia -Uploaders: Roberto C. Morano -Build-Depends: cdbs, - debhelper (>= 8), - pkg-config, - libglib2.0-dev, - libgtk-3-dev, - librsvg2-dev, - gnome-common, - intltool -Standards-Version: 3.9.2 -Homepage: http://github.com/danigm/TBO +Maintainer: Jaime +Build-Depends: + debhelper-compat (= 13), + meson, + pkgconf, + gettext, + libgtk-4-dev, + libcairo2-dev, + librsvg2-dev, + desktop-file-utils, + xvfb +Standards-Version: 4.7.0 +Rules-Requires-Root: no +Homepage: https://github.com/j4imefoo/TBO Package: tbo Architecture: any -Depends: ${shlibs:Depends}, - libgtk-3-0, - ${misc:Depends} -Description: Intuitive GNOME comic creator - TBO is a simple and easy drag and drop comic creator. +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Comic editor built with GTK4 + TBO is a comic and illustrated presentation editor. + It provides page-based editing, frame-based layout, doodles, bubbles, + text tools and export options in a GTK4 desktop application. diff --git a/debian/copyright b/debian/copyright index 3a1738a..ba05ad7 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,42 +1,26 @@ -This package was debianized by: - - Daniel Garcia on Mon, 26 Apr 2010 09:51:33 +0200 - -It was downloaded from: - - - -Upstream Author(s): - - Daniel Garcia - -Copyright: - - Copyright (C) 2010 Daniel Garcia - -License: - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -On Debian systems, the complete text of the GNU General -Public License version 3 can be found in `/usr/share/common-licenses/GPL-3'. - -The Debian packaging is: - - Copyright (C) 2010 Daniel Garcia - -and is licensed under the GPL version 3, see above. - -# Please also look if there are files or directories which have a -# different copyright/license attached and list them here. +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: TBO +Upstream-Contact: Jaime +Source: https://github.com/j4imefoo/TBO + +Files: * +Copyright: 2010 Daniel Garcia Moreno +License: GPL-3+ + +Files: debian/* +Copyright: 2026 Jaime +License: GPL-3+ + +License: GPL-3+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + On Debian systems, the full text of the GNU General Public License + version 3 can be found in /usr/share/common-licenses/GPL-3. diff --git a/debian/dirs b/debian/dirs deleted file mode 100644 index ca882bb..0000000 --- a/debian/dirs +++ /dev/null @@ -1,2 +0,0 @@ -usr/bin -usr/sbin diff --git a/debian/docs b/debian/docs deleted file mode 100644 index 5502ed8..0000000 --- a/debian/docs +++ /dev/null @@ -1,3 +0,0 @@ -NEWS -README -TODO diff --git a/debian/rules b/debian/rules index 08623a3..721f9d3 100755 --- a/debian/rules +++ b/debian/rules @@ -1,7 +1,7 @@ #!/usr/bin/make -f -include /usr/share/cdbs/1/rules/debhelper.mk -include /usr/share/cdbs/1/class/autotools.mk +%: + dh $@ --buildsystem=meson -# Add here any variable or target overrides you need. -# DEB_CONFIGURE_SCRIPT := ./autogen.sh +override_dh_auto_test: + xvfb-run -a meson test -C obj-$(DEB_HOST_GNU_TYPE) --no-rebuild --print-errorlogs --num-processes 1 diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..14557b0 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,3 @@ +Tests: smoke +Depends: @, xvfb +Restrictions: superficial diff --git a/debian/tests/smoke b/debian/tests/smoke new file mode 100755 index 0000000..6afae46 --- /dev/null +++ b/debian/tests/smoke @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +xvfb-run -a sh -c '/usr/bin/tbo >/dev/null 2>&1 & pid=$!; sleep 2; kill -TERM "$pid"; wait "$pid" || true' diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..88969e0 --- /dev/null +++ b/meson.build @@ -0,0 +1,153 @@ +project( + 'tbo', + 'c', + version: '1.0', + default_options: ['c_std=gnu11'] +) + +subdir('po') + +cc = meson.get_compiler('c') + +gtk_dep = dependency('gtk4', version: '>= 4.22') +cairo_dep = dependency('cairo') +rsvg_dep = dependency('librsvg-2.0') +math_dep = cc.find_library('m', required: false) + +deps = [gtk_dep, cairo_dep, rsvg_dep, math_dep] + +data_dir = join_paths(get_option('prefix'), get_option('datadir'), 'tbo') +locale_dir = join_paths(get_option('prefix'), get_option('localedir')) + +conf = configuration_data() +conf.set_quoted('GETTEXT_PACKAGE', 'tbo') +conf.set_quoted('VERSION', meson.project_version()) +conf.set10('ENABLE_NLS', true) +configure_file(output: 'config.h', configuration: conf) + +add_project_arguments( + '-DG_LOG_DOMAIN="tbo"', + '-DGNOMELOCALEDIR="' + locale_dir + '"', + '-DDATA_DIR="' + data_dir + '"', + '-DSOURCE_DATA_DIR="' + join_paths(meson.project_source_root(), 'data') + '"', + language: 'c' +) + +inc = include_directories('src') + +common_sources = files( + 'src/tbo-window.c', + 'src/tbo-file-dialog.c', + 'src/tbo-widget.c', + 'src/comic.c', + 'src/comic-new-dialog.c', + 'src/comic-saveas-dialog.c', + 'src/comic-open-dialog.c', + 'src/frame.c', + 'src/page.c', + 'src/ui-menu.c', + 'src/doodle-treeview.c', + 'src/tbo-tooltip.c', + 'src/export.c', + 'src/dnd.c', + 'src/comic-load.c', + 'src/tbo-utils.c', + 'src/tbo-files.c', + 'src/tbo-ui-utils.c', + 'src/tbo-object-base.c', + 'src/tbo-object-svg.c', + 'src/tbo-object-text.c', + 'src/tbo-object-pixmap.c', + 'src/tbo-object-group.c', + 'src/tbo-tool-base.c', + 'src/tbo-tool-selector.c', + 'src/tbo-tool-frame.c', + 'src/tbo-tool-doodle.c', + 'src/tbo-tool-bubble.c', + 'src/tbo-tool-text.c', + 'src/tbo-toolbar.c', + 'src/tbo-drawing.c', + 'src/tbo-undo.c' +) + +executable( + 'tbo', + common_sources + files('src/tbo.c'), + dependencies: deps, + include_directories: inc, + install: true +) + +load_render_check = executable( + 'load-render-check', + common_sources + files('tests/load_render_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_tool_check = executable( + 'frame-tool-check', + common_sources + files('tests/frame_tool_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +save_roundtrip_check = executable( + 'save-roundtrip-check', + common_sources + files('tests/save_roundtrip_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +asset_bounds_check = executable( + 'asset-bounds-check', + common_sources + files('tests/asset_bounds_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +test( + 'load-render-check', + load_render_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-tool-check', + frame_tool_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'save-roundtrip-check', + save_roundtrip_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'asset-bounds-check', + asset_bounds_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +install_data( + 'data/tutorial.pdf', + 'data/tut.tbo', + 'data/icon.png', + install_dir: join_paths(get_option('datadir'), 'tbo') +) + +install_subdir('data/ui', install_dir: join_paths(get_option('datadir'), 'tbo')) +install_subdir('data/icons', install_dir: join_paths(get_option('datadir'), 'tbo')) +install_subdir('data/doodle', install_dir: join_paths(get_option('datadir'), 'tbo')) + +install_data('data/applications/net.danigm.tbo.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) +install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'pixmaps')) diff --git a/po/Makevars b/po/Makevars deleted file mode 100644 index f579fea..0000000 --- a/po/Makevars +++ /dev/null @@ -1,43 +0,0 @@ -# Makefile variables for PO directory in any package using GNU gettext. - -# Usually the message domain is the same as the package name. -DOMAIN = $(GETTEXT_PACKAGE) - -# These two variables depend on the location of this directory. -subdir = po -top_builddir = .. - -# These options get passed to xgettext. -#XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ --keyword=Q_ - -# This is the copyright holder that gets inserted into the header of the -# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding -# package. (Note that the msgstr strings, extracted from the package's -# sources, belong to the copyright holder of the package.) Translators are -# expected to transfer the copyright for their translations to this person -# or entity, or to disclaim their copyright. The empty string stands for -# the public domain; in this case the translators are expected to disclaim -# their copyright. -COPYRIGHT_HOLDER = GNOME Translation Project - -# This is the email address or URL to which the translators shall report -# bugs in the untranslated strings: -# - Strings which are not entire sentences, see the maintainer guidelines -# in the GNU gettext documentation, section 'Preparing Strings'. -# - Strings which use unclear terms or require additional context to be -# understood. -# - Strings which make invalid assumptions about notation of date, time or -# money. -# - Pluralisation problems. -# - Incorrect English spelling. -# - Incorrect formatting. -# It can be your email address, or a mailing list address where translators -# can write to without being subscribed, or the URL of a web page through -# which the translators can contact you. -MSGID_BUGS_ADDRESS = dani@danigm.net - -# This is the list of locale categories, beyond LC_MESSAGES, for which the -# message catalogs shall be used. It is usually empty. -EXTRA_LOCALE_CATEGORIES = - -POFILES = es.po diff --git a/po/POTFILES b/po/POTFILES new file mode 100644 index 0000000..1549d56 --- /dev/null +++ b/po/POTFILES @@ -0,0 +1,31 @@ +src/comic.c +src/comic-load.c +src/comic-new-dialog.c +src/comic-open-dialog.c +src/comic-saveas-dialog.c +src/dnd.c +src/doodle-treeview.c +src/export.c +src/frame.c +src/page.c +src/tbo.c +src/tbo-drawing.c +src/tbo-file-dialog.c +src/tbo-files.c +src/tbo-object-base.c +src/tbo-object-group.c +src/tbo-object-pixmap.c +src/tbo-object-svg.c +src/tbo-object-text.c +src/tbo-toolbar.c +src/tbo-tool-base.c +src/tbo-tool-bubble.c +src/tbo-tool-doodle.c +src/tbo-tool-frame.c +src/tbo-tool-selector.c +src/tbo-tool-text.c +src/tbo-tooltip.c +src/tbo-ui-utils.c +src/tbo-utils.c +src/tbo-window.c +src/ui-menu.c diff --git a/po/POTFILES.in b/po/POTFILES.in deleted file mode 100644 index a796f0e..0000000 --- a/po/POTFILES.in +++ /dev/null @@ -1,62 +0,0 @@ -[encoding: UTF-8] -src/comic.c -src/comic-load.c -src/comic-new-dialog.c -src/comic-open-dialog.c -src/comic-saveas-dialog.c -src/custom-stock.c -src/dnd.c -src/doodle-treeview.c -src/export.c -src/frame.c -src/page.c -src/tbo.c -src/tbo-drawing.c -src/tbo-files.c -src/tbo-object-base.c -src/tbo-object-pixmap.c -src/tbo-object-svg.c -src/tbo-object-text.c -src/tbo-toolbar.c -src/tbo-tool-base.c -src/tbo-tool-bubble.c -src/tbo-tool-doodle.c -src/tbo-tool-frame.c -src/tbo-tool-selector.c -src/tbo-tool-text.c -src/tbo-tooltip.c -src/tbo-ui-utils.c -src/tbo-utils.c -src/tbo-window.c -src/typestest.c -src/ui-menu.c -src/comic.h -src/comic-load.h -src/comic-new-dialog.h -src/comic-open-dialog.h -src/comic-saveas-dialog.h -src/custom-stock.h -src/dnd.h -src/doodle-treeview.h -src/export.h -src/frame.h -src/page.h -src/tbo-drawing.h -src/tbo-files.h -src/tbo-object-base.h -src/tbo-object-pixmap.h -src/tbo-object-svg.h -src/tbo-object-text.h -src/tbo-toolbar.h -src/tbo-tool-base.h -src/tbo-tool-bubble.h -src/tbo-tool-doodle.h -src/tbo-tool-frame.h -src/tbo-tool-selector.h -src/tbo-tool-text.h -src/tbo-tooltip.h -src/tbo-types.h -src/tbo-ui-utils.h -src/tbo-utils.h -src/tbo-window.h -src/ui-menu.h diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..b8c3226 --- /dev/null +++ b/po/meson.build @@ -0,0 +1,7 @@ +i18n = import('i18n') + +i18n.gettext( + 'tbo', + args: ['--from-code=UTF-8'], + preset: 'glib' +) diff --git a/src/Makefile.am b/src/Makefile.am deleted file mode 100644 index ffbfecd..0000000 --- a/src/Makefile.am +++ /dev/null @@ -1,90 +0,0 @@ -## Process this file with automake to generate a Makefile.in -## To build all programs with GTK+ uncomment these lines. - -AM_CPPFLAGS = -I$(top_srcdir) -I$(includedir) $(GNOME_INCLUDEDIR) \ - -DG_LOG_DOMAIN=\"tbo\" - -bin_PROGRAMS = tbo -noinst_PROGRAMS = typestest undotest - -SOURCES = \ - tbo-window.c \ - comic.c \ - comic-new-dialog.c \ - comic-saveas-dialog.c \ - comic-open-dialog.c \ - frame.c \ - page.c \ - ui-menu.c \ - custom-stock.c \ - doodle-treeview.c \ - tbo-tooltip.c \ - export.c \ - dnd.c \ - comic-load.c \ - tbo-utils.c \ - tbo-files.c \ - tbo-ui-utils.c \ - tbo-files.h \ - tbo-window.h \ - comic.h \ - comic-new-dialog.h \ - comic-saveas-dialog.h \ - comic-open-dialog.h \ - frame.h \ - page.h \ - tbo-types.h \ - ui-menu.h \ - doodle-treeview.h \ - tbo-tooltip.h \ - dnd.h \ - comic-load.h \ - tbo-utils.h \ - export.h \ - tbo-ui-utils.h \ - tbo-object-base.h \ - tbo-object-base.c \ - tbo-object-svg.h \ - tbo-object-svg.c \ - tbo-object-text.h \ - tbo-object-text.c \ - tbo-object-pixmap.h \ - tbo-object-pixmap.c \ - tbo-object-group.h \ - tbo-object-group.c \ - tbo-tool-base.h \ - tbo-tool-base.c \ - tbo-tool-selector.h \ - tbo-tool-selector.c \ - tbo-tool-frame.h \ - tbo-tool-frame.c \ - tbo-tool-doodle.h \ - tbo-tool-doodle.c \ - tbo-tool-bubble.h \ - tbo-tool-bubble.c \ - tbo-tool-text.h \ - tbo-tool-text.c \ - tbo-toolbar.h \ - tbo-toolbar.c \ - tbo-drawing.h \ - tbo-drawing.c \ - custom-stock.h \ - tbo-undo.h \ - tbo-undo.c - -AM_CFLAGS = @GTK_CFLAGS@ \ - $(PACKAGE_CFLAGS) \ - -DGNOMELOCALEDIR=\"$(datadir)/locale\" \ - -DDATA_DIR=\""$(pkgdatadir)"\" -tbo_LDADD = @GTK_LIBS@ \ - $(PACKAGE_LIBS) -typestest_LDADD = @GTK_LIBS@ \ - $(PACKAGE_LIBS) -undotest_LDADD = @GTK_LIBS@ \ - $(PACKAGE_LIBS) - -typestest_SOURCES = $(SOURCES) typestest.c -undotest_SOURCES = $(SOURCES) undotest.c -tbo_SOURCES = $(SOURCES) tbo.c - -CLEANFILES = *~ diff --git a/src/comic-load.c b/src/comic-load.c index 01609af..f29b0ac 100644 --- a/src/comic-load.c +++ b/src/comic-load.c @@ -24,6 +24,7 @@ #include #include #include "comic-load.h" +#include "tbo-widget.h" #include "tbo-types.h" #include "comic.h" #include "page.h" @@ -46,6 +47,25 @@ struct attr { void *pointer; }; +static gchar * +get_attr_string (const gchar **attribute_names, + const gchar **attribute_values, + const gchar *name) +{ + const gchar **name_cursor = attribute_names; + const gchar **value_cursor = attribute_values; + + while (*name_cursor) + { + if (strcmp (*name_cursor, name) == 0) + return g_strdup (*value_cursor); + name_cursor++; + value_cursor++; + } + + return NULL; +} + void parse_attrs (struct attr attrs[], int attrs_size, @@ -61,12 +81,7 @@ parse_attrs (struct attr attrs[], { if (strcmp (*name_cursor, attrs[i].name) == 0) { - if (strcmp (attrs[i].format, "%s") == 0) - { - sprintf(attrs[i].pointer, "%s", *value_cursor); - } - else - sscanf (*value_cursor, attrs[i].format, attrs[i].pointer); + sscanf (*value_cursor, attrs[i].format, attrs[i].pointer); } } name_cursor++; @@ -128,7 +143,7 @@ create_tbo_piximage (const gchar **attribute_names, const gchar **attribute_valu int width=0, height=0; float angle=0.0; int flipv=0, fliph=0; - char path[255]; + gchar *path = NULL; struct attr attrs[] = { {"x", "%d", &x}, @@ -138,10 +153,10 @@ create_tbo_piximage (const gchar **attribute_names, const gchar **attribute_valu {"flipv", "%d", &flipv}, {"fliph", "%d", &fliph}, {"angle", "%f", &angle}, - {"path", "%s", path}, }; parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); + path = get_attr_string (attribute_names, attribute_values, "path"); pix = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new_with_params (x, y, width, height, path)); obj = TBO_OBJECT_BASE (pix); @@ -149,6 +164,7 @@ create_tbo_piximage (const gchar **attribute_names, const gchar **attribute_valu obj->flipv = flipv; obj->fliph = fliph; tbo_frame_add_obj (CURRENT_FRAME, obj); + g_free (path); } void @@ -160,7 +176,7 @@ create_tbo_svgimage (const gchar **attribute_names, const gchar **attribute_valu int width=0, height=0; float angle=0.0; int flipv=0, fliph=0; - char path[255]; + gchar *path = NULL; struct attr attrs[] = { {"x", "%d", &x}, @@ -170,10 +186,10 @@ create_tbo_svgimage (const gchar **attribute_names, const gchar **attribute_valu {"flipv", "%d", &flipv}, {"fliph", "%d", &fliph}, {"angle", "%f", &angle}, - {"path", "%s", path}, }; parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); + path = get_attr_string (attribute_names, attribute_values, "path"); svg = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (x, y, width, height, path)); obj = TBO_OBJECT_BASE (svg); @@ -181,6 +197,7 @@ create_tbo_svgimage (const gchar **attribute_names, const gchar **attribute_valu obj->flipv = flipv; obj->fliph = fliph; tbo_frame_add_obj (CURRENT_FRAME, obj); + g_free (path); } void @@ -188,12 +205,12 @@ create_tbo_text (const gchar **attribute_names, const gchar **attribute_values) { TboObjectText *textobj; TboObjectBase *obj; - GdkColor color; + GdkRGBA color; int x=0, y=0; int width=0, height=0; float angle=0.0; int flipv=0, fliph=0; - char font[255]; + gchar *font = NULL; float r=0.0, g=0.0, b=0.0; struct attr attrs[] = { @@ -204,16 +221,17 @@ create_tbo_text (const gchar **attribute_names, const gchar **attribute_values) {"flipv", "%d", &flipv}, {"fliph", "%d", &fliph}, {"angle", "%f", &angle}, - {"font", "%s", font}, {"r", "%f", &r}, {"g", "%f", &g}, {"b", "%f", &b}, }; parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); - color.red = (int)(r * COLORMAX); - color.green = (int)(g * COLORMAX); - color.blue = (int)(b * COLORMAX); + font = get_attr_string (attribute_names, attribute_values, "font"); + color.red = r; + color.green = g; + color.blue = b; + color.alpha = 1.0; textobj = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, width, height, "text", font, &color)); obj = TBO_OBJECT_BASE (textobj); obj->angle = angle; @@ -221,6 +239,7 @@ create_tbo_text (const gchar **attribute_names, const gchar **attribute_values) obj->fliph = fliph; CURRENT_TEXT = textobj; tbo_frame_add_obj (CURRENT_FRAME, obj); + g_free (font); } /* The handler functions. */ @@ -275,7 +294,7 @@ text (GMarkupParseContext *context, if (strlen(text3)) { tbo_object_text_set_text (CURRENT_TEXT, text3); } else { - tbo_frame_del_obj (CURRENT_FRAME, CURRENT_TEXT); + tbo_frame_del_obj (CURRENT_FRAME, TBO_OBJECT_BASE (CURRENT_TEXT)); CURRENT_TEXT = NULL; } g_free (text2); @@ -322,22 +341,12 @@ tbo_comic_load (char *filename) TITLE = g_strdup(base_name); if (g_file_get_contents (filename, &text, &length, NULL) == FALSE) { - GtkWidget *dialog = gtk_message_dialog_new (NULL, - GTK_DIALOG_MODAL, - GTK_MESSAGE_ERROR, - GTK_BUTTONS_CLOSE, - _("Couldn't load file")); - gtk_dialog_run (GTK_DIALOG (dialog)); + tbo_alert_show (NULL, _("Couldn't load file"), NULL); return NULL; } if (g_markup_parse_context_parse (context, text, length, NULL) == FALSE) { - GtkWidget *dialog = gtk_message_dialog_new (NULL, - GTK_DIALOG_MODAL, - GTK_MESSAGE_ERROR, - GTK_BUTTONS_CLOSE, - _("Couldn't parse file")); - gtk_dialog_run (GTK_DIALOG (dialog)); + tbo_alert_show (NULL, _("Couldn't parse file"), NULL); return NULL; } diff --git a/src/comic-new-dialog.c b/src/comic-new-dialog.c index d577ee8..5cfd62a 100644 --- a/src/comic-new-dialog.c +++ b/src/comic-new-dialog.c @@ -21,71 +21,119 @@ #include #include #include "comic-new-dialog.h" +#include "tbo-widget.h" #include "tbo-window.h" +struct new_comic_dialog_data { + GMainLoop *loop; + gint response; +}; + +static gboolean +new_comic_close_request_cb (GtkWindow *dialog, struct new_comic_dialog_data *data) +{ + if (data->response == GTK_RESPONSE_NONE) + data->response = GTK_RESPONSE_REJECT; + g_main_loop_quit (data->loop); + return TRUE; +} + +static void +new_comic_button_cb (GtkButton *button, GtkWindow *dialog) +{ + struct new_comic_dialog_data *data = g_object_get_data (G_OBJECT (dialog), "tbo-new-comic-data"); + gint response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tbo-response")); + + data->response = response; + gtk_window_close (dialog); +} + gboolean tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) { GtkWidget *dialog; GtkWidget *vbox; GtkWidget *hbox; + GtkWidget *actions; + GtkWidget *button; GtkWidget *label; GtkWidget *spin_w; GtkWidget *spin_h; GtkAdjustment *adjustment; - gint response; + struct new_comic_dialog_data data; int width; int height; - dialog = gtk_dialog_new_with_buttons (_("New Comic"), - GTK_WINDOW (window->window), - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_STOCK_OK, - GTK_RESPONSE_ACCEPT, - GTK_STOCK_CANCEL, - GTK_RESPONSE_REJECT, - NULL); + dialog = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (dialog), _("New Comic")); + gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window->window)); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); - vbox = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_margin_start (vbox, 12); + gtk_widget_set_margin_end (vbox, 12); + gtk_widget_set_margin_top (vbox, 12); + gtk_widget_set_margin_bottom (vbox, 12); + tbo_widget_add_child (dialog, vbox); - hbox = gtk_hbox_new (FALSE, FALSE); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); label = gtk_label_new (_("width: ")); gtk_widget_set_size_request (label, 60, -1); - gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); adjustment = gtk_adjustment_new (800, 0, 10000, 100, 100, 0); spin_w = gtk_spin_button_new (GTK_ADJUSTMENT (adjustment), 1, 0); - gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (hbox), spin_w, TRUE, TRUE, 0); - gtk_container_add (GTK_CONTAINER (vbox), hbox); + tbo_box_pack_start (hbox, label, FALSE, FALSE, 0); + tbo_box_pack_start (hbox, spin_w, TRUE, TRUE, 0); + tbo_widget_add_child (vbox, hbox); - hbox = gtk_hbox_new (FALSE, FALSE); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); label = gtk_label_new (_("height: ")); gtk_widget_set_size_request (label, 60, -1); - gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); adjustment = gtk_adjustment_new (500, 0, 10000, 100, 100, 0); spin_h = gtk_spin_button_new (GTK_ADJUSTMENT (adjustment), 1, 0); - gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (hbox), spin_h, TRUE, TRUE, 0); - gtk_container_add (GTK_CONTAINER (vbox), hbox); + tbo_box_pack_start (hbox, label, FALSE, FALSE, 0); + tbo_box_pack_start (hbox, spin_h, TRUE, TRUE, 0); + tbo_widget_add_child (vbox, hbox); + + actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_halign (actions, GTK_ALIGN_END); + + button = gtk_button_new_with_mnemonic (_("_Cancel")); + g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_REJECT)); + g_signal_connect (button, "clicked", G_CALLBACK (new_comic_button_cb), dialog); + tbo_widget_add_child (actions, button); + + button = gtk_button_new_with_mnemonic (_("_OK")); + gtk_widget_add_css_class (button, "suggested-action"); + g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_ACCEPT)); + g_signal_connect (button, "clicked", G_CALLBACK (new_comic_button_cb), dialog); + tbo_widget_add_child (actions, button); - gtk_widget_show_all (vbox); + tbo_widget_add_child (vbox, actions); - response = gtk_dialog_run (GTK_DIALOG (dialog)); + data.loop = g_main_loop_new (NULL, FALSE); + data.response = GTK_RESPONSE_NONE; + g_object_set_data (G_OBJECT (dialog), "tbo-new-comic-data", &data); + g_signal_connect (dialog, "close-request", G_CALLBACK (new_comic_close_request_cb), &data); + tbo_widget_show_all (dialog); + gtk_window_present (GTK_WINDOW (dialog)); - if (response == GTK_RESPONSE_ACCEPT) + g_main_loop_run (data.loop); + + if (data.response == GTK_RESPONSE_ACCEPT) { width = (int) gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin_w)); height = (int) gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin_h)); - tbo_new_tbo (width, height); - } - else - { - printf ("NOK\n"); + tbo_new_tbo (gtk_window_get_application (GTK_WINDOW (window->window)), width, height); } - gtk_widget_destroy ((GtkWidget *) dialog); + gtk_window_destroy (GTK_WINDOW (dialog)); + g_main_loop_unref (data.loop); return FALSE; } - diff --git a/src/comic-open-dialog.c b/src/comic-open-dialog.c index 66ac023..eed6207 100644 --- a/src/comic-open-dialog.c +++ b/src/comic-open-dialog.c @@ -21,70 +21,25 @@ #include #include #include "comic-open-dialog.h" +#include "tbo-file-dialog.h" #include "tbo-drawing.h" #include "tbo-window.h" #include "comic.h" -static GtkFileFilter * AddFilters (GtkWidget *filechooser); -static GtkWidget * FileChooserWidget (TboWindow *window); - - gboolean tbo_comic_open_dialog (GtkWidget *widget, TboWindow *window) { - gint response; - GtkWidget *filechooser; - GtkFileFilter *filter; - char *filename; - - filechooser = FileChooserWidget (window); - filter = AddFilters (filechooser); + gchar *filename = tbo_file_dialog_open_project (window); - response = gtk_dialog_run (GTK_DIALOG (filechooser)); - - if (response == GTK_RESPONSE_ACCEPT) + if (filename != NULL) { - filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (filechooser)); + tbo_window_set_browse_path (window, filename); tbo_comic_open (window, filename); - tbo_window_set_path (window, filename); tbo_drawing_update (TBO_DRAWING (window->drawing)); tbo_window_update_status (window, 0, 0); + g_free (filename); } - gtk_widget_destroy ((GtkWidget *) filechooser); - return FALSE; } - -//Return the widget to choose file -static GtkWidget * -FileChooserWidget (TboWindow *window) -{ - GtkWidget *filechooser; - filechooser = gtk_file_chooser_dialog_new (_("Open"), - GTK_WINDOW (window->window), - GTK_FILE_CHOOSER_ACTION_OPEN, - GTK_STOCK_CANCEL, - GTK_RESPONSE_CANCEL, - GTK_STOCK_OPEN, - GTK_RESPONSE_ACCEPT, - NULL); - return filechooser; -} - -//Return the files' filters -static GtkFileFilter * -AddFilters (GtkWidget *filechooser) -{ - GtkFileFilter *filter; - filter = gtk_file_filter_new (); - gtk_file_filter_set_name (filter, _("TBO files")); - gtk_file_filter_add_pattern (filter, "*.tbo"); - gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (filechooser), filter); - filter = gtk_file_filter_new (); - gtk_file_filter_set_name (filter, _("All files")); - gtk_file_filter_add_pattern (filter, "*"); - gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (filechooser), filter); - return filter; -} diff --git a/src/comic-saveas-dialog.c b/src/comic-saveas-dialog.c index ec05259..43221d6 100644 --- a/src/comic-saveas-dialog.c +++ b/src/comic-saveas-dialog.c @@ -18,9 +18,11 @@ #include +#include #include #include #include "comic-saveas-dialog.h" +#include "tbo-file-dialog.h" #include "tbo-window.h" #include "comic.h" @@ -28,43 +30,33 @@ gboolean tbo_comic_save_dialog (GtkWidget *widget, TboWindow *window) { if (window->path) - tbo_comic_save (window, window->path); + return tbo_comic_save (window, window->path); else - tbo_comic_saveas_dialog (widget, window); - return FALSE; + return tbo_comic_saveas_dialog (widget, window); } gboolean tbo_comic_saveas_dialog (GtkWidget *widget, TboWindow *window) { - gint response; - GtkWidget *filechooser; - char *filename; - char buffer[255]; - - filechooser = gtk_file_chooser_dialog_new (_("Save as"), - GTK_WINDOW (window->window), - GTK_FILE_CHOOSER_ACTION_SAVE, - GTK_STOCK_CANCEL, - GTK_RESPONSE_CANCEL, - GTK_STOCK_SAVE, - GTK_RESPONSE_ACCEPT, - NULL); + gchar *filename; + char buffer[260]; - snprintf (buffer, 250, "%s", window->comic->title); + g_strlcpy (buffer, window->comic->title, sizeof (buffer)); if (!g_str_has_suffix ((window->comic->title), ".tbo")) strcat (buffer, ".tbo"); - gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (filechooser), buffer); - response = gtk_dialog_run (GTK_DIALOG (filechooser)); + filename = tbo_file_dialog_save_project (window, buffer); - if (response == GTK_RESPONSE_ACCEPT) + if (filename != NULL) { - filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (filechooser)); - tbo_comic_save (window, filename); - tbo_window_set_path (window, filename); + tbo_window_set_browse_path (window, filename); + if (tbo_comic_save (window, filename)) + { + tbo_window_set_path (window, filename); + g_free (filename); + return TRUE; + } + g_free (filename); } - gtk_widget_destroy ((GtkWidget *) filechooser); - return FALSE; } diff --git a/src/comic.c b/src/comic.c index 4ded367..9df0f80 100644 --- a/src/comic.c +++ b/src/comic.c @@ -22,14 +22,16 @@ #include #include #include -#include +#include #include #include "comic.h" #include "tbo-types.h" #include "tbo-window.h" #include "page.h" #include "comic-load.h" +#include "tbo-drawing.h" #include "tbo-utils.h" +#include "tbo-widget.h" Comic * tbo_comic_new (const char *title, int width, int height) @@ -172,7 +174,7 @@ tbo_comic_del_current_page (Comic *comic) return TRUE; } -void +gboolean tbo_comic_save (TboWindow *tbo, char *filename) { GList *p; @@ -182,16 +184,11 @@ tbo_comic_save (TboWindow *tbo, char *filename) if (!file) { - GtkWidget *dialog = gtk_message_dialog_new (NULL, - GTK_DIALOG_MODAL, - GTK_MESSAGE_ERROR, - GTK_BUTTONS_CLOSE, - _("Failed saving: %s"), - strerror (errno)); perror (_("failed saving")); - gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy ((GtkWidget *) dialog); - return; + tbo_alert_show (GTK_WINDOW (tbo->window), + _("Failed saving"), + strerror (errno)); + return FALSE; } get_base_name (filename, comic->title, 255); gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); @@ -209,32 +206,42 @@ tbo_comic_save (TboWindow *tbo, char *filename) snprintf (buffer, 255, "\n"); fwrite (buffer, sizeof (char), strlen (buffer), file); fclose (file); + tbo_window_mark_clean (tbo); + return TRUE; } void tbo_comic_open (TboWindow *window, char *filename) { Comic *newcomic = tbo_comic_load (filename); + Comic *oldcomic; int nth; + int n_pages; + if (newcomic) { - tbo_comic_free (window->comic); + oldcomic = window->comic; window->comic = newcomic; gtk_window_set_title (GTK_WINDOW (window->window), window->comic->title); - for (nth=gtk_notebook_get_n_pages (GTK_NOTEBOOK (window->notebook)); nth>=0; nth--) + n_pages = tbo_window_get_page_count (window); + for (nth = n_pages - 1; nth >= 0; nth--) { - gtk_notebook_remove_page (GTK_NOTEBOOK (window->notebook), nth); + tbo_window_remove_page_widget (window, nth); } + tbo_comic_free (oldcomic); + for (nth=0; nthcomic); nth++) { - gtk_notebook_insert_page (GTK_NOTEBOOK (window->notebook), - create_darea (window), - NULL, - nth); + tbo_window_add_page_widget (window, create_darea (window)); } + + tbo_window_set_path (window, filename); + tbo_window_set_current_tab_page (window, TRUE); + tbo_drawing_adjust_scroll (TBO_DRAWING (window->drawing)); + tbo_drawing_update (TBO_DRAWING (window->drawing)); + tbo_window_update_status (window, 0, 0); + tbo_window_mark_clean (window); } - tbo_toolbar_set_selected_tool (window->toolbar, TBO_TOOLBAR_NONE); - tbo_toolbar_set_selected_tool (window->toolbar, TBO_TOOLBAR_SELECTOR); } diff --git a/src/comic.h b/src/comic.h index 8b4a706..6bdb95a 100644 --- a/src/comic.h +++ b/src/comic.h @@ -39,8 +39,7 @@ Page *tbo_comic_prev_page (Comic *comic); Page *tbo_comic_get_current_page (Comic *comic); void tbo_comic_set_current_page (Comic *comic, Page *page); void tbo_comic_set_current_page_nth (Comic *comic, int nth); -void tbo_comic_save (TboWindow *tbo, char *filename); +gboolean tbo_comic_save (TboWindow *tbo, char *filename); void tbo_comic_open (TboWindow *window, char *filename); #endif - diff --git a/src/custom-stock.c b/src/custom-stock.c deleted file mode 100644 index 4fa6dca..0000000 --- a/src/custom-stock.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This file is part of TBO, a gnome comic editor - * Copyright (C) 2010 Daniel Garcia Moreno - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -#include -#include -#include "custom-stock.h" - -#define ICONDIR "/icons/" - -typedef struct -{ - char *image; - char *stockid; -} icon; - -void load_custom_stock () -{ - GtkIconFactory *factory; - GtkIconSet *iconset; - GdkPixbuf *image; - GError *error = NULL; - - icon icons[] = { - {DATA_DIR ICONDIR "frame.svg", TBO_STOCK_FRAME}, - {DATA_DIR ICONDIR "selector.svg", TBO_STOCK_SELECTOR}, - {DATA_DIR ICONDIR "doodle.svg", TBO_STOCK_DOODLE}, - {DATA_DIR ICONDIR "text.svg", TBO_STOCK_TEXT}, - {DATA_DIR ICONDIR "pix.svg", TBO_STOCK_PIX}, - {DATA_DIR ICONDIR "bubble.svg", TBO_STOCK_BUBBLE}, - }; - - int i; - - factory = gtk_icon_factory_new (); - - for (i=0; i - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -#ifndef __TBO_CUSTOM_STOCK__ -#define __TBO_CUSTOM_STOCK__ - -#define TBO_STOCK_FRAME "tbo-newframe" -#define TBO_STOCK_SELECTOR "tbo-selector" -#define TBO_STOCK_DOODLE "tbo-doodle" -#define TBO_STOCK_TEXT "tbo-text" -#define TBO_STOCK_PIX "tbo-pix" -#define TBO_STOCK_BUBBLE "tbo-bubble" - -void load_custom_stock (); - -#endif - diff --git a/src/dnd.c b/src/dnd.c index 3e21702..99dee4d 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -24,117 +24,138 @@ #include "frame.h" #include "tbo-object-svg.h" #include "tbo-object-pixmap.h" +#include "tbo-files.h" #include "tbo-window.h" +#include "tbo-tool-selector.h" +#include "tbo-widget.h" -static GtkWidget *DND_IMAGE = NULL; +static TboObjectBase * +create_asset (const gchar *asset_path, gint x, gint y) +{ + if (tbo_files_is_svg_file ((gchar *) asset_path)) + return TBO_OBJECT_BASE (tbo_object_svg_new_with_params (x, y, 0, 0, (gchar *) asset_path)); -void -drag_data_received_handl (GtkWidget *widget, - GdkDragContext *context, - gint x, gint y, - GtkSelectionData *selection_data, - guint target_type, - guint time, - TboWindow *tbo) + return TBO_OBJECT_BASE (tbo_object_pixmap_new_with_params (x, y, 0, 0, (gchar *) asset_path)); +} + +static void +select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset) +{ + TboToolSelector *selector; + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + tbo_tool_selector_set_selected (selector, frame); + tbo_tool_selector_set_selected_obj (selector, asset); +} + +static const gchar * +get_drag_asset_path (GtkWidget *widget, const gchar *key, gpointer fallback) { + const gchar *path = g_object_get_data (G_OBJECT (widget), key); + + if (path != NULL) + return path; + + return fallback; +} + +static GdkContentProvider * +drag_prepare_handl (GtkDragSource *source, + gdouble x, + gdouble y, + gpointer user_data) +{ + GtkWidget *widget = GTK_WIDGET (user_data); + const gchar *asset_path = get_drag_asset_path (widget, "tbo-asset-relative-path", NULL); + + if (asset_path == NULL) + return NULL; + + return gdk_content_provider_new_typed (G_TYPE_STRING, asset_path); +} + +static void +drag_begin_handl (GtkDragSource *source, + GdkDrag *drag, + gpointer user_data) +{ + GtkWidget *widget = GTK_WIDGET (user_data); + GtkWidget *child = tbo_widget_get_first_child (widget); + GdkPaintable *paintable = NULL; + + if (GTK_IS_PICTURE (child)) + paintable = gtk_picture_get_paintable (GTK_PICTURE (child)); + else if (GTK_IS_IMAGE (child)) + paintable = gtk_image_get_paintable (GTK_IMAGE (child)); + + if (paintable != NULL) + gtk_drag_source_set_icon (source, paintable, 0, 0); +} + +static gboolean +drop_handl (GtkDropTarget *target, + const GValue *value, + gdouble x, + gdouble y, + gpointer user_data) +{ + TboWindow *tbo = user_data; GtkAdjustment *adj; - float zoom = tbo_drawing_get_zoom (TBO_DRAWING (tbo->drawing)); - const gchar *_sdata; - - gboolean dnd_success = FALSE; - gboolean delete_selection_data = FALSE; - - /* Deal with what we are given from source */ - if ((selection_data != NULL) && (gtk_selection_data_get_length (selection_data) >= 0)) - { - if (gdk_drag_context_get_selected_action (context) == GDK_ACTION_ASK) - { - /* Ask the user to move or copy, then set the context action. */ - } - - if (gdk_drag_context_get_selected_action (context) == GDK_ACTION_MOVE) - delete_selection_data = TRUE; - - /* Check that we got the format we can use */ - switch (target_type) - { - case TARGET_STRING: - _sdata = gtk_selection_data_get_data (selection_data); - - TboObjectBase *image; - Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); - adj = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); - int rx = tbo_frame_get_base_x ((x + gtk_adjustment_get_value(adj)) / zoom); - adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); - int ry = tbo_frame_get_base_y ((y + gtk_adjustment_get_value(adj)) / zoom); - - if (tbo_files_is_svg_file ((gchar *)_sdata)) { - image = TBO_OBJECT_BASE (tbo_object_svg_new_with_params (rx, ry, 0, 0, (gchar*)_sdata)); - } else { - image = TBO_OBJECT_BASE (tbo_object_pixmap_new_with_params (rx, ry, 0, 0, (gchar*)_sdata)); - } - - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - tbo_frame_add_obj (frame, TBO_OBJECT_BASE (image)); - - dnd_success = TRUE; - break; - - default: - g_print ("nothing good"); - } - } - - if (dnd_success == FALSE) - { - g_print ("DnD data transfer failed!\n"); - } - - gtk_drag_finish (context, dnd_success, delete_selection_data, time); + gdouble zoom = tbo_drawing_get_zoom (TBO_DRAWING (tbo->drawing)); + const gchar *asset_path = g_value_get_string (value); + gint rx; + gint ry; + + if (asset_path == NULL) + return FALSE; + + adj = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); + rx = tbo_frame_get_base_x ((x + gtk_adjustment_get_value (adj)) / zoom); + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); + ry = tbo_frame_get_base_y ((y + gtk_adjustment_get_value (adj)) / zoom); + + return tbo_dnd_insert_asset (tbo, asset_path, rx, ry) != NULL; } -void -drag_data_get_handl (GtkWidget *widget, - GdkDragContext *context, - GtkSelectionData *selection_data, - guint target_type, - guint time, - char *svg) +TboObjectBase * +tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y) { - g_assert (selection_data != NULL); - switch (target_type) - { - case TARGET_STRING: - gtk_selection_data_set (selection_data, - gtk_selection_data_get_target (selection_data), - 8*sizeof(char), - (guchar*) svg, - strlen (svg)); - break; - default: - /* Default to some a safe target instead of fail. */ - g_assert_not_reached (); - } + Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); + TboObjectBase *asset; + + if (frame == NULL || asset_path == NULL || *asset_path == '\0') + return NULL; + + if (x < 0 || y < 0 || x > frame->width || y > frame->height) + return NULL; + + asset = create_asset (asset_path, x, y); + tbo_frame_add_obj (frame, asset); + select_inserted_asset (tbo, frame, asset); + tbo_window_mark_dirty (tbo); + tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + return asset; } void -drag_begin_handl (GtkWidget *widget, - GdkDragContext *context, - char *svg) +tbo_dnd_setup_asset_source (GtkWidget *widget, const gchar *full_path, const gchar *relative_path) { - DND_IMAGE = gtk_image_new_from_file (svg); - GdkPixbuf *pix = gtk_image_get_pixbuf (GTK_IMAGE (DND_IMAGE)); - gtk_drag_set_icon_pixbuf (context, pix, 0, 0); + GtkDragSource *source = gtk_drag_source_new (); + + gtk_drag_source_set_actions (source, GDK_ACTION_COPY); + g_object_set_data_full (G_OBJECT (widget), "tbo-asset-full-path", g_strdup (full_path), g_free); + g_object_set_data_full (G_OBJECT (widget), "tbo-asset-relative-path", g_strdup (relative_path), g_free); + g_signal_connect (source, "prepare", G_CALLBACK (drag_prepare_handl), widget); + g_signal_connect (source, "drag-begin", G_CALLBACK (drag_begin_handl), widget); + gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER (source)); } void -drag_end_handl (GtkWidget *widget, - GdkDragContext *context, - gpointer user_data) +tbo_dnd_setup_drawing_dest (TboDrawing *drawing, TboWindow *tbo) { - if (DND_IMAGE) - { - gtk_widget_destroy (GTK_WIDGET (DND_IMAGE)); - DND_IMAGE = NULL; - } + GtkDropTarget *target = gtk_drop_target_new (G_TYPE_STRING, GDK_ACTION_COPY); + + g_signal_connect (target, "drop", G_CALLBACK (drop_handl), tbo); + gtk_widget_add_controller (GTK_WIDGET (drawing), GTK_EVENT_CONTROLLER (target)); } diff --git a/src/dnd.h b/src/dnd.h index 133ba1d..196698b 100644 --- a/src/dnd.h +++ b/src/dnd.h @@ -21,26 +21,12 @@ #define __TBO_DND__ #include +#include "tbo-object-base.h" +#include "tbo-drawing.h" #include "tbo-window.h" -enum { - TARGET_STRING, -}; - - -static GtkTargetEntry TARGET_LIST[] = { - { "STRING", 0, TARGET_STRING }, - { "text/plain", 0, TARGET_STRING }, -}; - -static guint N_TARGETS = G_N_ELEMENTS (TARGET_LIST); - -// destination signals -void drag_data_received_handl (GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, TboWindow *tbo); - -// source signals -void drag_data_get_handl (GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, guint target_type, guint time, char *svg); -void drag_begin_handl (GtkWidget *widget, GdkDragContext *context, char *svg); -void drag_end_handl (GtkWidget *widget, GdkDragContext *context, gpointer user_data); +void tbo_dnd_setup_asset_source (GtkWidget *widget, const gchar *full_path, const gchar *relative_path); +void tbo_dnd_setup_drawing_dest (TboDrawing *drawing, TboWindow *tbo); +TboObjectBase *tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y); #endif diff --git a/src/doodle-treeview.c b/src/doodle-treeview.c index 524fceb..8099dbb 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -28,14 +28,23 @@ #include "dnd.h" #include "tbo-utils.h" #include "tbo-files.h" +#include "tbo-widget.h" void free_gstring_array (GArray *arr); static GArray *TO_FREE = NULL; +static GHashTable *THUMB_CACHE = NULL; static TboWindow *TBO = NULL; +static void +free_gstring_data (gpointer data, GClosure *closure) +{ + if (data != NULL) + g_string_free ((GString *) data, TRUE); +} + void -doodle_free_all () +doodle_free_all (void) { int i; if (!TO_FREE) return; @@ -45,27 +54,66 @@ doodle_free_all () } g_array_free (TO_FREE, TRUE); TO_FREE = NULL; + + if (THUMB_CACHE != NULL) + g_hash_table_remove_all (THUMB_CACHE); } -void doodle_add_to_free (GArray *arr) +static GdkPixbuf * +get_thumbnail_pixbuf (const gchar *path, const gchar *relative_path) { - if (!TO_FREE) - TO_FREE = g_array_new (FALSE, FALSE, sizeof(GArray*)); + GdkPixbuf *pixbuf; + gint width; + gint height; + gint max_dim; + gdouble scale; + gboolean is_body; - g_array_append_val (TO_FREE, arr); + if (THUMB_CACHE == NULL) + THUMB_CACHE = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + + pixbuf = g_hash_table_lookup (THUMB_CACHE, path); + if (pixbuf != NULL) + return g_object_ref (pixbuf); + + pixbuf = gdk_pixbuf_new_from_file (path, NULL); + if (pixbuf == NULL) + return NULL; + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + max_dim = MAX (width, height); + is_body = g_strrstr (relative_path, "/body/") != NULL || g_str_has_prefix (relative_path, "body/"); + + if (is_body && max_dim < 128) + scale = 128.0 / max_dim; + else if (is_body && max_dim > 160) + scale = 160.0 / max_dim; + else + scale = 1.0; + + if (scale != 1.0) + { + GdkPixbuf *scaled = gdk_pixbuf_scale_simple (pixbuf, + MAX (1, round (width * scale)), + MAX (1, round (height * scale)), + GDK_INTERP_BILINEAR); + g_object_unref (pixbuf); + pixbuf = scaled; + if (pixbuf == NULL) + return NULL; + } + + g_hash_table_insert (THUMB_CACHE, g_strdup (path), g_object_ref (pixbuf)); + return pixbuf; } -gboolean -on_doodle_click_cb (GtkWidget *widget, - GdkEventButton *event, - gpointer *data) +void doodle_add_to_free (GArray *arr) { - Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (TBO->drawing)); - TboObjectSvg *svgimage = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (0, 0, 0, 0, (gchar*)data)); - tbo_frame_add_obj (frame, TBO_OBJECT_BASE (svgimage)); - tbo_drawing_update (TBO_DRAWING (TBO->drawing)); + if (!TO_FREE) + TO_FREE = g_array_new (FALSE, FALSE, sizeof(GArray*)); - return FALSE; + g_array_append_val (TO_FREE, arr); } void @@ -86,7 +134,6 @@ GArray * get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) { GError *error = NULL; - gchar complete_dir[255]; const gchar *filename; struct stat filestat; int st; @@ -100,14 +147,19 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) while ((filename = g_dir_read_name (dir))) { - size_t strsize = sizeof (char) * (strlen (base_dir) + strlen (filename) + 2); - snprintf (complete_dir, strsize, "%s/%s", base_dir, filename); + gchar *complete_dir = g_build_filename (base_dir, filename, NULL); st = stat (complete_dir, &filestat); if (isdir && bubble_mode && strcmp (filename, "bubble")) + { + g_free (complete_dir); continue; + } if (!strcmp (filename, "bubble") && !bubble_mode) + { + g_free (complete_dir); continue; + } if (isdir && S_ISDIR (filestat.st_mode)) { @@ -119,6 +171,8 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) GString *filename_to_append = g_string_new (complete_dir); g_array_append_val (array, filename_to_append); } + + g_free (complete_dir); } g_dir_close (dir); @@ -131,62 +185,62 @@ doodle_add_images (gchar *dir) { int i; gchar *dirname; - GtkWidget *table; + GtkWidget *grid; GtkWidget *image; - GtkWidget *ebox; + GtkWidget *button; GdkPixbuf *pixbuf; - int r, c=2; int left, top; - int w=50, h; + int columns = 2; + int thumb_width; + int thumb_height; dirname = dir; GArray *arr = get_files (dirname, FALSE, FALSE); - r = (arr->len / c) + 1; - table = gtk_table_new (r, c, TRUE); + grid = gtk_grid_new (); + gtk_grid_set_row_spacing (GTK_GRID (grid), 8); + gtk_grid_set_column_spacing (GTK_GRID (grid), 8); GString *mystr; for (i=0; ilen; i++) { - top = i / 2; - left = i % 2; + const gchar *relative_path; + int prefix_len; + + top = i / columns; + left = i % columns; mystr = g_array_index (arr, GString*, i); - image = gtk_image_new_from_file (mystr->str); - pixbuf = gtk_image_get_pixbuf (GTK_IMAGE (image)); - - h = gdk_pixbuf_get_height (pixbuf) * 50 / (float)gdk_pixbuf_get_width (pixbuf); - pixbuf = gdk_pixbuf_scale_simple (pixbuf, w, h, GDK_INTERP_BILINEAR); - - gtk_widget_destroy (GTK_WIDGET (image)); - image = gtk_image_new_from_pixbuf (pixbuf); - ebox = gtk_event_box_new (); - gtk_widget_add_events (ebox, GDK_BUTTON_PRESS_MASK | - GDK_BUTTON_RELEASE_MASK | - GDK_POINTER_MOTION_MASK); - - //g_signal_connect (ebox, "button_press_event", G_CALLBACK (on_doodle_click_cb), mystr->str); - - // dnd - gtk_drag_source_set (ebox, - GDK_BUTTON1_MASK, - TARGET_LIST, - N_TARGETS, - GDK_ACTION_COPY); - g_signal_connect (ebox, "drag-data-get", G_CALLBACK (drag_data_get_handl), - mystr->str + tbo_files_prefix_len (mystr->str)); - g_signal_connect (ebox, "drag-begin", G_CALLBACK (drag_begin_handl), mystr->str); - g_signal_connect (ebox, "drag-end", G_CALLBACK (drag_end_handl), mystr->str); - - gtk_container_add (GTK_CONTAINER (ebox), image); - gtk_table_attach_defaults (GTK_TABLE (table), ebox, left, left + 1, top, top + 1); + prefix_len = tbo_files_prefix_len (mystr->str); + relative_path = prefix_len > 0 ? mystr->str + prefix_len : mystr->str; + + pixbuf = get_thumbnail_pixbuf (mystr->str, relative_path); + if (pixbuf == NULL) + continue; + + thumb_width = gdk_pixbuf_get_width (pixbuf); + thumb_height = gdk_pixbuf_get_height (pixbuf); + + image = tbo_picture_new_for_pixbuf (pixbuf); + gtk_picture_set_can_shrink (GTK_PICTURE (image), FALSE); + gtk_widget_set_size_request (image, thumb_width, thumb_height); + button = gtk_button_new (); + gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); + gtk_widget_set_can_focus (button, FALSE); + gtk_widget_set_size_request (button, thumb_width + 12, thumb_height + 12); + + tbo_dnd_setup_asset_source (button, mystr->str, relative_path); + + tbo_widget_add_child (button, image); + gtk_grid_attach (GTK_GRID (grid), button, left, top, 1, 1); + g_object_unref (pixbuf); } doodle_add_to_free (arr); - gtk_widget_show_all (GTK_WIDGET (table)); - return table; + tbo_widget_show_all (GTK_WIDGET (grid)); + return grid; } void @@ -195,32 +249,46 @@ doodle_add_dir_images (gchar *dir, GtkWidget *box) char base_name[255]; get_base_name (dir, base_name, 255); GtkWidget *expander = gtk_expander_new (base_name); - GtkWidget *table = doodle_add_images (dir); - gtk_container_add (GTK_CONTAINER (expander), table); + GtkWidget *grid = doodle_add_images (dir); + tbo_widget_add_child (expander, grid); gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); - gtk_container_add (GTK_CONTAINER (box), expander); + tbo_widget_add_child (box, expander); } -gboolean -on_expand_cb (GtkExpander *expander, GString *str) +void +on_expand_cb (GtkExpander *expander, GParamSpec *pspec, GString *str) { GString *mystr2; int i; - GtkWidget *vbox = g_list_first (gtk_container_get_children (GTK_CONTAINER (expander)))->data; - int numofchilds = g_list_length (gtk_container_get_children (GTK_CONTAINER (vbox))); + GtkWidget *vbox = gtk_expander_get_child (expander); + int numofchilds = 0; + if (vbox == NULL || !gtk_expander_get_expanded (expander)) + return; + + numofchilds = tbo_widget_get_child_count (vbox); + if (numofchilds == 0) { GArray *subdir = get_files (str->str, TRUE, FALSE); - for (i=0; ilen; i++) + + if (subdir != NULL && subdir->len > 0) { - mystr2 = g_array_index (subdir, GString*, i); - doodle_add_dir_images (mystr2->str, vbox); + for (i=0; ilen; i++) + { + mystr2 = g_array_index (subdir, GString*, i); + doodle_add_dir_images (mystr2->str, vbox); + } } - free_gstring_array (subdir); - g_string_free (str, TRUE); + else + { + GtkWidget *grid = doodle_add_images (str->str); + tbo_widget_add_child (vbox, grid); + } + + if (subdir != NULL) + free_gstring_array (subdir); } - gtk_widget_show_all (GTK_WIDGET (vbox)); - return FALSE; + tbo_widget_show_all (GTK_WIDGET (vbox)); } GtkWidget * @@ -237,7 +305,7 @@ doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode) char label_format[255]; int i, k; - vbox = gtk_vbox_new (FALSE, 5); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); GArray *arr = NULL; GString *mystr, *mystr2; @@ -252,21 +320,26 @@ doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode) { mystr = g_array_index (arr, GString*, i); - vbox2 = gtk_vbox_new (FALSE, 5); + vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); get_base_name (mystr->str, dirname, 255); snprintf (label_format, 255, "%s", dirname); expander = gtk_expander_new (label_format); gtk_expander_set_use_markup (GTK_EXPANDER (expander), TRUE); - gtk_box_pack_start (GTK_BOX (vbox), expander, FALSE, FALSE, 5); - gtk_container_add (GTK_CONTAINER (expander), vbox2); + tbo_box_pack_start (vbox, expander, FALSE, FALSE, 5); + tbo_widget_add_child (expander, vbox2); mystr2 = g_string_new (mystr->str); - g_signal_connect (GTK_EXPANDER (expander), "activate", G_CALLBACK (on_expand_cb), mystr2); + g_signal_connect_data (GTK_EXPANDER (expander), + "notify::expanded", + G_CALLBACK (on_expand_cb), + mystr2, + free_gstring_data, + 0); if (bubble_mode) { gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); - on_expand_cb (GTK_EXPANDER (expander), mystr2); + on_expand_cb (GTK_EXPANDER (expander), NULL, mystr2); } } free_gstring_array (arr); diff --git a/src/doodle-treeview.h b/src/doodle-treeview.h index 00fe5bf..180039b 100644 --- a/src/doodle-treeview.h +++ b/src/doodle-treeview.h @@ -24,6 +24,6 @@ #include "tbo-window.h" GtkWidget * doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode); -void doodle_free_all (); +void doodle_free_all (void); #endif diff --git a/src/export.c b/src/export.c index 777be1b..c74156a 100644 --- a/src/export.c +++ b/src/export.c @@ -25,9 +25,11 @@ #include #include "export.h" +#include "tbo-file-dialog.h" #include "tbo-drawing.h" #include "tbo-ui-utils.h" #include "tbo-types.h" +#include "tbo-widget.h" static int LOCK = 0; @@ -38,6 +40,41 @@ struct export_spin_args { gdouble *scale; }; +struct export_file_args { + TboWindow *tbo; + GtkEntry *entry; +}; + +struct export_dialog_data { + GMainLoop *loop; + gint response; +}; + +static gboolean +export_close_request_cb (GtkWindow *dialog, struct export_dialog_data *data) +{ + if (data->response == GTK_RESPONSE_NONE) + data->response = GTK_RESPONSE_CANCEL; + g_main_loop_quit (data->loop); + return TRUE; +} + +static void +export_button_cb (GtkButton *button, GtkWindow *dialog) +{ + struct export_dialog_data *data = g_object_get_data (G_OBJECT (dialog), "tbo-export-data"); + gint response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tbo-response")); + + data->response = response; + gtk_window_close (dialog); +} + +static void +show_export_error (TboWindow *tbo, const gchar *message) +{ + tbo_alert_show (GTK_WINDOW (tbo->window), message, NULL); +} + static gboolean export_size_cb (GtkWidget *widget, struct export_spin_args *args) { @@ -62,28 +99,16 @@ export_size_cb (GtkWidget *widget, struct export_spin_args *args) gboolean filedialog_cb (GtkWidget *widget, gpointer data) { - gint response; - gchar *filename; - GtkWidget *filechooserdialog; - GtkEntry *entry = GTK_ENTRY (data); - - filechooserdialog = gtk_file_chooser_dialog_new (_("Export as"), - NULL, - GTK_FILE_CHOOSER_ACTION_SAVE, - GTK_STOCK_CANCEL, - GTK_RESPONSE_CANCEL, - GTK_STOCK_SAVE, - GTK_RESPONSE_ACCEPT, - NULL); - response = gtk_dialog_run (GTK_DIALOG (filechooserdialog)); + struct export_file_args *args = data; + const gchar *current_text = gtk_editable_get_text (GTK_EDITABLE (args->entry)); + gchar *filename = tbo_file_dialog_save_export (args->tbo, current_text); - if (response == GTK_RESPONSE_ACCEPT) + if (filename != NULL) { - filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (filechooserdialog)); - gtk_entry_set_text (entry, filename); + gtk_editable_set_text (GTK_EDITABLE (args->entry), filename); + tbo_window_set_export_path (args->tbo, filename); + g_free (filename); } - - gtk_widget_destroy (GTK_WIDGET (filechooserdialog)); return FALSE; } @@ -94,17 +119,18 @@ tbo_export (TboWindow *tbo) cairo_t *cr; gint width = tbo->comic->width; gint height = tbo->comic->height; - gchar rpath[255]; - gchar format_pages[255]; - gchar *filename; + gchar *filename = NULL; + gchar *base_filename = NULL; + gchar *format_pages = NULL; GList *page_list; gint i, n, n2; gint response; gdouble scale = 1.0; - gchar *export_to; + gchar *export_to = NULL; gint export_to_index; struct export_spin_args spin_args; struct export_spin_args spin_args2; + struct export_file_args file_args; GtkWidget *dialog; GtkWidget *vbox; @@ -114,33 +140,52 @@ tbo_export (TboWindow *tbo) GtkWidget *filebutton; GtkWidget *spinw; GtkWidget *spinh; - GtkWidget *combobox; + GtkWidget *dropdown; + GtkWidget *actions; GtkWidget *button; - - dialog = gtk_dialog_new_with_buttons (_("Export as"), - GTK_WINDOW (tbo->window), - GTK_DIALOG_MODAL, - GTK_STOCK_CANCEL, - GTK_RESPONSE_CANCEL, - GTK_STOCK_SAVE, - GTK_RESPONSE_ACCEPT, - NULL); - - button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); - gtk_widget_grab_focus (GTK_WIDGET (button)); - - filebutton = gtk_button_new_from_stock (GTK_STOCK_OPEN); - vbox = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); - - hbox = gtk_hbox_new (FALSE, 5); + gchar *basename = NULL; + const char *export_formats[] = { + "guess by extension", + ".png", + ".pdf", + ".svg", + NULL, + }; + + struct export_dialog_data data; + + dialog = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (dialog), _("Export as")); + gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (tbo->window)); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_default_size (GTK_WINDOW (dialog), 420, -1); + + filebutton = gtk_button_new_with_label (_("Choose file")); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_margin_start (vbox, 12); + gtk_widget_set_margin_end (vbox, 12); + gtk_widget_set_margin_top (vbox, 12); + gtk_widget_set_margin_bottom (vbox, 12); + tbo_widget_add_child (dialog, vbox); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); filelabel = gtk_label_new (_("Filename: ")); fileinput = gtk_entry_new (); - gtk_entry_set_text (GTK_ENTRY (fileinput), tbo->comic->title); - gtk_container_add (GTK_CONTAINER (hbox), filelabel); - gtk_container_add (GTK_CONTAINER (hbox), fileinput); - gtk_container_add (GTK_CONTAINER (hbox), filebutton); - gtk_container_add (GTK_CONTAINER (vbox), hbox); + if (tbo->export_path != NULL) + { + basename = g_path_get_basename (tbo->export_path); + gtk_editable_set_text (GTK_EDITABLE (fileinput), basename); + g_free (basename); + } + else + { + gtk_editable_set_text (GTK_EDITABLE (fileinput), tbo->comic->title); + } + tbo_widget_add_child (hbox, filelabel); + tbo_widget_add_child (hbox, fileinput); + tbo_widget_add_child (hbox, filebutton); + tbo_widget_add_child (vbox, hbox); spinw = add_spin_with_label (vbox, _("width: "), tbo->comic->width); spinh = add_spin_with_label (vbox, _("height: "), tbo->comic->height); @@ -157,73 +202,122 @@ tbo_export (TboWindow *tbo) spin_args2.scale = &scale; g_signal_connect (spinh, "value-changed", G_CALLBACK (export_size_cb), &spin_args2); - combobox = gtk_combo_box_text_new (); - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combobox), _("guess by extension")); - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combobox), ".png"); - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combobox), ".pdf"); - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combobox), ".svg"); - gtk_combo_box_set_active (GTK_COMBO_BOX (combobox), 0); - gtk_container_add (GTK_CONTAINER (vbox), combobox); + dropdown = gtk_drop_down_new_from_strings (export_formats); + gtk_drop_down_set_selected (GTK_DROP_DOWN (dropdown), 0); + tbo_widget_add_child (vbox, dropdown); + + actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_halign (actions, GTK_ALIGN_END); + + button = gtk_button_new_with_mnemonic (_("_Cancel")); + g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_CANCEL)); + g_signal_connect (button, "clicked", G_CALLBACK (export_button_cb), dialog); + tbo_widget_add_child (actions, button); + + button = gtk_button_new_with_mnemonic (_("_Save")); + gtk_widget_add_css_class (button, "suggested-action"); + g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_ACCEPT)); + g_signal_connect (button, "clicked", G_CALLBACK (export_button_cb), dialog); + tbo_widget_add_child (actions, button); - gtk_widget_show_all (GTK_WIDGET (vbox)); + tbo_widget_add_child (vbox, actions); - g_signal_connect (filebutton, "clicked", G_CALLBACK (filedialog_cb), fileinput); + tbo_widget_show_all (GTK_WIDGET (vbox)); - response = gtk_dialog_run (GTK_DIALOG (dialog)); + file_args.tbo = tbo; + file_args.entry = GTK_ENTRY (fileinput); + g_signal_connect (filebutton, "clicked", G_CALLBACK (filedialog_cb), &file_args); + + data.loop = g_main_loop_new (NULL, FALSE); + data.response = GTK_RESPONSE_NONE; + g_object_set_data (G_OBJECT (dialog), "tbo-export-data", &data); + g_signal_connect (dialog, "close-request", G_CALLBACK (export_close_request_cb), &data); + gtk_window_present (GTK_WINDOW (dialog)); + g_main_loop_run (data.loop); + + response = data.response; if (response == GTK_RESPONSE_ACCEPT) { width = (gint) (width * scale); height = (gint) (height * scale); - filename = (gchar *)gtk_entry_get_text (GTK_ENTRY (fileinput)); + filename = g_strdup (gtk_editable_get_text (GTK_EDITABLE (fileinput))); + if (filename == NULL || *filename == '\0') + { + show_export_error (tbo, _("Please choose a filename to export.")); + g_free (filename); + gtk_window_destroy (GTK_WINDOW (dialog)); + return FALSE; + } + + tbo_window_set_export_path (tbo, filename); /* 0 guess, 1 png, 2 pdf, 3 svg */ - export_to_index = gtk_combo_box_get_active (GTK_COMBO_BOX (combobox)); + export_to_index = gtk_drop_down_get_selected (GTK_DROP_DOWN (dropdown)); switch (export_to_index) { case 0: - //guess - if (strlen (filename) > 4) + { + gchar *dot = strrchr (filename, '.'); + + if (dot != NULL && dot[1] != '\0') { - export_to = filename + strlen (filename) - 3; - filename = g_strndup (filename, strlen(filename) - 4); + export_to = g_ascii_strdown (dot + 1, -1); + base_filename = g_strndup (filename, dot - filename); } else { - filename = g_strdup (filename); - export_to = "png"; + base_filename = g_strdup (filename); + export_to = g_strdup ("png"); } break; + } case 1: - export_to = "png"; + export_to = g_strdup ("png"); + base_filename = g_strdup (filename); break; case 2: - export_to = "pdf"; + export_to = g_strdup ("pdf"); + base_filename = g_strdup (filename); break; case 3: - export_to = "svg"; + export_to = g_strdup ("svg"); + base_filename = g_strdup (filename); break; default: - export_to = "png"; + export_to = g_strdup ("png"); + base_filename = g_strdup (filename); break; } + if (g_strcmp0 (export_to, "png") != 0 && + g_strcmp0 (export_to, "pdf") != 0 && + g_strcmp0 (export_to, "svg") != 0) + { + g_free (export_to); + export_to = g_strdup ("png"); + } + n = g_list_length (tbo->comic->pages); n2 = n; for (i=0; n; n=n/10, i++); - snprintf (format_pages, 255, "%%s%%0%dd.%%s", i); + format_pages = g_strdup_printf ("%%s%%0%dd.%%s", i); for (i=0, page_list = g_list_first (tbo->comic->pages); page_list; i++, page_list = page_list->next) { - snprintf (rpath, 255, format_pages, filename, i, export_to); + gchar *rpath = g_strdup_printf (format_pages, base_filename, i, export_to); if (n2 == 1) - snprintf (rpath, 255, "%s.%s", filename, export_to); + { + g_free (rpath); + rpath = g_strdup_printf ("%s.%s", base_filename, export_to); + } // PDF if (strcmp (export_to, "pdf") == 0) { if (!surface) { - snprintf (rpath, 255, "%s.%s", filename, export_to); + g_free (rpath); + rpath = g_strdup_printf ("%s.%s", base_filename, export_to); surface = cairo_pdf_surface_create (rpath, width, height); cr = cairo_create (surface); } @@ -249,7 +343,11 @@ tbo_export (TboWindow *tbo) if (strcmp (export_to, "pdf") == 0) cairo_show_page (cr); else if (strcmp (export_to, "png") == 0) - cairo_surface_write_to_png (surface, rpath); + { + cairo_status_t status = cairo_surface_write_to_png (surface, rpath); + if (status != CAIRO_STATUS_SUCCESS) + show_export_error (tbo, cairo_status_to_string (status)); + } cairo_scale (cr, 1/scale, 1/scale); @@ -260,6 +358,8 @@ tbo_export (TboWindow *tbo) cairo_destroy (cr); surface = NULL; } + + g_free (rpath); } if (surface) @@ -268,10 +368,14 @@ tbo_export (TboWindow *tbo) cairo_destroy (cr); } } - if (!export_to_index) - g_free (filename); - gtk_widget_destroy (GTK_WIDGET (dialog)); + g_free (format_pages); + g_free (base_filename); + g_free (export_to); + g_free (filename); + + gtk_window_destroy (GTK_WINDOW (dialog)); + g_main_loop_unref (data.loop); return FALSE; } diff --git a/src/frame.c b/src/frame.c index 8e6fe75..7c57491 100644 --- a/src/frame.c +++ b/src/frame.c @@ -266,7 +266,7 @@ tbo_frame_add_obj (Frame *frame, TboObjectBase *obj) } float -tbo_frame_get_scale_factor () +tbo_frame_get_scale_factor (void) { return SCALE_FACTOR; } @@ -279,11 +279,11 @@ tbo_frame_del_obj (Frame *frame, TboObjectBase *obj) } void -tbo_frame_set_color (Frame *frame, GdkColor *color) +tbo_frame_set_color (Frame *frame, GdkRGBA *color) { - frame->color->r = color->red / 65535.0; - frame->color->g = color->green / 65535.0; - frame->color->b = color->blue / 65535.0; + frame->color->r = color->red; + frame->color->g = color->green; + frame->color->b = color->blue; BASE_COLOR.r = frame->color->r; BASE_COLOR.g = frame->color->g; BASE_COLOR.b = frame->color->b; diff --git a/src/frame.h b/src/frame.h index b61b9af..c5a2b02 100644 --- a/src/frame.h +++ b/src/frame.h @@ -41,12 +41,11 @@ int tbo_frame_point_inside_obj (TboObjectBase *obj, int x, int y); void tbo_frame_add_obj (Frame *frame, TboObjectBase *obj); void tbo_frame_del_obj (Frame *frame, TboObjectBase *obj); void tbo_frame_get_obj_relative (TboObjectBase *obj, int *x, int *y, int *w, int *h); -float tbo_frame_get_scale_factor (); +float tbo_frame_get_scale_factor (void); int tbo_frame_get_base_y (int y); int tbo_frame_get_base_x (int x); -void tbo_frame_set_color (Frame *frame, GdkColor *color); +void tbo_frame_set_color (Frame *frame, GdkRGBA *color); void tbo_frame_save (Frame *frame, FILE *file); Frame *tbo_frame_clone (Frame *frame); #endif - diff --git a/src/page.c b/src/page.c index ca2d116..5bc9f6f 100644 --- a/src/page.c +++ b/src/page.c @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include "comic.h" #include "page.h" #include "frame.h" diff --git a/src/tbo-drawing.c b/src/tbo-drawing.c index f5d9ac5..949c82e 100644 --- a/src/tbo-drawing.c +++ b/src/tbo-drawing.c @@ -31,79 +31,108 @@ #include "tbo-tool-doodle.h" #include "tbo-tooltip.h" -G_DEFINE_TYPE (TboDrawing, tbo_drawing, GTK_TYPE_LAYOUT); +G_DEFINE_TYPE (TboDrawing, tbo_drawing, GTK_TYPE_DRAWING_AREA); -/* private methods */ static gboolean -expose_event (GtkWidget *widget, cairo_t *cr1, gpointer dara) +queue_redraw_cb (gpointer data) { - cairo_t *cr; - gint w, h; - TboDrawing *self = TBO_DRAWING (widget); + TboDrawing *self = TBO_DRAWING (data); + + self->redraw_source_id = 0; + gtk_widget_queue_draw (GTK_WIDGET (self)); + return G_SOURCE_REMOVE; +} - cr = gdk_cairo_create(gtk_layout_get_bin_window (GTK_LAYOUT (widget))); - w = gdk_window_get_width (gtk_layout_get_bin_window (GTK_LAYOUT (widget))); - h = gdk_window_get_height (gtk_layout_get_bin_window (GTK_LAYOUT (widget))); +static void +get_view_size (TboDrawing *self, gint *width, gint *height) +{ + GtkWidget *scrolled; + + scrolled = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_SCROLLED_WINDOW); + if (scrolled != NULL) + { + *width = gtk_widget_get_width (scrolled); + *height = gtk_widget_get_height (scrolled); + } + else + { + *width = gtk_widget_get_width (GTK_WIDGET (self)); + *height = gtk_widget_get_height (GTK_WIDGET (self)); + } +} + +/* private methods */ +static void +draw_func (GtkDrawingArea *area, cairo_t *cr, gint width, gint height, gpointer data) +{ + TboDrawing *self = TBO_DRAWING (area); cairo_set_source_rgb (cr, 0, 0, 0); - cairo_rectangle (cr, 0, 0, w, h); + cairo_rectangle (cr, 0, 0, width, height); cairo_fill (cr); - tbo_drawing_draw (TBO_DRAWING (widget), cr); + tbo_drawing_draw (TBO_DRAWING (area), cr); tbo_tooltip_draw (cr); // Update drawing helpers if (self->tool) self->tool->drawing (self->tool, cr); - - cairo_destroy(cr); - - return FALSE; } -static gboolean -motion_notify_event (GtkWidget *widget, GdkEventMotion *event) +static void +motion_notify_cb (GtkEventControllerMotion *controller, gdouble x, gdouble y, gpointer user_data) { - TboDrawing *self = TBO_DRAWING (widget); - event->x = event->x / self->zoom; - event->y = event->y / self->zoom; + TboDrawing *self = TBO_DRAWING (user_data); + TboPointerEvent event = { + .x = x / self->zoom, + .y = y / self->zoom, + .button = 0, + .n_press = 0, + .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (controller)), + }; if (self->tool) - self->tool->on_move (self->tool, widget, event); - - return FALSE; + self->tool->on_move (self->tool, GTK_WIDGET (self), &event); } -static gboolean -button_press_event (GtkWidget *widget, GdkEventButton *event) +static void +click_pressed_cb (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { - TboDrawing *self = TBO_DRAWING (widget); - event->x = event->x / self->zoom; - event->y = event->y / self->zoom; + TboDrawing *self = TBO_DRAWING (user_data); + TboPointerEvent event = { + .x = x / self->zoom, + .y = y / self->zoom, + .button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)), + .n_press = n_press, + .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)), + }; + + gtk_widget_grab_focus (GTK_WIDGET (self)); if (self->tool) { if (TBO_IS_TOOL_BUBBLE (self->tool) || TBO_IS_TOOL_DOODLE (self->tool)) { tbo_toolbar_set_selected_tool (self->tool->tbo->toolbar, TBO_TOOLBAR_SELECTOR); } - self->tool->on_click (self->tool, widget, event); + self->tool->on_click (self->tool, GTK_WIDGET (self), &event); } - - return FALSE; } -static gboolean -button_release_event (GtkWidget *widget, GdkEventButton *event) +static void +click_released_cb (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { - TboDrawing *self = TBO_DRAWING (widget); - event->x = event->x / self->zoom; - event->y = event->y / self->zoom; + TboDrawing *self = TBO_DRAWING (user_data); + TboPointerEvent event = { + .x = x / self->zoom, + .y = y / self->zoom, + .button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)), + .n_press = n_press, + .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)), + }; if (self->tool) - self->tool->on_release (self->tool, widget, event); - - return FALSE; + self->tool->on_release (self->tool, GTK_WIDGET (self), &event); } /* init methods */ @@ -111,31 +140,34 @@ button_release_event (GtkWidget *widget, GdkEventButton *event) static void tbo_drawing_init (TboDrawing *self) { + GtkEventController *motion; + GtkGesture *click; + self->current_frame = NULL; self->zoom = 1; self->comic = NULL; self->tool = NULL; -} + self->redraw_source_id = 0; + gtk_widget_set_focusable (GTK_WIDGET (self), TRUE); -static void -tbo_drawing_realize (GtkWidget *widget) -{ - GdkWindow *bin_window; + gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (self), draw_func, NULL, NULL); - if (GTK_WIDGET_CLASS (tbo_drawing_parent_class)->realize) - (* GTK_WIDGET_CLASS (tbo_drawing_parent_class)->realize) (widget); + motion = gtk_event_controller_motion_new (); + g_signal_connect (motion, "motion", G_CALLBACK (motion_notify_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), motion); - bin_window = gtk_layout_get_bin_window (GTK_LAYOUT (widget)); - gdk_window_set_events (bin_window, - (gdk_window_get_events (bin_window) | - GDK_BUTTON_PRESS_MASK | - GDK_BUTTON_RELEASE_MASK | - GDK_POINTER_MOTION_MASK)); + click = gtk_gesture_click_new (); + g_signal_connect (click, "pressed", G_CALLBACK (click_pressed_cb), self); + g_signal_connect (click, "released", G_CALLBACK (click_released_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (click)); } static void tbo_drawing_finalize (GObject *self) { + if (TBO_DRAWING (self)->redraw_source_id != 0) + g_source_remove (TBO_DRAWING (self)->redraw_source_id); + /* Chain up to the parent class */ G_OBJECT_CLASS (tbo_drawing_parent_class)->finalize (self); } @@ -143,21 +175,15 @@ tbo_drawing_finalize (GObject *self) static void tbo_drawing_class_init (TboDrawingClass *klass) { - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - widget_class->draw = expose_event; - widget_class->motion_notify_event = motion_notify_event; - widget_class->button_press_event = button_press_event; - widget_class->button_release_event = button_release_event; - widget_class->realize = tbo_drawing_realize; gobject_class->finalize = tbo_drawing_finalize; } /* object functions */ GtkWidget * -tbo_drawing_new () +tbo_drawing_new (void) { GtkWidget *drawing; drawing = g_object_new (TBO_TYPE_DRAWING, NULL); @@ -169,7 +195,7 @@ tbo_drawing_new_with_params (Comic *comic) { GtkWidget *drawing = tbo_drawing_new (); TBO_DRAWING (drawing)->comic = comic; - gtk_layout_set_size (GTK_LAYOUT (drawing), comic->width+2, comic->height+2); + gtk_widget_set_size_request (drawing, comic->width + 2, comic->height + 2); return drawing; } @@ -177,12 +203,13 @@ tbo_drawing_new_with_params (Comic *comic) void tbo_drawing_update (TboDrawing *self) { - GtkAllocation alloc; - gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); - gtk_widget_queue_draw_area (GTK_WIDGET (self), - 0, 0, - alloc.width, - alloc.height); + if (self->redraw_source_id != 0) + return; + + self->redraw_source_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + queue_redraw_cb, + g_object_ref (self), + g_object_unref); } void @@ -281,10 +308,8 @@ tbo_drawing_zoom_fit (TboDrawing *self) { float z1, z2; int w, h; - GtkAllocation alloc; - gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); - w = alloc.width; - h = alloc.height; + + get_view_size (self, &w, &h); z1 = fabs ((float)w / (float)self->comic->width); z2 = fabs ((float)h / (float)self->comic->height); @@ -301,15 +326,20 @@ tbo_drawing_get_zoom (TboDrawing *self) void tbo_drawing_adjust_scroll (TboDrawing *self) { + gint width; + gint height; + if (!self->comic) return; - gtk_layout_set_size (GTK_LAYOUT (self), self->comic->width*self->zoom, self->comic->height*self->zoom); + + width = MAX (1, ceil (self->comic->width * self->zoom)); + height = MAX (1, ceil (self->comic->height * self->zoom)); + gtk_widget_set_size_request (GTK_WIDGET (self), width, height); tbo_drawing_update (self); } void tbo_drawing_init_dnd (TboDrawing *self, TboWindow *tbo) { - gtk_drag_dest_set (GTK_WIDGET (self), GTK_DEST_DEFAULT_ALL, TARGET_LIST, N_TARGETS, GDK_ACTION_COPY); - g_signal_connect (self, "drag-data-received", G_CALLBACK(drag_data_received_handl), tbo); + tbo_dnd_setup_drawing_dest (self, tbo); } diff --git a/src/tbo-drawing.h b/src/tbo-drawing.h index 47a029d..1bcfd85 100644 --- a/src/tbo-drawing.h +++ b/src/tbo-drawing.h @@ -40,18 +40,19 @@ typedef struct _TboDrawingClass TboDrawingClass; struct _TboDrawing { - GtkLayout parent_instance; + GtkDrawingArea parent_instance; /* instance members */ TboToolBase *tool; Frame *current_frame; gdouble zoom; Comic *comic; + guint redraw_source_id; }; struct _TboDrawingClass { - GtkLayoutClass parent_class; + GtkDrawingAreaClass parent_class; /* class members */ }; @@ -63,7 +64,7 @@ GType tbo_drawing_get_type (void); * Method definitions. */ -GtkWidget * tbo_drawing_new (); +GtkWidget * tbo_drawing_new (void); GtkWidget * tbo_drawing_new_with_params (Comic *comic); void tbo_drawing_update (TboDrawing *self); void tbo_drawing_set_current_frame (TboDrawing *self, Frame *frame); @@ -79,4 +80,3 @@ void tbo_drawing_adjust_scroll (TboDrawing *self); void tbo_drawing_init_dnd (TboDrawing *self, TboWindow *tbo); #endif /* __TBO_DRAWING_H__ */ - diff --git a/src/tbo-file-dialog.c b/src/tbo-file-dialog.c new file mode 100644 index 0000000..6702d80 --- /dev/null +++ b/src/tbo-file-dialog.c @@ -0,0 +1,195 @@ +/* + * This file is part of TBO, a gnome comic editor + * Copyright (C) 2010 Daniel Garcia Moreno + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include +#include "tbo-file-dialog.h" + +struct file_dialog_data { + GMainLoop *loop; + GFile *file; + GError *error; +}; + +static void +file_open_cb (GObject *source, GAsyncResult *result, gpointer user_data) +{ + GtkFileDialog *dialog = GTK_FILE_DIALOG (source); + struct file_dialog_data *data = user_data; + + data->file = gtk_file_dialog_open_finish (dialog, result, &data->error); + g_main_loop_quit (data->loop); +} + +static void +file_save_cb (GObject *source, GAsyncResult *result, gpointer user_data) +{ + GtkFileDialog *dialog = GTK_FILE_DIALOG (source); + struct file_dialog_data *data = user_data; + + data->file = gtk_file_dialog_save_finish (dialog, result, &data->error); + g_main_loop_quit (data->loop); +} + +static GtkFileDialog * +create_dialog (const gchar *title, TboWindow *window, const gchar *accept_label) +{ + GtkFileDialog *dialog = gtk_file_dialog_new (); + + gtk_file_dialog_set_title (dialog, title); + gtk_file_dialog_set_modal (dialog, TRUE); + gtk_file_dialog_set_accept_label (dialog, accept_label); + + return dialog; +} + +static gchar * +finish_dialog (struct file_dialog_data *data) +{ + gchar *path = NULL; + + if (data->file != NULL) + { + path = g_file_get_path (data->file); + g_object_unref (data->file); + } + + if (data->error != NULL) + g_error_free (data->error); + g_main_loop_unref (data->loop); + return path; +} + +static GListStore * +create_project_filters (void) +{ + GListStore *filters = g_list_store_new (GTK_TYPE_FILE_FILTER); + GtkFileFilter *filter = gtk_file_filter_new (); + + gtk_file_filter_set_name (filter, _("TBO files")); + gtk_file_filter_add_pattern (filter, "*.tbo"); + g_list_store_append (filters, filter); + g_object_unref (filter); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("All files")); + gtk_file_filter_add_pattern (filter, "*"); + g_list_store_append (filters, filter); + g_object_unref (filter); + + return filters; +} + +static void +set_initial_folder (GtkFileDialog *dialog, gchar *dirname) +{ + GFile *folder = g_file_new_for_path (dirname); + + gtk_file_dialog_set_initial_folder (dialog, folder); + g_object_unref (folder); + g_free (dirname); +} + +gchar * +tbo_file_dialog_open_project (TboWindow *window) +{ + GtkFileDialog *dialog = create_dialog (_("Open"), window, _("_Open")); + GListStore *filters = create_project_filters (); + struct file_dialog_data data = {0}; + + gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_open (dialog, GTK_WINDOW (window->window), NULL, file_open_cb, &data); + g_main_loop_run (data.loop); + + g_object_unref (filters); + g_object_unref (dialog); + return finish_dialog (&data); +} + +gchar * +tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) +{ + GtkFileDialog *dialog = create_dialog (_("Save as"), window, _("_Save")); + GListStore *filters = create_project_filters (); + struct file_dialog_data data = {0}; + + gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + if (suggested_name != NULL && *suggested_name != '\0') + gtk_file_dialog_set_initial_name (dialog, suggested_name); + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_save (dialog, GTK_WINDOW (window->window), NULL, file_save_cb, &data); + g_main_loop_run (data.loop); + + g_object_unref (filters); + g_object_unref (dialog); + return finish_dialog (&data); +} + +gchar * +tbo_file_dialog_open_image (TboWindow *window) +{ + GtkFileDialog *dialog = create_dialog (_("Add an Image"), window, _("_Open")); + GListStore *filters = g_list_store_new (GTK_TYPE_FILE_FILTER); + GtkFileFilter *filter = gtk_file_filter_new (); + struct file_dialog_data data = {0}; + + gtk_file_filter_set_name (filter, _("Image files")); + gtk_file_filter_add_mime_type (filter, "image/*"); + g_list_store_append (filters, filter); + g_object_unref (filter); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("All files")); + gtk_file_filter_add_pattern (filter, "*"); + g_list_store_append (filters, filter); + g_object_unref (filter); + + gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_open (dialog, GTK_WINDOW (window->window), NULL, file_open_cb, &data); + g_main_loop_run (data.loop); + + g_object_unref (filters); + g_object_unref (dialog); + return finish_dialog (&data); +} + +gchar * +tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) +{ + GtkFileDialog *dialog = create_dialog (_("Export as"), window, _("_Save")); + struct file_dialog_data data = {0}; + + set_initial_folder (dialog, tbo_window_get_export_dir (window)); + + if (current_text != NULL && *current_text != '\0') + { + if (g_path_is_absolute (current_text)) + { + GFile *file = g_file_new_for_path (current_text); + gtk_file_dialog_set_initial_file (dialog, file); + g_object_unref (file); + } + else + { + gtk_file_dialog_set_initial_name (dialog, current_text); + } + } + + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_save (dialog, GTK_WINDOW (window->window), NULL, file_save_cb, &data); + g_main_loop_run (data.loop); + + g_object_unref (dialog); + return finish_dialog (&data); +} diff --git a/src/tbo-file-dialog.h b/src/tbo-file-dialog.h new file mode 100644 index 0000000..2b4c094 --- /dev/null +++ b/src/tbo-file-dialog.h @@ -0,0 +1,22 @@ +/* + * This file is part of TBO, a gnome comic editor + * Copyright (C) 2010 Daniel Garcia Moreno + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef __TBO_FILE_DIALOG_H__ +#define __TBO_FILE_DIALOG_H__ + +#include +#include "tbo-window.h" + +gchar *tbo_file_dialog_open_project (TboWindow *window); +gchar *tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name); +gchar *tbo_file_dialog_open_image (TboWindow *window); +gchar *tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text); + +#endif diff --git a/src/tbo-files.c b/src/tbo-files.c index 06b414e..a7e6a17 100644 --- a/src/tbo-files.c +++ b/src/tbo-files.c @@ -23,19 +23,15 @@ #include #include "tbo-files.h" #include +#include "tbo-utils.h" -char **tbo_files_get_dirs () +char **tbo_files_get_dirs (void) { - // Possible doodle dirs - char **possible_dirs = malloc (4*sizeof(char*)); - possible_dirs[0] = malloc (255*sizeof(char*)); - possible_dirs[1] = malloc (255*sizeof(char*)); - possible_dirs[2] = malloc (255*sizeof(char*)); - possible_dirs[3] = NULL; + char **possible_dirs = g_new0 (char *, 4); - strcat (strcpy (possible_dirs[0], getenv("HOME")), "/.tbo/doodle"); - strcat (strcpy (possible_dirs[1], g_get_user_data_dir ()), "/tbo/doodle"); - strcpy (possible_dirs[2], DATA_DIR "/doodle"); + possible_dirs[0] = g_build_filename (g_get_home_dir (), ".tbo", "doodle", NULL); + possible_dirs[1] = g_build_filename (g_get_user_data_dir (), "tbo", "doodle", NULL); + possible_dirs[2] = tbo_get_data_path ("doodle"); return possible_dirs; } @@ -43,7 +39,7 @@ char **tbo_files_get_dirs () int tbo_files_prefix_len (char *str) { - int n, i = 0; + int n = 0, i = 0; char **possible_dirs = tbo_files_get_dirs (); while (possible_dirs[i]) { @@ -64,31 +60,39 @@ tbo_files_free (char **files) int i = 0; while(files[i]) { - free (files[i]); + g_free (files[i]); i++; } - free (files); + g_free (files); } -void -tbo_files_expand_path (char *source, char *dest) +gchar * +tbo_files_expand_path (const gchar *source) { int st, i = 0; char **possible_dirs = tbo_files_get_dirs (); struct stat filestat; + gchar *dest = NULL; + while (possible_dirs[i]) { - snprintf (dest, 255, "%s/%s", possible_dirs[i], source); + g_free (dest); + dest = g_build_filename (possible_dirs[i], source, NULL); st = stat (dest, &filestat); if (!st) break; - else - snprintf (dest, 255, "%s", source); i++; } + if (dest == NULL || stat (dest, &filestat) != 0) + { + g_free (dest); + dest = g_strdup (source); + } + tbo_files_free (possible_dirs); + return dest; } gboolean diff --git a/src/tbo-files.h b/src/tbo-files.h index 4fdb95f..1910ba5 100644 --- a/src/tbo-files.h +++ b/src/tbo-files.h @@ -22,10 +22,10 @@ #include -char **tbo_files_get_dirs (); +char **tbo_files_get_dirs (void); int tbo_files_prefix_len (char *str); void tbo_files_free (char **files); -void tbo_files_expand_path (char *source, char *dest); +gchar *tbo_files_expand_path (const gchar *source); gboolean tbo_files_is_svg_file (char *source); #endif diff --git a/src/tbo-object-base.c b/src/tbo-object-base.c index db47519..8b7a0b2 100644 --- a/src/tbo-object-base.c +++ b/src/tbo-object-base.c @@ -117,7 +117,7 @@ tbo_object_base_class_init (TboObjectBaseClass *klass) /* object functions */ GObject * -tbo_object_base_new () +tbo_object_base_new (void) { GObject *tbo_object; tbo_object = g_object_new (TBO_TYPE_OBJECT_BASE, NULL); diff --git a/src/tbo-object-base.h b/src/tbo-object-base.h index d643114..f17f69d 100644 --- a/src/tbo-object-base.h +++ b/src/tbo-object-base.h @@ -85,7 +85,7 @@ GType tbo_object_base_get_type (void); * Method definitions. */ -GObject * tbo_object_base_new (); +GObject * tbo_object_base_new (void); void tbo_object_base_flipv (TboObjectBase *self); void tbo_object_base_fliph (TboObjectBase *self); void tbo_object_base_get_flip_matrix (TboObjectBase *self, cairo_matrix_t *mx); @@ -95,4 +95,3 @@ void tbo_object_base_move (TboObjectBase *self, enum MOVE_OPT type); void tbo_object_base_resize (TboObjectBase *self, enum RESIZE_OPT type); #endif /* __TBO_OBJECT_BASE_H__ */ - diff --git a/src/tbo-object-group.c b/src/tbo-object-group.c index 71b2d5b..d9a5647 100644 --- a/src/tbo-object-group.c +++ b/src/tbo-object-group.c @@ -113,7 +113,7 @@ tbo_object_group_class_init (TboObjectGroupClass *klass) /* object functions */ GObject * -tbo_object_group_new () +tbo_object_group_new (void) { GObject *tbo_object; tbo_object = g_object_new (TBO_TYPE_OBJECT_GROUP, NULL); @@ -220,4 +220,3 @@ tbo_object_group_update_status (TboObjectGroup *self) tbo_object_group_unset_vars (tbo_object); } - diff --git a/src/tbo-object-group.h b/src/tbo-object-group.h index 779302f..ee3f27c 100644 --- a/src/tbo-object-group.h +++ b/src/tbo-object-group.h @@ -57,7 +57,7 @@ GType tbo_object_group_get_type (void); * Method definitions. */ -GObject * tbo_object_group_new (); +GObject * tbo_object_group_new (void); void tbo_object_group_add (TboObjectGroup *self, TboObjectBase *obj); void tbo_object_group_del (TboObjectGroup *self, TboObjectBase *obj); void tbo_object_group_set_vars (TboObjectBase *self); @@ -65,4 +65,3 @@ void tbo_object_group_unset_vars (TboObjectBase *self); void tbo_object_group_update_status (TboObjectGroup *self); #endif /* __TBO_OBJECT_GROUP_H__ */ - diff --git a/src/tbo-object-pixmap.c b/src/tbo-object-pixmap.c index 4dfa314..0b5ae38 100644 --- a/src/tbo-object-pixmap.c +++ b/src/tbo-object-pixmap.c @@ -19,10 +19,12 @@ #include #include +#include #include #include #include #include "tbo-types.h" +#include "tbo-files.h" #include "tbo-object-pixmap.h" G_DEFINE_TYPE (TboObjectPixmap, tbo_object_pixmap, TBO_TYPE_OBJECT_BASE); @@ -31,32 +33,150 @@ static void draw (TboObjectBase *, Frame *, cairo_t *); static void save (TboObjectBase *, FILE *); static TboObjectBase * tclone (TboObjectBase *); -static void -draw (TboObjectBase *self, Frame *frame, cairo_t *cr) +static gboolean +ensure_pixbuf (TboObjectPixmap *pixmap) { - TboObjectPixmap *pixmap = TBO_OBJECT_PIXMAP (self); - int w, h; - cairo_surface_t *image; - GdkPixbuf *pixbuf; GError *error = NULL; - char path[255]; + gchar *path; + + if (pixmap->pixbuf != NULL) + return TRUE; + + path = tbo_files_expand_path (pixmap->path->str); + pixmap->pixbuf = gdk_pixbuf_new_from_file (path, &error); + if (pixmap->pixbuf == NULL) + { + if (error != NULL) + { + g_warning ("%s", error->message); + g_error_free (error); + } + g_free (path); + return FALSE; + } - tbo_files_expand_path (pixmap->path->str, path); - pixbuf = gdk_pixbuf_new_from_file (path, &error); + g_free (path); + return TRUE; +} - if (!pixbuf) { - g_warning ("There's a problem here: %s", error->message); - return; +static gboolean +update_surface_cache (TboObjectPixmap *pixmap) +{ + cairo_surface_t *surface; + unsigned char *data; + int stride; + int width; + int height; + int src_stride; + guchar *src; + int x; + int y; + + if (pixmap->scaled_pixbuf == NULL) + return FALSE; + + if (pixmap->surface != NULL) + { + cairo_surface_destroy (pixmap->surface); + pixmap->surface = NULL; + } + + width = gdk_pixbuf_get_width (pixmap->scaled_pixbuf); + height = gdk_pixbuf_get_height (pixmap->scaled_pixbuf); + src_stride = gdk_pixbuf_get_rowstride (pixmap->scaled_pixbuf); + src = gdk_pixbuf_get_pixels (pixmap->scaled_pixbuf); + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) + { + cairo_surface_destroy (surface); + return FALSE; + } + + data = cairo_image_surface_get_data (surface); + stride = cairo_image_surface_get_stride (surface); + + for (y = 0; y < height; y++) + { + uint32_t *dest = (uint32_t *) (data + (y * stride)); + guchar *row = src + (y * src_stride); + + for (x = 0; x < width; x++) + { + guchar r = row[x * 4 + 0]; + guchar g = row[x * 4 + 1]; + guchar b = row[x * 4 + 2]; + guchar a = row[x * 4 + 3]; + + if (a != 255) + { + r = (guchar) ((r * a + 127) / 255); + g = (guchar) ((g * a + 127) / 255); + b = (guchar) ((b * a + 127) / 255); + } + + dest[x] = ((uint32_t) a << 24) | + ((uint32_t) r << 16) | + ((uint32_t) g << 8) | + (uint32_t) b; + } + } + + cairo_surface_mark_dirty (surface); + pixmap->surface = surface; + return TRUE; +} + +static gboolean +ensure_scaled_pixbuf (TboObjectBase *self, TboObjectPixmap *pixmap) +{ + if (!ensure_pixbuf (pixmap)) + return FALSE; + + if (!self->width) + self->width = gdk_pixbuf_get_width (pixmap->pixbuf); + if (!self->height) + self->height = gdk_pixbuf_get_height (pixmap->pixbuf); + + if (self->width <= 0 || self->height <= 0) + return FALSE; + + if (pixmap->scaled_pixbuf != NULL && + pixmap->cache_width == self->width && + pixmap->cache_height == self->height) + return TRUE; + + if (pixmap->scaled_pixbuf != NULL) + { + g_object_unref (pixmap->scaled_pixbuf); + pixmap->scaled_pixbuf = NULL; } - w = gdk_pixbuf_get_width (pixbuf); - h = gdk_pixbuf_get_height (pixbuf); + pixmap->scaled_pixbuf = gdk_pixbuf_scale_simple (pixmap->pixbuf, + self->width, + self->height, + GDK_INTERP_BILINEAR); + if (pixmap->scaled_pixbuf == NULL) + return FALSE; + + if (!update_surface_cache (pixmap)) + { + g_object_unref (pixmap->scaled_pixbuf); + pixmap->scaled_pixbuf = NULL; + return FALSE; + } - if (!self->width) self->width = w; - if (!self->height) self->height = h; + pixmap->cache_width = self->width; + pixmap->cache_height = self->height; + return TRUE; +} - float factorw = (float)self->width / (float)w; - float factorh = (float)self->height / (float)h; +static void +draw (TboObjectBase *self, Frame *frame, cairo_t *cr) +{ + TboObjectPixmap *pixmap = TBO_OBJECT_PIXMAP (self); + if (!ensure_scaled_pixbuf (self, pixmap)) + return; cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; tbo_object_base_get_flip_matrix (self, &mx); @@ -66,18 +186,14 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_translate (cr, frame->x+self->x, frame->y+self->y); cairo_rotate (cr, self->angle); cairo_transform (cr, &mx); - cairo_scale (cr, factorw, factorh); - gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_set_source_surface (cr, pixmap->surface, 0, 0); cairo_paint (cr); - cairo_scale (cr, 1/factorw, 1/factorh); cairo_transform (cr, &mx); cairo_rotate (cr, -self->angle); cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); cairo_reset_clip (cr); - - cairo_surface_destroy (image); } static void @@ -122,6 +238,11 @@ static void tbo_object_pixmap_init (TboObjectPixmap *self) { self->path = NULL; + self->pixbuf = NULL; + self->scaled_pixbuf = NULL; + self->surface = NULL; + self->cache_width = 0; + self->cache_height = 0; self->parent_instance.draw = draw; self->parent_instance.save = save; @@ -131,6 +252,12 @@ tbo_object_pixmap_init (TboObjectPixmap *self) static void tbo_object_pixmap_finalize (GObject *self) { + if (TBO_OBJECT_PIXMAP (self)->scaled_pixbuf) + g_object_unref (TBO_OBJECT_PIXMAP (self)->scaled_pixbuf); + if (TBO_OBJECT_PIXMAP (self)->surface) + cairo_surface_destroy (TBO_OBJECT_PIXMAP (self)->surface); + if (TBO_OBJECT_PIXMAP (self)->pixbuf) + g_object_unref (TBO_OBJECT_PIXMAP (self)->pixbuf); if (TBO_OBJECT_PIXMAP (self)->path) g_string_free (TBO_OBJECT_PIXMAP (self)->path, TRUE); /* Chain up to the parent class */ @@ -147,7 +274,7 @@ tbo_object_pixmap_class_init (TboObjectPixmapClass *klass) /* object functions */ GObject * -tbo_object_pixmap_new () +tbo_object_pixmap_new (void) { GObject *tbo_object; TboObjectPixmap *pixmap; @@ -177,4 +304,3 @@ tbo_object_pixmap_new_with_params (gint x, return G_OBJECT (pixmap); } - diff --git a/src/tbo-object-pixmap.h b/src/tbo-object-pixmap.h index e5f87a1..3d9bb1f 100644 --- a/src/tbo-object-pixmap.h +++ b/src/tbo-object-pixmap.h @@ -21,6 +21,7 @@ #define __TBO_OBJECT_PIXMAP_H__ #include +#include #include "tbo-object-base.h" #define TBO_TYPE_OBJECT_PIXMAP (tbo_object_pixmap_get_type ()) @@ -39,6 +40,11 @@ struct _TboObjectPixmap /* instance members */ GString *path; + GdkPixbuf *pixbuf; + GdkPixbuf *scaled_pixbuf; + cairo_surface_t *surface; + gint cache_width; + gint cache_height; }; struct _TboObjectPixmapClass @@ -55,7 +61,7 @@ GType tbo_object_pixmap_get_type (void); * Method definitions. */ -GObject * tbo_object_pixmap_new (); +GObject * tbo_object_pixmap_new (void); GObject * tbo_object_pixmap_new_with_params (gint x, gint y, gint width, @@ -63,4 +69,3 @@ GObject * tbo_object_pixmap_new_with_params (gint x, gchar *path); #endif /* __TBO_OBJECT_PIXMAP_H__ */ - diff --git a/src/tbo-object-svg.c b/src/tbo-object-svg.c index 0f94936..5885971 100644 --- a/src/tbo-object-svg.c +++ b/src/tbo-object-svg.c @@ -24,7 +24,6 @@ #include #include #include -#include #include "tbo-types.h" #include "tbo-files.h" #include "tbo-object-svg.h" @@ -35,59 +34,121 @@ static void draw (TboObjectBase *, Frame *, cairo_t *); static void save (TboObjectBase *, FILE *); static TboObjectBase * tclone (TboObjectBase *); -static void -draw (TboObjectBase *self, Frame *frame, cairo_t *cr) +static gboolean +ensure_handle (TboObjectSvg *svg) { GError *error = NULL; - RsvgHandle *rsvg_handle = NULL; - RsvgDimensionData rsvg_dimension_data; - TboObjectSvg *svg = TBO_OBJECT_SVG (self); - char path[255]; + gchar *path; - tbo_files_expand_path (svg->path->str, path); - rsvg_handle = rsvg_handle_new_from_file (path, &error); - if (!rsvg_handle) + if (svg->handle != NULL) + return TRUE; + + path = tbo_files_expand_path (svg->path->str); + svg->handle = rsvg_handle_new_from_file (path, &error); + if (svg->handle == NULL) { - g_print (_("Couldn't load %s\n"), path); - return; + if (error != NULL) + { + g_warning ("%s", error->message); + g_error_free (error); + } + else + { + g_warning ("Couldn't load %s", path); + } + g_free (path); + return FALSE; } - if (error != NULL) + + g_free (path); + return TRUE; +} + +static gboolean +ensure_surface (TboObjectBase *self, TboObjectSvg *svg) +{ + GError *error = NULL; + cairo_t *surface_cr; + gdouble width_px = 0; + gdouble height_px = 0; + RsvgRectangle viewport; + + if (!ensure_handle (svg)) + return FALSE; + + if (!rsvg_handle_get_intrinsic_size_in_pixels (svg->handle, &width_px, &height_px)) { - g_print ("%s\n", error->message); - g_error_free (error); - return; + width_px = self->width ? self->width : 128; + height_px = self->height ? self->height : 128; } - else + + if (!self->width) + self->width = ceil (width_px); + if (!self->height) + self->height = ceil (height_px); + + if (self->width <= 0 || self->height <= 0) + return FALSE; + + if (svg->surface != NULL && + svg->cache_width == self->width && + svg->cache_height == self->height) + return TRUE; + + if (svg->surface != NULL) { - rsvg_handle_get_dimensions (rsvg_handle, &rsvg_dimension_data); - int w = rsvg_dimension_data.width; - int h = rsvg_dimension_data.height; - if (!self->width) self->width = w; - if (!self->height) self->height = h; - - float factorw = (float)self->width / (float)w; - float factorh = (float)self->height / (float)h; - - cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; - tbo_object_base_get_flip_matrix (self, &mx); - - cairo_rectangle(cr, frame->x+2, frame->y+2, frame->width-4, frame->height-4); - cairo_clip (cr); - cairo_translate (cr, frame->x+self->x, frame->y+self->y); - cairo_rotate (cr, self->angle); - cairo_transform (cr, &mx); - cairo_scale (cr, factorw, factorh); - - rsvg_handle_render_cairo (rsvg_handle, cr); - - cairo_scale (cr, 1/factorw, 1/factorh); - cairo_transform (cr, &mx); - cairo_rotate (cr, -self->angle); - cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); - cairo_reset_clip (cr); - - g_object_unref (rsvg_handle); + cairo_surface_destroy (svg->surface); + svg->surface = NULL; } + + svg->surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + self->width, + self->height); + surface_cr = cairo_create (svg->surface); + viewport.x = 0; + viewport.y = 0; + viewport.width = self->width; + viewport.height = self->height; + rsvg_handle_render_document (svg->handle, surface_cr, &viewport, &error); + cairo_destroy (surface_cr); + + if (error != NULL) + { + g_warning ("%s", error->message); + g_error_free (error); + cairo_surface_destroy (svg->surface); + svg->surface = NULL; + return FALSE; + } + + svg->cache_width = self->width; + svg->cache_height = self->height; + return TRUE; +} + +static void +draw (TboObjectBase *self, Frame *frame, cairo_t *cr) +{ + TboObjectSvg *svg = TBO_OBJECT_SVG (self); + + if (!ensure_surface (self, svg)) + return; + + cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; + tbo_object_base_get_flip_matrix (self, &mx); + + cairo_rectangle(cr, frame->x+2, frame->y+2, frame->width-4, frame->height-4); + cairo_clip (cr); + cairo_translate (cr, frame->x+self->x, frame->y+self->y); + cairo_rotate (cr, self->angle); + cairo_transform (cr, &mx); + cairo_set_source_surface (cr, svg->surface, 0, 0); + cairo_paint (cr); + + cairo_transform (cr, &mx); + cairo_rotate (cr, -self->angle); + cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); + cairo_reset_clip (cr); } static void @@ -133,6 +194,10 @@ static void tbo_object_svg_init (TboObjectSvg *self) { self->path = NULL; + self->handle = NULL; + self->surface = NULL; + self->cache_width = 0; + self->cache_height = 0; self->parent_instance.draw = draw; self->parent_instance.save = save; @@ -142,6 +207,10 @@ tbo_object_svg_init (TboObjectSvg *self) static void tbo_object_svg_finalize (GObject *self) { + if (TBO_OBJECT_SVG (self)->surface) + cairo_surface_destroy (TBO_OBJECT_SVG (self)->surface); + if (TBO_OBJECT_SVG (self)->handle) + g_object_unref (TBO_OBJECT_SVG (self)->handle); if (TBO_OBJECT_SVG (self)->path) g_string_free (TBO_OBJECT_SVG (self)->path, TRUE); /* Chain up to the parent class */ @@ -158,7 +227,7 @@ tbo_object_svg_class_init (TboObjectSvgClass *klass) /* object functions */ GObject * -tbo_object_svg_new () +tbo_object_svg_new (void) { GObject *tbo_object; TboObjectSvg *svg; diff --git a/src/tbo-object-svg.h b/src/tbo-object-svg.h index 9ce0d02..05931b1 100644 --- a/src/tbo-object-svg.h +++ b/src/tbo-object-svg.h @@ -21,6 +21,7 @@ #define __TBO_OBJECT_SVG_H__ #include +#include #include "tbo-object-base.h" #define TBO_TYPE_OBJECT_SVG (tbo_object_svg_get_type ()) @@ -39,6 +40,10 @@ struct _TboObjectSvg /* instance members */ GString *path; + RsvgHandle *handle; + cairo_surface_t *surface; + gint cache_width; + gint cache_height; }; struct _TboObjectSvgClass @@ -55,7 +60,7 @@ GType tbo_object_svg_get_type (void); * Method definitions. */ -GObject * tbo_object_svg_new (); +GObject * tbo_object_svg_new (void); GObject * tbo_object_svg_new_with_params (gint x, gint y, gint width, @@ -63,4 +68,3 @@ GObject * tbo_object_svg_new_with_params (gint x, gchar *path); #endif /* __TBO_OBJECT_SVG_H__ */ - diff --git a/src/tbo-object-text.c b/src/tbo-object-text.c index 943fcc5..0661d3e 100644 --- a/src/tbo-object-text.c +++ b/src/tbo-object-text.c @@ -21,6 +21,7 @@ #include #include #include +#include #include "tbo-types.h" #include "tbo-object-text.h" @@ -46,7 +47,7 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) return; } - gdk_cairo_set_source_color (cr, textobj->font_color); + gdk_cairo_set_source_rgba (cr, textobj->font_color); layout = pango_cairo_create_layout (cr); pango_layout_set_text (layout, text, -1); @@ -94,9 +95,9 @@ save (TboObjectBase *self, FILE *file) TboObjectText *text = TBO_OBJECT_TEXT (self); - r = (float)text->font_color->red / (float)COLORMAX; - g = (float)text->font_color->green / (float)COLORMAX; - b = (float)text->font_color->blue / (float)COLORMAX; + r = text->font_color->red; + g = text->font_color->green; + b = text->font_color->blue; snprintf (buffer, 1024, " x, self->y, self->width, self->height, text->text->str, - tbo_object_text_get_string (text), + font, text->font_color)); + g_free (font); newtext->angle = self->angle; newtext->flipv = self->flipv; newtext->fliph = self->fliph; @@ -160,7 +164,7 @@ tbo_object_text_finalize (GObject *self) if (text->description) pango_font_description_free (text->description); if (text->font_color) - gdk_color_free (text->font_color); + gdk_rgba_free (text->font_color); /* Chain up to the parent class */ G_OBJECT_CLASS (tbo_object_text_parent_class)->finalize (self); } @@ -175,16 +179,16 @@ tbo_object_text_class_init (TboObjectTextClass *klass) /* object functions */ GObject * -tbo_object_text_new () +tbo_object_text_new (void) { GObject *tbo_object; TboObjectText *text; - GdkColor color = { 0, 0, 0, 0 }; + GdkRGBA color = { 0, 0, 0, 1 }; tbo_object = g_object_new (TBO_TYPE_OBJECT_TEXT, NULL); text = TBO_OBJECT_TEXT (tbo_object); text->text = g_string_new (_("text")); text->description = pango_font_description_from_string ("Sans Normal 27"); - text->font_color = gdk_color_copy (&color); + text->font_color = gdk_rgba_copy (&color); return tbo_object; } @@ -196,7 +200,7 @@ tbo_object_text_new_with_params (gint x, gint height, gchar *text, gchar *fontname, - GdkColor *color) + GdkRGBA *color) { TboObjectBase *obj; TboObjectText *textobj; @@ -209,8 +213,12 @@ tbo_object_text_new_with_params (gint x, obj->height = height; g_string_assign (textobj->text, text); + if (textobj->description) + pango_font_description_free (textobj->description); textobj->description = pango_font_description_from_string (fontname); - textobj->font_color = gdk_color_copy (color); + if (textobj->font_color) + gdk_rgba_free (textobj->font_color); + textobj->font_color = gdk_rgba_copy (color); return G_OBJECT (obj); } @@ -238,11 +246,11 @@ tbo_object_text_change_font (TboObjectText *self, gchar *font) } void -tbo_object_text_change_color (TboObjectText *self, GdkColor *color) +tbo_object_text_change_color (TboObjectText *self, GdkRGBA *color) { if (self->font_color) - gdk_color_free (self->font_color); - self->font_color = gdk_color_copy (color); + gdk_rgba_free (self->font_color); + self->font_color = gdk_rgba_copy (color); } gchar * diff --git a/src/tbo-object-text.h b/src/tbo-object-text.h index d709be3..71af51f 100644 --- a/src/tbo-object-text.h +++ b/src/tbo-object-text.h @@ -31,8 +31,6 @@ #define TBO_IS_OBJECT_TEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TBO_TYPE_OBJECT_TEXT)) #define TBO_OBJECT_TEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TBO_TYPE_OBJECT_TEXT, TboObjectTextClass)) -#define COLORMAX 65535 - typedef struct _TboObjectText TboObjectText; typedef struct _TboObjectTextClass TboObjectTextClass; @@ -43,7 +41,7 @@ struct _TboObjectText /* instance members */ GString *text; PangoFontDescription *description; - GdkColor *font_color; + GdkRGBA *font_color; }; struct _TboObjectTextClass @@ -60,19 +58,18 @@ GType tbo_object_text_get_type (void); * Method definitions. */ -GObject * tbo_object_text_new (); +GObject * tbo_object_text_new (void); GObject * tbo_object_text_new_with_params (gint x, gint y, gint width, gint height, gchar *text, gchar *fontname, - GdkColor *color); + GdkRGBA *color); gchar * tbo_object_text_get_text (TboObjectText *self); void tbo_object_text_set_text (TboObjectText *self, const gchar *text); void tbo_object_text_change_font (TboObjectText *self, gchar *font); -void tbo_object_text_change_color (TboObjectText *self, GdkColor *color); +void tbo_object_text_change_color (TboObjectText *self, GdkRGBA *color); gchar * tbo_object_text_get_string (TboObjectText *self); #endif /* __TBO_OBJECT_TEXT_H__ */ - diff --git a/src/tbo-tool-base.c b/src/tbo-tool-base.c index ee6e7bb..ff04c12 100644 --- a/src/tbo-tool-base.c +++ b/src/tbo-tool-base.c @@ -23,10 +23,10 @@ G_DEFINE_TYPE (TboToolBase, tbo_tool_base, G_TYPE_OBJECT); static void on_select (TboToolBase *tool) {} static void on_unselect (TboToolBase *tool) {} -static void on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) {} -static void on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) {} -static void on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) {} -static void on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event) {} +static void on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) {} +static void on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) {} +static void on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) {} +static void on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event) {} static void drawing (TboToolBase *tool, cairo_t *cr) {} /* init methods */ @@ -64,7 +64,7 @@ tbo_tool_base_class_init (TboToolBaseClass *klass) /* object functions */ GObject * -tbo_tool_base_new () +tbo_tool_base_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_BASE, NULL); @@ -88,4 +88,3 @@ tbo_tool_base_set_action (TboToolBase *self, gchar *action) if (action) self->action = g_strdup (action); } - diff --git a/src/tbo-tool-base.h b/src/tbo-tool-base.h index ed57cd1..2811535 100644 --- a/src/tbo-tool-base.h +++ b/src/tbo-tool-base.h @@ -45,10 +45,10 @@ struct _TboToolBase void (*on_select) (TboToolBase *tool); void (*on_unselect) (TboToolBase *tool); - void (*on_move) (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event); - void (*on_click) (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); - void (*on_release) (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); - void (*on_key) (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event); + void (*on_move) (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); + void (*on_click) (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); + void (*on_release) (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); + void (*on_key) (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event); void (*drawing) (TboToolBase *tool, cairo_t *cr); }; @@ -65,9 +65,8 @@ GType tbo_tool_base_get_type (void); /* * Method definitions. */ -GObject * tbo_tool_base_new (); +GObject * tbo_tool_base_new (void); GObject * tbo_tool_base_new_with_params (TboWindow *tbo); void tbo_tool_base_set_action (TboToolBase *self, gchar *action); #endif /* __TBO_TOOL_BASE_H__ */ - diff --git a/src/tbo-tool-bubble.c b/src/tbo-tool-bubble.c index 4d65ac6..4fbdabe 100644 --- a/src/tbo-tool-bubble.c +++ b/src/tbo-tool-bubble.c @@ -19,6 +19,7 @@ #include "doodle-treeview.h" #include "tbo-tool-bubble.h" +#include "tbo-widget.h" G_DEFINE_TYPE (TboToolBubble, tbo_tool_bubble, TBO_TYPE_TOOL_DOODLE); @@ -28,8 +29,8 @@ setup_tree (TboToolDoodle *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; self->tree = doodle_setup_tree (tbo, TRUE); - gtk_widget_show_all (self->tree); - self->tree = g_object_ref (self->tree); + g_object_ref_sink (self->tree); + tbo_widget_show_all (self->tree); } /* init methods */ @@ -48,7 +49,7 @@ tbo_tool_bubble_class_init (TboToolBubbleClass *klass) /* object functions */ GObject * -tbo_tool_bubble_new () +tbo_tool_bubble_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_BUBBLE, NULL); @@ -65,4 +66,3 @@ tbo_tool_bubble_new_with_params (TboWindow *tbo) tbo_tool_base->tbo = tbo; return tbo_tool; } - diff --git a/src/tbo-tool-bubble.h b/src/tbo-tool-bubble.h index 4dbaeba..35d85f8 100644 --- a/src/tbo-tool-bubble.h +++ b/src/tbo-tool-bubble.h @@ -54,8 +54,7 @@ GType tbo_tool_bubble_get_type (void); /* * Method definitions. */ -GObject * tbo_tool_bubble_new (); +GObject * tbo_tool_bubble_new (void); GObject * tbo_tool_bubble_new_with_params (TboWindow *tbo); #endif /* __TBO_TOOL_BUBBLE_H__ */ - diff --git a/src/tbo-tool-doodle.c b/src/tbo-tool-doodle.c index 78f418f..3f91bda 100644 --- a/src/tbo-tool-doodle.c +++ b/src/tbo-tool-doodle.c @@ -19,6 +19,7 @@ #include "doodle-treeview.h" #include "tbo-tool-doodle.h" +#include "tbo-widget.h" G_DEFINE_TYPE (TboToolDoodle, tbo_tool_doodle, TBO_TYPE_TOOL_BASE); @@ -26,6 +27,7 @@ G_DEFINE_TYPE (TboToolDoodle, tbo_tool_doodle, TBO_TYPE_TOOL_BASE); static void on_select (TboToolBase *tool); static void on_unselect (TboToolBase *tool); +static void finalize (GObject *object); /* Definitions */ @@ -40,6 +42,7 @@ update_scroll_cb (gpointer data) gtk_adjustment_set_value (adjust, self->hadjust); adjust = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->scroll2)); gtk_adjustment_set_value (adjust, self->vadjust); + self->scroll_source_id = 0; return FALSE; } @@ -47,8 +50,8 @@ static void setup_tree (TboToolDoodle *self) { self->tree = doodle_setup_tree (TBO_TOOL_BASE (self)->tbo, FALSE); - gtk_widget_show_all (self->tree); - self->tree = g_object_ref (self->tree); + g_object_ref_sink (self->tree); + tbo_widget_show_all (self->tree); } /* tool signal */ @@ -57,16 +60,23 @@ on_select (TboToolBase *tool) { TboToolDoodle *self = TBO_TOOL_DOODLE (tool); TboWindow *tbo = tool->tbo; - if (!self->tree) - { + + if (self->tree == NULL) self->setup_tree (self); - } + + if (gtk_widget_get_parent (self->tree) == GTK_WIDGET (tbo->toolarea)) + return; tbo_empty_tool_area (tbo); - gtk_container_add (GTK_CONTAINER (tbo->toolarea), self->tree); + tbo_widget_add_child (tbo->toolarea, self->tree); + if (self->scroll_source_id != 0) + g_source_remove (self->scroll_source_id); - g_timeout_add (5, update_scroll_cb, self); + self->scroll_source_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + update_scroll_cb, + g_object_ref (self), + g_object_unref); } static void @@ -76,6 +86,12 @@ on_unselect (TboToolBase *tool) TboToolDoodle *self = TBO_TOOL_DOODLE (tool); TboWindow *tbo = tool->tbo; + if (self->scroll_source_id != 0) + { + g_source_remove (self->scroll_source_id); + self->scroll_source_id = 0; + } + if (GTK_IS_WIDGET (self->tree) && gtk_widget_get_parent (self->tree) == GTK_WIDGET (tbo->toolarea)) { adjust = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (tbo->scroll2)); @@ -83,7 +99,7 @@ on_unselect (TboToolBase *tool) adjust = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->scroll2)); self->vadjust = gtk_adjustment_get_value (adjust); - gtk_container_remove (GTK_CONTAINER (tbo->toolarea), self->tree); + tbo_widget_remove_child (tbo->toolarea, self->tree); } tbo_empty_tool_area (tbo); @@ -97,6 +113,7 @@ tbo_tool_doodle_init (TboToolDoodle *self) self->tree = NULL; self->hadjust = 0.0; self->vadjust = 0.0; + self->scroll_source_id = 0; self->setup_tree = setup_tree; self->parent_instance.on_select = on_select; @@ -106,12 +123,29 @@ tbo_tool_doodle_init (TboToolDoodle *self) static void tbo_tool_doodle_class_init (TboToolDoodleClass *klass) { + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = finalize; +} + +static void +finalize (GObject *object) +{ + TboToolDoodle *self = TBO_TOOL_DOODLE (object); + + if (self->scroll_source_id != 0) + g_source_remove (self->scroll_source_id); + + if (self->tree != NULL) + g_object_unref (self->tree); + + G_OBJECT_CLASS (tbo_tool_doodle_parent_class)->finalize (object); } /* object functions */ GObject * -tbo_tool_doodle_new () +tbo_tool_doodle_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_DOODLE, NULL); @@ -128,4 +162,3 @@ tbo_tool_doodle_new_with_params (TboWindow *tbo) tbo_tool_base->tbo = tbo; return tbo_tool; } - diff --git a/src/tbo-tool-doodle.h b/src/tbo-tool-doodle.h index 1be8de2..a41ce97 100644 --- a/src/tbo-tool-doodle.h +++ b/src/tbo-tool-doodle.h @@ -43,6 +43,7 @@ struct _TboToolDoodle GtkWidget *tree; gdouble hadjust; gdouble vadjust; + guint scroll_source_id; void (*setup_tree) (TboToolDoodle *self); }; @@ -59,8 +60,7 @@ GType tbo_tool_doodle_get_type (void); /* * Method definitions. */ -GObject * tbo_tool_doodle_new (); +GObject * tbo_tool_doodle_new (void); GObject * tbo_tool_doodle_new_with_params (TboWindow *tbo); #endif /* __TBO_TOOL_DOODLE_H__ */ - diff --git a/src/tbo-tool-frame.c b/src/tbo-tool-frame.c index 58af073..7a9542c 100644 --- a/src/tbo-tool-frame.c +++ b/src/tbo-tool-frame.c @@ -29,16 +29,16 @@ G_DEFINE_TYPE (TboToolFrame, tbo_tool_frame, TBO_TYPE_TOOL_BASE); #define MINIMUM(x, y) x < y ? (int)x : (int)y /* Headers */ -static void on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event); -static void on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); -static void on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); +static void on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); static void drawing (TboToolBase *tool, cairo_t *cr); /* Definitions */ /* tool signal */ static void -on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) +on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int x, y; TboToolFrame *self = TBO_TOOL_FRAME (tool); @@ -67,7 +67,7 @@ on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) } static void -on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { TboToolFrame *self = TBO_TOOL_FRAME (tool); self->n_frame_x = (int)event->x; @@ -75,7 +75,7 @@ on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) } static void -on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int w, h; TboWindow *tbo = tool->tbo; @@ -89,6 +89,7 @@ on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) tbo_page_new_frame (tbo_comic_get_current_page (tbo->comic), MINIMUM (self->n_frame_x, event->x), MINIMUM (self->n_frame_y, event->y), w, h); + tbo_window_mark_dirty (tbo); } self->n_frame_x = -1; @@ -134,7 +135,7 @@ tbo_tool_frame_class_init (TboToolFrameClass *klass) /* object functions */ GObject * -tbo_tool_frame_new () +tbo_tool_frame_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_FRAME, NULL); @@ -151,4 +152,3 @@ tbo_tool_frame_new_with_params (TboWindow *tbo) tbo_tool_base->tbo = tbo; return tbo_tool; } - diff --git a/src/tbo-tool-frame.h b/src/tbo-tool-frame.h index 0703424..3435513 100644 --- a/src/tbo-tool-frame.h +++ b/src/tbo-tool-frame.h @@ -60,8 +60,7 @@ GType tbo_tool_frame_get_type (void); /* * Method definitions. */ -GObject * tbo_tool_frame_new (); +GObject * tbo_tool_frame_new (void); GObject * tbo_tool_frame_new_with_params (TboWindow *tbo); #endif /* __TBO_TOOL_FRAME_H__ */ - diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index ba498c7..97bf78c 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -24,10 +24,13 @@ #include "comic.h" #include "frame.h" #include "page.h" +#include "tbo-window.h" #include "tbo-ui-utils.h" +#include "tbo-widget.h" #include "tbo-tool-selector.h" #include "tbo-drawing.h" #include "tbo-object-group.h" +#include "tbo-tool-text.h" #include "ui-menu.h" #include "tbo-tooltip.h" #include "tbo-undo.h" @@ -35,19 +38,24 @@ G_DEFINE_TYPE (TboToolSelector, tbo_tool_selector, TBO_TYPE_TOOL_BASE); /* Headers */ -static void on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event); -static void on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); -static void on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); -static void on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event); +static void on_select (TboToolBase *tool); +static void on_unselect (TboToolBase *tool); +static void on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event); static void drawing (TboToolBase *tool, cairo_t *cr); -static void frame_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event); -static void page_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event); -static void frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); -static void page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); +static void frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void page_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void frame_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); +static void page_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); static void frame_view_drawing (TboToolBase *tool, cairo_t *cr); static void page_view_drawing (TboToolBase *tool, cairo_t *cr); -static void frame_view_on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event); +static void frame_view_on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event); +static gboolean delete_selected (TboToolSelector *self); +static void open_text_editor (TboToolSelector *self, TboObjectText *text); +static void finalize (GObject *object); /* Definitions */ @@ -68,56 +76,41 @@ update_selected_cb (GtkSpinButton *widget, TboToolSelector *tool) return FALSE; } -static gboolean -update_color_cb (GtkColorButton *button, TboToolSelector *tool) +static void +update_color_cb (GtkWidget *button, GParamSpec *pspec, TboToolSelector *tool) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (tool)->tbo->drawing); if (tool->resizing || tool->clicked || tool->selected_frame == NULL) - return FALSE; + return; - GdkColor color = { 0, 0, 0, 0 }; - gtk_color_button_get_color (button, &color); - tbo_frame_set_color (tool->selected_frame, &color); + const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (button)); + tbo_frame_set_color (tool->selected_frame, (GdkRGBA *) color); tbo_drawing_update (drawing); - return FALSE; } static gboolean -update_border_cb (GtkToggleButton *button, TboToolSelector *tool) +update_border_cb (GtkCheckButton *button, TboToolSelector *tool) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (tool)->tbo->drawing); if (tool->resizing || tool->clicked || tool->selected_frame == NULL) return FALSE; - tool->selected_frame->border = !tool->selected_frame->border; + tool->selected_frame->border = gtk_check_button_get_active (button); tbo_drawing_update (drawing); return FALSE; } -static void -empty_tool_area (TboToolSelector *self) -{ - tbo_empty_tool_area (TBO_TOOL_BASE (self)->tbo); - self->spin_x = NULL; - self->spin_y = NULL; - self->spin_h = NULL; - self->spin_w = NULL; -} - static void update_tool_area (TboToolSelector *self) { - TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; - GtkWidget *toolarea = tbo->toolarea; + GtkWidget *toolarea = self->toolarea_widget; GtkWidget *hpanel; GtkWidget *label; - GtkWidget *color; - GtkWidget *border; - GdkColor gdk_color = { 0, 0, 0, 0 }; + GdkRGBA gdk_color = { 0, 0, 0, 1 }; + GtkColorDialog *color_dialog; if (!self->spin_x) { - empty_tool_area (self); self->spin_x = add_spin_with_label (toolarea, "x: ", self->selected_frame->x); self->spin_y = add_spin_with_label (toolarea, "y: ", self->selected_frame->y); self->spin_w = add_spin_with_label (toolarea, "w: ", self->selected_frame->width); @@ -128,32 +121,71 @@ update_tool_area (TboToolSelector *self) g_signal_connect (self->spin_w, "value-changed", G_CALLBACK (update_selected_cb), self); g_signal_connect (self->spin_h, "value-changed", G_CALLBACK (update_selected_cb), self); - hpanel = gtk_hbox_new (FALSE, 0); + hpanel = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); label = gtk_label_new (_("Background color: ")); - gtk_misc_set_alignment (GTK_MISC (label), 0, 0); - color = gtk_color_button_new (); - gdk_color.red = self->selected_frame->color->r * 65535; - gdk_color.green = self->selected_frame->color->g * 65535; - gdk_color.blue = self->selected_frame->color->b * 65535; - gtk_color_button_set_color (GTK_COLOR_BUTTON (color), &gdk_color); - - gtk_box_pack_start (GTK_BOX (hpanel), label, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (hpanel), color, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (toolarea), hpanel, FALSE, FALSE, 5); - g_signal_connect (color, "color-set", G_CALLBACK (update_color_cb), self); - - border = gtk_check_button_new_with_label (_("border")); - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (border), self->selected_frame->border); - gtk_box_pack_start (GTK_BOX (toolarea), border, FALSE, FALSE, 5); - g_signal_connect (border, "toggled", G_CALLBACK (update_border_cb), self); - - gtk_widget_show_all (toolarea); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + color_dialog = gtk_color_dialog_new (); + self->color_button = gtk_color_dialog_button_new (color_dialog); + gdk_color.red = self->selected_frame->color->r; + gdk_color.green = self->selected_frame->color->g; + gdk_color.blue = self->selected_frame->color->b; + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); + + tbo_box_pack_start (hpanel, label, TRUE, TRUE, 5); + tbo_box_pack_start (hpanel, self->color_button, TRUE, TRUE, 5); + tbo_box_pack_start (toolarea, hpanel, FALSE, FALSE, 5); + g_signal_connect (self->color_button, "notify::rgba", G_CALLBACK (update_color_cb), self); + + self->border_button = gtk_check_button_new_with_label (_("border")); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), self->selected_frame->border); + tbo_box_pack_start (toolarea, self->border_button, FALSE, FALSE, 5); + g_signal_connect (self->border_button, "toggled", G_CALLBACK (update_border_cb), self); + + tbo_widget_show_all (toolarea); } gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_x), self->selected_frame->x); gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_y), self->selected_frame->y); gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_w), self->selected_frame->width); gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_h), self->selected_frame->height); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), self->selected_frame->border); + gdk_color.red = self->selected_frame->color->r; + gdk_color.green = self->selected_frame->color->g; + gdk_color.blue = self->selected_frame->color->b; + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); +} + +static void +on_select (TboToolBase *tool) +{ + TboToolSelector *self = TBO_TOOL_SELECTOR (tool); + + if (self->toolarea_widget == NULL) + { + self->toolarea_widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + g_object_ref_sink (self->toolarea_widget); + } + + tbo_empty_tool_area (tool->tbo); + tbo_widget_add_child (tool->tbo->toolarea, self->toolarea_widget); + + if (self->selected_frame != NULL) + update_tool_area (self); + else + update_menubar (tool->tbo); +} + +static void +on_unselect (TboToolBase *tool) +{ + TboToolSelector *self = TBO_TOOL_SELECTOR (tool); + + if (self->toolarea_widget != NULL && + gtk_widget_get_parent (self->toolarea_widget) == GTK_WIDGET (tool->tbo->toolarea)) + { + tbo_widget_remove_child (tool->tbo->toolarea, self->toolarea_widget); + } } static gboolean @@ -245,7 +277,7 @@ moved_object (TboToolSelector *tool) /* tool signal */ static void -on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) +on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); Frame *frame = tbo_drawing_get_current_frame (drawing); @@ -258,7 +290,7 @@ on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) } static void -on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); Frame *frame = tbo_drawing_get_current_frame (drawing); @@ -271,10 +303,11 @@ on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) } static void -on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { TboToolSelector *self = TBO_TOOL_SELECTOR (tool); TboWindow *tbo = tool->tbo; + gboolean should_open_text_editor = FALSE; // TODO create undo actions for movements / resizing and rotating if (self->selected_object && moved_object (self)) { tbo_undo_stack_insert (tbo->undo_stack, @@ -283,29 +316,96 @@ on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) self->start_m_y, self->selected_object->x, self->selected_object->y)); + tbo_window_mark_dirty (tbo); } else if (self->selected_frame && moved_frame (self)) { tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_move_new (self->selected_frame, self->start_m_x, self->start_m_y, - self->selected_frame->x, - self->selected_frame->y)); + self->selected_frame->x, + self->selected_frame->y)); + tbo_window_mark_dirty (tbo); } + + should_open_text_editor = self->edit_text_on_release && + self->selected_object != NULL && + TBO_IS_OBJECT_TEXT (self->selected_object) && + !moved_object (self) && + !self->resizing && + !self->rotating; + self->start_x = 0; self->start_y = 0; self->clicked = FALSE; + self->edit_text_on_release = FALSE; self->resizing = FALSE; self->rotating = FALSE; + + if (should_open_text_editor) + open_text_editor (self, TBO_OBJECT_TEXT (self->selected_object)); } static void -on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event) +on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event) { + TboToolSelector *self = TBO_TOOL_SELECTOR (tool); TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); Frame *frame = tbo_drawing_get_current_frame (drawing); + + if (event.keyval == GDK_KEY_Delete || event.keyval == GDK_KEY_KP_Delete) + { + if (delete_selected (self)) + tbo_drawing_update (drawing); + return; + } + if (frame) frame_view_on_key (tool, widget, event); + else if (self->selected_frame != NULL && + (event.keyval == GDK_KEY_Return || event.keyval == GDK_KEY_KP_Enter)) + tbo_window_enter_frame (tool->tbo, self->selected_frame); +} + +static gboolean +delete_selected (TboToolSelector *self) +{ + TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + TboDrawing *drawing = TBO_DRAWING (tbo->drawing); + TboObjectBase *obj = self->selected_object; + Frame *frame = self->selected_frame; + Page *page = tbo_comic_get_current_page (tbo->comic); + + if (obj != NULL && tbo_drawing_get_current_frame (drawing) != NULL) + { + tbo_frame_del_obj (frame, obj); + self->selected_object = NULL; + tbo_window_mark_dirty (tbo); + update_menubar (tbo); + return TRUE; + } + + if (frame != NULL && tbo_drawing_get_current_frame (drawing) == NULL) + { + tbo_page_del_frame (page, frame); + tbo_tool_selector_set_selected (self, NULL); + tbo_window_mark_dirty (tbo); + return TRUE; + } + + return FALSE; +} + +static void +open_text_editor (TboToolSelector *self, TboObjectText *text) +{ + TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + TboToolText *text_tool; + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + text_tool = TBO_TOOL_TEXT (tbo->toolbar->tools[TBO_TOOLBAR_TEXT]); + tbo_tool_text_set_selected (text_tool, text); + tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } static void @@ -321,7 +421,7 @@ drawing (TboToolBase *tool, cairo_t *cr) /* frame view */ static void -frame_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) +frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int x, y, offset_x, offset_y; TboToolSelector *self = TBO_TOOL_SELECTOR (tool); @@ -395,7 +495,7 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) } static void -frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +frame_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { TboToolSelector *self = TBO_TOOL_SELECTOR (tool); int x, y; @@ -408,6 +508,7 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event x = (int)event->x; y = (int)event->y; + self->edit_text_on_release = FALSE; // resizing tbo_object_group_set_vars (self->selected_object); @@ -453,6 +554,10 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event tbo_object_group_add (group, obj2); obj2 = TBO_OBJECT_BASE (group); } + + self->edit_text_on_release = (obj2 == self->selected_object) && + TBO_IS_OBJECT_TEXT (obj2) && + !(event->state & GDK_SHIFT_MASK); tbo_tool_selector_set_selected_obj (self, obj2); } } @@ -487,6 +592,19 @@ frame_view_drawing (TboToolBase *tool, cairo_t *cr) TboToolSelector *self = TBO_TOOL_SELECTOR (tool); TboObjectBase *current_obj = self->selected_object; TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); + Frame *frame = tbo_drawing_get_current_frame (drawing); + + if (current_obj != NULL && !G_IS_OBJECT (current_obj)) + { + self->selected_object = NULL; + current_obj = NULL; + } + + if (current_obj != NULL && frame != NULL && g_list_find (frame->objects, current_obj) == NULL) + { + self->selected_object = NULL; + current_obj = NULL; + } if (current_obj != NULL) { @@ -574,23 +692,20 @@ frame_view_drawing (TboToolBase *tool, cairo_t *cr) } static void -frame_view_on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *event) +frame_view_on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event) { TboToolSelector *self = TBO_TOOL_SELECTOR (tool); TboObjectBase *current_obj = self->selected_object; TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); - if (self->selected_frame != NULL && event->keyval == GDK_KEY_Escape) + if (self->selected_frame != NULL && event.keyval == GDK_KEY_Escape) { - tbo_tool_selector_set_selected (self, NULL); - tbo_drawing_set_current_frame (drawing, NULL); - update_menubar (tool->tbo); - tbo_toolbar_update (tool->tbo->toolbar); + tbo_window_leave_frame (tool->tbo); } if (current_obj != NULL) { - switch (event->keyval) + switch (event.keyval) { case GDK_KEY_less: tbo_object_base_resize (current_obj, RESIZE_LESS); @@ -677,7 +792,7 @@ page_view_drawing (TboToolBase *tool, cairo_t *cr) } static void -page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +page_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int x, y; GList *frame_list; @@ -688,7 +803,6 @@ page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) TboWindow *tbo = tool->tbo; TboToolSelector *self = TBO_TOOL_SELECTOR (tool); Frame *selected; - TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); x = (int)event->x; y = (int)event->y; @@ -716,15 +830,12 @@ page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) tbo_tool_selector_set_selected (self, NULL); // double click, frame view - if (selected && event->type == GDK_2BUTTON_PRESS) + if (selected && event->n_press == 2) { - tbo_drawing_set_current_frame (drawing, selected); - empty_tool_area (self); - tbo_tooltip_set (NULL, 0, 0, tbo); - // TODO add tooltip_notify - tbo_tooltip_set_center_timeout (_("press esc to go back"), 3000, tbo); - update_menubar (tbo); - tbo_toolbar_update (tbo->toolbar); + self->clicked = FALSE; + self->resizing = FALSE; + tbo_window_enter_frame (tbo, selected); + return; } self->start_x = x; @@ -742,7 +853,7 @@ page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) } static void -page_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) +page_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int x, y, offset_x, offset_y; TboWindow *tbo = tool->tbo; @@ -801,7 +912,7 @@ page_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *event) frame = (Frame*)frame_list->data; x1 = frame->x + (frame->width / 2); y1 = frame->y + (frame->height / 2); - tbo_tooltip_set (_("double click here"), x1, y1, tbo); + tbo_tooltip_set (_("double click or press Enter"), x1, y1, tbo); found = TRUE; } } @@ -823,15 +934,21 @@ tbo_tool_selector_init (TboToolSelector *self) self->start_m_w = 0; self->start_m_h = 0; self->clicked = FALSE; + self->edit_text_on_release = FALSE; self->over_resizer = FALSE; self->over_rotater = FALSE; self->resizing = FALSE; self->rotating = FALSE; + self->toolarea_widget = NULL; self->spin_w = NULL; self->spin_h = NULL; self->spin_x = NULL; self->spin_y = NULL; + self->color_button = NULL; + self->border_button = NULL; + self->parent_instance.on_select = on_select; + self->parent_instance.on_unselect = on_unselect; self->parent_instance.on_move = on_move; self->parent_instance.on_click = on_click; self->parent_instance.on_release = on_release; @@ -842,12 +959,26 @@ tbo_tool_selector_init (TboToolSelector *self) static void tbo_tool_selector_class_init (TboToolSelectorClass *klass) { + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = finalize; +} + +static void +finalize (GObject *object) +{ + TboToolSelector *self = TBO_TOOL_SELECTOR (object); + + if (self->toolarea_widget != NULL) + g_object_unref (self->toolarea_widget); + + G_OBJECT_CLASS (tbo_tool_selector_parent_class)->finalize (object); } /* object functions */ GObject * -tbo_tool_selector_new () +tbo_tool_selector_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_SELECTOR, NULL); @@ -880,8 +1011,13 @@ tbo_tool_selector_get_selected_obj (TboToolSelector *self) void tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame) { + if (self->selected_frame == frame) + { + update_menubar (TBO_TOOL_BASE (self)->tbo); + return; + } + self->selected_frame = frame; - empty_tool_area (self); if (self->selected_frame != NULL) update_tool_area (self); update_menubar (TBO_TOOL_BASE (self)->tbo); @@ -894,12 +1030,19 @@ tbo_tool_selector_set_selected_obj (TboToolSelector *self, TboObjectBase *obj) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing); Frame *frame = tbo_drawing_get_current_frame (drawing); - tbo_frame_del_obj (frame, self->selected_object); + if (frame != NULL && g_list_find (frame->objects, self->selected_object) != NULL) + tbo_frame_del_obj (frame, self->selected_object); } self->selected_object = obj; update_menubar (TBO_TOOL_BASE (self)->tbo); } +gboolean +tbo_tool_selector_delete_selected (TboToolSelector *self) +{ + return delete_selected (self); +} + static void frame_move_do (TboAction *act) diff --git a/src/tbo-tool-selector.h b/src/tbo-tool-selector.h index 49aaef2..39de908 100644 --- a/src/tbo-tool-selector.h +++ b/src/tbo-tool-selector.h @@ -56,14 +56,18 @@ struct _TboToolSelector gint start_m_w; gint start_m_h; gboolean clicked; + gboolean edit_text_on_release; gboolean over_resizer; gboolean over_rotater; gboolean resizing; gboolean rotating; + GtkWidget *toolarea_widget; GtkWidget *spin_w; GtkWidget *spin_h; GtkWidget *spin_x; GtkWidget *spin_y; + GtkWidget *color_button; + GtkWidget *border_button; }; struct _TboToolSelectorClass @@ -83,7 +87,8 @@ Frame * tbo_tool_selector_get_selected_frame (TboToolSelector *self); TboObjectBase * tbo_tool_selector_get_selected_obj (TboToolSelector *self); void tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame); void tbo_tool_selector_set_selected_obj (TboToolSelector *self, TboObjectBase *obj); -GObject * tbo_tool_selector_new (); +gboolean tbo_tool_selector_delete_selected (TboToolSelector *self); +GObject * tbo_tool_selector_new (void); GObject * tbo_tool_selector_new_with_params (TboWindow *tbo); /* @@ -116,4 +121,3 @@ TboAction * tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, i #endif /* __TBO_TOOL_SELECTOR_H__ */ - diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index fd5c24e..bd27a85 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -19,8 +19,10 @@ #include #include "frame.h" #include "tbo-window.h" +#include "tbo-widget.h" #include "tbo-drawing.h" #include "tbo-object-base.h" +#include "tbo-tool-selector.h" #include "tbo-tool-text.h" G_DEFINE_TYPE (TboToolText, tbo_tool_text, TBO_TYPE_TOOL_BASE); @@ -30,26 +32,22 @@ G_DEFINE_TYPE (TboToolText, tbo_tool_text, TBO_TYPE_TOOL_BASE); /* Headers */ static void on_select (TboToolBase *tool); static void on_unselect (TboToolBase *tool); -static void on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event); +static void on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); static void drawing (TboToolBase *tool, cairo_t *cr); +static void finalize (GObject *object); + +static gint tbo_tool_text_get_font_size (TboToolText *self); +static gchar *tbo_tool_text_build_font (TboToolText *self); +static void tbo_tool_text_sync_font_controls (TboToolText *self, const gchar *font_string); /* Definitions */ /* aux */ -static gboolean -on_tview_focus_in (GtkWidget *view, GdkEventFocus *event, TboToolText *self) +static void +on_tview_focus_changed (GtkWidget *view, GParamSpec *pspec, TboToolText *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; - tbo_window_set_key_binder (tbo, FALSE); - return FALSE; -} - -static gboolean -on_tview_focus_out (GtkWidget *view, GdkEventFocus *event, TboToolText *self) -{ - TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; - tbo_window_set_key_binder (tbo, TRUE); - return FALSE; + tbo_window_set_key_binder (tbo, !gtk_widget_has_focus (view)); } @@ -63,36 +61,47 @@ on_text_change (GtkTextBuffer *buf, TboToolText *self) if (self->text_selected) { - tbo_object_text_set_text (self->text_selected, gtk_text_buffer_get_text (buf, &start, &end, FALSE)); + gchar *text = gtk_text_buffer_get_text (buf, &start, &end, FALSE); + tbo_object_text_set_text (self->text_selected, text); + g_free (text); + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } return FALSE; } -static gboolean -on_font_change (GtkFontButton *fbutton, TboToolText *self) +static void +on_font_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; if (self->text_selected) { - tbo_object_text_change_font (self->text_selected, tbo_tool_text_get_pango_font (self)); + gchar *font = tbo_tool_text_build_font (self); + tbo_object_text_change_font (self->text_selected, font); + g_free (font); + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } - return FALSE; } static gboolean -on_color_change (GtkColorButton *cbutton, TboToolText *self) +on_font_size_change (GtkSpinButton *spin, TboToolText *self) +{ + on_font_change (NULL, NULL, self); + return FALSE; +} + +static void +on_color_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; if (self->text_selected) { - GdkColor color; - gtk_color_button_get_color (GTK_COLOR_BUTTON (self->font_color), &color); - tbo_object_text_change_color (self->text_selected, &color); + const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color)); + tbo_object_text_change_color (self->text_selected, (GdkRGBA *) color); + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } - return FALSE; } GtkWidget * @@ -102,42 +111,67 @@ setup_toolarea (TboToolText *self) GtkWidget *hbox; GtkWidget *font_color_label = gtk_label_new (_("Text color:")); GtkWidget *font_label = gtk_label_new (_("Font:")); + GtkWidget *font_size_label = gtk_label_new (_("Size:")); GtkWidget *scroll; GtkWidget *view; - - gtk_misc_set_alignment (GTK_MISC (font_label), 0, 0); - gtk_misc_set_alignment (GTK_MISC (font_color_label), 0, 0); - - self->font = gtk_font_button_new (); - g_signal_connect (self->font, "font-set", G_CALLBACK (on_font_change), self); - self->font_color = gtk_color_button_new (); - g_signal_connect (self->font_color, "color-set", G_CALLBACK (on_color_change), self); - - vbox = gtk_vbox_new (FALSE, 5); - - hbox = gtk_hbox_new (FALSE, 5); - gtk_box_pack_start (GTK_BOX (hbox), font_label, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (hbox), self->font, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 5); - - hbox = gtk_hbox_new (FALSE, 5); - gtk_box_pack_start (GTK_BOX (hbox), font_color_label, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (hbox), self->font_color, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 5); - - scroll = gtk_scrolled_window_new (NULL, NULL); + GtkAdjustment *font_size_adjustment; + GtkFontDialog *font_dialog; + GtkColorDialog *color_dialog; + GdkRGBA default_color = { 0, 0, 0, 1 }; + + gtk_label_set_xalign (GTK_LABEL (font_label), 0.0); + gtk_label_set_yalign (GTK_LABEL (font_label), 0.5); + gtk_label_set_xalign (GTK_LABEL (font_size_label), 0.0); + gtk_label_set_yalign (GTK_LABEL (font_size_label), 0.5); + gtk_label_set_xalign (GTK_LABEL (font_color_label), 0.0); + gtk_label_set_yalign (GTK_LABEL (font_color_label), 0.5); + + font_dialog = gtk_font_dialog_new (); + self->font = gtk_font_dialog_button_new (font_dialog); + gtk_font_dialog_button_set_use_size (GTK_FONT_DIALOG_BUTTON (self->font), FALSE); + g_signal_connect (self->font, "notify::font-desc", G_CALLBACK (on_font_change), self); + + font_size_adjustment = gtk_adjustment_new (27, 1, 300, 1, 5, 0); + self->font_size = gtk_spin_button_new (font_size_adjustment, 1, 0); + gtk_editable_set_alignment (GTK_EDITABLE (self->font_size), 0.5); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (self->font_size), TRUE); + g_signal_connect (self->font_size, "value-changed", G_CALLBACK (on_font_size_change), self); + + color_dialog = gtk_color_dialog_new (); + self->font_color = gtk_color_dialog_button_new (color_dialog); + g_signal_connect (self->font_color, "notify::rgba", G_CALLBACK (on_color_change), self); + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), &default_color); + + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); + tbo_box_pack_start (hbox, font_label, TRUE, TRUE, 5); + tbo_box_pack_start (hbox, self->font, TRUE, TRUE, 5); + tbo_box_pack_start (vbox, hbox, FALSE, FALSE, 5); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); + tbo_box_pack_start (hbox, font_size_label, TRUE, TRUE, 5); + tbo_box_pack_start (hbox, self->font_size, TRUE, TRUE, 5); + tbo_box_pack_start (vbox, hbox, FALSE, FALSE, 5); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5); + tbo_box_pack_start (hbox, font_color_label, TRUE, TRUE, 5); + tbo_box_pack_start (hbox, self->font_color, TRUE, TRUE, 5); + tbo_box_pack_start (vbox, hbox, FALSE, FALSE, 5); + + tbo_tool_text_sync_font_controls (self, DEFAULT_PANGO_FONT); + + scroll = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); view = gtk_text_view_new (); - gtk_widget_add_events (view, GDK_FOCUS_CHANGE_MASK); - g_signal_connect (view, "focus-in-event", G_CALLBACK (on_tview_focus_in), self); - g_signal_connect (view, "focus-out-event", G_CALLBACK (on_tview_focus_out), self); + g_signal_connect (view, "notify::has-focus", G_CALLBACK (on_tview_focus_changed), self); gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD); self->text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); gtk_text_buffer_set_text (self->text_buffer, "", -1); g_signal_connect (self->text_buffer, "changed", G_CALLBACK (on_text_change), self); - gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scroll), view); - gtk_box_pack_start (GTK_BOX (vbox), scroll, FALSE, FALSE, 5); + tbo_scrolled_window_set_child (scroll, view); + tbo_box_pack_start (vbox, scroll, FALSE, FALSE, 5); return vbox; } @@ -147,21 +181,23 @@ static void on_select (TboToolBase *tool) { GtkWidget *toolarea = setup_toolarea (TBO_TOOL_TEXT (tool)); - gtk_widget_show_all (GTK_WIDGET (toolarea)); + tbo_widget_show_all (toolarea); tbo_empty_tool_area (tool->tbo); - gtk_container_add (GTK_CONTAINER (tool->tbo->toolarea), toolarea); + tbo_widget_add_child (tool->tbo->toolarea, toolarea); } static void on_unselect (TboToolBase *tool) { - /* TODO remove widgets from toolarea to not destroy it */ + TboToolText *self = TBO_TOOL_TEXT (tool); + + tbo_tool_text_set_selected (self, NULL); tbo_empty_tool_area (tool->tbo); tbo_window_set_key_binder (tool->tbo, TRUE); } static void -on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) +on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { int x = (int)event->x; int y = (int)event->y; @@ -169,10 +205,23 @@ on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) GList *obj_list; TboObjectBase *obj; TboObjectText *text; - GdkColor color; + GdkRGBA color; TboToolText *self = TBO_TOOL_TEXT (tool); Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tool->tbo->drawing)); + if (self->text_selected != NULL) + { + TboObjectText *current_text = g_object_ref (self->text_selected); + TboToolSelector *selector; + + tbo_toolbar_set_selected_tool (tool->tbo->toolbar, TBO_TOOLBAR_SELECTOR); + selector = TBO_TOOL_SELECTOR (tool->tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + tbo_tool_selector_set_selected_obj (selector, TBO_OBJECT_BASE (current_text)); + tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); + g_object_unref (current_text); + return; + } + for (obj_list = g_list_first (frame->objects); obj_list; obj_list = obj_list->next) { obj = TBO_OBJECT_BASE (obj_list->data); @@ -186,12 +235,19 @@ on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) { x = tbo_frame_get_base_x (x); y = tbo_frame_get_base_y (y); - gtk_color_button_get_color (GTK_COLOR_BUTTON (self->font_color), &color); + + if (x < 0 || y < 0 || x > frame->width || y > frame->height) + return; + + gchar *font = tbo_tool_text_build_font (self); + color = *gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color)); text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, 100, 0, _("Text"), - tbo_tool_text_get_pango_font (self), + font, &color)); + g_free (font); tbo_frame_add_obj (frame, TBO_OBJECT_BASE (text)); + tbo_window_mark_dirty (tool->tbo); } tbo_tool_text_set_selected (self, text); tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); @@ -228,6 +284,7 @@ static void tbo_tool_text_init (TboToolText *self) { self->font = NULL; + self->font_size = NULL; self->font_color = NULL; self->text_selected = NULL; self->text_buffer = NULL; @@ -241,12 +298,21 @@ tbo_tool_text_init (TboToolText *self) static void tbo_tool_text_class_init (TboToolTextClass *klass) { + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = finalize; +} + +static void +finalize (GObject *object) +{ + G_OBJECT_CLASS (tbo_tool_text_parent_class)->finalize (object); } /* object functions */ GObject * -tbo_tool_text_new () +tbo_tool_text_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_TEXT, NULL); @@ -267,12 +333,7 @@ tbo_tool_text_new_with_params (TboWindow *tbo) gchar * tbo_tool_text_get_pango_font (TboToolText *self) { - if (self->font) - { - return (gchar *)gtk_font_button_get_font_name (GTK_FONT_BUTTON (self->font)); - } - - return DEFAULT_PANGO_FONT; + return tbo_tool_text_build_font (self); } gchar * tbo_tool_text_get_font_name (TboToolText *self) @@ -281,14 +342,71 @@ tbo_tool_text_get_font_name (TboToolText *self) if (self->font) { - pango_font = pango_font_description_from_string ( - gtk_font_button_get_font_name (GTK_FONT_BUTTON (self->font))); - return (gchar *)pango_font_description_get_family (pango_font); + const PangoFontDescription *font = gtk_font_dialog_button_get_font_desc (GTK_FONT_DIALOG_BUTTON (self->font)); + pango_font = pango_font_description_copy (font); + gchar *family = g_strdup (pango_font_description_get_family (pango_font)); + pango_font_description_free (pango_font); + return family; } return NULL; } +static gint +tbo_tool_text_get_font_size (TboToolText *self) +{ + if (self->font_size) + return MAX (1, gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->font_size))); + + return 27; +} + +static gchar * +tbo_tool_text_build_font (TboToolText *self) +{ + gchar *font_string; + PangoFontDescription *description; + gchar *result; + + if (self->font) + { + const PangoFontDescription *font = gtk_font_dialog_button_get_font_desc (GTK_FONT_DIALOG_BUTTON (self->font)); + font_string = pango_font_description_to_string (font); + } + else + font_string = g_strdup (DEFAULT_PANGO_FONT); + + description = pango_font_description_from_string (font_string); + pango_font_description_set_size (description, tbo_tool_text_get_font_size (self) * PANGO_SCALE); + result = pango_font_description_to_string (description); + + pango_font_description_free (description); + g_free (font_string); + return result; +} + +static void +tbo_tool_text_sync_font_controls (TboToolText *self, const gchar *font_string) +{ + PangoFontDescription *description; + gint size; + + if (self->font == NULL || self->font_size == NULL) + return; + + description = pango_font_description_from_string (font_string); + size = pango_font_description_get_size (description); + if (size <= 0) + size = 27; + else + size /= PANGO_SCALE; + + gtk_font_dialog_button_set_font_desc (GTK_FONT_DIALOG_BUTTON (self->font), description); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->font_size), size); + + pango_font_description_free (description); +} + void tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) { @@ -300,13 +418,24 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) } if (!text) { + if (self->text_buffer) + gtk_text_buffer_set_text (self->text_buffer, "", -1); + if (self->font) + tbo_tool_text_sync_font_controls (self, DEFAULT_PANGO_FONT); + if (self->font_color) + { + GdkRGBA default_color = { 0, 0, 0, 1 }; + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), &default_color); + } return; } str = tbo_object_text_get_text (text); - gtk_font_button_set_font_name (GTK_FONT_BUTTON (self->font), tbo_object_text_get_string (text)); - gtk_color_button_set_color (GTK_COLOR_BUTTON (self->font_color), text->font_color); + gchar *font = tbo_object_text_get_string (text); + tbo_tool_text_sync_font_controls (self, font); + g_free (font); + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), text->font_color); gtk_text_buffer_set_text (self->text_buffer, str, -1); self->text_selected = g_object_ref (text); } diff --git a/src/tbo-tool-text.h b/src/tbo-tool-text.h index caef357..0242fa7 100644 --- a/src/tbo-tool-text.h +++ b/src/tbo-tool-text.h @@ -43,6 +43,7 @@ struct _TboToolText /* instance members */ GtkWidget *font; + GtkWidget *font_size; GtkWidget *font_color; TboObjectText *text_selected; GtkTextBuffer *text_buffer; @@ -61,11 +62,10 @@ GType tbo_tool_text_get_type (void); /* * Method definitions. */ -GObject * tbo_tool_text_new (); +GObject * tbo_tool_text_new (void); GObject * tbo_tool_text_new_with_params (TboWindow *tbo); gchar * tbo_tool_text_get_pango_font (TboToolText *self); gchar * tbo_tool_text_get_font_name (TboToolText *self); void tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text); #endif /* __TBO_TOOL_TEXT_H__ */ - diff --git a/src/tbo-toolbar.c b/src/tbo-toolbar.c index 8f63973..5ddcfa8 100644 --- a/src/tbo-toolbar.c +++ b/src/tbo-toolbar.c @@ -17,7 +17,9 @@ */ +#include #include + #include "tbo-window.h" #include "tbo-toolbar.h" #include "comic.h" @@ -25,8 +27,9 @@ #include "comic-new-dialog.h" #include "comic-open-dialog.h" #include "comic-saveas-dialog.h" -#include "custom-stock.h" +#include "tbo-file-dialog.h" #include "tbo-drawing.h" +#include "dnd.h" #include "frame.h" #include "tbo-tool-selector.h" #include "tbo-tool-frame.h" @@ -35,41 +38,120 @@ #include "tbo-tool-text.h" #include "ui-menu.h" #include "tbo-undo.h" +#include "tbo-widget.h" +#include "tbo-utils.h" G_DEFINE_TYPE (TboToolbar, tbo_toolbar, G_TYPE_OBJECT); +static void on_tool_button_toggled (GtkToggleButton *button, TboToolbar *toolbar); + +static GtkWidget * +create_icon_from_name (const gchar *icon_name) +{ + GtkWidget *image = gtk_image_new_from_icon_name (icon_name); + gtk_image_set_pixel_size (GTK_IMAGE (image), 20); + return image; +} + +static GtkWidget * +create_icon_from_file (const gchar *path) +{ + GtkWidget *wrapper; + GtkWidget *image; + + wrapper = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_size_request (wrapper, 20, 20); + + image = gtk_picture_new_for_filename (path); + gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); + gtk_picture_set_content_fit (GTK_PICTURE (image), GTK_CONTENT_FIT_CONTAIN); + gtk_widget_set_halign (image, GTK_ALIGN_CENTER); + gtk_widget_set_valign (image, GTK_ALIGN_CENTER); -static gboolean select_tool (GtkAction *action, TboToolbar *toolbar); + tbo_widget_add_child (wrapper, image); + return wrapper; +} + +static GtkWidget * +create_project_icon (const gchar *relative_path) +{ + gchar *path = tbo_get_data_path (relative_path); + GtkWidget *image = create_icon_from_file (path); + g_free (path); + return image; +} + +static GtkWidget * +create_button (GtkWidget *image, const gchar *tooltip) +{ + GtkWidget *button = gtk_button_new (); + + gtk_button_set_child (GTK_BUTTON (button), image); + gtk_widget_set_tooltip_text (button, tooltip); + + return button; +} + +static GtkWidget * +create_toggle_button (GtkWidget *image, const gchar *tooltip) +{ + GtkWidget *button = gtk_toggle_button_new (); + + gtk_button_set_child (GTK_BUTTON (button), image); + gtk_widget_set_tooltip_text (button, tooltip); + + return button; +} + +static GtkWidget * +create_section_box (void) +{ + GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + + gtk_widget_add_css_class (box, "linked"); + gtk_widget_add_css_class (box, "tbo-toolbar-section"); + + return box; +} + +static void +register_tool_button (TboToolbar *self, enum Tool tool, GtkWidget *button) +{ + self->tool_buttons[tool] = GTK_TOGGLE_BUTTON (button); + g_object_set_data (G_OBJECT (button), "tool-id", GINT_TO_POINTER (tool)); + g_signal_connect (button, "toggled", G_CALLBACK (on_tool_button_toggled), self); +} /* callbacks */ static gboolean -add_new_page (GtkAction *action, TboWindow *tbo) +add_new_page (GtkWidget *widget, TboWindow *tbo) { Page *page = tbo_comic_new_page (tbo->comic); - int nth = tbo_comic_page_nth (tbo->comic, page); - gtk_notebook_insert_page (GTK_NOTEBOOK (tbo->notebook), - create_darea (tbo), - NULL, - nth); + + tbo_window_add_page_widget (tbo, create_darea (tbo)); + tbo_comic_set_current_page (tbo->comic, page); + tbo_window_set_current_tab_page (tbo, TRUE); + tbo_window_mark_dirty (tbo); tbo_window_update_status (tbo, 0, 0); tbo_toolbar_update (tbo->toolbar); return FALSE; } static gboolean -del_current_page (GtkAction *action, TboWindow *tbo) +del_current_page (GtkWidget *widget, TboWindow *tbo) { int nth = tbo_comic_page_index (tbo->comic); tbo_comic_del_current_page (tbo->comic); + tbo_window_remove_page_widget (tbo, nth); tbo_window_set_current_tab_page (tbo, TRUE); - gtk_notebook_remove_page (GTK_NOTEBOOK (tbo->notebook), nth); + tbo_window_mark_dirty (tbo); tbo_window_update_status (tbo, 0, 0); tbo_toolbar_update (tbo->toolbar); return FALSE; } static gboolean -next_page (GtkAction *action, TboWindow *tbo) +next_page (GtkWidget *widget, TboWindow *tbo) { tbo_comic_next_page (tbo->comic); tbo_window_set_current_tab_page (tbo, TRUE); @@ -81,7 +163,7 @@ next_page (GtkAction *action, TboWindow *tbo) } static gboolean -prev_page (GtkAction *action, TboWindow *tbo) +prev_page (GtkWidget *widget, TboWindow *tbo) { tbo_comic_prev_page (tbo->comic); tbo_window_set_current_tab_page (tbo, TRUE); @@ -93,234 +175,182 @@ prev_page (GtkAction *action, TboWindow *tbo) } static gboolean -zoom_100 (GtkAction *action, TboWindow *tbo) +zoom_100 (GtkWidget *widget, TboWindow *tbo) { tbo_drawing_zoom_100 (TBO_DRAWING (tbo->drawing)); return FALSE; } static gboolean -zoom_fit (GtkAction *action, TboWindow *tbo) +zoom_fit (GtkWidget *widget, TboWindow *tbo) { tbo_drawing_zoom_fit (TBO_DRAWING (tbo->drawing)); return FALSE; } static gboolean -zoom_in (GtkAction *action, TboWindow *tbo) +zoom_in (GtkWidget *widget, TboWindow *tbo) { tbo_drawing_zoom_in (TBO_DRAWING (tbo->drawing)); return FALSE; } static gboolean -zoom_out (GtkAction *action, TboWindow *tbo) +zoom_out (GtkWidget *widget, TboWindow *tbo) { tbo_drawing_zoom_out (TBO_DRAWING (tbo->drawing)); return FALSE; } static gboolean -add_pix (GtkAction *action, TboWindow *tbo) +add_pix (GtkWidget *widget, TboWindow *tbo) { - GtkWidget *dialog; - GtkFileFilter *filter; - - dialog = gtk_file_chooser_dialog_new (_("Add an Image"), - GTK_WINDOW (tbo->window), - GTK_FILE_CHOOSER_ACTION_OPEN, - GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, - GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, - NULL); - - filter = gtk_file_filter_new (); - gtk_file_filter_set_name (filter, _("Image files")); - gtk_file_filter_add_pixbuf_formats (filter); - gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); - filter = gtk_file_filter_new (); - gtk_file_filter_set_name (filter, _("All files")); - gtk_file_filter_add_pattern (filter, "*"); - gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); - - if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) + gchar *filename = tbo_file_dialog_open_image (tbo); + + if (filename != NULL) { - gchar *filename; - filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); - TboObjectPixmap *piximage = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new_with_params (0, 0, 0, 0, filename)); - tbo_frame_add_obj (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)), TBO_OBJECT_BASE (piximage)); - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_dnd_insert_asset (tbo, filename, 0, 0); g_free (filename); } - - gtk_widget_destroy (dialog); return FALSE; } -/* actions */ - -static const GtkActionEntry tbo_tools_entries [] = { - { "NewFileTool", GTK_STOCK_NEW, N_("_New"), "N", - N_("New Comic"), - G_CALLBACK (tbo_comic_new_dialog) }, - - { "OpenFileTool", GTK_STOCK_OPEN, N_("_Open"), "O", - N_("Open comic"), - G_CALLBACK (tbo_comic_open_dialog) }, - - { "SaveFileTool", GTK_STOCK_SAVE, N_("_Save"), "S", - N_("Save current document"), - G_CALLBACK (tbo_comic_save_dialog) }, - - // Undo and Redo - { "Undo", GTK_STOCK_UNDO, N_("_Undo"), "Z", - N_("Undo the last action"), - G_CALLBACK (tbo_window_undo_cb) }, - { "Redo", GTK_STOCK_REDO, N_("_Redo"), "Y", - N_("Undo the last action"), - G_CALLBACK (tbo_window_redo_cb) }, - - // Page tools - { "NewPage", GTK_STOCK_ADD, N_("New Page"), "P", - N_("New page"), - G_CALLBACK (add_new_page) }, - - { "DelPage", GTK_STOCK_DELETE, N_("Delete Page"), "", - N_("Delete current page"), - G_CALLBACK (del_current_page) }, - - { "PrevPage", GTK_STOCK_GO_BACK, N_("Prev Page"), "", - N_("Prev page"), - G_CALLBACK (prev_page) }, - - { "NextPage", GTK_STOCK_GO_FORWARD, N_("Next Page"), "", - N_("Next page"), - G_CALLBACK (next_page) }, - - // Zoom tools - { "Zoomin", GTK_STOCK_ZOOM_IN, N_("Zoom in"), "", - N_("Zoom in"), - G_CALLBACK (zoom_in) }, - { "Zoom100", GTK_STOCK_ZOOM_100, N_("Zoom 1:1"), "", - N_("Zoom 1:1"), - G_CALLBACK (zoom_100) }, - { "Zoomfit", GTK_STOCK_ZOOM_FIT, N_("Zoom fit"), "", - N_("Zoom fit"), - G_CALLBACK (zoom_fit) }, - { "Zoomout", GTK_STOCK_ZOOM_OUT, N_("Zoom out"), "", - N_("Zoom out"), - G_CALLBACK (zoom_out) }, - - // Png image tool - { "Pix", TBO_STOCK_PIX, N_("Image"), "", - N_("Image"), - G_CALLBACK (add_pix) }, -}; - -/* toggle actions */ -static const GtkToggleActionEntry tbo_tools_toggle_entries [] = { - // Page view tools - { "NewFrame", TBO_STOCK_FRAME, N_("New _Frame"), "f", - N_("New Frame"), - G_CALLBACK (select_tool), FALSE }, - - { "Selector", TBO_STOCK_SELECTOR, N_("Selector"), "s", - N_("Selector"), - G_CALLBACK (select_tool), FALSE }, - - // Frame view tools - { "Doodle", TBO_STOCK_DOODLE, N_("Doodle"), "d", - N_("Doodle"), - G_CALLBACK (select_tool), FALSE }, - { "Bubble", TBO_STOCK_BUBBLE, N_("Booble"), "b", - N_("Bubble"), - G_CALLBACK (select_tool), FALSE }, - { "Text", TBO_STOCK_TEXT, N_("Text"), "t", - N_("Text"), - G_CALLBACK (select_tool), FALSE }, -}; - -/* aux */ - static void -unselect_tool (TboToolbar *self) +on_tool_button_toggled (GtkToggleButton *button, TboToolbar *toolbar) { - GtkToggleAction *action; + enum Tool tool; - if (!self->selected_tool) + if (toolbar->syncing_tool_buttons) return; - self->selected_tool->on_unselect (self->selected_tool); - action = (GtkToggleAction *) gtk_action_group_get_action (self->action_group, - self->selected_tool->action); - gtk_toggle_action_set_active (action, FALSE); -} - -static gboolean -select_tool (GtkAction *action, TboToolbar *toolbar) -{ - GtkToggleAction *toggle_action; - int i; - const gchar *name; - TboWindow *tbo = toolbar->tbo; - TboToolBase *tool; - - toggle_action = (GtkToggleAction *) action; - name = gtk_action_get_name (action); + tool = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tool-id")); - /* starting at 1 because 0 is NULL, TBO_TOOLBAR_NONE */ - for (i=1; i < TBO_TOOLBAR_N_TOOLS; i++) - { - tool = toolbar->tools[i]; - if (strcmp (tool->action, name) == 0) - break; - } - - if (gtk_toggle_action_get_active (toggle_action)) - tbo_toolbar_set_selected_tool (toolbar, i); - else + if (gtk_toggle_button_get_active (button)) + tbo_toolbar_set_selected_tool (toolbar, tool); + else if (toolbar->selected_tool == toolbar->tools[tool]) tbo_toolbar_set_selected_tool (toolbar, TBO_TOOLBAR_NONE); - tbo_window_update_status (tbo, 0, 0); - return FALSE; } static GtkWidget * generate_toolbar (TboToolbar *self) { GtkWidget *toolbar; - GtkUIManager *manager; - GError *error = NULL; - - manager = gtk_ui_manager_new (); - gtk_ui_manager_add_ui_from_file (manager, DATA_DIR "/ui/tbo-toolbar-ui.xml", &error); - if (error != NULL) - { - g_warning ("Could not merge tbo-toolbar-ui.xml: %s", error->message); - g_error_free (error); - } - - self->action_group = gtk_action_group_new ("ToolsActions"); - gtk_action_group_set_translation_domain (self->action_group, NULL); - gtk_action_group_add_actions (self->action_group, tbo_tools_entries, - G_N_ELEMENTS (tbo_tools_entries), self->tbo); - gtk_action_group_add_toggle_actions (self->action_group, tbo_tools_toggle_entries, - G_N_ELEMENTS (tbo_tools_toggle_entries), self); - - gtk_ui_manager_insert_action_group (manager, self->action_group, 0); + GtkWidget *section; + + toolbar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_widget_set_name (toolbar, "tbo-toolbar"); + gtk_widget_set_margin_start (toolbar, 12); + gtk_widget_set_margin_end (toolbar, 12); + gtk_widget_set_margin_top (toolbar, 8); + gtk_widget_set_margin_bottom (toolbar, 8); + + section = create_section_box (); + self->button_new = create_button (create_project_icon ("icons/new.svg"), _("New comic")); + self->button_open = create_button (create_icon_from_name ("document-open-symbolic"), _("Open comic")); + self->button_save = create_button (create_icon_from_name ("document-save-as-symbolic"), _("Save comic as")); + g_signal_connect (self->button_new, "clicked", G_CALLBACK (tbo_comic_new_dialog), self->tbo); + g_signal_connect (self->button_open, "clicked", G_CALLBACK (tbo_comic_open_dialog), self->tbo); + g_signal_connect (self->button_save, "clicked", G_CALLBACK (tbo_comic_saveas_dialog), self->tbo); + tbo_box_pack_start (section, self->button_new, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_open, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_save, FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); + + section = create_section_box (); + self->button_undo = create_button (create_project_icon ("icons/undo.svg"), _("Undo")); + self->button_redo = create_button (create_project_icon ("icons/redo.svg"), _("Redo")); + g_signal_connect (self->button_undo, "clicked", G_CALLBACK (tbo_window_undo_cb), self->tbo); + g_signal_connect (self->button_redo, "clicked", G_CALLBACK (tbo_window_redo_cb), self->tbo); + tbo_box_pack_start (section, self->button_undo, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_redo, FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); + + section = create_section_box (); + self->button_new_page = create_button (create_icon_from_name ("list-add-symbolic"), _("New page")); + self->button_delete_page = create_button (create_icon_from_name ("edit-delete-symbolic"), _("Delete page")); + self->button_prev_page = create_button (create_icon_from_name ("go-previous-symbolic"), _("Previous page")); + self->button_next_page = create_button (create_icon_from_name ("go-next-symbolic"), _("Next page")); + g_signal_connect (self->button_new_page, "clicked", G_CALLBACK (add_new_page), self->tbo); + g_signal_connect (self->button_delete_page, "clicked", G_CALLBACK (del_current_page), self->tbo); + g_signal_connect (self->button_prev_page, "clicked", G_CALLBACK (prev_page), self->tbo); + g_signal_connect (self->button_next_page, "clicked", G_CALLBACK (next_page), self->tbo); + tbo_box_pack_start (section, self->button_new_page, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_delete_page, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_prev_page, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_next_page, FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); + + section = create_section_box (); + register_tool_button (self, TBO_TOOLBAR_SELECTOR, + create_toggle_button (create_project_icon ("icons/selector.svg"), _("Selector"))); + register_tool_button (self, TBO_TOOLBAR_FRAME, + create_toggle_button (create_project_icon ("icons/frame.svg"), _("New frame"))); + register_tool_button (self, TBO_TOOLBAR_DOODLE, + create_toggle_button (create_project_icon ("icons/doodle.svg"), _("Doodle"))); + register_tool_button (self, TBO_TOOLBAR_BUBBLE, + create_toggle_button (create_project_icon ("icons/bubble.svg"), _("Bubble"))); + register_tool_button (self, TBO_TOOLBAR_TEXT, + create_toggle_button (create_project_icon ("icons/text.svg"), _("Text"))); + tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_SELECTOR]), FALSE, FALSE, 0); + tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_FRAME]), FALSE, FALSE, 0); + tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_DOODLE]), FALSE, FALSE, 0); + tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_BUBBLE]), FALSE, FALSE, 0); + tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_TEXT]), FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); + + section = create_section_box (); + self->button_pix = create_button (create_project_icon ("icons/pix.svg"), _("Image")); + g_signal_connect (self->button_pix, "clicked", G_CALLBACK (add_pix), self->tbo); + tbo_box_pack_start (section, self->button_pix, FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); + + section = create_section_box (); + self->button_zoom_100 = create_button (create_icon_from_name ("zoom-original-symbolic"), _("Zoom 1:1")); + self->button_zoom_out = create_button (create_icon_from_name ("zoom-out-symbolic"), _("Zoom out")); + self->button_zoom_in = create_button (create_icon_from_name ("zoom-in-symbolic"), _("Zoom in")); + self->button_zoom_fit = create_button (create_project_icon ("icons/zoom-fit.svg"), _("Zoom fit")); + g_signal_connect (self->button_zoom_100, "clicked", G_CALLBACK (zoom_100), self->tbo); + g_signal_connect (self->button_zoom_out, "clicked", G_CALLBACK (zoom_out), self->tbo); + g_signal_connect (self->button_zoom_in, "clicked", G_CALLBACK (zoom_in), self->tbo); + g_signal_connect (self->button_zoom_fit, "clicked", G_CALLBACK (zoom_fit), self->tbo); + tbo_box_pack_start (section, self->button_zoom_100, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_zoom_out, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_zoom_in, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_zoom_fit, FALSE, FALSE, 0); + tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); - toolbar = gtk_ui_manager_get_widget (manager, "/toolbar"); return toolbar; } - /* init methods */ static void tbo_toolbar_init (TboToolbar *self) { + int i; + self->tbo = NULL; self->selected_tool = NULL; - self->action_group = NULL; + self->toolbar = NULL; + self->button_new = NULL; + self->button_open = NULL; + self->button_save = NULL; + self->button_undo = NULL; + self->button_redo = NULL; + self->button_new_page = NULL; + self->button_delete_page = NULL; + self->button_prev_page = NULL; + self->button_next_page = NULL; + self->button_zoom_in = NULL; + self->button_zoom_100 = NULL; + self->button_zoom_fit = NULL; + self->button_zoom_out = NULL; + self->button_pix = NULL; + self->syncing_tool_buttons = FALSE; self->tools = NULL; + + for (i = 0; i < TBO_TOOLBAR_N_TOOLS; i++) + self->tool_buttons[i] = NULL; } static void @@ -336,9 +366,6 @@ tbo_toolbar_finalize (GObject *self) g_free (TBO_TOOLBAR (self)->tools); } - if (TBO_TOOLBAR (self)->toolbar) - g_object_unref (G_OBJECT (TBO_TOOLBAR (self)->toolbar)); - /* Chain up to the parent class */ G_OBJECT_CLASS (tbo_toolbar_parent_class)->finalize (self); } @@ -352,7 +379,7 @@ tbo_toolbar_class_init (TboToolbarClass *klass) /* object functions */ GObject * -tbo_toolbar_new () +tbo_toolbar_new (void) { GObject *toolbar; toolbar = g_object_new (TBO_TYPE_TOOLBAR, NULL); @@ -366,35 +393,29 @@ tbo_toolbar_new_with_params (TboWindow *tbo) TboToolbar *toolbar; TboToolBase *tool; obj = tbo_toolbar_new (); - + toolbar = TBO_TOOLBAR (obj); toolbar->tbo = tbo; - /* Adding tools */ - toolbar->tools = g_new (TboToolBase*, TBO_TOOLBAR_N_TOOLS); + toolbar->tools = g_new (TboToolBase *, TBO_TOOLBAR_N_TOOLS); toolbar->tools[TBO_TOOLBAR_NONE] = NULL; - /* selector */ tool = TBO_TOOL_BASE (tbo_tool_selector_new_with_params (tbo)); tbo_tool_base_set_action (tool, "Selector"); toolbar->tools[TBO_TOOLBAR_SELECTOR] = tool; - /* frame */ tool = TBO_TOOL_BASE (tbo_tool_frame_new_with_params (tbo)); tbo_tool_base_set_action (tool, "NewFrame"); toolbar->tools[TBO_TOOLBAR_FRAME] = tool; - /* doodle */ tool = TBO_TOOL_BASE (tbo_tool_doodle_new_with_params (tbo)); tbo_tool_base_set_action (tool, "Doodle"); toolbar->tools[TBO_TOOLBAR_DOODLE] = tool; - /* bubble */ tool = TBO_TOOL_BASE (tbo_tool_bubble_new_with_params (tbo)); tbo_tool_base_set_action (tool, "Bubble"); toolbar->tools[TBO_TOOLBAR_BUBBLE] = tool; - /* text */ tool = TBO_TOOL_BASE (tbo_tool_text_new_with_params (tbo)); tbo_tool_base_set_action (tool, "Text"); toolbar->tools[TBO_TOOLBAR_TEXT] = tool; @@ -413,26 +434,34 @@ tbo_toolbar_get_selected_tool (TboToolbar *self) void tbo_toolbar_set_selected_tool (TboToolbar *self, enum Tool tool) { - GtkToggleAction *action; - TboToolBase *t; + TboToolBase *t = NULL; if (self->selected_tool == self->tools[tool]) return; - unselect_tool (self); + if (self->selected_tool != NULL) + self->selected_tool->on_unselect (self->selected_tool); + self->selected_tool = NULL; - if (self->tools[tool]) + + if (tool != TBO_TOOLBAR_NONE && self->tools[tool] != NULL) { t = self->tools[tool]; - action = GTK_TOGGLE_ACTION (gtk_action_group_get_action (self->action_group, t->action)); - - if (gtk_action_is_sensitive (GTK_ACTION (action))) + if (gtk_widget_is_sensitive (GTK_WIDGET (self->tool_buttons[tool]))) { self->selected_tool = t; self->selected_tool->on_select (self->selected_tool); - gtk_toggle_action_set_active (action, TRUE); } } + + self->syncing_tool_buttons = TRUE; + for (int i = 1; i < TBO_TOOLBAR_N_TOOLS; i++) + { + gtk_toggle_button_set_active (self->tool_buttons[i], + self->selected_tool == self->tools[i]); + } + self->syncing_tool_buttons = FALSE; + TBO_DRAWING (self->tbo->drawing)->tool = self->selected_tool; tbo_toolbar_update (self); } @@ -446,74 +475,36 @@ tbo_toolbar_get_toolbar (TboToolbar *self) void tbo_toolbar_update (TboToolbar *self) { - GtkAction *prev; - GtkAction *next; - GtkAction *delete; + TboWindow *tbo; + gboolean in_frame_view; - GtkAction *doodle; - GtkAction *bubble; - GtkAction *text; - GtkAction *new_frame; - GtkAction *pix; + if (!self || self->tbo == NULL || self->tbo->destroying) + return; - GtkAction *undo; - GtkAction *redo; + tbo = self->tbo; + in_frame_view = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL; - if (!self) - return; + gtk_widget_set_sensitive (self->button_undo, tbo_undo_active_undo (tbo->undo_stack)); + gtk_widget_set_sensitive (self->button_redo, tbo_undo_active_redo (tbo->undo_stack)); - TboWindow *tbo = self->tbo; + gtk_widget_set_sensitive (self->button_prev_page, !tbo_comic_page_first (tbo->comic)); + gtk_widget_set_sensitive (self->button_next_page, !tbo_comic_page_last (tbo->comic)); + gtk_widget_set_sensitive (self->button_delete_page, tbo_comic_len (tbo->comic) > 1); - if (!self->action_group) - return; + gtk_widget_set_sensitive (GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_DOODLE]), in_frame_view); + gtk_widget_set_sensitive (GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_BUBBLE]), in_frame_view); + gtk_widget_set_sensitive (GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_TEXT]), in_frame_view); + gtk_widget_set_sensitive (self->button_pix, in_frame_view); + gtk_widget_set_sensitive (GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_FRAME]), !in_frame_view); - undo = gtk_action_group_get_action (self->action_group, "Undo"); - redo = gtk_action_group_get_action (self->action_group, "Redo"); - - gtk_action_set_sensitive (undo, tbo_undo_active_undo (tbo->undo_stack)); - gtk_action_set_sensitive (redo, tbo_undo_active_redo (tbo->undo_stack)); - - // Page next, prev and delete button sensitive - prev = gtk_action_group_get_action (self->action_group, "PrevPage"); - next = gtk_action_group_get_action (self->action_group, "NextPage"); - delete = gtk_action_group_get_action (self->action_group, "DelPage"); - - if (tbo_comic_page_first (tbo->comic)) - gtk_action_set_sensitive (prev, FALSE); - else - gtk_action_set_sensitive (prev, TRUE); - - if (tbo_comic_page_last (tbo->comic)) - gtk_action_set_sensitive (next, FALSE); - else - gtk_action_set_sensitive (next, TRUE); - if (tbo_comic_len (tbo->comic) > 1) - gtk_action_set_sensitive (delete, TRUE); - else - gtk_action_set_sensitive (delete, FALSE); - - // Frame view disabled in page view - doodle = gtk_action_group_get_action (self->action_group, "Doodle"); - bubble = gtk_action_group_get_action (self->action_group, "Bubble"); - text = gtk_action_group_get_action (self->action_group, "Text"); - new_frame = gtk_action_group_get_action (self->action_group, "NewFrame"); - pix = gtk_action_group_get_action (self->action_group, "Pix"); - - if (!tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing))) + if (!in_frame_view && self->selected_tool != NULL && + (self->selected_tool == self->tools[TBO_TOOLBAR_DOODLE] || + self->selected_tool == self->tools[TBO_TOOLBAR_BUBBLE] || + self->selected_tool == self->tools[TBO_TOOLBAR_TEXT])) { - gtk_action_set_sensitive (doodle, FALSE); - gtk_action_set_sensitive (bubble, FALSE); - gtk_action_set_sensitive (text, FALSE); - gtk_action_set_sensitive (pix, FALSE); - gtk_action_set_sensitive (new_frame, TRUE); - } - else - { - gtk_action_set_sensitive (doodle, TRUE); - gtk_action_set_sensitive (bubble, TRUE); - gtk_action_set_sensitive (text, TRUE); - gtk_action_set_sensitive (pix, TRUE); - gtk_action_set_sensitive (new_frame, FALSE); + tbo_toolbar_set_selected_tool (self, TBO_TOOLBAR_SELECTOR); + return; } + update_menubar (tbo); } diff --git a/src/tbo-toolbar.h b/src/tbo-toolbar.h index cdafd8b..c98a148 100644 --- a/src/tbo-toolbar.h +++ b/src/tbo-toolbar.h @@ -35,6 +35,17 @@ typedef struct _TboToolbar TboToolbar; typedef struct _TboToolbarClass TboToolbarClass; +enum Tool +{ + TBO_TOOLBAR_NONE, + TBO_TOOLBAR_SELECTOR, + TBO_TOOLBAR_FRAME, + TBO_TOOLBAR_DOODLE, + TBO_TOOLBAR_BUBBLE, + TBO_TOOLBAR_TEXT, + TBO_TOOLBAR_N_TOOLS +}; + struct _TboToolbar { GObject parent_instance; @@ -43,8 +54,23 @@ struct _TboToolbar TboWindow *tbo; TboToolBase *selected_tool; - GtkActionGroup *action_group; GtkWidget *toolbar; + GtkWidget *button_new; + GtkWidget *button_open; + GtkWidget *button_save; + GtkWidget *button_undo; + GtkWidget *button_redo; + GtkWidget *button_new_page; + GtkWidget *button_delete_page; + GtkWidget *button_prev_page; + GtkWidget *button_next_page; + GtkWidget *button_zoom_in; + GtkWidget *button_zoom_100; + GtkWidget *button_zoom_fit; + GtkWidget *button_zoom_out; + GtkWidget *button_pix; + GtkToggleButton *tool_buttons[TBO_TOOLBAR_N_TOOLS]; + gboolean syncing_tool_buttons; TboToolBase **tools; }; @@ -62,18 +88,7 @@ GType tbo_toolbar_get_type (void); * Method definitions. */ -enum Tool -{ - TBO_TOOLBAR_NONE, - TBO_TOOLBAR_SELECTOR, - TBO_TOOLBAR_FRAME, - TBO_TOOLBAR_DOODLE, - TBO_TOOLBAR_BUBBLE, - TBO_TOOLBAR_TEXT, - TBO_TOOLBAR_N_TOOLS -}; - -GObject * tbo_toolbar_new (); +GObject * tbo_toolbar_new (void); GObject * tbo_toolbar_new_with_params (TboWindow *tbo); TboToolBase * tbo_toolbar_get_selected_tool (TboToolbar *self); @@ -82,4 +97,3 @@ GtkWidget * tbo_toolbar_get_toolbar (TboToolbar *self); void tbo_toolbar_update (TboToolbar *self); #endif /* __TBO_TOOLBAR_H__ */ - diff --git a/src/tbo-tooltip.c b/src/tbo-tooltip.c index 9b56cd1..d48b6df 100644 --- a/src/tbo-tooltip.c +++ b/src/tbo-tooltip.c @@ -87,7 +87,7 @@ tbo_tooltip_set (const char *tooltip, int x, int y, TboWindow *tbo) else { // if it's the same passing - if (x == X && y == Y && !strcmp (tooltip, TOOLTIP->str)) + if (tooltip != NULL && x == X && y == Y && !strcmp (tooltip, TOOLTIP->str)) return; // removing tooltip if tooltip == NULL @@ -112,7 +112,7 @@ tbo_tooltip_set (const char *tooltip, int x, int y, TboWindow *tbo) } GString * -tbo_tooltip_get () +tbo_tooltip_get (void) { return TOOLTIP; } @@ -153,10 +153,8 @@ void tbo_tooltip_set_center_timeout (const char *tooltip, int timeout, TboWindow *tbo) { int x, y; - GtkAllocation alloc; - gtk_widget_get_allocation (tbo->drawing, &alloc); - x = alloc.width / 2; - y = alloc.height / 2; + x = gtk_widget_get_width (tbo->drawing) / 2; + y = gtk_widget_get_height (tbo->drawing) / 2; tbo_tooltip_set (tooltip, x, y, tbo); g_timeout_add (timeout, quit_tooltip_cb, tbo); diff --git a/src/tbo-tooltip.h b/src/tbo-tooltip.h index 4c9177a..446e532 100644 --- a/src/tbo-tooltip.h +++ b/src/tbo-tooltip.h @@ -26,7 +26,7 @@ void tbo_tooltip_set (const char *tooltip, int x, int y, TboWindow *tbo); void tbo_tooltip_set_center_timeout (const char *tooltip, int timeout, TboWindow *tbo); -GString *tbo_tooltip_get (); +GString *tbo_tooltip_get (void); void tbo_tooltip_draw (cairo_t *cr); #endif diff --git a/src/tbo-types.h b/src/tbo-types.h index 9681e1a..9ec37e6 100644 --- a/src/tbo-types.h +++ b/src/tbo-types.h @@ -59,8 +59,22 @@ typedef struct } Frame; +typedef struct +{ + double x; + double y; + guint button; + guint n_press; + GdkModifierType state; +} TboPointerEvent; + +typedef struct +{ + guint keyval; + GdkModifierType state; +} TboKeyEvent; + struct _TboWindow; typedef struct _TboWindow TboWindow; #endif - diff --git a/src/tbo-ui-utils.c b/src/tbo-ui-utils.c index 0a90926..45d5d55 100644 --- a/src/tbo-ui-utils.c +++ b/src/tbo-ui-utils.c @@ -18,6 +18,7 @@ #include +#include "tbo-widget.h" #include "tbo-ui-utils.h" GtkWidget * @@ -28,14 +29,15 @@ add_spin_with_label (GtkWidget *container, const gchar *string, gint value) GtkAdjustment *adjustment; GtkWidget *hpanel; - hpanel = gtk_hbox_new (FALSE, 0); + hpanel = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); label = gtk_label_new (string); - gtk_misc_set_alignment (GTK_MISC (label), 0, 0); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); adjustment = gtk_adjustment_new (value, 0, 10000, 1, 1, 0); spin = gtk_spin_button_new (GTK_ADJUSTMENT (adjustment), 1, 0); - gtk_box_pack_start (GTK_BOX (hpanel), label, TRUE, TRUE, 5); - gtk_box_pack_start (GTK_BOX (hpanel), spin, FALSE, FALSE, 5); - gtk_box_pack_start (GTK_BOX (container), hpanel, FALSE, FALSE, 5); + tbo_box_pack_start (hpanel, label, TRUE, TRUE, 5); + tbo_box_pack_start (hpanel, spin, FALSE, FALSE, 5); + tbo_box_pack_start (container, hpanel, FALSE, FALSE, 5); return spin; } diff --git a/src/tbo-undo.c b/src/tbo-undo.c index 2346309..0fd86f1 100644 --- a/src/tbo-undo.c +++ b/src/tbo-undo.c @@ -43,7 +43,7 @@ tbo_action_del_data (TboAction *action, gpointer user_data) } TboUndoStack * -tbo_undo_stack_new () +tbo_undo_stack_new (void) { TboUndoStack *stack = malloc (sizeof (TboUndoStack)); stack->first = NULL; diff --git a/src/tbo-undo.h b/src/tbo-undo.h index e03c97d..a8bf17a 100644 --- a/src/tbo-undo.h +++ b/src/tbo-undo.h @@ -46,8 +46,8 @@ struct _TboUndoStack { gboolean last_flag; }; -TboUndoStack * tbo_undo_stack_new (); -void tbo_undo_stack_del (); +TboUndoStack * tbo_undo_stack_new (void); +void tbo_undo_stack_del (TboUndoStack *stack); void tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action); void tbo_undo_stack_undo (TboUndoStack *stack); void tbo_undo_stack_redo (TboUndoStack *stack); diff --git a/src/tbo-utils.c b/src/tbo-utils.c index 1b187aa..a55c55c 100644 --- a/src/tbo-utils.c +++ b/src/tbo-utils.c @@ -18,6 +18,7 @@ #include +#include #include "tbo-utils.h" @@ -33,3 +34,15 @@ get_base_name (gchar *str, gchar *ret, int size) snprintf (ret, size, "%s", *dirname); g_strfreev (paths); } + +gchar * +tbo_get_data_path (const gchar *relative_path) +{ + gchar *installed_path = g_build_filename (DATA_DIR, relative_path, NULL); + + if (g_file_test (installed_path, G_FILE_TEST_EXISTS)) + return installed_path; + + g_free (installed_path); + return g_build_filename (SOURCE_DATA_DIR, relative_path, NULL); +} diff --git a/src/tbo-utils.h b/src/tbo-utils.h index 21d2741..d84cd83 100644 --- a/src/tbo-utils.h +++ b/src/tbo-utils.h @@ -23,5 +23,6 @@ #include void get_base_name (gchar *str, gchar *ret, int size); +gchar *tbo_get_data_path (const gchar *relative_path); #endif diff --git a/src/tbo-widget.c b/src/tbo-widget.c new file mode 100644 index 0000000..64d8276 --- /dev/null +++ b/src/tbo-widget.c @@ -0,0 +1,273 @@ +/* + * This file is part of TBO, a gnome comic editor + * Copyright (C) 2010 Daniel Garcia Moreno + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include "tbo-widget.h" + +struct alert_run_data { + GMainLoop *loop; + gint response; +}; + +static void +alert_response_cb (GObject *source, GAsyncResult *result, gpointer user_data) +{ + GtkAlertDialog *dialog = GTK_ALERT_DIALOG (source); + struct alert_run_data *data = user_data; + GError *error = NULL; + + data->response = gtk_alert_dialog_choose_finish (dialog, result, &error); + if (error != NULL) + { + g_error_free (error); + data->response = -1; + } + g_main_loop_quit (data->loop); +} + +GtkWidget * +tbo_widget_get_first_child (GtkWidget *widget) +{ + return gtk_widget_get_first_child (widget); +} + +gint +tbo_widget_get_child_count (GtkWidget *widget) +{ + GtkWidget *child = gtk_widget_get_first_child (widget); + gint count = 0; + + while (child != NULL) + { + count++; + child = gtk_widget_get_next_sibling (child); + } + + return count; +} + +void +tbo_widget_add_child (GtkWidget *parent, GtkWidget *child) +{ + if (GTK_IS_WINDOW (parent)) + gtk_window_set_child (GTK_WINDOW (parent), child); + else if (GTK_IS_SCROLLED_WINDOW (parent)) + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (parent), child); + else if (GTK_IS_BOX (parent)) + gtk_box_append (GTK_BOX (parent), child); + else if (GTK_IS_BUTTON (parent)) + gtk_button_set_child (GTK_BUTTON (parent), child); + else if (GTK_IS_EXPANDER (parent)) + gtk_expander_set_child (GTK_EXPANDER (parent), child); +} + +void +tbo_widget_remove_child (GtkWidget *parent, GtkWidget *child) +{ + if (GTK_IS_SCROLLED_WINDOW (parent)) + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (parent), NULL); + else if (GTK_IS_BOX (parent)) + gtk_box_remove (GTK_BOX (parent), child); + else if (GTK_IS_BUTTON (parent)) + gtk_button_set_child (GTK_BUTTON (parent), NULL); + else if (GTK_IS_EXPANDER (parent)) + gtk_expander_set_child (GTK_EXPANDER (parent), NULL); + else if (GTK_IS_WINDOW (parent)) + gtk_window_set_child (GTK_WINDOW (parent), NULL); +} + +void +tbo_widget_destroy_all_children (GtkWidget *parent) +{ + GtkWidget *child; + + while ((child = gtk_widget_get_first_child (parent)) != NULL) + { + tbo_widget_remove_child (parent, child); + } +} + +void +tbo_box_pack_start (GtkWidget *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding) +{ + GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (box)); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_set_hexpand (child, expand); + else + gtk_widget_set_vexpand (child, expand); + + gtk_widget_set_margin_start (child, padding); + gtk_widget_set_margin_end (child, padding); + gtk_widget_set_margin_top (child, padding); + gtk_widget_set_margin_bottom (child, padding); + gtk_box_append (GTK_BOX (box), child); +} + +void +tbo_paned_pack_start (GtkWidget *paned, GtkWidget *child, gboolean resize, gboolean shrink) +{ + gtk_paned_set_start_child (GTK_PANED (paned), child); + gtk_paned_set_resize_start_child (GTK_PANED (paned), resize); + gtk_paned_set_shrink_start_child (GTK_PANED (paned), shrink); +} + +void +tbo_paned_pack_end (GtkWidget *paned, GtkWidget *child, gboolean resize, gboolean shrink) +{ + gtk_paned_set_end_child (GTK_PANED (paned), child); + gtk_paned_set_resize_end_child (GTK_PANED (paned), resize); + gtk_paned_set_shrink_end_child (GTK_PANED (paned), shrink); +} + +GtkWidget * +tbo_scrolled_window_get_child (GtkWidget *scrolled) +{ + GtkWidget *child = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scrolled)); + + if (GTK_IS_VIEWPORT (child)) + child = gtk_widget_get_first_child (child); + + return child; +} + +void +tbo_scrolled_window_set_child (GtkWidget *scrolled, GtkWidget *child) +{ + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled), child); +} + +gint +tbo_alert_choose (GtkWindow *parent, + const gchar *message, + const gchar *detail, + const gchar * const *buttons, + gint cancel_button, + gint default_button) +{ + GtkAlertDialog *dialog; + struct alert_run_data data; + + dialog = gtk_alert_dialog_new ("%s", message); + gtk_alert_dialog_set_detail (dialog, detail); + gtk_alert_dialog_set_buttons (dialog, buttons); + gtk_alert_dialog_set_cancel_button (dialog, cancel_button); + gtk_alert_dialog_set_default_button (dialog, default_button); + + data.loop = g_main_loop_new (NULL, FALSE); + data.response = -1; + gtk_alert_dialog_choose (dialog, parent, NULL, alert_response_cb, &data); + g_main_loop_run (data.loop); + g_main_loop_unref (data.loop); + g_object_unref (dialog); + + return data.response; +} + +void +tbo_alert_show (GtkWindow *parent, const gchar *message, const gchar *detail) +{ + static const gchar *buttons[] = {"Close", NULL}; + + tbo_alert_choose (parent, message, detail, buttons, 0, 0); +} + +void +tbo_widget_show_all (GtkWidget *widget) +{ + GtkWidget *child; + + if (widget == NULL) + return; + + if (GTK_IS_POPOVER (widget)) + { + gtk_widget_set_visible (widget, FALSE); + return; + } + + gtk_widget_set_visible (widget, TRUE); + + for (child = gtk_widget_get_first_child (widget); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + tbo_widget_show_all (child); + } +} + +GtkWidget * +tbo_picture_new_for_pixbuf (GdkPixbuf *pixbuf) +{ + gchar *buffer = NULL; + gsize size = 0; + GBytes *bytes; + GdkTexture *texture; + GtkWidget *picture; + GError *error = NULL; + + if (pixbuf == NULL) + return gtk_picture_new (); + + if (!gdk_pixbuf_save_to_buffer (pixbuf, &buffer, &size, "png", &error, NULL)) + { + if (error != NULL) + g_error_free (error); + return gtk_picture_new (); + } + + bytes = g_bytes_new_take (buffer, size); + texture = gdk_texture_new_from_bytes (bytes, &error); + g_bytes_unref (bytes); + if (texture == NULL) + { + if (error != NULL) + g_error_free (error); + return gtk_picture_new (); + } + + picture = gtk_picture_new_for_paintable (GDK_PAINTABLE (texture)); + g_object_unref (texture); + return picture; +} + +GtkWidget * +tbo_image_new_for_pixbuf (GdkPixbuf *pixbuf) +{ + gchar *buffer = NULL; + gsize size = 0; + GBytes *bytes; + GdkTexture *texture; + GtkWidget *image; + GError *error = NULL; + + if (pixbuf == NULL) + return gtk_image_new (); + + if (!gdk_pixbuf_save_to_buffer (pixbuf, &buffer, &size, "png", &error, NULL)) + { + if (error != NULL) + g_error_free (error); + return gtk_image_new (); + } + + bytes = g_bytes_new_take (buffer, size); + texture = gdk_texture_new_from_bytes (bytes, &error); + g_bytes_unref (bytes); + if (texture == NULL) + { + if (error != NULL) + g_error_free (error); + return gtk_image_new (); + } + + image = gtk_image_new_from_paintable (GDK_PAINTABLE (texture)); + g_object_unref (texture); + return image; +} diff --git a/src/tbo-widget.h b/src/tbo-widget.h new file mode 100644 index 0000000..77a2915 --- /dev/null +++ b/src/tbo-widget.h @@ -0,0 +1,38 @@ +/* + * This file is part of TBO, a gnome comic editor + * Copyright (C) 2010 Daniel Garcia Moreno + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef __TBO_WIDGET_H__ +#define __TBO_WIDGET_H__ + +#include +#include + +GtkWidget *tbo_widget_get_first_child (GtkWidget *widget); +gint tbo_widget_get_child_count (GtkWidget *widget); +void tbo_widget_add_child (GtkWidget *parent, GtkWidget *child); +void tbo_widget_remove_child (GtkWidget *parent, GtkWidget *child); +void tbo_widget_destroy_all_children (GtkWidget *parent); +void tbo_box_pack_start (GtkWidget *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding); +void tbo_paned_pack_start (GtkWidget *paned, GtkWidget *child, gboolean resize, gboolean shrink); +void tbo_paned_pack_end (GtkWidget *paned, GtkWidget *child, gboolean resize, gboolean shrink); +GtkWidget *tbo_scrolled_window_get_child (GtkWidget *scrolled); +void tbo_scrolled_window_set_child (GtkWidget *scrolled, GtkWidget *child); +gint tbo_alert_choose (GtkWindow *parent, + const gchar *message, + const gchar *detail, + const gchar * const *buttons, + gint cancel_button, + gint default_button); +void tbo_alert_show (GtkWindow *parent, const gchar *message, const gchar *detail); +void tbo_widget_show_all (GtkWidget *widget); +GtkWidget *tbo_picture_new_for_pixbuf (GdkPixbuf *pixbuf); +GtkWidget *tbo_image_new_for_pixbuf (GdkPixbuf *pixbuf); + +#endif diff --git a/src/tbo-window.c b/src/tbo-window.c index 8b93bd4..9ff517f 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -18,10 +18,9 @@ #include -#include +#include #include #include -#include #include "tbo-types.h" #include "tbo-window.h" #include "comic.h" @@ -30,15 +29,75 @@ #include "tbo-toolbar.h" #include "tbo-drawing.h" #include "tbo-tool-selector.h" +#include "tbo-tooltip.h" +#include "tbo-utils.h" +#include "tbo-widget.h" +#include "comic-saveas-dialog.h" -static int NWINDOWS = 0; static gboolean KEY_BINDER = TRUE; +static void +apply_theme_preferences (void) +{ + GtkSettings *settings = gtk_settings_get_default (); + + if (settings == NULL) + return; + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-theme-name") != NULL) + { + g_object_set (settings, "gtk-theme-name", "Adwaita-dark", NULL); + } + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme") != NULL) + { + g_object_set (settings, "gtk-application-prefer-dark-theme", TRUE, NULL); + } +} + +static void +apply_window_icon (GtkWidget *window) +{ + gtk_window_set_default_icon_name ("tbo"); + gtk_window_set_icon_name (GTK_WINDOW (window), "tbo"); +} + +static GtkWidget * +get_page_widget (TboWindow *tbo, gint nth) +{ + return gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), nth); +} + +static GtkWidget * +create_page_tab_label (gint nth) +{ + gchar *text = g_strdup_printf (_("Page %d"), nth + 1); + GtkWidget *label = gtk_label_new (text); + + g_free (text); + return label; +} + +static void +refresh_page_tab_labels (TboWindow *tbo) +{ + gint i; + gint count = gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); + + for (i = 0; i < count; i++) + { + GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), i); + GtkWidget *label = create_page_tab_label (i); + + gtk_notebook_set_tab_label (GTK_NOTEBOOK (tbo->notebook), page, label); + } +} + static gboolean -notebook_switch_page_cb (GtkNotebook *notebook, - gpointer *page, - guint page_num, - TboWindow *tbo) +notebook_switch_page_cb (GtkNotebook *notebook, + GtkWidget *page, + guint page_num, + TboWindow *tbo) { tbo_comic_set_current_page_nth (tbo->comic, page_num); tbo_window_set_current_tab_page (tbo, FALSE); @@ -48,23 +107,139 @@ notebook_switch_page_cb (GtkNotebook *notebook, return FALSE; } +static void +destroy_cb (GtkWidget *widget, TboWindow *tbo) +{ + tbo_window_free (tbo); +} + +static void +set_window_path (gchar **slot, const gchar *path) +{ + g_free (*slot); + *slot = path != NULL ? g_strdup (path) : NULL; +} + +static gchar * +get_dirname_or_home (const gchar *path) +{ + if (path != NULL && *path != '\0') + return g_path_get_dirname (path); + + return g_strdup (g_get_home_dir ()); +} + +static gboolean +confirm_close (TboWindow *tbo) +{ + gint response; + static const gchar *buttons[] = { + "_Cancel", + "_Don't Save", + "_Save", + NULL, + }; + + if (!tbo_window_has_unsaved_changes (tbo)) + return TRUE; + + response = tbo_alert_choose (GTK_WINDOW (tbo->window), + _("Do you want to save your work before closing?"), + _("Unsaved changes will be lost if you close this window."), + buttons, + 0, + 2); + + if (response == 2) + return tbo_comic_save_dialog (NULL, tbo); + + return response == 1; +} + +static void +update_statusbar (TboWindow *tbo, int x, int y) +{ + char buffer[200]; + gboolean in_frame_view = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL; + + if (in_frame_view) + { + snprintf (buffer, 200, _("editing frame [ %5d,%5d ] | Esc: back to page"), x, y); + } + else + { + snprintf (buffer, 200, _("page: %d of %d [ %5d,%5d ] | frames: %d | Enter: frame"), + tbo_comic_page_index (tbo->comic), + tbo_comic_len (tbo->comic), + x, y, + tbo_page_len (tbo_comic_get_current_page (tbo->comic))); + } + gtk_label_set_text (GTK_LABEL (tbo->status), buffer); +} + +static void +load_app_css (void) +{ + static gboolean loaded = FALSE; + GtkCssProvider *provider; + const gchar *css; + + if (loaded) + return; + + css = + "#tbo-toolbar {" + " background: shade(@theme_bg_color, 1.02);" + " border-bottom: 1px solid alpha(@theme_fg_color, 0.08);" + "}" + ".tbo-toolbar-section button {" + " min-width: 36px;" + " min-height: 36px;" + "}" + "#tbo-sidebar {" + " background: shade(@theme_base_color, 0.98);" + "}" + "#tbo-status {" + " padding: 8px 12px;" + " border-top: 1px solid alpha(@theme_fg_color, 0.08);" + " background: shade(@theme_bg_color, 1.01);" + "}" + "#tbo-toolarea {" + " padding: 12px;" + "}"; + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_string (provider, css); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + loaded = TRUE; +} + static gboolean -on_key_cb (GtkWidget *widget, - GdkEventKey *event, - TboWindow *tbo) +on_key_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + TboWindow *tbo) { TboToolBase *tool; TboDrawing *drawing = TBO_DRAWING (tbo->drawing); + TboKeyEvent event = { .keyval = keyval, .state = state }; + + if (tbo->drawing == NULL || !gtk_widget_has_focus (GTK_WIDGET (tbo->drawing))) + return FALSE; tool = tbo_toolbar_get_selected_tool (tbo->toolbar); if (tool) - tool->on_key (tool, widget, event); + tool->on_key (tool, GTK_WIDGET (tbo->window), event); tbo_window_update_status (tbo, 0, 0); - if (KEY_BINDER) + if (KEY_BINDER && (state & (GDK_CONTROL_MASK | GDK_ALT_MASK | GDK_META_MASK)) == 0) { - switch (event->keyval) + switch (keyval) { case GDK_KEY_plus: tbo_drawing_zoom_in (drawing); @@ -101,14 +276,32 @@ on_key_cb (GtkWidget *widget, } static gboolean -on_move_cb (GtkWidget *widget, - GdkEventMotion *event, - TboWindow *tbo) +global_key_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + TboWindow *tbo) { - tbo_window_update_status (tbo, (int)event->x, (int)event->y); + if (keyval == GDK_KEY_Escape && + tbo->drawing != NULL && + tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL) + { + tbo_window_leave_frame (tbo); + return TRUE; + } + return FALSE; } +static void +on_move_cb (GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + TboWindow *tbo) +{ + update_statusbar (tbo, (int)x, (int)y); +} + TboWindow * tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, GtkWidget *scroll2, @@ -116,14 +309,12 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, GtkWidget *status, GtkWidget *vbox, Comic *comic) { TboWindow *tbo; - GList *list; tbo = malloc (sizeof (TboWindow)); tbo->window = window; tbo->dw_scroll = dw_scroll; tbo->scroll2 = scroll2; - list = gtk_container_get_children (GTK_CONTAINER (dw_scroll)); - tbo->drawing = GTK_WIDGET (list->data); + tbo->drawing = tbo_scrolled_window_get_child (dw_scroll); tbo->status = status; tbo->vbox = vbox; tbo->comic = comic; @@ -132,6 +323,10 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, tbo->undo_stack = tbo_undo_stack_new (); tbo->path = NULL; + tbo->browse_path = NULL; + tbo->export_path = NULL; + tbo->dirty = FALSE; + tbo->destroying = FALSE; return tbo; } @@ -140,73 +335,154 @@ void tbo_window_free (TboWindow *tbo) { tbo_comic_free (tbo->comic); - gtk_widget_destroy (tbo->window); - if (tbo->path) - free (tbo->path); + if (tbo->toolbar) + g_object_unref (tbo->toolbar); + g_free (tbo->path); + g_free (tbo->browse_path); + g_free (tbo->export_path); tbo_undo_stack_del (tbo->undo_stack); free (tbo); } void -tbo_window_set_path (TboWindow *tbo, const char *path) +tbo_window_set_path (TboWindow *tbo, const gchar *path) +{ + set_window_path (&tbo->path, path); + tbo_window_set_browse_path (tbo, path); +} + +void +tbo_window_set_browse_path (TboWindow *tbo, const gchar *path) +{ + set_window_path (&tbo->browse_path, path); +} + +void +tbo_window_set_export_path (TboWindow *tbo, const gchar *path) +{ + set_window_path (&tbo->export_path, path); +} + +gchar * +tbo_window_get_open_dir (TboWindow *tbo) +{ + if (tbo->browse_path != NULL) + return get_dirname_or_home (tbo->browse_path); + + return get_dirname_or_home (tbo->path); +} + +gchar * +tbo_window_get_export_dir (TboWindow *tbo) +{ + if (tbo->export_path != NULL) + return g_path_get_dirname (tbo->export_path); + + return tbo_window_get_open_dir (tbo); +} + +void +tbo_window_mark_dirty (TboWindow *tbo) +{ + tbo->dirty = TRUE; +} + +void +tbo_window_mark_clean (TboWindow *tbo) +{ + tbo->dirty = FALSE; +} + +gboolean +tbo_window_has_unsaved_changes (TboWindow *tbo) +{ + return tbo->dirty; +} + +void +tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page) +{ + gint count = gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); + gtk_notebook_append_page (GTK_NOTEBOOK (tbo->notebook), page, create_page_tab_label (count)); +} + +void +tbo_window_remove_page_widget (TboWindow *tbo, gint nth) +{ + GtkWidget *page = get_page_widget (tbo, nth); + + if (page != NULL) + { + gtk_notebook_remove_page (GTK_NOTEBOOK (tbo->notebook), nth); + refresh_page_tab_labels (tbo); + } +} + +gint +tbo_window_get_page_count (TboWindow *tbo) { - if (tbo->path) - free (tbo->path); - tbo->path = malloc (255 * sizeof (char)); - snprintf (tbo->path, 255, "%s", path); + return gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); } gboolean -tbo_window_free_cb (GtkWidget *widget, GdkEventExpose *event, +tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, TboWindow *tbo) { - tbo_window_free (tbo); - NWINDOWS--; - if (!NWINDOWS) - gtk_main_quit (); - return FALSE; + return !confirm_close (tbo); } -GdkPixbuf *create_pixbuf (const gchar * filename) +gboolean +tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo) { - GdkPixbuf *pixbuf; - GError *error = NULL; - pixbuf = gdk_pixbuf_new_from_file(filename, &error); - if(!pixbuf) { - fprintf(stderr, "%s\n", error->message); - g_error_free(error); - } + if (confirm_close (tbo)) + { + tbo->destroying = TRUE; + return FALSE; + } - return pixbuf; + return TRUE; } + GtkWidget * create_darea (TboWindow *tbo) { + GtkEventController *key; + GtkEventController *motion; GtkWidget *scrolled; GtkWidget *darea; - scrolled = gtk_scrolled_window_new (NULL, NULL); + scrolled = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); darea = tbo_drawing_new_with_params (tbo->comic); - gtk_container_add (GTK_CONTAINER (scrolled), darea); + tbo_scrolled_window_set_child (scrolled, darea); tbo_drawing_init_dnd (TBO_DRAWING (darea), tbo); - g_signal_connect_after (darea, "motion_notify_event", G_CALLBACK (on_move_cb), tbo); - gtk_widget_show_all (scrolled); + motion = gtk_event_controller_motion_new (); + g_signal_connect (motion, "motion", G_CALLBACK (on_move_cb), tbo); + gtk_widget_add_controller (darea, motion); + + key = gtk_event_controller_key_new (); + g_signal_connect (key, "key-pressed", G_CALLBACK (on_key_cb), tbo); + gtk_widget_add_controller (darea, key); + tbo_widget_show_all (scrolled); return scrolled; } TboWindow * -tbo_new_tbo (int width, int height) +tbo_new_tbo (GtkApplication *app, int width, int height) { + const int sidebar_width = 300; + const int window_width = MAX (width + sidebar_width + 80, 1100); + const int window_height = MAX (height + 180, 720); TboWindow *tbo; Comic *comic; GtkWidget *window; GtkWidget *container; GtkWidget *tool_paned; GtkWidget *menu; + GtkWidget *headerbar; TboToolbar *toolbar; GtkWidget *scrolled; GtkWidget *scrolled2; @@ -214,38 +490,57 @@ tbo_new_tbo (int width, int height) GtkWidget *status; GtkWidget *hpaned; GtkWidget *notebook; + GtkEventController *global_key; - NWINDOWS++; + window = app != NULL ? gtk_application_window_new (app) : gtk_window_new (); + gtk_window_set_default_size (GTK_WINDOW (window), window_width, window_height); + gchar *icon_path = tbo_get_data_path ("icon.png"); + g_free (icon_path); - window = gtk_window_new (GTK_WINDOW_TOPLEVEL); - gtk_window_set_default_size (GTK_WINDOW (window), width, height); - gtk_window_set_icon (GTK_WINDOW (window), create_pixbuf (DATA_DIR "/icon.png")); + apply_theme_preferences (); + load_app_css (); - // El contenedor principal - container = gtk_vbox_new (FALSE, 0); - gtk_container_add (GTK_CONTAINER (window), container); + headerbar = gtk_header_bar_new (); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); + gtk_window_set_titlebar (GTK_WINDOW (window), headerbar); + gtk_widget_add_css_class (window, "dark"); + + container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class (container, "dark"); + tbo_widget_add_child (window, container); comic = tbo_comic_new (_("Untitled"), width, height); gtk_window_set_title (GTK_WINDOW (window), comic->title); - scrolled = gtk_scrolled_window_new (NULL, NULL); + scrolled = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); darea = tbo_drawing_new_with_params (comic); - gtk_container_add (GTK_CONTAINER (scrolled), darea); + tbo_scrolled_window_set_child (scrolled, darea); notebook = gtk_notebook_new (); gtk_notebook_set_scrollable (GTK_NOTEBOOK (notebook), TRUE); - gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, NULL); - - hpaned = gtk_hpaned_new (); - tool_paned = gtk_vbox_new (FALSE, 0); - scrolled2 = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_hexpand (notebook, TRUE); + gtk_widget_set_vexpand (notebook, TRUE); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, create_page_tab_label (0)); + + hpaned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_hexpand (hpaned, TRUE); + gtk_widget_set_vexpand (hpaned, TRUE); + tool_paned = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name (tool_paned, "tbo-toolarea"); + scrolled2 = gtk_scrolled_window_new (); + gtk_widget_set_name (scrolled2, "tbo-sidebar"); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled2), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); - gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled2), tool_paned); + tbo_scrolled_window_set_child (scrolled2, tool_paned); + gtk_widget_set_size_request (scrolled2, sidebar_width, -1); + gtk_widget_set_vexpand (scrolled2, TRUE); - gtk_paned_set_position (GTK_PANED (hpaned), width - 200); - gtk_paned_pack1 (GTK_PANED (hpaned), notebook, TRUE, FALSE); - gtk_paned_pack2 (GTK_PANED (hpaned), scrolled2, TRUE, FALSE); + gtk_paned_set_position (GTK_PANED (hpaned), window_width - sidebar_width); + tbo_paned_pack_start (hpaned, notebook, TRUE, FALSE); + tbo_paned_pack_end (hpaned, scrolled2, FALSE, FALSE); - status = gtk_statusbar_new (); + status = gtk_label_new (NULL); + gtk_widget_set_name (status, "tbo-status"); + gtk_label_set_xalign (GTK_LABEL (status), 0.0); + gtk_label_set_ellipsize (GTK_LABEL (status), PANGO_ELLIPSIZE_END); tbo = tbo_window_new (window, scrolled, scrolled2, notebook, tool_paned, status, container, comic); @@ -258,20 +553,23 @@ tbo_new_tbo (int width, int height) // key press event g_signal_connect (tbo->notebook, "switch-page", G_CALLBACK (notebook_switch_page_cb), tbo); - g_signal_connect (tbo->window, "key_press_event", G_CALLBACK (on_key_cb), tbo); - g_signal_connect (window, "delete-event", G_CALLBACK (tbo_window_free_cb), tbo); + global_key = gtk_event_controller_key_new (); + gtk_event_controller_set_propagation_phase (global_key, GTK_PHASE_CAPTURE); + g_signal_connect (global_key, "key-pressed", G_CALLBACK (global_key_cb), tbo); + gtk_widget_add_controller (window, global_key); + g_signal_connect (window, "close-request", G_CALLBACK (tbo_window_close_request_cb), tbo); + g_signal_connect (window, "destroy", G_CALLBACK (destroy_cb), tbo); - // Generando el menu de la aplicacion menu = generate_menu (tbo); + gtk_header_bar_pack_end (GTK_HEADER_BAR (headerbar), menu); + tbo_box_pack_start (container, toolbar->toolbar, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (container), menu, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (container), toolbar->toolbar, FALSE, FALSE, 0); + tbo_widget_add_child (container, hpaned); - gtk_container_add (GTK_CONTAINER (container), hpaned); + tbo_box_pack_start (container, status, FALSE, FALSE, 0); - gtk_box_pack_start (GTK_BOX (container), status, FALSE, FALSE, 0); - - gtk_widget_show_all (window); + tbo_widget_show_all (window); + apply_window_icon (window); tbo_toolbar_set_selected_tool (toolbar, TBO_TOOLBAR_SELECTOR); tbo_window_update_status (tbo, 0, 0); @@ -281,27 +579,17 @@ tbo_new_tbo (int width, int height) void tbo_window_update_status (TboWindow *tbo, int x, int y) { - char buffer[200]; - snprintf (buffer, 200, _("page: %d of %d [ %5d,%5d ] | frames: %d"), - tbo_comic_page_index (tbo->comic), - tbo_comic_len (tbo->comic), - x, y, - tbo_page_len (tbo_comic_get_current_page (tbo->comic))); - gtk_statusbar_push (GTK_STATUSBAR (tbo->status), 0, buffer); - tbo_toolbar_update (tbo->toolbar); -} + if (tbo == NULL || tbo->destroying) + return; -gboolean -remove_cb (GtkWidget *widget, gpointer data) -{ - gtk_widget_destroy (widget); - return FALSE; + update_statusbar (tbo, x, y); + tbo_toolbar_update (tbo->toolbar); } void tbo_empty_tool_area (TboWindow *tbo) { - gtk_container_foreach (GTK_CONTAINER (tbo->toolarea), (GtkCallback)remove_cb, NULL); + tbo_widget_destroy_all_children (tbo->toolarea); } void @@ -322,27 +610,77 @@ tbo_window_set_current_tab_page (TboWindow *tbo, gboolean setit) nth = tbo_comic_page_index (tbo->comic); if (setit) gtk_notebook_set_current_page (GTK_NOTEBOOK (tbo->notebook), nth); - tbo->dw_scroll = gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), nth); - tbo->drawing = gtk_bin_get_child (GTK_BIN (tbo->dw_scroll)); + tbo->dw_scroll = get_page_widget (tbo, nth); + tbo->drawing = tbo_scrolled_window_get_child (tbo->dw_scroll); + TBO_DRAWING (tbo->drawing)->tool = tbo->toolbar->selected_tool; tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); tbo_tool_selector_set_selected (TBO_TOOL_SELECTOR (tbo->toolbar->selected_tool), NULL); tbo_tool_selector_set_selected_obj (TBO_TOOL_SELECTOR (tbo->toolbar->selected_tool), NULL); } +void +tbo_window_enter_frame (TboWindow *tbo, Frame *frame) +{ + TboDrawing *drawing; + TboToolSelector *selector; + + if (frame == NULL) + return; + + drawing = TBO_DRAWING (tbo->drawing); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + tbo_tool_selector_set_selected_obj (selector, NULL); + tbo_page_set_current_frame (tbo_comic_get_current_page (tbo->comic), frame); + tbo_drawing_set_current_frame (drawing, frame); + gtk_widget_grab_focus (tbo->drawing); + tbo_tooltip_set (NULL, 0, 0, tbo); + tbo_tooltip_set_center_timeout (_("press Esc to go back"), 3000, tbo); + tbo_window_update_status (tbo, 0, 0); + tbo_drawing_adjust_scroll (drawing); +} + +void +tbo_window_leave_frame (TboWindow *tbo) +{ + TboDrawing *drawing; + TboToolSelector *selector; + Frame *frame; + + drawing = TBO_DRAWING (tbo->drawing); + frame = tbo_drawing_get_current_frame (drawing); + if (frame == NULL) + return; + + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_drawing_set_current_frame (drawing, NULL); + tbo_tool_selector_set_selected (selector, frame); + tbo_tool_selector_set_selected_obj (selector, NULL); + gtk_widget_grab_focus (tbo->drawing); + tbo_tooltip_set (NULL, 0, 0, tbo); + tbo_window_update_status (tbo, 0, 0); + tbo_drawing_adjust_scroll (drawing); +} + gboolean -tbo_window_undo_cb (GtkAction *action, TboWindow *tbo) { +tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo) { tbo_undo_stack_undo (tbo->undo_stack); + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); tbo_toolbar_update (tbo->toolbar); return FALSE; } gboolean -tbo_window_redo_cb (GtkAction *action, TboWindow *tbo) { +tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo) { tbo_undo_stack_redo (tbo->undo_stack); + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); tbo_toolbar_update (tbo->toolbar); return FALSE; diff --git a/src/tbo-window.h b/src/tbo-window.h index e64fa49..4ebbe3b 100644 --- a/src/tbo-window.h +++ b/src/tbo-window.h @@ -38,22 +38,38 @@ struct _TboWindow TboToolbar *toolbar; TboUndoStack *undo_stack; Comic *comic; - char *path; + gchar *path; + gchar *browse_path; + gchar *export_path; + gboolean dirty; + gboolean destroying; }; TboWindow *tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, GtkWidget *scroll2, GtkWidget *notebook, GtkWidget *toolarea, GtkWidget *status, GtkWidget *vbox, Comic *comic); void tbo_window_free (TboWindow *tbo); -gboolean tbo_window_free_cb (GtkWidget *widget, GdkEventExpose *event, TboWindow *tbo); -GdkPixbuf *create_pixbuf (const gchar * filename); -TboWindow * tbo_new_tbo (int width, int height); +gboolean tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, TboWindow *tbo); +gboolean tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo); +TboWindow * tbo_new_tbo (GtkApplication *app, int width, int height); void tbo_window_update_status (TboWindow *tbo, int x, int y); void tbo_empty_tool_area (TboWindow *tbo); -void tbo_window_set_path (TboWindow *tbo, const char *path); +void tbo_window_set_path (TboWindow *tbo, const gchar *path); +void tbo_window_set_browse_path (TboWindow *tbo, const gchar *path); +void tbo_window_set_export_path (TboWindow *tbo, const gchar *path); +gchar *tbo_window_get_open_dir (TboWindow *tbo); +gchar *tbo_window_get_export_dir (TboWindow *tbo); +void tbo_window_mark_dirty (TboWindow *tbo); +void tbo_window_mark_clean (TboWindow *tbo); +gboolean tbo_window_has_unsaved_changes (TboWindow *tbo); +void tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page); +void tbo_window_remove_page_widget (TboWindow *tbo, gint nth); +gint tbo_window_get_page_count (TboWindow *tbo); void tbo_window_set_current_tab_page (TboWindow *tbo, gboolean setit); GtkWidget *create_darea (TboWindow *tbo); void tbo_window_set_key_binder (TboWindow *tbo, gboolean keyb); -gboolean tbo_window_undo_cb (GtkAction *action, TboWindow *tbo); -gboolean tbo_window_redo_cb (GtkAction *action, TboWindow *tbo); +void tbo_window_enter_frame (TboWindow *tbo, Frame *frame); +void tbo_window_leave_frame (TboWindow *tbo); +gboolean tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo); +gboolean tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo); #endif diff --git a/src/tbo.c b/src/tbo.c index 3e92a95..c8a6c8a 100644 --- a/src/tbo.c +++ b/src/tbo.c @@ -19,14 +19,61 @@ #include #include - #include "config.h" -#include "custom-stock.h" #include "tbo-window.h" #include "comic.h" +static void +present_window (TboWindow *tbo) +{ + GdkSurface *surface; + GdkToplevelLayout *layout; + + gtk_window_present (GTK_WINDOW (tbo->window)); + surface = gtk_native_get_surface (GTK_NATIVE (tbo->window)); + if (surface != NULL && GDK_IS_TOPLEVEL (surface)) + { + layout = gdk_toplevel_layout_new (); + gdk_toplevel_present (GDK_TOPLEVEL (surface), layout); + gdk_toplevel_focus (GDK_TOPLEVEL (surface), GDK_CURRENT_TIME); + gdk_toplevel_layout_unref (layout); + } +} + +static void +activate_cb (GtkApplication *app, gpointer user_data) +{ + TboWindow *tbo = tbo_new_tbo (app, 800, 450); + present_window (tbo); +} + +static void +open_cb (GtkApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data) +{ + gint i; + + for (i = 0; i < n_files; i++) + { + TboWindow *tbo; + gchar *path = g_file_get_path (files[i]); + + tbo = tbo_new_tbo (app, 800, 450); + if (path != NULL) + { + tbo_comic_open (tbo, path); + tbo_window_set_path (tbo, path); + g_free (path); + } + present_window (tbo); + } +} + int main (int argc, char**argv){ + GtkApplication *app; + int status; + + g_set_application_name ("TBO"); #ifdef ENABLE_NLS /* Initialize the i18n stuff */ @@ -35,16 +82,12 @@ int main (int argc, char**argv){ textdomain (GETTEXT_PACKAGE); #endif - TboWindow *tbo; - - gtk_init (&argc, &argv); - load_custom_stock (); - tbo = tbo_new_tbo (800, 450); - if (argc == 2) - tbo_comic_open (tbo, argv[1]); + app = gtk_application_new ("net.danigm.tbo", G_APPLICATION_HANDLES_OPEN); + g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL); + g_signal_connect (app, "open", G_CALLBACK (open_cb), NULL); - gtk_main (); + status = g_application_run (G_APPLICATION (app), argc, argv); + g_object_unref (app); - return 0; + return status; } - diff --git a/src/typestest.c b/src/typestest.c deleted file mode 100644 index 8ac71bc..0000000 --- a/src/typestest.c +++ /dev/null @@ -1,81 +0,0 @@ -#include -#include "tbo-object-base.h" -#include "tbo-object-svg.h" -#include "tbo-object-text.h" -#include "tbo-object-pixmap.h" - -void -print_tbo_object (TboObjectBase *obj) -{ - printf ("obj:\n x, y: (%d, %d)\n w, h: (%d, %d)\nangle: %f\n", - obj->x, obj->y, obj->width, obj->height, obj->angle); -} - -void -test_object_svg () -{ - /* simple svg object */ - TboObjectSvg *svg = TBO_OBJECT_SVG (tbo_object_svg_new ()); - - print_tbo_object (TBO_OBJECT_BASE (svg)); - printf ("path: '%s'\n", svg->path->str); - - g_object_unref (svg); - - /* svg object with params */ - svg = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (100, 200, - 150, 300, "/path/to/svgfile.svg")); - - print_tbo_object (TBO_OBJECT_BASE (svg)); - printf ("path: '%s'\n", svg->path->str); - - g_object_unref (svg); -} - -void -test_object_text () -{ - /* text object with params */ - TboObjectText *text; - GdkColor color = { 0, 0xffff, 0xffff, 0xffff }; - text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (100, 200, - 150, 300, "text", "", &color)); - - print_tbo_object (TBO_OBJECT_BASE (text)); - printf ("text: '%s'\n", text->text->str); - printf ("color: '%d, %d, %d'\n", text->font_color->red, - text->font_color->green, - text->font_color->blue); - - g_object_unref (text); -} - -void -test_object_pixmap () -{ - TboObjectPixmap *pixmap = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new ()); - - /* pixmap object with params */ - pixmap = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new_with_params (100, 200, - 150, 300, "/path/to/pngfile.png")); - - print_tbo_object (TBO_OBJECT_BASE (pixmap)); - printf ("path: '%s'\n", pixmap->path->str); - - g_object_unref (pixmap); -} - -int -main (int argc, char **argv) -{ - g_type_init (); - - printf ("\nobject svg\n---------------\n"); - test_object_svg (); - printf ("\nobject text\n--------------\n"); - test_object_text (); - printf ("\nobject pixmap\n--------------\n"); - test_object_pixmap (); - - return 0; -} diff --git a/src/ui-menu.c b/src/ui-menu.c index b381ecf..d9c3788 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -17,7 +17,6 @@ */ -#include #include #include @@ -35,65 +34,99 @@ #include "page.h" #include "tbo-object-base.h" #include "tbo-undo.h" +#include "tbo-utils.h" -static GtkActionGroup *MENU_ACTION_GROUP = NULL; -static GtkAccelGroup *ACCEL = NULL; -static gboolean ACCEL_SET = FALSE; +struct menu_accel +{ + const gchar *action_name; + const gchar * const *accels; +}; -void -update_menubar (TboWindow *tbo) +static const gchar *ACCEL_NEW[] = {"n", NULL}; +static const gchar *ACCEL_OPEN[] = {"o", NULL}; +static const gchar *ACCEL_SAVE[] = {"s", NULL}; +static const gchar *ACCEL_UNDO[] = {"z", NULL}; +static const gchar *ACCEL_REDO[] = {"y", NULL}; +static const gchar *ACCEL_CLONE[] = {"d", NULL}; +static const gchar *ACCEL_DELETE[] = {"Delete", NULL}; +static const gchar *ACCEL_FLIP_H[] = {"h", NULL}; +static const gchar *ACCEL_FLIP_V[] = {"v", NULL}; +static const gchar *ACCEL_ORDER_UP[] = {"Page_Up", NULL}; +static const gchar *ACCEL_ORDER_DOWN[] = {"Page_Down", NULL}; +static const gchar *ACCEL_QUIT[] = {"q", NULL}; + +static const struct menu_accel MENU_ACCELS[] = { + {"win.new", ACCEL_NEW}, + {"win.open", ACCEL_OPEN}, + {"win.save", ACCEL_SAVE}, + {"win.undo", ACCEL_UNDO}, + {"win.redo", ACCEL_REDO}, + {"win.clone", ACCEL_CLONE}, + {"win.delete", ACCEL_DELETE}, + {"win.flip-h", ACCEL_FLIP_H}, + {"win.flip-v", ACCEL_FLIP_V}, + {"win.order-up", ACCEL_ORDER_UP}, + {"win.order-down", ACCEL_ORDER_DOWN}, + {"win.quit", ACCEL_QUIT}, +}; + +static GtkApplication * +get_app (TboWindow *tbo) { - GtkAction *action; - int i; - char *actions[20] = {"FlipHObj", "FlipVObj", "OrderUpObj", "OrderDownObj", "DeleteObj", "CloneObj"}; - int elements = 6; - int obj_n_elements = 4; + return gtk_window_get_application (GTK_WINDOW (tbo->window)); +} - if (!tbo->toolbar) - return; +static void +toggle_menu_cb (GtkButton *button, GtkPopover *popover) +{ + if (gtk_widget_get_visible (GTK_WIDGET (popover))) + gtk_popover_popdown (popover); + else + gtk_popover_popup (popover); +} - TboDrawing *drawing = TBO_DRAWING (tbo->drawing); - TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); - TboObjectBase *obj = selector->selected_object; - Frame *frame = selector->selected_frame; +static void +menu_button_destroy_cb (GtkWidget *button, GtkWidget *popover) +{ + if (gtk_widget_get_parent (popover) == button) + gtk_widget_unparent (popover); +} - if (!MENU_ACTION_GROUP) +static GSimpleAction * +lookup_action (TboWindow *tbo, const gchar *name) +{ + return G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (tbo->window), name)); +} + +static void +set_action_enabled (TboWindow *tbo, const gchar *name, gboolean enabled) +{ + GSimpleAction *action = lookup_action (tbo, name); + + if (action != NULL) + g_simple_action_set_enabled (action, enabled); +} + +static void +set_accels_enabled (TboWindow *tbo, gboolean enabled) +{ + GtkApplication *app = get_app (tbo); + static const gchar *EMPTY[] = {NULL}; + guint i; + + if (app == NULL) return; - if (tbo_drawing_get_current_frame (drawing) && obj) - { - for (i=0; iundo_stack)); - action = gtk_action_group_get_action (MENU_ACTION_GROUP, "Redo"); - gtk_action_set_sensitive (action, tbo_undo_active_redo (tbo->undo_stack)); } -gboolean -clone_obj_cb (GtkWidget *widget, TboWindow *tbo) +static void +clone_selection (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; @@ -119,97 +152,91 @@ clone_obj_cb (GtkWidget *widget, TboWindow *tbo) tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); tbo_tool_selector_set_selected_obj (selector, cloned_obj); } - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; + + tbo_window_mark_dirty (tbo); + tbo_drawing_update (drawing); } -gboolean -delete_obj_cb (GtkWidget *widget, TboWindow *tbo) +static void +delete_selection (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); - TboObjectBase *obj = selector->selected_object; - Frame *frame = selector->selected_frame; - Page *page = tbo_comic_get_current_page (tbo->comic); - TboDrawing *drawing = TBO_DRAWING (tbo->drawing); - if (obj && tbo_drawing_get_current_frame (drawing)) + if (tbo_tool_selector_delete_selected (selector)) { - tbo_frame_del_obj (frame, obj); - tbo_tool_selector_set_selected_obj (selector, NULL); + tbo_window_mark_dirty (tbo); + tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } - else if (!tbo_drawing_get_current_frame (drawing) && frame) - { - tbo_page_del_frame (page, frame); - tbo_tool_selector_set_selected (selector, NULL); - } - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -flip_v_cb (GtkWidget *widget, TboWindow *tbo) +static void +flip_selection_h (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; - if (obj) - tbo_object_base_flipv (obj); + + if (obj != NULL) + tbo_object_base_fliph (obj); + + if (obj != NULL) + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -flip_h_cb (GtkWidget *widget, TboWindow *tbo) +static void +flip_selection_v (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; - if (obj) - tbo_object_base_fliph (obj); + + if (obj != NULL) + tbo_object_base_flipv (obj); + + if (obj != NULL) + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -order_up_cb (GtkWidget *widget, TboWindow *tbo) +static void +order_selection_up (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; - Frame *current_frame = selector->selected_frame; - if (obj) - tbo_object_base_order_up (obj, current_frame); + + if (obj != NULL) + tbo_object_base_order_up (obj, selector->selected_frame); + + if (obj != NULL) + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -order_down_cb (GtkWidget *widget, TboWindow *tbo) +static void +order_selection_down (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; - Frame *current_frame = selector->selected_frame; - if (obj) - tbo_object_base_order_down (obj, current_frame); + + if (obj != NULL) + tbo_object_base_order_down (obj, selector->selected_frame); + + if (obj != NULL) + tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean close_cb (GtkWidget *widget, TboWindow *tbo) +static void +open_tutorial (TboWindow *tbo) { - tbo_window_free_cb (widget, NULL, tbo); - return FALSE; -} + gchar *filename = tbo_get_data_path ("tut.tbo"); -gboolean -tutorial_cb (GtkWidget *widget, TboWindow *tbo){ - char *filename = DATA_DIR "/tut.tbo"; tbo_comic_open (tbo, filename); - tbo_window_set_path (tbo, filename); - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - tbo_window_update_status (tbo, 0, 0); - return FALSE; + g_free (filename); } -gboolean -about_cb (GtkWidget *widget, TboWindow *tbo){ +static void +show_about (TboWindow *tbo) +{ const gchar *authors[] = {"danigm ", NULL}; const gchar *artists[] = {"danigm ", "", @@ -229,140 +256,173 @@ about_cb (GtkWidget *widget, TboWindow *tbo){ NULL}; gtk_show_about_dialog (GTK_WINDOW (tbo->window), - "name", _("TBO comic editor"), - "version", VERSION, - "authors", authors, - "artists", artists, - "website", "http://trac.danigm.net/tbo", - "translator-credits", _("translator-credits"), - NULL); - - return FALSE; + "name", _("TBO comic editor"), + "version", VERSION, + "logo-icon-name", "tbo", + "authors", authors, + "artists", artists, + "website", "http://trac.danigm.net/tbo", + "translator-credits", _("translator-credits"), + NULL); } -gboolean -tbo_menu_export (GtkWidget *widget, TboWindow *tbo) +static void action_new (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_new_dialog (NULL, user_data); } +static void action_open (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_open_dialog (NULL, user_data); } +static void action_save (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_save_dialog (NULL, user_data); } +static void action_save_as (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_saveas_dialog (NULL, user_data); } +static void action_export (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_export (user_data); } +static void action_undo (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_window_undo_cb (NULL, user_data); } +static void action_redo (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_window_redo_cb (NULL, user_data); } +static void action_clone (GSimpleAction *action, GVariant *parameter, gpointer user_data) { clone_selection (user_data); } +static void action_delete (GSimpleAction *action, GVariant *parameter, gpointer user_data) { delete_selection (user_data); } +static void action_flip_h (GSimpleAction *action, GVariant *parameter, gpointer user_data) { flip_selection_h (user_data); } +static void action_flip_v (GSimpleAction *action, GVariant *parameter, gpointer user_data) { flip_selection_v (user_data); } +static void action_order_up (GSimpleAction *action, GVariant *parameter, gpointer user_data) { order_selection_up (user_data); } +static void action_order_down (GSimpleAction *action, GVariant *parameter, gpointer user_data) { order_selection_down (user_data); } +static void action_tutorial (GSimpleAction *action, GVariant *parameter, gpointer user_data) { open_tutorial (user_data); } +static void action_about (GSimpleAction *action, GVariant *parameter, gpointer user_data) { show_about (user_data); } +static void action_quit (GSimpleAction *action, GVariant *parameter, gpointer user_data) { gtk_window_close (GTK_WINDOW (((TboWindow *) user_data)->window)); } + +static void +install_actions (TboWindow *tbo) { - tbo_export (tbo); - return FALSE; + static const GActionEntry entries[] = { + {"new", action_new, NULL, NULL, NULL}, + {"open", action_open, NULL, NULL, NULL}, + {"save", action_save, NULL, NULL, NULL}, + {"save-as", action_save_as, NULL, NULL, NULL}, + {"export", action_export, NULL, NULL, NULL}, + {"undo", action_undo, NULL, NULL, NULL}, + {"redo", action_redo, NULL, NULL, NULL}, + {"clone", action_clone, NULL, NULL, NULL}, + {"delete", action_delete, NULL, NULL, NULL}, + {"flip-h", action_flip_h, NULL, NULL, NULL}, + {"flip-v", action_flip_v, NULL, NULL, NULL}, + {"order-up", action_order_up, NULL, NULL, NULL}, + {"order-down", action_order_down, NULL, NULL, NULL}, + {"tutorial", action_tutorial, NULL, NULL, NULL}, + {"about", action_about, NULL, NULL, NULL}, + {"quit", action_quit, NULL, NULL, NULL}, + }; + + g_action_map_add_action_entries (G_ACTION_MAP (tbo->window), + entries, + G_N_ELEMENTS (entries), + tbo); } -static const GtkActionEntry tbo_menu_entries [] = { - /* Toplevel */ - - { "File", NULL, N_("_File") }, - { "Edit", NULL, N_("_Edit") }, - { "Help", NULL, N_("Help") }, - - /* File menu */ - - { "NewFile", GTK_STOCK_NEW, N_("_New"), "N", - N_("Create a new file"), - G_CALLBACK (tbo_comic_new_dialog) }, - - { "OpenFile", GTK_STOCK_OPEN, N_("_Open"), "O", - N_("Open a new file"), - G_CALLBACK (tbo_comic_open_dialog) }, - - { "SaveFile", GTK_STOCK_SAVE, N_("_Save"), "S", - N_("Save current document"), - G_CALLBACK (tbo_comic_save_dialog) }, - - { "SaveFileAs", GTK_STOCK_SAVE_AS, N_("_Save as"), "", - N_("Save current document as ..."), - G_CALLBACK (tbo_comic_saveas_dialog) }, - - { "ToPNG", GTK_STOCK_FILE, N_("Export as..."), "", - N_("Save current document as..."), - G_CALLBACK (tbo_menu_export) }, - - { "Quit", GTK_STOCK_QUIT, N_("_Quit"), "Q", - N_("Quit"), - G_CALLBACK (close_cb) }, - - /* edit menu */ - { "Undo", GTK_STOCK_UNDO, N_("_Undo"), "Z", - N_("Undo the last action"), - G_CALLBACK (tbo_window_undo_cb) }, - { "Redo", GTK_STOCK_REDO, N_("_Redo"), "Y", - N_("Undo the last action"), - G_CALLBACK (tbo_window_redo_cb) }, - - { "CloneObj", GTK_STOCK_COPY, N_("Clone"), "d", - N_("Clone"), - G_CALLBACK (clone_obj_cb) }, - { "DeleteObj", GTK_STOCK_DELETE, N_("Delete"), "Delete", - N_("Delete"), - G_CALLBACK (delete_obj_cb) }, - { "FlipHObj", NULL, N_("Horizontal flip"), "h", - N_("Horizontal flip"), - G_CALLBACK (flip_h_cb) }, - { "FlipVObj", NULL, N_("Vertical flip"), "v", - N_("Vertical flip"), - G_CALLBACK (flip_v_cb) }, - { "OrderUpObj", NULL, N_("Move to front"), "Page_Up", - N_("Move to front"), - G_CALLBACK ( order_up_cb ) }, - { "OrderDownObj", NULL, N_("Move to back"), "Page_Down", - N_("Move to back"), - G_CALLBACK ( order_down_cb ) }, - - /* Help menu */ - { "Tutorial", NULL, N_("Tutorial"), "", - N_("Tutorial"), - G_CALLBACK (tutorial_cb) }, - - { "About", GTK_STOCK_ABOUT, N_("About"), "", - N_("About"), - G_CALLBACK (about_cb) }, -}; +static GMenuModel * +build_menu_model (void) +{ + GMenu *root = g_menu_new (); + GMenu *section; + + section = g_menu_new (); + g_menu_append (section, _("New"), "win.new"); + g_menu_append (section, _("Open"), "win.open"); + g_menu_append (section, _("Save"), "win.save"); + g_menu_append (section, _("Save As"), "win.save-as"); + g_menu_append (section, _("Export"), "win.export"); + g_menu_append_section (root, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + section = g_menu_new (); + g_menu_append (section, _("Undo"), "win.undo"); + g_menu_append (section, _("Redo"), "win.redo"); + g_menu_append (section, _("Clone"), "win.clone"); + g_menu_append (section, _("Delete"), "win.delete"); + g_menu_append (section, _("Horizontal flip"), "win.flip-h"); + g_menu_append (section, _("Vertical flip"), "win.flip-v"); + g_menu_append (section, _("Move to front"), "win.order-up"); + g_menu_append (section, _("Move to back"), "win.order-down"); + g_menu_append_section (root, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + section = g_menu_new (); + g_menu_append (section, _("Tutorial"), "win.tutorial"); + g_menu_append (section, _("About"), "win.about"); + g_menu_append (section, _("Quit"), "win.quit"); + g_menu_append_section (root, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + return G_MENU_MODEL (root); +} + +void +update_menubar (TboWindow *tbo) +{ + gboolean active = FALSE; + gboolean object_selected = FALSE; + TboDrawing *drawing; + TboToolSelector *selector; + TboObjectBase *obj; + Frame *frame; + + if (tbo->toolbar == NULL || tbo->destroying) + return; -GtkWidget *generate_menu (TboWindow *window){ - GtkWidget *menu; - GtkUIManager *manager; - GError *error = NULL; + drawing = TBO_DRAWING (tbo->drawing); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + obj = selector->selected_object; + frame = selector->selected_frame; - manager = gtk_ui_manager_new (); - gtk_ui_manager_add_ui_from_file (manager, DATA_DIR "/ui/tbo-menu-ui.xml", &error); - if (error != NULL) + if (tbo_drawing_get_current_frame (drawing) && obj) { - g_warning (_("Could not merge tbo-menu-ui.xml: %s"), error->message); - g_error_free (error); + active = TRUE; + object_selected = TRUE; + } + else if (!tbo_drawing_get_current_frame (drawing) && frame) + { + active = TRUE; } - MENU_ACTION_GROUP = gtk_action_group_new ("MenuActions"); - gtk_action_group_set_translation_domain (MENU_ACTION_GROUP, NULL); - gtk_action_group_add_actions (MENU_ACTION_GROUP, tbo_menu_entries, - G_N_ELEMENTS (tbo_menu_entries), window); - - gtk_ui_manager_insert_action_group (manager, MENU_ACTION_GROUP, 0); - - menu = gtk_ui_manager_get_widget (manager, "/menubar"); - - ACCEL = gtk_ui_manager_get_accel_group (manager); - gtk_window_add_accel_group (GTK_WINDOW (window->window), ACCEL); - ACCEL_SET = TRUE; - - return menu; + set_action_enabled (tbo, "undo", tbo_undo_active_undo (tbo->undo_stack)); + set_action_enabled (tbo, "redo", tbo_undo_active_redo (tbo->undo_stack)); + set_action_enabled (tbo, "clone", active); + set_action_enabled (tbo, "delete", active); + set_action_enabled (tbo, "flip-h", object_selected); + set_action_enabled (tbo, "flip-v", object_selected); + set_action_enabled (tbo, "order-up", object_selected); + set_action_enabled (tbo, "order-down", object_selected); } +GtkWidget * +generate_menu (TboWindow *window) +{ + GtkWidget *button; + GtkWidget *icon; + GtkWidget *popover; + GMenuModel *model; + + install_actions (window); + set_accels_enabled (window, TRUE); + + model = build_menu_model (); + button = gtk_button_new (); + icon = gtk_image_new_from_icon_name ("open-menu-symbolic"); + gtk_image_set_pixel_size (GTK_IMAGE (icon), 12); + gtk_button_set_child (GTK_BUTTON (button), icon); + gtk_widget_set_focusable (button, FALSE); + gtk_widget_set_tooltip_text (button, _("Menu")); + popover = gtk_popover_menu_new_from_model (model); + gtk_widget_set_parent (popover, button); + gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM); + g_signal_connect (button, "clicked", G_CALLBACK (toggle_menu_cb), popover); + g_signal_connect (button, "destroy", G_CALLBACK (menu_button_destroy_cb), popover); + g_object_unref (model); + + update_menubar (window); + return button; +} void tbo_menu_enable_accel_keys (TboWindow *tbo) { - if (ACCEL && !ACCEL_SET) - { - gtk_window_add_accel_group (GTK_WINDOW (tbo->window), ACCEL); - ACCEL_SET = TRUE; - } + set_accels_enabled (tbo, TRUE); } void tbo_menu_disable_accel_keys (TboWindow *tbo) { - if (ACCEL && ACCEL_SET) - { - gtk_window_remove_accel_group (GTK_WINDOW (tbo->window), ACCEL); - ACCEL_SET = FALSE; - } + set_accels_enabled (tbo, FALSE); } diff --git a/src/undotest.c b/src/undotest.c deleted file mode 100644 index 4e82a24..0000000 --- a/src/undotest.c +++ /dev/null @@ -1,84 +0,0 @@ -#include -#include "tbo-undo.h" - -// Defining a custom TboAction with custom data -typedef struct _TboActionString TboActionString; - -struct _TboActionString { - void (*action_do) (TboAction *action); - void (*action_undo) (TboAction *action); - char *data; -}; - -void -testdo (TboAction *act) { - TboActionString *action = (TboActionString*)act; - printf (" + doing %s\n", action->data); -} - -void -testundo (TboAction *act) { - TboActionString *action = (TboActionString*)act; - printf (" - UNdoing %s\n", action->data); -} - -TboAction * -tbo_action_string_new (char *str) { - TboAction *act = tbo_action_new (TboActionString); - TboActionString *action = (TboActionString*)act; - action->data = str; - tbo_action_set (act, testdo, testundo); - return act; -} - -int -main (int argc, char **argv) -{ - TboUndoStack *stack; - TboAction *action; - - printf ("Testing TBO undo\n"); - - stack = tbo_undo_stack_new (); - - action = tbo_action_string_new ("Test action1"); - tbo_undo_stack_insert (stack, action); - action = tbo_action_string_new ("Test action2"); - tbo_undo_stack_insert (stack, action); - action = tbo_action_string_new ("Test action3"); - tbo_undo_stack_insert (stack, action); - - tbo_undo_stack_undo (stack); - tbo_undo_stack_undo (stack); - tbo_undo_stack_undo (stack); - - printf ("\nUndoing nothing\n"); - printf ("problem?\n"); - tbo_undo_stack_undo (stack); - printf ("problem?\n"); - tbo_undo_stack_undo (stack); - - printf ("\nNow redoing\n"); - // redoing - tbo_undo_stack_redo (stack); - tbo_undo_stack_redo (stack); - tbo_undo_stack_redo (stack); - - printf ("\nRedoing nothing\n"); - printf ("problem?\n"); - tbo_undo_stack_redo (stack); - printf ("problem?\n"); - tbo_undo_stack_redo (stack); - - printf ("\nNow undo and redo\n"); - tbo_undo_stack_undo (stack); - action = tbo_action_string_new ("Test action4"); - tbo_undo_stack_insert (stack, action); - tbo_undo_stack_redo (stack); - tbo_undo_stack_undo (stack); - tbo_undo_stack_undo (stack); - tbo_undo_stack_redo (stack); - - printf ("All OK\n"); - return 0; -} diff --git a/tbo.doap b/tbo.doap deleted file mode 100644 index 8c88751..0000000 --- a/tbo.doap +++ /dev/null @@ -1,19 +0,0 @@ - - - TBO - TBO is an easy and fun program to draw comic and make your presentations funnier. - - - - - - Daniel Garcia Moreno - - danigm - - - diff --git a/test/cairo1.py b/test/cairo1.py deleted file mode 100644 index e3e5a2e..0000000 --- a/test/cairo1.py +++ /dev/null @@ -1,42 +0,0 @@ -#! /usr/bin/env python -import pygtk -pygtk.require('2.0') -import gtk, gobject, cairo - -# Create a GTK+ widget on which we will draw using Cairo -class Screen(gtk.DrawingArea): - - # Draw in response to an expose-event - __gsignals__ = { "expose-event": "override" } - - # Handle the expose-event by drawing - def do_expose_event(self, event): - - # Create the cairo context - cr = self.window.cairo_create() - - # Restrict Cairo to the exposed area; avoid extra work - cr.rectangle(event.area.x, event.area.y, - event.area.width, event.area.height) - cr.clip() - - self.draw(cr, *self.window.get_size()) - - def draw(self, cr, width, height): - # Fill the background with gray - cr.set_source_rgb(0.5, 0.5, 0.5) - cr.rectangle(0, 0, width, height) - cr.fill() - -# GTK mumbo-jumbo to show the widget in a window and quit when it's closed -def run(Widget): - window = gtk.Window() - window.connect("delete-event", gtk.main_quit) - widget = Widget() - widget.show() - window.add(widget) - window.present() - gtk.main() - -if __name__ == "__main__": - run(Screen) diff --git a/test/cairosamples/Makefile b/test/cairosamples/Makefile deleted file mode 100644 index c90d1be..0000000 --- a/test/cairosamples/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -GTK = `pkg-config --cflags --libs gtk+-2.0` - -topng: topng.c - gcc topng.c -o topng $(GTK) - -topdf: topdf.c - gcc topdf.c -o topdf $(GTK) - -togtk: - gcc togtk.c -o togtk $(GTK) - diff --git a/test/cairosamples/togtk.c b/test/cairosamples/togtk.c deleted file mode 100644 index 9ab62e5..0000000 --- a/test/cairosamples/togtk.c +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include - -static gboolean -on_expose_event(GtkWidget *widget, - GdkEventExpose *event, - gpointer data) -{ - cairo_t *cr; - - cr = gdk_cairo_create(widget->window); - - cairo_set_source_rgb(cr, 255, 255, 255); - cairo_rectangle(cr, 0, 0, 400, 90); - cairo_fill(cr); - cairo_set_source_rgb(cr, 0, 0, 0); - cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, 40.0); - - cairo_move_to(cr, 10.0, 50.0); - cairo_show_text(cr, "Disziplin ist Macht."); - - cairo_destroy(cr); - - return FALSE; -} - -int -main (int argc, char *argv[]) -{ - - GtkWidget *window; - - gtk_init(&argc, &argv); - - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - - g_signal_connect(window, "expose-event", - G_CALLBACK (on_expose_event), NULL); - g_signal_connect(window, "destroy", - G_CALLBACK (gtk_main_quit), NULL); - - gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); - gtk_window_set_default_size(GTK_WINDOW(window), 400, 90); - gtk_widget_set_app_paintable(window, TRUE); - - gtk_widget_show_all(window); - - gtk_main(); - - return 0; -} - diff --git a/test/cairosamples/topdf.c b/test/cairosamples/topdf.c deleted file mode 100644 index 5719ec0..0000000 --- a/test/cairosamples/topdf.c +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include - -int main() { - - cairo_surface_t *surface; - cairo_t *cr; - - surface = cairo_pdf_surface_create("pdffile.pdf", 504, 648); - cr = cairo_create(surface); - - cairo_set_source_rgb(cr, 0, 0, 0); - cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size (cr, 40.0); - - cairo_move_to(cr, 10.0, 50.0); - cairo_show_text(cr, "Disziplin ist Macht."); - - cairo_show_page(cr); - - cairo_surface_destroy(surface); - cairo_destroy(cr); - - return 0; -} - diff --git a/test/cairosamples/topng.c b/test/cairosamples/topng.c deleted file mode 100644 index 5783c63..0000000 --- a/test/cairosamples/topng.c +++ /dev/null @@ -1,26 +0,0 @@ -#include - -int main (int argc, char *argv[]) -{ - cairo_surface_t *surface; - cairo_t *cr; - - surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 390, 60); - cr = cairo_create(surface); - - cairo_set_source_rgb(cr, 0, 0, 0); - cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, 40.0); - - cairo_move_to(cr, 10.0, 50.0); - cairo_show_text(cr, "Disziplin ist Macht."); - - cairo_surface_write_to_png(surface, "image.png"); - - cairo_destroy(cr); - cairo_surface_destroy(surface); - - return 0; -} - diff --git a/test/cairotest.py b/test/cairotest.py deleted file mode 100644 index e3ef384..0000000 --- a/test/cairotest.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -import pygtk -pygtk.require('2.0') -import gtk, gobject, cairo -import math -import rsvg -pi = math.pi - -BLACK = (0.0,0.0,0.0) -WHITE = (1.0,1.0,1.0) - -class TBObject: - def __init__(self, x=0, y=0, w=0, h=0): - self.width = w - self.height = h - self.x = x - self.y = y - - def scale(self, w=0, h=0): - self.width = w - self.height = h - - def move(self, x, y): - self.x = x - self.y = y - -class Text(TBObject): - def __init__(self, text, color=BLACK, font="Sans", fontsize=12, **kwargs): - TBObject.__init__(self, **kwargs) - self.text = text.split("\n") - self.color = color - self.font = font - self.fontsize = fontsize - - def draw(self, cr): - x, y = self.x, self.y - for line in self.text: - cr.set_source_rgb(*self.color) - cr.select_font_face(self.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) - cr.set_font_size(self.fontsize) - x_bearing, y_bearing, width, height = cr.text_extents(line)[:4] - cr.move_to(x - width / 2 - x_bearing, y - height / 2 - y_bearing) - cr.show_text(line) - y -= y_bearing - 5 - -class SVG(TBObject): - def __init__(self, file, **kwargs): - TBObject.__init__(self, **kwargs) - self.file = file - self.svg = rsvg.Handle(file) - - def draw(self, cr): - x, y = self.x, self.y - ws, hs = self.scale_svg() - cr.translate(x, y) - cr.scale(ws, hs) - self.svg.render_cairo(cr) - cr.scale(1/ws,1/hs) - cr.translate(-x, -y) - - def scale_svg(self): - w, h = self.svg.props.width, self.svg.props.height - if not self.width or not self.height: - self.width, self.height = w, h - - w_scale = self.width / float(w) - h_scale = self.height / float(h) - return w_scale, h_scale - -class Rectangle(TBObject): - def __init__(self, color=BLACK, fill=WHITE, line_width=1, **kwargs): - TBObject.__init__(self, **kwargs) - self.color = color - self.fill = fill - self.line_width = line_width - - def draw(self, cr): - cr.set_line_width(self.line_width) - cr.set_source_rgb(*self.color) - cr.rectangle(self.x, self.y, self.width, self.height) - cr.stroke() - cr.set_source_rgb(*self.fill) - cr.rectangle(self.x, self.y, self.width, self.height) - cr.fill() - -class DArea(gtk.DrawingArea): - def __init__(self): - gtk.DrawingArea.__init__(self) - self.connect("expose-event", self.expose) - - self.add_events(gtk.gdk.BUTTON_PRESS_MASK | - gtk.gdk.BUTTON1_MOTION_MASK) - - self.connect("expose_event", self.expose) - self.connect("button_press_event", self.pressing) - self.connect("motion_notify_event", self.moving) - - def expose(self, widget, event): - self.context = self.window.cairo_create() - - self.context.rectangle(event.area.x, event.area.y, - event.area.width, event.area.height) - self.context.clip() - - self.draw(self.context, *self.window.get_size()) - - def draw(self, cr, width, height): - r = Rectangle(x=10, y=10, w=width-20, h=height-20, line_width=10) - r.draw(cr) - - # draw lines - cr.set_source_rgb(0.0, 0.0, 0.8) - cr.move_to(width / 3.0, height / 3.0) - cr.rel_line_to(0, height / 6.0) - cr.move_to(2 * width / 3.0, height / 3.0) - cr.rel_line_to(0, height / 6.0) - cr.stroke() - - # and a circle - cr.set_source_rgb(1.0, 0.0, 0.0) - radius = min(width, height) - cr.arc(width / 2.0, height / 2.0, radius / 2.0 - 20, 0, 2 * pi) - cr.stroke() - cr.arc(width / 2.0, height / 2.0, radius / 3.0 - 10, pi / 3, 2 * pi / 3) - cr.stroke() - - globo = SVG("globo.svg", x=width-300, y=40, w=200, h=120) - globo.draw(cr) - - text = ''' -este texto -está escrito -en varias -líneas - ''' - - tbotext = Text(text, x=width-200, y=60, fontsize=12, font="Kid Kosmic") - tbotext.draw(cr) - - def pressing(self, widget, event): - print "pressing", event.x, event.y - - def moving(self, widget, event): - print "moving", event.x, event.y - -def main(): - w = gtk.Window() - w.set_title("TBO") - w.set_default_size(800, 500) - darea = DArea() - w.add(darea) - w.show_all() - w.connect('destroy', gtk.main_quit) - gtk.main() - -if __name__ == '__main__': - main() diff --git a/test/ejemplo_2.py b/test/ejemplo_2.py deleted file mode 100644 index c51f1d9..0000000 --- a/test/ejemplo_2.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -import gtk -import gtk.gdk -import cairo -import math - -class SemiCirculo(gtk.DrawingArea): - def __init__(self): - gtk.DrawingArea.__init__(self) - self.connect("expose-event", self.expose) - - def expose(self, widget, event): - #Creamos un contexto de dibujo cairo - self.context = widget.window.cairo_create() - - #Ajustamos el tamaño del contexto al del widget - self.context.rectangle(event.area.x, event.area.y, - event.area.width, event.area.height) - self.context.clip() - - #Llamamos a la función de dibujado - self.draw(self.context) - return False - - def draw(self, context): - #Adquirimos las coordenadas de origen - #y el tamaño del rectangulo del widget, - #situando en las variable x e y - #el centro del rectangulo. - rect = self.get_allocation() - x = rect.x + rect.width / 2 - y = rect.y + rect.height / 2 - - #hallamos el radio - radius = min(rect.width / 2, rect.height / 2) - 5 - - #Dibujamos un arco - mx = cairo.Matrix(-1,0,0,-1,0,0) - context.translate(x, y) - context.transform(mx) - context.arc(0, 0, radius/2, 0,(1 * math.pi)) - context.arc(0, 0, radius/10, 0,(2 * math.pi)) - - #Elegimos el color de relleno y lo vertemos - context.set_source_rgb(0.7, 0.8, 0.1) - context.fill_preserve() - - #Elegimos el color del borde y lo dibujamos - context.set_source_rgb(0, 0, 0) - context.stroke() - -def main(): - window = gtk.Window() - semicirculo = SemiCirculo() - - # Añadimos nuestro widget a la ventana - window.add(semicirculo) - # Conectamos el evento destroy con la salida del bucle de eventos - window.connect("destroy", gtk.main_quit) - # Dibujamos toda la ventana - window.show_all() - - # Comenzamos el bucle de eventos - gtk.main() - -if __name__ == "__main__": - main() diff --git a/test/globo.svg b/test/globo.svg deleted file mode 100644 index 651a32e..0000000 --- a/test/globo.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/test/rsvgtest.py b/test/rsvgtest.py deleted file mode 100644 index 80f58fa..0000000 --- a/test/rsvgtest.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -import cairo, gtk, rsvg, sys - -class myApp: - def __init__(self, filename): - mw = gtk.Window(gtk.WINDOW_TOPLEVEL) - mw.connect("delete_event", gtk.main_quit) - - svg = rsvg.Handle(filename) - - da = gtk.DrawingArea() - da.set_size_request(svg.props.width, svg.props.height) - da.connect("expose_event", self.expose, svg) - - mw.add(da) - mw.show_all() - - - def expose(self, da, event, svg): - ctx = da.window.cairo_create() - svg.render_cairo(ctx) - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print "Uso: %s fichero.svg" % sys.argv[0] - else: - try: - app = myApp(sys.argv[1]) - gtk.main() - except KeyboardInterrupt: - pass - diff --git a/test/xml/gmarkup.c b/test/xml/gmarkup.c deleted file mode 100644 index c946047..0000000 --- a/test/xml/gmarkup.c +++ /dev/null @@ -1,90 +0,0 @@ -#include -#include -#include -#include - -gchar *current_animal_noise = NULL; - -/* The handler functions. */ - -void start_element (GMarkupParseContext *context, - const gchar *element_name, - const gchar **attribute_names, - const gchar **attribute_values, - gpointer user_data, - GError **error) { - - const gchar **name_cursor = attribute_names; - const gchar **value_cursor = attribute_values; - - while (*name_cursor) { - if (strcmp (*name_cursor, "noise") == 0) - current_animal_noise = g_strdup (*value_cursor); - - name_cursor++; - value_cursor++; - } -} - -void text(GMarkupParseContext *context, - const gchar *text, - gsize text_len, - gpointer user_data, - GError **error) -{ - /* Note that "text" is not a regular C string: it is - * not null-terminated. This is the reason for the - * unusual %*s format below. - */ - if (current_animal_noise) - printf("I am a %*s and I go %s. Can you do it?\n", - text_len, text, current_animal_noise); -} - -void end_element (GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, - GError **error) -{ - if (current_animal_noise) - { - g_free (current_animal_noise); - current_animal_noise = NULL; - } -} - -/* The list of what handler does what. */ -static GMarkupParser parser = { - start_element, - end_element, - text, - NULL, - NULL -}; - -/* Code to grab the file into memory and parse it. */ -int main() { - char *text; - gsize length; - GMarkupParseContext *context = g_markup_parse_context_new ( - &parser, - 0, - NULL, - NULL); - - /* seriously crummy error checking */ - - if (g_file_get_contents ("simple.xml", &text, &length, NULL) == FALSE) { - printf("Couldn't load XML\n"); - exit(255); - } - - if (g_markup_parse_context_parse (context, text, length, NULL) == FALSE) { - printf("Parse failed\n"); - exit(255); - } - - g_free(text); - g_markup_parse_context_free (context); -} -/* EOF */ diff --git a/test/xml/simple.xml b/test/xml/simple.xml deleted file mode 100644 index 368875c..0000000 --- a/test/xml/simple.xml +++ /dev/null @@ -1,6 +0,0 @@ - - lion - bunny - cat - - diff --git a/tests/asset_bounds_check.c b/tests/asset_bounds_check.c new file mode 100644 index 0000000..1ade239 --- /dev/null +++ b/tests/asset_bounds_check.c @@ -0,0 +1,56 @@ +#include + +#include "tbo-window.h" +#include "comic.h" +#include "page.h" +#include "frame.h" +#include "dnd.h" + +int main(int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + GList *frames; + gint before; + gint after_inside; + gint after_outside; + + if (argc != 2) + return 2; + + gtk_init(); + + app = gtk_application_new ("net.danigm.tbo.assetbounds", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_open (tbo, argv[1]); + + page = tbo_comic_get_current_page (tbo->comic); + frames = tbo_page_get_frames (page); + if (frames == NULL) + return 4; + + frame = frames->data; + tbo_window_enter_frame (tbo, frame); + + before = g_list_length (frame->objects); + if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", frame->width / 2, frame->height / 2) == NULL) + return 5; + + after_inside = g_list_length (frame->objects); + if (after_inside != before + 1) + return 6; + + if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", frame->width + 50, frame->height + 50) != NULL) + return 7; + + after_outside = g_list_length (frame->objects); + if (after_outside != after_inside) + return 8; + + return 0; +} diff --git a/tests/frame_tool_check.c b/tests/frame_tool_check.c new file mode 100644 index 0000000..fb1b517 --- /dev/null +++ b/tests/frame_tool_check.c @@ -0,0 +1,48 @@ +#include + +#include "tbo-window.h" +#include "comic.h" +#include "page.h" +#include "tbo-toolbar.h" +#include "tbo-drawing.h" + +int main(int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + GList *frames; + + if (argc != 2) + return 2; + + gtk_init(); + + app = gtk_application_new ("net.danigm.tbo.framecheck", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_open (tbo, argv[1]); + + page = tbo_comic_get_current_page (tbo->comic); + frames = tbo_page_get_frames (page); + if (frames == NULL) + return 4; + + tbo_window_enter_frame (tbo, frames->data); + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) == NULL) + return 5; + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_DOODLE); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_window_leave_frame (tbo); + + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL) + return 6; + + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/load_render_check.c b/tests/load_render_check.c new file mode 100644 index 0000000..2bb4b48 --- /dev/null +++ b/tests/load_render_check.c @@ -0,0 +1,51 @@ +#include +#include + +#include "comic-load.h" +#include "comic.h" +#include "page.h" +#include "tbo-drawing.h" + +int main(int argc, char **argv) +{ + Comic *comic; + Page *page; + GtkWidget *drawing; + cairo_surface_t *surface; + cairo_t *cr; + + if (argc != 2) + return 2; + + gtk_init(); + + comic = tbo_comic_load (argv[1]); + if (comic == NULL) + return 3; + + if (tbo_comic_len (comic) <= 0) + return 4; + + page = tbo_comic_get_current_page (comic); + if (page == NULL || tbo_page_len (page) <= 0) + return 5; + + drawing = tbo_drawing_new_with_params (comic); + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, comic->width, comic->height); + cr = cairo_create (surface); + tbo_drawing_draw_page (TBO_DRAWING (drawing), cr, page, comic->width, comic->height); + + if (cairo_status (cr) != CAIRO_STATUS_SUCCESS || + cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) + { + cairo_destroy (cr); + cairo_surface_destroy (surface); + tbo_comic_free (comic); + return 6; + } + + cairo_destroy (cr); + cairo_surface_destroy (surface); + tbo_comic_free (comic); + return 0; +} diff --git a/tests/save_roundtrip_check.c b/tests/save_roundtrip_check.c new file mode 100644 index 0000000..07dd6e9 --- /dev/null +++ b/tests/save_roundtrip_check.c @@ -0,0 +1,68 @@ +#include +#include + +#include "tbo-window.h" +#include "comic.h" +#include "comic-load.h" +#include "page.h" + +int main(int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + Comic *reloaded; + Page *page; + gchar *tmpname; + gint fd; + gint original_pages; + gint reloaded_pages; + gint original_frames; + gint reloaded_frames; + + if (argc != 2) + return 2; + + gtk_init(); + + app = gtk_application_new ("net.danigm.tbo.saveroundtrip", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_open (tbo, argv[1]); + + original_pages = tbo_comic_len (tbo->comic); + page = tbo_comic_get_current_page (tbo->comic); + original_frames = tbo_page_len (page); + + tmpname = g_build_filename (g_get_tmp_dir (), "tbo-roundtrip-XXXXXX.tbo", NULL); + fd = g_mkstemp (tmpname); + if (fd < 0) + return 4; + close (fd); + + if (!tbo_comic_save (tbo, tmpname)) + return 5; + + reloaded = tbo_comic_load (tmpname); + if (reloaded == NULL) + return 6; + + reloaded_pages = tbo_comic_len (reloaded); + page = tbo_comic_get_current_page (reloaded); + reloaded_frames = tbo_page_len (page); + + g_remove (tmpname); + g_free (tmpname); + tbo_comic_free (reloaded); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + + if (original_pages != reloaded_pages) + return 7; + if (original_frames != reloaded_frames) + return 8; + + return 0; +} From bc1b546611d52db8d431c8ea324f8ca12a5d7fbc Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 18:39:37 +0200 Subject: [PATCH 02/22] Update Arch PKGBUILD to skip tests when xvfb is unavailable --- archlinux/PKGBUILD | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index 20118d3..bddb42b 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -9,7 +9,6 @@ url="https://github.com/j4imefoo/TBO" license=('GPL3') depends=('gtk4' 'cairo' 'librsvg') makedepends=('git' 'meson' 'ninja' 'pkgconf' 'gettext') -checkdepends=('xorg-server-xvfb') source=("git+https://github.com/j4imefoo/TBO.git") sha256sums=('SKIP') @@ -24,7 +23,11 @@ build() { } check() { - xvfb-run -a meson test -C build --no-rebuild --print-errorlogs --num-processes 1 + if command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a meson test -C build --no-rebuild --print-errorlogs --num-processes 1 + else + echo "==> Skipping tests: xvfb-run not available" + fi } package() { From 964f11eaafd181519b5df4d5e98f8f83f9f25f3f Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 18:43:35 +0200 Subject: [PATCH 03/22] Fix Arch PKGBUILD Meson prefix handling --- archlinux/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index bddb42b..0a2d0e0 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -18,7 +18,7 @@ pkgver() { } build() { - arch-meson TBO build -Dprefix=/usr + arch-meson TBO build meson compile -C build } From cf471d5b68194ecfa0f202f1d28fed1e559871af Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 18:59:44 +0200 Subject: [PATCH 04/22] Add task bar icon --- meson.build | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meson.build b/meson.build index 88969e0..8d54dd4 100644 --- a/meson.build +++ b/meson.build @@ -24,6 +24,7 @@ conf.set_quoted('GETTEXT_PACKAGE', 'tbo') conf.set_quoted('VERSION', meson.project_version()) conf.set10('ENABLE_NLS', true) configure_file(output: 'config.h', configuration: conf) +configured_icon_png = configure_file(input: 'data/icon.png', output: 'tbo.png', copy: true) add_project_arguments( '-DG_LOG_DOMAIN="tbo"', @@ -151,3 +152,5 @@ install_subdir('data/doodle', install_dir: join_paths(get_option('datadir'), 'tb install_data('data/applications/net.danigm.tbo.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'pixmaps')) +install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps')) +install_data(configured_icon_png, install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '128x128', 'apps')) From 68e6055b2bf23fa2ab2df0ad9a726a11a9986bed Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 19:02:50 +0200 Subject: [PATCH 05/22] Disable startup notification in desktop file --- data/applications/net.danigm.tbo.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/applications/net.danigm.tbo.desktop b/data/applications/net.danigm.tbo.desktop index fbe5d4c..70ebb9c 100644 --- a/data/applications/net.danigm.tbo.desktop +++ b/data/applications/net.danigm.tbo.desktop @@ -6,6 +6,6 @@ Comment=Create comic strips and illustrated presentations Exec=tbo %f Icon=tbo Terminal=false -StartupNotify=true +StartupNotify=false StartupWMClass=net.danigm.tbo Categories=Graphics;VectorGraphics;GTK; From 4408902e2fe58d9d92f4ec58097dbf212b2bf388 Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 19:10:44 +0200 Subject: [PATCH 06/22] Repair double icon in system tray --- data/applications/net.danigm.tbo.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/applications/net.danigm.tbo.desktop b/data/applications/net.danigm.tbo.desktop index 70ebb9c..ebe82aa 100644 --- a/data/applications/net.danigm.tbo.desktop +++ b/data/applications/net.danigm.tbo.desktop @@ -7,5 +7,5 @@ Exec=tbo %f Icon=tbo Terminal=false StartupNotify=false -StartupWMClass=net.danigm.tbo +StartupWMClass=tbo Categories=Graphics;VectorGraphics;GTK; From 8d9938d21fce0e57b40113e1ef9b608ed3a4be51 Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 19:28:10 +0200 Subject: [PATCH 07/22] Spanish translations of missing items --- po/es.po | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/po/es.po b/po/es.po index bd11cb2..77b89c6 100644 --- a/po/es.po +++ b/po/es.po @@ -50,7 +50,7 @@ msgstr "altura: " #: ../src/comic-open-dialog.c:65 msgid "Open" -msgstr "Abrir" +msgstr "Abrir cómic" #: ../src/comic-open-dialog.c:82 msgid "TBO files" @@ -64,6 +64,55 @@ msgstr "Todos los archivos" msgid "Save as" msgstr "Guardar como" +#: ../src/tbo-toolbar.c:248 +msgid "New comic" +msgstr "Cómic nuevo" + +#: ../src/tbo-toolbar.c:250 +msgid "Save comic as" +msgstr "Guardar como" + +#: ../src/tbo-toolbar.c:260 +msgid "Undo" +msgstr "Deshacer" + +#: ../src/tbo-toolbar.c:261 +msgid "Redo" +msgstr "Rehacer" + +#: ../src/tbo-toolbar.c:270 +msgid "Delete page" +msgstr "Eliminar página" + +#: ../src/tbo-toolbar.c:271 +msgid "Previous page" +msgstr "Página anterior" + +#: ../src/tbo-toolbar.c:287 +msgid "New frame" +msgstr "Nueva viñeta" + +#: ../src/ui-menu.c:321 +msgid "New" +msgstr "Cómic nuevo" + +#: ../src/ui-menu.c:323 +msgid "Save" +msgstr "Guardar" + +#: ../src/ui-menu.c:324 +msgid "Save As" +msgstr "Guardar como" + +#: ../src/ui-menu.c:325 +msgid "Export" +msgstr "Exportar" + +#: ../src/tbo-window.c:74 +#, c-format +msgid "Page %d" +msgstr "Página %d" + #: ../src/custom-stock.c:57 #, c-format msgid "error loading image %s\n" From 253537dbad100f5c2914bdade00ab83694293b9b Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 19:46:27 +0200 Subject: [PATCH 08/22] Added About and Credits --- AUTHORS | 2 ++ meson.build | 2 +- src/ui-menu.c | 11 +++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 689bfa2..90d0f16 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,3 +9,5 @@ Art: * Samuel Navas Portillo * Daniel Pavón Pérez * Juan Jesús Pérez Luna + +* Updated by jaime: https://github.com/j4imefoo/TBO diff --git a/meson.build b/meson.build index 8d54dd4..596232e 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'tbo', 'c', - version: '1.0', + version: '2.0', default_options: ['c_std=gnu11'] ) diff --git a/src/ui-menu.c b/src/ui-menu.c index d9c3788..c6740ee 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -237,8 +237,15 @@ open_tutorial (TboWindow *tbo) static void show_about (TboWindow *tbo) { - const gchar *authors[] = {"danigm ", NULL}; + const gchar *authors[] = { + "danigm ", + "Jaime (2026) https://github.com/j4imefoo/TBO", + NULL + }; const gchar *artists[] = {"danigm ", + "", + "Updated by Jaime 2026", + "Jaime: https://github.com/j4imefoo/TBO", "", "Arcadia http://www.arcadiaproject.org :", "Samuel Navas Portillo", @@ -261,7 +268,7 @@ show_about (TboWindow *tbo) "logo-icon-name", "tbo", "authors", authors, "artists", artists, - "website", "http://trac.danigm.net/tbo", + "website", "https://github.com/danigm/TBO/", "translator-credits", _("translator-credits"), NULL); } From 01f225acd1c57eff326e361a5096c4e8e431df3f Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 20:09:19 +0200 Subject: [PATCH 09/22] Repaired about, credits and tutorial. Changed doodle and buble behaviour --- data/tut.tbo | 68 ++++++++++++++++++++++++------------------------ src/dnd.c | 7 +++++ src/tbo-window.c | 56 +++++++++++++++++++++++++++++++++++++++ src/ui-menu.c | 11 +++----- 4 files changed, 100 insertions(+), 42 deletions(-) diff --git a/data/tut.tbo b/data/tut.tbo index 15ddf31..f08d5fe 100644 --- a/data/tut.tbo +++ b/data/tut.tbo @@ -1,18 +1,18 @@ - - + + - + Tutorial - - + + - + Bienvenido al tutorial de TBO. Te voy a ir explicando cómo utilizar este fantabuloso editor de cómics. En este editor existen tres cosas básicas que en conjunción @@ -25,10 +25,10 @@ que pueden ser dibujos, globos, texto, etc. - - + + - + Cuando ejecutas TBO se te abrirá un documento con una página y un tamaño determinado. Si quieres otro tamaño puedes pulsar Archivo->Nuevo, y podrás elegir el tamaño del nuevo cómic. @@ -36,17 +36,17 @@ y podrás elegir el tamaño del nuevo cómic. Con los botones de la barra de herramientas puedes añadir páginas, borrar páginas y moverte por estas. - + Prueba a añadir una página y luego a borrarla - - + + - + Ahora que ya dominas las páginas vamos a algo más complejo, las viñetas. Las viñetas solo pueden dibujarse en el modo página, que es el @@ -61,10 +61,10 @@ el menú Editar->Eliminar. - - + + - + Para seleccionar se utiliza la "herramienta de selección" (s) una flecha. Una vez seleccionada esta herramienta puedes pulsar sobre las viñetas para seleccionarlas. @@ -81,10 +81,10 @@ opciones donde podrás modificar tu viñeta. - - + + - + Ahora que sabes todo acerca de las vieñetas y el modo de edición de página vamos a pasar al modo de edición de viñeta. @@ -101,10 +101,10 @@ Para volver al modo página, pulsa escape. - - + + - + Para añadir un monigote, pulsa sobre la herramienta, y en el panel lateral verás una lista de monigotes disponibles. Para añadirlo a tu escena tan solo tienes que arrastrarlo hasta la posición deseada. @@ -118,7 +118,7 @@ En el menú editar puedes ver las opciones que te ofrece TBO para modificar la escena, una vez seleccionado un monigote, junto con los atajos de teclado correspondientes. - + Truco: utiliza las teclas ">" y "<" para escalar proporcionalmente, mientras tienes un objeto seleccionado. @@ -126,7 +126,7 @@ objeto seleccionado. - + @@ -137,7 +137,7 @@ objeto seleccionado. - + Los "globos" son exactamente lo mismo que los monigotes. Arrastra el globo deseado a la zona correspondiente y modificalo como creas oportuno. @@ -145,12 +145,12 @@ como creas oportuno. - - + + - + - + La herramienta de texto es un poco diferente a las anteriores. Una vez seleccionada, pulsa sobre una zona del dibujo para añadir un objeto de texto. Al añadir este objeto, en la parte derecha verás que puedes seleccionar la @@ -164,13 +164,13 @@ Si quieres editar algún texto, debes coger la herramienta de texto y pinchar sobre el texto que quieras cambiar, en la caja de texto debe salir el texto actual del objeto en cuestión. - + - - + + Para añadir una imagen externa está la herramienta de "imagen". Pulsando sobre esta herramienta podrás añadir un fichero .png que será como un objeto más. @@ -186,8 +186,8 @@ el png y no salga correctamente. - - + + Fin del tutorial A partir de aquí tu imaginación es la encargada. diff --git a/src/dnd.c b/src/dnd.c index 99dee4d..24b3ddb 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -42,6 +42,13 @@ static void select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset) { TboToolSelector *selector; + TboToolBase *current_tool = tbo->toolbar->selected_tool; + + if (current_tool == tbo->toolbar->tools[TBO_TOOLBAR_DOODLE] || + current_tool == tbo->toolbar->tools[TBO_TOOLBAR_BUBBLE]) + { + return; + } tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); diff --git a/src/tbo-window.c b/src/tbo-window.c index 9ff517f..a225793 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -21,6 +21,7 @@ #include #include #include +#include #include "tbo-types.h" #include "tbo-window.h" #include "comic.h" @@ -120,6 +121,51 @@ set_window_path (gchar **slot, const gchar *path) *slot = path != NULL ? g_strdup (path) : NULL; } +static gchar * +get_state_file_path (void) +{ + gchar *dir = g_build_filename (g_get_user_config_dir (), "tbo", NULL); + gchar *path; + + g_mkdir_with_parents (dir, 0755); + path = g_build_filename (dir, "state.ini", NULL); + g_free (dir); + return path; +} + +static gchar * +load_persisted_path (const gchar *key) +{ + GKeyFile *kf = g_key_file_new (); + gchar *state_file = get_state_file_path (); + gchar *value = NULL; + + if (g_key_file_load_from_file (kf, state_file, G_KEY_FILE_NONE, NULL)) + value = g_key_file_get_string (kf, "paths", key, NULL); + + g_key_file_unref (kf); + g_free (state_file); + return value; +} + +static void +store_persisted_path (const gchar *key, const gchar *value) +{ + GKeyFile *kf = g_key_file_new (); + gchar *state_file = get_state_file_path (); + gchar *content; + gsize len; + + g_key_file_load_from_file (kf, state_file, G_KEY_FILE_NONE, NULL); + g_key_file_set_string (kf, "paths", key, value); + content = g_key_file_to_data (kf, &len, NULL); + g_file_set_contents (state_file, content, len, NULL); + + g_free (content); + g_key_file_unref (kf); + g_free (state_file); +} + static gchar * get_dirname_or_home (const gchar *path) { @@ -355,17 +401,24 @@ void tbo_window_set_browse_path (TboWindow *tbo, const gchar *path) { set_window_path (&tbo->browse_path, path); + if (path != NULL) + store_persisted_path ("browse_path", path); } void tbo_window_set_export_path (TboWindow *tbo, const gchar *path) { set_window_path (&tbo->export_path, path); + if (path != NULL) + store_persisted_path ("export_path", path); } gchar * tbo_window_get_open_dir (TboWindow *tbo) { + if (tbo->browse_path == NULL) + tbo->browse_path = load_persisted_path ("browse_path"); + if (tbo->browse_path != NULL) return get_dirname_or_home (tbo->browse_path); @@ -375,6 +428,9 @@ tbo_window_get_open_dir (TboWindow *tbo) gchar * tbo_window_get_export_dir (TboWindow *tbo) { + if (tbo->export_path == NULL) + tbo->export_path = load_persisted_path ("export_path"); + if (tbo->export_path != NULL) return g_path_get_dirname (tbo->export_path); diff --git a/src/ui-menu.c b/src/ui-menu.c index c6740ee..4a3ddae 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -237,15 +237,8 @@ open_tutorial (TboWindow *tbo) static void show_about (TboWindow *tbo) { - const gchar *authors[] = { - "danigm ", - "Jaime (2026) https://github.com/j4imefoo/TBO", - NULL - }; + const gchar *authors[] = {"danigm ", NULL}; const gchar *artists[] = {"danigm ", - "", - "Updated by Jaime 2026", - "Jaime: https://github.com/j4imefoo/TBO", "", "Arcadia http://www.arcadiaproject.org :", "Samuel Navas Portillo", @@ -260,6 +253,8 @@ show_about (TboWindow *tbo) "", "Facilware:", "VIcente Pons ", + "", + "Actualizado por Jaime 2026 - https://github.com/j4imefoo/TBO", NULL}; gtk_show_about_dialog (GTK_WINDOW (tbo->window), From 6f3ca03bffb314c01a50d0c05f9a5ba51815f461 Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 20:20:48 +0200 Subject: [PATCH 10/22] Credits --- src/ui-menu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui-menu.c b/src/ui-menu.c index 4a3ddae..51fa047 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -239,6 +239,8 @@ show_about (TboWindow *tbo) { const gchar *authors[] = {"danigm ", NULL}; const gchar *artists[] = {"danigm ", + "", + "Actualizado por Jaime, 2026, https://github.com/j4imefoo/TBO", "", "Arcadia http://www.arcadiaproject.org :", "Samuel Navas Portillo", @@ -253,8 +255,6 @@ show_about (TboWindow *tbo) "", "Facilware:", "VIcente Pons ", - "", - "Actualizado por Jaime 2026 - https://github.com/j4imefoo/TBO", NULL}; gtk_show_about_dialog (GTK_WINDOW (tbo->window), From 89f499f5483dfead7d758c243bab850bd088f55c Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 20:31:46 +0200 Subject: [PATCH 11/22] Credits --- src/ui-menu.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ui-menu.c b/src/ui-menu.c index 51fa047..6b69eb9 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -237,11 +237,13 @@ open_tutorial (TboWindow *tbo) static void show_about (TboWindow *tbo) { - const gchar *authors[] = {"danigm ", NULL}; + const gchar *authors[] = { + "danigm ", + "Actualizado por Jaime, 2026, https://github.com/j4imefoo/TBO", + NULL + }; const gchar *artists[] = {"danigm ", - "", - "Actualizado por Jaime, 2026, https://github.com/j4imefoo/TBO", - "", + "", "Arcadia http://www.arcadiaproject.org :", "Samuel Navas Portillo", "Daniel Pavón Pérez", From e4ab2c31f1bcc09908f5a50d5ff721b5771de0be Mon Sep 17 00:00:00 2001 From: jaime Date: Fri, 17 Apr 2026 20:50:44 +0200 Subject: [PATCH 12/22] Credits --- src/ui-menu.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui-menu.c b/src/ui-menu.c index 6b69eb9..f158875 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -239,7 +239,8 @@ show_about (TboWindow *tbo) { const gchar *authors[] = { "danigm ", - "Actualizado por Jaime, 2026, https://github.com/j4imefoo/TBO", + "", + "Actualizado por Jaime, 2026", NULL }; const gchar *artists[] = {"danigm ", @@ -249,7 +250,7 @@ show_about (TboWindow *tbo) "Daniel Pavón Pérez", "Juan Jesús Pérez Luna", "", - "Zapatero & Rajoy:", + "Zapatero y Rajoy:", "Alfonso de Cala", "", "South park style:", From 069e6025c8534c257f1128fdabf297d822205271 Mon Sep 17 00:00:00 2001 From: jaime Date: Sun, 19 Apr 2026 10:02:13 +0200 Subject: [PATCH 13/22] debian update --- debian/changelog | 2 +- debian/control | 3 ++- debian/tests/control | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index babaf21..2d7ffac 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -tbo (1.0-1) unstable; urgency=medium +tbo (2.0) unstable; urgency=medium * Repackage project on top of Meson and GTK4. diff --git a/debian/control b/debian/control index 24e7a21..93d21b0 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,8 @@ Build-Depends: libcairo2-dev, librsvg2-dev, desktop-file-utils, - xvfb + xvfb, + xauth Standards-Version: 4.7.0 Rules-Requires-Root: no Homepage: https://github.com/j4imefoo/TBO diff --git a/debian/tests/control b/debian/tests/control index 14557b0..a887aa7 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,3 +1,3 @@ Tests: smoke -Depends: @, xvfb +Depends: @, xvfb, xauth Restrictions: superficial From ce84e3c982b427ae6bfd07b326ce36aedef6e943 Mon Sep 17 00:00:00 2001 From: jaime Date: Mon, 20 Apr 2026 22:03:31 +0200 Subject: [PATCH 14/22] Refactor the document model, harden undo/redo, and improve UI status and localization --- NEXT-STEPS.md | 92 ++++ meson.build | 355 ++++++++++++++ po/es.po | 57 +++ src/comic-load.c | 646 +++++++++++++++++------- src/comic-load.h | 1 + src/comic-new-dialog.c | 43 +- src/comic-open-dialog.c | 2 +- src/comic-saveas-dialog.c | 4 +- src/comic.c | 196 ++++++-- src/comic.h | 17 + src/dnd.c | 5 +- src/export.c | 329 +++++++------ src/export.h | 5 + src/frame.c | 378 ++++++++++---- src/frame.h | 30 ++ src/page.c | 180 +++++-- src/page.h | 15 +- src/tbo-drawing.c | 110 ++++- src/tbo-drawing.h | 8 + src/tbo-file-dialog.c | 56 ++- src/tbo-object-base.c | 5 +- src/tbo-object-pixmap.c | 32 +- src/tbo-object-svg.c | 32 +- src/tbo-object-text.c | 53 +- src/tbo-tool-frame.c | 43 +- src/tbo-tool-frame.h | 1 + src/tbo-tool-selector.c | 625 ++++++++++++++++++++--- src/tbo-tool-selector.h | 58 +++ src/tbo-tool-text.c | 251 +++++++++- src/tbo-tool-text.h | 7 + src/tbo-toolbar.c | 22 +- src/tbo-tooltip.c | 120 +++-- src/tbo-tooltip.h | 9 +- src/tbo-types.h | 30 +- src/tbo-undo.c | 710 ++++++++++++++++++++++++++- src/tbo-undo.h | 36 +- src/tbo-utils.c | 151 ++++++ src/tbo-utils.h | 12 + src/tbo-widget.c | 52 ++ src/tbo-widget.h | 12 + src/tbo-window.c | 299 +++++++++-- src/tbo-window.h | 3 +- src/tbo.c | 9 +- src/ui-menu.c | 81 ++- tests/ascii_locale_roundtrip_check.c | 121 +++++ tests/asset_bounds_check.c | 16 +- tests/clone_dirty_check.c | 48 ++ tests/comic_gobject_lifetime_check.c | 38 ++ tests/create_undo_check.c | 71 +++ tests/dirty_state_check.c | 90 ++++ tests/document_state_reset_check.c | 120 +++++ tests/enter_frame_key_check.c | 78 +++ tests/export_formats_check.c | 104 ++++ tests/export_result_check.c | 56 +++ tests/frame_count_status_check.c | 44 ++ tests/frame_gobject_lifetime_check.c | 100 ++++ tests/i18n_check.c | 38 ++ tests/invalid_tbo_check.c | 52 ++ tests/invalid_tbo_variants_check.c | 68 +++ tests/legacy_decimal_load_check.c | 60 +++ tests/load_render_check.c | 8 +- tests/model_current_state_check.c | 81 +++ tests/page_gobject_lifetime_check.c | 45 ++ tests/page_status_check.c | 38 ++ tests/page_undo_check.c | 48 ++ tests/resize_rotate_undo_check.c | 96 ++++ tests/status_hierarchy_check.c | 62 +++ tests/text_undo_check.c | 76 +++ tests/toolbar_save_button_check.c | 60 +++ tests/tooltip_scope_check.c | 55 +++ tests/xml_roundtrip_check.c | 122 +++++ 71 files changed, 6106 insertions(+), 871 deletions(-) create mode 100644 NEXT-STEPS.md create mode 100644 tests/ascii_locale_roundtrip_check.c create mode 100644 tests/clone_dirty_check.c create mode 100644 tests/comic_gobject_lifetime_check.c create mode 100644 tests/create_undo_check.c create mode 100644 tests/dirty_state_check.c create mode 100644 tests/document_state_reset_check.c create mode 100644 tests/enter_frame_key_check.c create mode 100644 tests/export_formats_check.c create mode 100644 tests/export_result_check.c create mode 100644 tests/frame_count_status_check.c create mode 100644 tests/frame_gobject_lifetime_check.c create mode 100644 tests/i18n_check.c create mode 100644 tests/invalid_tbo_check.c create mode 100644 tests/invalid_tbo_variants_check.c create mode 100644 tests/legacy_decimal_load_check.c create mode 100644 tests/model_current_state_check.c create mode 100644 tests/page_gobject_lifetime_check.c create mode 100644 tests/page_status_check.c create mode 100644 tests/page_undo_check.c create mode 100644 tests/resize_rotate_undo_check.c create mode 100644 tests/status_hierarchy_check.c create mode 100644 tests/text_undo_check.c create mode 100644 tests/toolbar_save_button_check.c create mode 100644 tests/tooltip_scope_check.c create mode 100644 tests/xml_roundtrip_check.c diff --git a/NEXT-STEPS.md b/NEXT-STEPS.md new file mode 100644 index 0000000..5860b73 --- /dev/null +++ b/NEXT-STEPS.md @@ -0,0 +1,92 @@ +# Next Steps + +Actualizado: 2026-04-20 + +## Estado actual + +- Refactor grande de modelo/ownership: hecho +- Calidad: hecha +- Patrones detectados: hechos +- Cobertura final: hecha +- Bugs extra resueltos durante la sesión: hechos +- Suite actual: `29/29` tests OK + +## Cambios importantes ya consolidados + +- `Comic`, `Page` y `Frame` ya son `GObject` +- `undo/redo` cubre ya altas/borrados y la mayoría de mutaciones principales +- El botón principal del toolbar es `Save` +- La barra baja ahora expresa mejor la jerarquía y es traducible +- Las coordenadas del panel inferior se eliminaron por rendimiento + +## Lista global pendiente, en orden recomendado + +1. Plantillas al crear cómic +2. Autosave + recientes + reabrir último proyecto +3. Inspector contextual persistente + jerarquía `página -> frame -> objeto` +4. Miniaturas de páginas +5. Reordenación de páginas por drag&drop + duplicar página +6. Búsqueda instantánea y mejor clasificación de assets +7. Favoritos y recientes de assets +8. Presets de bocadillos y estilos de texto +9. Exportación guiada con preview + rango de páginas +10. Exportar página actual o selección +11. Guía integrada de atajos +12. Modo presentación / visor +13. Identidad visual y consistencia estética +14. Packaging/metainfo + +## Siguiente tarea a retomar + +### 1. Plantillas al crear cómic + +Objetivo: + +- Añadir presets útiles en `New Comic` +- Crear el documento inicial con tamaño y layout base acordes a la plantilla + +Plantillas previstas: + +- Storyboard `16:9` +- Tira cómica +- `A4` +- Presentación + +Punto de entrada principal: + +- `src/comic-new-dialog.c` + +Archivos previsibles a tocar: + +- `src/comic-new-dialog.c` +- `src/tbo-window.c` +- `src/tbo-window.h` +- quizá un helper nuevo pequeño si compensa, pero mejor cambio mínimo + +Implementación recomendada: + +1. Añadir selector de plantilla en el diálogo `New Comic` +2. Al cambiar plantilla, autocompletar `width/height` +3. Al aceptar, crear el cómic con el tamaño elegido +4. Aplicar layout inicial solo si la plantilla no es "vacía" implícita +5. Añadir test de regresión para al menos una plantilla con layout inicial + +Notas de diseño: + +- Mantener simple el diálogo +- No crear aún un sistema grande de presets persistentes +- Empezar con layouts estáticos y claros + +## Riesgos / cosas a vigilar al retomar + +- `undo/redo` ahora cubre mucho más, pero si una feature nueva crea contenido automáticamente debe decidir si entra o no en el stack +- Si una plantilla crea varios frames iniciales, conviene decidir si se considera estado base del documento o una acción deshacible +- La UI actual usa `GtkNotebook`; las miniaturas de páginas vendrán después, no mezclar ambas tareas + +## Punto de comprobación rápido al volver + +Ejecutar: + +```bash +meson compile -C build && meson test -C build --no-rebuild --print-errorlogs --num-processes 1 +``` diff --git a/meson.build b/meson.build index 596232e..f491a36 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,206 @@ asset_bounds_check = executable( install: false ) +document_state_reset_check = executable( + 'document-state-reset-check', + common_sources + files('tests/document_state_reset_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +model_current_state_check = executable( + 'model-current-state-check', + common_sources + files('tests/model_current_state_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_gobject_lifetime_check = executable( + 'frame-gobject-lifetime-check', + common_sources + files('tests/frame_gobject_lifetime_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +page_gobject_lifetime_check = executable( + 'page-gobject-lifetime-check', + common_sources + files('tests/page_gobject_lifetime_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +comic_gobject_lifetime_check = executable( + 'comic-gobject-lifetime-check', + common_sources + files('tests/comic_gobject_lifetime_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +ascii_locale_roundtrip_check = executable( + 'ascii-locale-roundtrip-check', + common_sources + files('tests/ascii_locale_roundtrip_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +legacy_decimal_load_check = executable( + 'legacy-decimal-load-check', + common_sources + files('tests/legacy_decimal_load_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +dirty_state_check = executable( + 'dirty-state-check', + common_sources + files('tests/dirty_state_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +resize_rotate_undo_check = executable( + 'resize-rotate-undo-check', + common_sources + files('tests/resize_rotate_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +xml_roundtrip_check = executable( + 'xml-roundtrip-check', + common_sources + files('tests/xml_roundtrip_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +invalid_tbo_check = executable( + 'invalid-tbo-check', + common_sources + files('tests/invalid_tbo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +i18n_check = executable( + 'i18n-check', + common_sources + files('tests/i18n_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_result_check = executable( + 'export-result-check', + common_sources + files('tests/export_result_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +page_status_check = executable( + 'page-status-check', + common_sources + files('tests/page_status_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +clone_dirty_check = executable( + 'clone-dirty-check', + common_sources + files('tests/clone_dirty_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +enter_frame_key_check = executable( + 'enter-frame-key-check', + common_sources + files('tests/enter_frame_key_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_count_status_check = executable( + 'frame-count-status-check', + common_sources + files('tests/frame_count_status_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_formats_check = executable( + 'export-formats-check', + common_sources + files('tests/export_formats_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +invalid_tbo_variants_check = executable( + 'invalid-tbo-variants-check', + common_sources + files('tests/invalid_tbo_variants_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +tooltip_scope_check = executable( + 'tooltip-scope-check', + common_sources + files('tests/tooltip_scope_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +toolbar_save_button_check = executable( + 'toolbar-save-button-check', + common_sources + files('tests/toolbar_save_button_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +status_hierarchy_check = executable( + 'status-hierarchy-check', + common_sources + files('tests/status_hierarchy_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +create_undo_check = executable( + 'create-undo-check', + common_sources + files('tests/create_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +page_undo_check = executable( + 'page-undo-check', + common_sources + files('tests/page_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +text_undo_check = executable( + 'text-undo-check', + common_sources + files('tests/text_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + test( 'load-render-check', load_render_check, @@ -139,6 +339,161 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'document-state-reset-check', + document_state_reset_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'model-current-state-check', + model_current_state_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-gobject-lifetime-check', + frame_gobject_lifetime_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'page-gobject-lifetime-check', + page_gobject_lifetime_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'comic-gobject-lifetime-check', + comic_gobject_lifetime_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'ascii-locale-roundtrip-check', + ascii_locale_roundtrip_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'legacy-decimal-load-check', + legacy_decimal_load_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'dirty-state-check', + dirty_state_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'resize-rotate-undo-check', + resize_rotate_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'xml-roundtrip-check', + xml_roundtrip_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'invalid-tbo-check', + invalid_tbo_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'i18n-check', + i18n_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-result-check', + export_result_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'page-status-check', + page_status_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'clone-dirty-check', + clone_dirty_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'enter-frame-key-check', + enter_frame_key_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-count-status-check', + frame_count_status_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-formats-check', + export_formats_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'invalid-tbo-variants-check', + invalid_tbo_variants_check, + args: [join_paths(meson.project_source_root(), 'data', 'tut.tbo')], + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'tooltip-scope-check', + tooltip_scope_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'toolbar-save-button-check', + toolbar_save_button_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'status-hierarchy-check', + status_hierarchy_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'create-undo-check', + create_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'page-undo-check', + page_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'text-undo-check', + text_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + install_data( 'data/tutorial.pdf', 'data/tut.tbo', diff --git a/po/es.po b/po/es.po index 77b89c6..431e5fc 100644 --- a/po/es.po +++ b/po/es.po @@ -72,6 +72,10 @@ msgstr "Cómic nuevo" msgid "Save comic as" msgstr "Guardar como" +#: ../src/tbo-toolbar.c:250 +msgid "Save comic" +msgstr "Guardar cómic" + #: ../src/tbo-toolbar.c:260 msgid "Undo" msgstr "Deshacer" @@ -304,6 +308,59 @@ msgstr "Sin título" msgid "page: %d of %d [ %5d,%5d ] | frames: %d" msgstr "página: %d de %d [ %5d,%5d ] | viñetas: %d" +#: ../src/tbo-window.c:285 +#, c-format +msgid "Page %d of %d" +msgstr "Página %d de %d" + +#: ../src/tbo-window.c:285 +#, c-format +msgid "Frames: %d" +msgstr "Viñetas: %d" + +#: ../src/tbo-window.c:285 +#, c-format +msgid "Editing frame %d" +msgstr "Editando viñeta %d" + +#: ../src/tbo-window.c:285 +msgid "Editing frame" +msgstr "Editando viñeta" + +#: ../src/tbo-window.c:285 +#, c-format +msgid "Frame %d selected" +msgstr "Viñeta %d seleccionada" + +#: ../src/tbo-window.c:285 +msgid "Frame selected" +msgstr "Viñeta seleccionada" + +#: ../src/tbo-window.c:285 +#, c-format +msgid "Object: %s" +msgstr "Objeto: %s" + +#: ../src/tbo-window.c:285 +msgid "Object" +msgstr "Objeto" + +#: ../src/tbo-window.c:285 +msgid "Group" +msgstr "Grupo" + +#: ../src/tbo-window.c:285 +msgid "SVG image" +msgstr "Imagen SVG" + +#: ../src/tbo-window.c:285 +msgid "Enter: frame" +msgstr "Intro: entrar en viñeta" + +#: ../src/tbo-window.c:285 +msgid "Esc: back to page" +msgstr "Esc: volver a la página" + #: ../src/ui-menu.c:232 msgid "TBO comic editor" msgstr "Editor de cómic TBO" diff --git a/src/comic-load.c b/src/comic-load.c index f29b0ac..1f736fe 100644 --- a/src/comic-load.c +++ b/src/comic-load.c @@ -34,31 +34,43 @@ #include "tbo-object-pixmap.h" #include "tbo-utils.h" -char *TITLE; - -Comic *COMIC = NULL; -Page *CURRENT_PAGE; -Frame *CURRENT_FRAME; -TboObjectText *CURRENT_TEXT = NULL; - -struct attr { - char *name; - char *format; - void *pointer; -}; +typedef enum +{ + ATTR_INT, + ATTR_DOUBLE, +} TboLoadAttrType; -static gchar * -get_attr_string (const gchar **attribute_names, +typedef struct +{ + const gchar *name; + TboLoadAttrType type; + gpointer pointer; + gboolean required; + gboolean seen; +} TboLoadAttr; + +typedef struct +{ + gchar *title; + Comic *comic; + Page *current_page; + Frame *current_frame; + TboObjectText *current_text; + GString *current_text_buffer; +} TboLoadContext; + +static const gchar * +find_attr_value (const gchar **attribute_names, const gchar **attribute_values, const gchar *name) { const gchar **name_cursor = attribute_names; const gchar **value_cursor = attribute_values; - while (*name_cursor) + while (*name_cursor != NULL) { if (strcmp (*name_cursor, name) == 0) - return g_strdup (*value_cursor); + return *value_cursor; name_cursor++; value_cursor++; } @@ -66,292 +78,552 @@ get_attr_string (const gchar **attribute_names, return NULL; } -void -parse_attrs (struct attr attrs[], - int attrs_size, +static gchar * +dup_required_attr_string (const gchar **attribute_names, + const gchar **attribute_values, + const gchar *element_name, + const gchar *attr_name, + GError **error) +{ + const gchar *value = find_attr_value (attribute_names, attribute_values, attr_name); + + if (value == NULL || *value == '\0') + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element '%s' is missing required attribute '%s'", + element_name, + attr_name); + return NULL; + } + + return g_strdup (value); +} + +static gboolean +parse_attrs (const gchar *element_name, + TboLoadAttr attrs[], + gsize attrs_size, const gchar **attribute_names, - const gchar **attribute_values) + const gchar **attribute_values, + GError **error) { - int i; const gchar **name_cursor = attribute_names; const gchar **value_cursor = attribute_values; + gsize i; + + for (i = 0; i < attrs_size; i++) + attrs[i].seen = FALSE; - while (*name_cursor) { - for (i=0; icomic != NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'tbo' appears more than once"); + return FALSE; + } + + if (!parse_attrs ("tbo", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) + return FALSE; - COMIC = tbo_comic_new (TITLE, width, height); - tbo_comic_del_page (COMIC, 0); + if (width <= 0 || height <= 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Comic size must be positive"); + return FALSE; + } + + context->comic = tbo_comic_new (context->title, width, height); + tbo_comic_del_page (context->comic, 0); + return TRUE; } -void -create_tbo_frame (const gchar **attribute_names, const gchar **attribute_values) +static gboolean +create_tbo_page (TboLoadContext *context, GError **error) { - int x=0, y=0; - int width=0, height=0; - int border=1; - float r=0.0, g=0.0, b=0.0; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"border", "%d", &border}, - {"r", "%f", &r}, - {"g", "%f", &g}, - {"b", "%f", &b}, + if (context->comic == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'page' must be inside 'tbo'"); + return FALSE; + } + + context->current_page = tbo_comic_new_page (context->comic); + context->current_frame = NULL; + return TRUE; +} + +static gboolean +create_tbo_frame (TboLoadContext *context, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + gint x = 0; + gint y = 0; + gint width = 0; + gint height = 0; + gint border = 1; + gdouble r = 0.0; + gdouble g = 0.0; + gdouble b = 0.0; + TboLoadAttr attrs[] = { + {"x", ATTR_INT, &x, TRUE, FALSE}, + {"y", ATTR_INT, &y, TRUE, FALSE}, + {"width", ATTR_INT, &width, TRUE, FALSE}, + {"height", ATTR_INT, &height, TRUE, FALSE}, + {"border", ATTR_INT, &border, FALSE, FALSE}, + {"r", ATTR_DOUBLE, &r, FALSE, FALSE}, + {"g", ATTR_DOUBLE, &g, FALSE, FALSE}, + {"b", ATTR_DOUBLE, &b, FALSE, FALSE}, }; - parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); + if (context->current_page == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'frame' must be inside 'page'"); + return FALSE; + } + + if (!parse_attrs ("frame", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) + return FALSE; + + if (width <= 0 || height <= 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Frame size must be positive"); + return FALSE; + } - CURRENT_FRAME = tbo_page_new_frame (CURRENT_PAGE, x, y, width, height); - CURRENT_FRAME->border = border; - CURRENT_FRAME->color->r = r; - CURRENT_FRAME->color->g = g; - CURRENT_FRAME->color->b = b; + context->current_frame = tbo_page_new_frame (context->current_page, x, y, width, height); + tbo_frame_set_border (context->current_frame, border != 0); + tbo_frame_set_color_rgb (context->current_frame, r, g, b); + return TRUE; } -void -create_tbo_piximage (const gchar **attribute_names, const gchar **attribute_values) +static gboolean +create_tbo_piximage (TboLoadContext *context, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) { TboObjectPixmap *pix; TboObjectBase *obj; - int x=0, y=0; - int width=0, height=0; - float angle=0.0; - int flipv=0, fliph=0; - gchar *path = NULL; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"flipv", "%d", &flipv}, - {"fliph", "%d", &fliph}, - {"angle", "%f", &angle}, + gint x = 0; + gint y = 0; + gint width = 0; + gint height = 0; + gint flipv = 0; + gint fliph = 0; + gdouble angle = 0.0; + gchar *path; + TboLoadAttr attrs[] = { + {"x", ATTR_INT, &x, TRUE, FALSE}, + {"y", ATTR_INT, &y, TRUE, FALSE}, + {"width", ATTR_INT, &width, TRUE, FALSE}, + {"height", ATTR_INT, &height, TRUE, FALSE}, + {"flipv", ATTR_INT, &flipv, FALSE, FALSE}, + {"fliph", ATTR_INT, &fliph, FALSE, FALSE}, + {"angle", ATTR_DOUBLE, &angle, FALSE, FALSE}, }; - parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); - path = get_attr_string (attribute_names, attribute_values, "path"); + if (context->current_frame == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'piximage' must be inside 'frame'"); + return FALSE; + } + + if (!parse_attrs ("piximage", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) + return FALSE; + + path = dup_required_attr_string (attribute_names, attribute_values, "piximage", "path", error); + if (path == NULL) + return FALSE; pix = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new_with_params (x, y, width, height, path)); obj = TBO_OBJECT_BASE (pix); obj->angle = angle; obj->flipv = flipv; obj->fliph = fliph; - tbo_frame_add_obj (CURRENT_FRAME, obj); + tbo_frame_add_obj (context->current_frame, obj); g_free (path); + return TRUE; } -void -create_tbo_svgimage (const gchar **attribute_names, const gchar **attribute_values) +static gboolean +create_tbo_svgimage (TboLoadContext *context, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) { TboObjectSvg *svg; TboObjectBase *obj; - int x=0, y=0; - int width=0, height=0; - float angle=0.0; - int flipv=0, fliph=0; - gchar *path = NULL; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"flipv", "%d", &flipv}, - {"fliph", "%d", &fliph}, - {"angle", "%f", &angle}, + gint x = 0; + gint y = 0; + gint width = 0; + gint height = 0; + gint flipv = 0; + gint fliph = 0; + gdouble angle = 0.0; + gchar *path; + TboLoadAttr attrs[] = { + {"x", ATTR_INT, &x, TRUE, FALSE}, + {"y", ATTR_INT, &y, TRUE, FALSE}, + {"width", ATTR_INT, &width, TRUE, FALSE}, + {"height", ATTR_INT, &height, TRUE, FALSE}, + {"flipv", ATTR_INT, &flipv, FALSE, FALSE}, + {"fliph", ATTR_INT, &fliph, FALSE, FALSE}, + {"angle", ATTR_DOUBLE, &angle, FALSE, FALSE}, }; - parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); - path = get_attr_string (attribute_names, attribute_values, "path"); + if (context->current_frame == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'svgimage' must be inside 'frame'"); + return FALSE; + } + + if (!parse_attrs ("svgimage", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) + return FALSE; + + path = dup_required_attr_string (attribute_names, attribute_values, "svgimage", "path", error); + if (path == NULL) + return FALSE; svg = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (x, y, width, height, path)); obj = TBO_OBJECT_BASE (svg); obj->angle = angle; obj->flipv = flipv; obj->fliph = fliph; - tbo_frame_add_obj (CURRENT_FRAME, obj); + tbo_frame_add_obj (context->current_frame, obj); g_free (path); + return TRUE; } -void -create_tbo_text (const gchar **attribute_names, const gchar **attribute_values) +static gboolean +create_tbo_text (TboLoadContext *context, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) { TboObjectText *textobj; TboObjectBase *obj; - GdkRGBA color; - int x=0, y=0; - int width=0, height=0; - float angle=0.0; - int flipv=0, fliph=0; - gchar *font = NULL; - float r=0.0, g=0.0, b=0.0; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"flipv", "%d", &flipv}, - {"fliph", "%d", &fliph}, - {"angle", "%f", &angle}, - {"r", "%f", &r}, - {"g", "%f", &g}, - {"b", "%f", &b}, + GdkRGBA color = { 0, 0, 0, 1 }; + gint x = 0; + gint y = 0; + gint width = 0; + gint height = 0; + gint flipv = 0; + gint fliph = 0; + gdouble angle = 0.0; + gdouble r = 0.0; + gdouble g = 0.0; + gdouble b = 0.0; + gchar *font; + TboLoadAttr attrs[] = { + {"x", ATTR_INT, &x, TRUE, FALSE}, + {"y", ATTR_INT, &y, TRUE, FALSE}, + {"width", ATTR_INT, &width, TRUE, FALSE}, + {"height", ATTR_INT, &height, TRUE, FALSE}, + {"flipv", ATTR_INT, &flipv, FALSE, FALSE}, + {"fliph", ATTR_INT, &fliph, FALSE, FALSE}, + {"angle", ATTR_DOUBLE, &angle, FALSE, FALSE}, + {"r", ATTR_DOUBLE, &r, FALSE, FALSE}, + {"g", ATTR_DOUBLE, &g, FALSE, FALSE}, + {"b", ATTR_DOUBLE, &b, FALSE, FALSE}, }; - parse_attrs (attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values); - font = get_attr_string (attribute_names, attribute_values, "font"); + if (context->current_frame == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'text' must be inside 'frame'"); + return FALSE; + } + + if (!parse_attrs ("text", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) + return FALSE; + + font = dup_required_attr_string (attribute_names, attribute_values, "text", "font", error); + if (font == NULL) + return FALSE; + color.red = r; color.green = g; color.blue = b; - color.alpha = 1.0; - textobj = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, width, height, "text", font, &color)); + + textobj = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, width, height, "", font, &color)); obj = TBO_OBJECT_BASE (textobj); obj->angle = angle; obj->flipv = flipv; obj->fliph = fliph; - CURRENT_TEXT = textobj; - tbo_frame_add_obj (CURRENT_FRAME, obj); + + context->current_text = textobj; + if (context->current_text_buffer != NULL) + g_string_free (context->current_text_buffer, TRUE); + context->current_text_buffer = g_string_new (NULL); + + tbo_frame_add_obj (context->current_frame, obj); g_free (font); + return TRUE; } -/* The handler functions. */ - -void -start_element (GMarkupParseContext *context, - const gchar *element_name, - const gchar **attribute_names, - const gchar **attribute_values, - gpointer user_data, - GError **error) +static void +start_element (GMarkupParseContext *markup_context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) { + TboLoadContext *context = user_data; if (strcmp (element_name, "tbo") == 0) { - create_tbo_comic (attribute_names, attribute_values); + create_tbo_comic (context, attribute_names, attribute_values, error); } else if (strcmp (element_name, "page") == 0) { - CURRENT_PAGE = tbo_comic_new_page (COMIC); + create_tbo_page (context, error); } else if (strcmp (element_name, "frame") == 0) { - create_tbo_frame (attribute_names, attribute_values); + create_tbo_frame (context, attribute_names, attribute_values, error); } else if (strcmp (element_name, "svgimage") == 0) { - create_tbo_svgimage (attribute_names, attribute_values); + create_tbo_svgimage (context, attribute_names, attribute_values, error); } else if (strcmp (element_name, "piximage") == 0) { - create_tbo_piximage (attribute_names, attribute_values); + create_tbo_piximage (context, attribute_names, attribute_values, error); } else if (strcmp (element_name, "text") == 0) { - create_tbo_text (attribute_names, attribute_values); + create_tbo_text (context, attribute_names, attribute_values, error); } + + (void) markup_context; } -void -text (GMarkupParseContext *context, - const gchar *text, - gsize text_len, - gpointer user_data, - GError **error) +static void +text_element (GMarkupParseContext *markup_context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) { - if (CURRENT_TEXT) - { - char *text2 = g_strndup (text, text_len); - - gchar *text3 = g_strstrip (text2); - if (strlen(text3)) { - tbo_object_text_set_text (CURRENT_TEXT, text3); - } else { - tbo_frame_del_obj (CURRENT_FRAME, TBO_OBJECT_BASE (CURRENT_TEXT)); - CURRENT_TEXT = NULL; - } - g_free (text2); - } + TboLoadContext *context = user_data; + + if (context->current_text_buffer != NULL) + g_string_append_len (context->current_text_buffer, text, text_len); + + (void) markup_context; + (void) error; } -void -end_element (GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, - GError **error) +static void +end_element (GMarkupParseContext *markup_context, + const gchar *element_name, + gpointer user_data, + GError **error) { - if (strcmp (element_name, "tbo") == 0) - { - g_free (TITLE); - } - else if (strcmp (element_name, "text") == 0) + TboLoadContext *context = user_data; + + if (strcmp (element_name, "text") == 0) { - CURRENT_TEXT = NULL; + gchar *normalized = NULL; + + if (context->current_text != NULL && context->current_text_buffer != NULL) + { + normalized = g_strdup (context->current_text_buffer->str); + g_strstrip (normalized); + if (*normalized != '\0') + { + tbo_object_text_set_text (context->current_text, normalized); + } + else if (context->current_frame != NULL) + { + tbo_frame_del_obj (context->current_frame, TBO_OBJECT_BASE (context->current_text)); + } + } + + g_free (normalized); + if (context->current_text_buffer != NULL) + { + g_string_free (context->current_text_buffer, TRUE); + context->current_text_buffer = NULL; + } + context->current_text = NULL; } + + (void) markup_context; + (void) error; } static GMarkupParser parser = { start_element, end_element, - text, + text_element, NULL, NULL }; Comic * -tbo_comic_load (char *filename) +tbo_comic_load_with_alerts (char *filename, gboolean show_alerts) { - char *text; - gsize length; - GMarkupParseContext *context = g_markup_parse_context_new ( - &parser, - 0, - NULL, - NULL); - - char base_name[255]; + TboLoadContext context = { 0 }; + GMarkupParseContext *markup_context; + GError *error = NULL; + gchar *file_text = NULL; + gsize length = 0; + gchar base_name[255]; + Comic *comic; + + markup_context = g_markup_parse_context_new (&parser, 0, &context, NULL); + get_base_name (filename, base_name, 255); - TITLE = g_strdup(base_name); + context.title = g_strdup (base_name); - if (g_file_get_contents (filename, &text, &length, NULL) == FALSE) { - tbo_alert_show (NULL, _("Couldn't load file"), NULL); + if (!g_file_get_contents (filename, &file_text, &length, &error)) + { + if (show_alerts) + tbo_alert_show (NULL, _("Couldn't load file"), error != NULL ? error->message : NULL); + g_clear_error (&error); + g_markup_parse_context_free (markup_context); + g_free (context.title); + return NULL; + } + + if (!g_markup_parse_context_parse (markup_context, file_text, length, &error) || + !g_markup_parse_context_end_parse (markup_context, &error)) + { + if (show_alerts) + tbo_alert_show (NULL, _("Couldn't parse file"), error != NULL ? error->message : NULL); + g_clear_error (&error); + if (context.current_text_buffer != NULL) + g_string_free (context.current_text_buffer, TRUE); + if (context.comic != NULL) + tbo_comic_free (context.comic); + g_markup_parse_context_free (markup_context); + g_free (file_text); + g_free (context.title); return NULL; } - if (g_markup_parse_context_parse (context, text, length, NULL) == FALSE) { - tbo_alert_show (NULL, _("Couldn't parse file"), NULL); + if (context.comic == NULL) + { + if (show_alerts) + tbo_alert_show (NULL, _("Couldn't parse file"), _("No comic data found in file")); + g_markup_parse_context_free (markup_context); + g_free (file_text); + g_free (context.title); return NULL; } - g_free(text); - g_markup_parse_context_free (context); + comic = context.comic; + context.comic = NULL; - return COMIC; + if (context.current_text_buffer != NULL) + g_string_free (context.current_text_buffer, TRUE); + g_markup_parse_context_free (markup_context); + g_free (file_text); + g_free (context.title); + + return comic; +} + +Comic * +tbo_comic_load (char *filename) +{ + return tbo_comic_load_with_alerts (filename, TRUE); } diff --git a/src/comic-load.h b/src/comic-load.h index 7e004c0..f4b5836 100644 --- a/src/comic-load.h +++ b/src/comic-load.h @@ -22,6 +22,7 @@ #include "tbo-types.h" +Comic *tbo_comic_load_with_alerts (char *filename, gboolean show_alerts); Comic *tbo_comic_load (char *filename); #endif diff --git a/src/comic-new-dialog.c b/src/comic-new-dialog.c index 5cfd62a..a1ada6e 100644 --- a/src/comic-new-dialog.c +++ b/src/comic-new-dialog.c @@ -24,30 +24,6 @@ #include "tbo-widget.h" #include "tbo-window.h" -struct new_comic_dialog_data { - GMainLoop *loop; - gint response; -}; - -static gboolean -new_comic_close_request_cb (GtkWindow *dialog, struct new_comic_dialog_data *data) -{ - if (data->response == GTK_RESPONSE_NONE) - data->response = GTK_RESPONSE_REJECT; - g_main_loop_quit (data->loop); - return TRUE; -} - -static void -new_comic_button_cb (GtkButton *button, GtkWindow *dialog) -{ - struct new_comic_dialog_data *data = g_object_get_data (G_OBJECT (dialog), "tbo-new-comic-data"); - gint response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tbo-response")); - - data->response = response; - gtk_window_close (dialog); -} - gboolean tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) { @@ -60,7 +36,7 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) GtkWidget *spin_w; GtkWidget *spin_h; GtkAdjustment *adjustment; - struct new_comic_dialog_data data; + TboDialogRunData data; int width; int height; @@ -105,25 +81,20 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) button = gtk_button_new_with_mnemonic (_("_Cancel")); g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_REJECT)); - g_signal_connect (button, "clicked", G_CALLBACK (new_comic_button_cb), dialog); + g_signal_connect (button, "clicked", G_CALLBACK (tbo_dialog_button_cb), dialog); tbo_widget_add_child (actions, button); button = gtk_button_new_with_mnemonic (_("_OK")); gtk_widget_add_css_class (button, "suggested-action"); g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_ACCEPT)); - g_signal_connect (button, "clicked", G_CALLBACK (new_comic_button_cb), dialog); + g_signal_connect (button, "clicked", G_CALLBACK (tbo_dialog_button_cb), dialog); tbo_widget_add_child (actions, button); tbo_widget_add_child (vbox, actions); - data.loop = g_main_loop_new (NULL, FALSE); - data.response = GTK_RESPONSE_NONE; - g_object_set_data (G_OBJECT (dialog), "tbo-new-comic-data", &data); - g_signal_connect (dialog, "close-request", G_CALLBACK (new_comic_close_request_cb), &data); - tbo_widget_show_all (dialog); - gtk_window_present (GTK_WINDOW (dialog)); - - g_main_loop_run (data.loop); + tbo_dialog_run_data_init (&data, GTK_RESPONSE_REJECT); + g_signal_connect (dialog, "close-request", G_CALLBACK (tbo_dialog_close_request_cb), &data); + tbo_dialog_run (GTK_WINDOW (dialog), &data); if (data.response == GTK_RESPONSE_ACCEPT) { @@ -133,7 +104,7 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) } gtk_window_destroy (GTK_WINDOW (dialog)); - g_main_loop_unref (data.loop); + tbo_dialog_run_data_clear (&data); return FALSE; } diff --git a/src/comic-open-dialog.c b/src/comic-open-dialog.c index eed6207..ea4cb27 100644 --- a/src/comic-open-dialog.c +++ b/src/comic-open-dialog.c @@ -37,7 +37,7 @@ tbo_comic_open_dialog (GtkWidget *widget, TboWindow *window) tbo_window_set_browse_path (window, filename); tbo_comic_open (window, filename); tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_update_status (window, 0, 0); + tbo_window_refresh_status (window); g_free (filename); } diff --git a/src/comic-saveas-dialog.c b/src/comic-saveas-dialog.c index 43221d6..a9f9f90 100644 --- a/src/comic-saveas-dialog.c +++ b/src/comic-saveas-dialog.c @@ -41,8 +41,8 @@ tbo_comic_saveas_dialog (GtkWidget *widget, TboWindow *window) gchar *filename; char buffer[260]; - g_strlcpy (buffer, window->comic->title, sizeof (buffer)); - if (!g_str_has_suffix ((window->comic->title), ".tbo")) + g_strlcpy (buffer, tbo_comic_get_title (window->comic), sizeof (buffer)); + if (!g_str_has_suffix (tbo_comic_get_title (window->comic), ".tbo")) strcat (buffer, ".tbo"); filename = tbo_file_dialog_save_project (window, buffer); diff --git a/src/comic.c b/src/comic.c index 9df0f80..5c12f8b 100644 --- a/src/comic.c +++ b/src/comic.c @@ -33,16 +33,84 @@ #include "tbo-utils.h" #include "tbo-widget.h" +struct _Comic +{ + GObject parent_instance; + + char title[255]; + int width; + int height; + GList *pages; + Page *current_page; +}; + +struct _ComicClass +{ + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (Comic, tbo_comic, G_TYPE_OBJECT); + +static GList * +comic_page_link (Comic *comic, Page *page) +{ + return g_list_find (comic->pages, page); +} + +static void +comic_set_current_page_fallback (Comic *comic, GList *hint) +{ + if (hint != NULL) + comic->current_page = hint->data; + else if (comic->pages != NULL) + comic->current_page = comic->pages->data; + else + comic->current_page = NULL; +} + +static void +tbo_comic_dispose (GObject *object) +{ + Comic *self = TBO_COMIC (object); + + self->current_page = NULL; + + if (self->pages != NULL) + { + g_list_free_full (self->pages, (GDestroyNotify) tbo_page_free); + self->pages = NULL; + } + + G_OBJECT_CLASS (tbo_comic_parent_class)->dispose (object); +} + +static void +tbo_comic_init (Comic *self) +{ + self->title[0] = '\0'; + self->width = 0; + self->height = 0; + self->pages = NULL; + self->current_page = NULL; +} + +static void +tbo_comic_class_init (ComicClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = tbo_comic_dispose; +} + Comic * tbo_comic_new (const char *title, int width, int height) { Comic *new_comic; - new_comic = malloc(sizeof(Comic)); + new_comic = g_object_new (TBO_TYPE_COMIC, NULL); snprintf (new_comic->title, 255, "%s", title); new_comic->width = width; new_comic->height = height; - new_comic->pages = NULL; tbo_comic_new_page (new_comic); return new_comic; @@ -51,61 +119,114 @@ tbo_comic_new (const char *title, int width, int height) void tbo_comic_free (Comic *comic) { - GList *p; + if (comic != NULL) + g_object_unref (comic); +} - for (p=g_list_first (comic->pages); p; p = g_list_next(p)) - { - tbo_page_free ((Page *) p->data); - } +const gchar * +tbo_comic_get_title (Comic *comic) +{ + return comic->title; +} + +gint +tbo_comic_get_width (Comic *comic) +{ + return comic->width; +} - g_list_free (g_list_first (comic->pages)); - free (comic); +gint +tbo_comic_get_height (Comic *comic) +{ + return comic->height; +} + +GList * +tbo_comic_get_pages (Comic *comic) +{ + return comic->pages; } Page * -tbo_comic_new_page (Comic *comic){ +tbo_comic_new_page (Comic *comic) +{ Page *page; page = tbo_page_new (comic); - comic->pages = g_list_append (g_list_first (comic->pages), page); + tbo_comic_insert_page (comic, page, -1); return page; } +void +tbo_comic_insert_page (Comic *comic, Page *page, int nth) +{ + if (nth < 0) + comic->pages = g_list_append (comic->pages, page); + else + comic->pages = g_list_insert (comic->pages, page, nth); + + if (comic->current_page == NULL) + comic->current_page = page; +} + void tbo_comic_del_page (Comic *comic, int nth) { Page *page; + GList *link; + GList *next_link; + GList *prev_link; + + page = (Page *) g_list_nth_data (comic->pages, nth); + if (page == NULL) + return; + + link = comic_page_link (comic, page); + next_link = link != NULL ? link->next : NULL; + prev_link = link != NULL ? link->prev : NULL; + + comic->pages = g_list_remove (comic->pages, page); + + if (comic->current_page == page) + comic_set_current_page_fallback (comic, next_link != NULL ? next_link : prev_link); - page = (Page *) g_list_nth_data (g_list_first (comic->pages), nth); - comic->pages = g_list_remove (g_list_first (comic->pages), page); tbo_page_free (page); } int tbo_comic_len (Comic *comic) { - return g_list_length (g_list_first (comic->pages)); + return g_list_length (comic->pages); } int tbo_comic_page_index (Comic *comic) { - return g_list_position ( g_list_first (comic->pages), comic->pages); + if (comic->current_page == NULL) + return -1; + + return g_list_index (comic->pages, comic->current_page); } int tbo_comic_page_nth (Comic *comic, Page *page) { - return g_list_index (g_list_first (comic->pages), page); + return g_list_index (comic->pages, page); } Page * tbo_comic_next_page (Comic *comic) { - if (comic->pages->next) + GList *current_link; + + if (comic->current_page == NULL) + return NULL; + + current_link = comic_page_link (comic, comic->current_page); + if (current_link != NULL && current_link->next != NULL) { - comic->pages = comic->pages->next; + comic->current_page = current_link->next->data; return tbo_comic_get_current_page (comic); } return NULL; @@ -114,9 +235,15 @@ tbo_comic_next_page (Comic *comic) Page * tbo_comic_prev_page (Comic *comic) { - if (comic->pages->prev) + GList *current_link; + + if (comic->current_page == NULL) + return NULL; + + current_link = comic_page_link (comic, comic->current_page); + if (current_link != NULL && current_link->prev != NULL) { - comic->pages = comic->pages->prev; + comic->current_page = current_link->prev->data; return tbo_comic_get_current_page (comic); } return NULL; @@ -125,19 +252,27 @@ tbo_comic_prev_page (Comic *comic) Page * tbo_comic_get_current_page (Comic *comic) { - return (Page *)comic->pages->data; + return comic->current_page; } void tbo_comic_set_current_page (Comic *comic, Page *page) { - comic->pages = g_list_find (g_list_first (comic->pages), page); + if (page == NULL) + { + comic->current_page = NULL; + return; + } + + comic->current_page = comic_page_link (comic, page) != NULL ? page : NULL; } void tbo_comic_set_current_page_nth (Comic *comic, int nth) { - comic->pages = g_list_nth (g_list_first (comic->pages), nth); + GList *link = g_list_nth (comic->pages, nth); + + comic->current_page = link != NULL ? link->data : NULL; } gboolean @@ -194,11 +329,11 @@ tbo_comic_save (TboWindow *tbo, char *filename) gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); snprintf (buffer, 255, "\n", - comic->width, - comic->height); + comic->width, + comic->height); fwrite (buffer, sizeof (char), strlen (buffer), file); - for (p = g_list_first (comic->pages); p; p = g_list_next(p)) + for (p = comic->pages; p; p = g_list_next (p)) { tbo_page_save ((Page *) p->data, file); } @@ -220,9 +355,8 @@ tbo_comic_open (TboWindow *window, char *filename) if (newcomic) { + tbo_window_reset_document_state (window); oldcomic = window->comic; - window->comic = newcomic; - gtk_window_set_title (GTK_WINDOW (window->window), window->comic->title); n_pages = tbo_window_get_page_count (window); for (nth = n_pages - 1; nth >= 0; nth--) @@ -230,9 +364,11 @@ tbo_comic_open (TboWindow *window, char *filename) tbo_window_remove_page_widget (window, nth); } + window->comic = newcomic; + gtk_window_set_title (GTK_WINDOW (window->window), tbo_comic_get_title (window->comic)); tbo_comic_free (oldcomic); - for (nth=0; nthcomic); nth++) + for (nth = 0; nth < tbo_comic_len (window->comic); nth++) { tbo_window_add_page_widget (window, create_darea (window)); } @@ -241,7 +377,7 @@ tbo_comic_open (TboWindow *window, char *filename) tbo_window_set_current_tab_page (window, TRUE); tbo_drawing_adjust_scroll (TBO_DRAWING (window->drawing)); tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_update_status (window, 0, 0); + tbo_window_refresh_status (window); tbo_window_mark_clean (window); } } diff --git a/src/comic.h b/src/comic.h index 6bdb95a..eaeea18 100644 --- a/src/comic.h +++ b/src/comic.h @@ -20,13 +20,30 @@ #ifndef __TBO_COMIC__ #define __TBO_COMIC__ +#include #include #include "tbo-types.h" #include "tbo-window.h" +#define TBO_TYPE_COMIC (tbo_comic_get_type ()) +#define TBO_COMIC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TBO_TYPE_COMIC, Comic)) +#define TBO_IS_COMIC(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TBO_TYPE_COMIC)) +#define TBO_COMIC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TBO_TYPE_COMIC, ComicClass)) +#define TBO_IS_COMIC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TBO_TYPE_COMIC)) +#define TBO_COMIC_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TBO_TYPE_COMIC, ComicClass)) + +typedef struct _ComicClass ComicClass; + +GType tbo_comic_get_type (void); + Comic *tbo_comic_new (const char *title, int width, int height); void tbo_comic_free (Comic *comic); +const gchar *tbo_comic_get_title (Comic *comic); +gint tbo_comic_get_width (Comic *comic); +gint tbo_comic_get_height (Comic *comic); +GList *tbo_comic_get_pages (Comic *comic); Page *tbo_comic_new_page (Comic *comic); +void tbo_comic_insert_page (Comic *comic, Page *page, int nth); void tbo_comic_del_page (Comic *comic, int nth); gboolean tbo_comic_del_current_page (Comic *comic); int tbo_comic_len (Comic *comic); diff --git a/src/dnd.c b/src/dnd.c index 24b3ddb..569480c 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -27,6 +27,7 @@ #include "tbo-files.h" #include "tbo-window.h" #include "tbo-tool-selector.h" +#include "tbo-undo.h" #include "tbo-widget.h" static TboObjectBase * @@ -134,14 +135,16 @@ tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y) if (frame == NULL || asset_path == NULL || *asset_path == '\0') return NULL; - if (x < 0 || y < 0 || x > frame->width || y > frame->height) + if (x < 0 || y < 0 || x > tbo_frame_get_width (frame) || y > tbo_frame_get_height (frame)) return NULL; asset = create_asset (asset_path, x, y); tbo_frame_add_obj (frame, asset); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, asset)); select_inserted_asset (tbo, frame, asset); tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_toolbar_update (tbo->toolbar); return asset; } diff --git a/src/export.c b/src/export.c index c74156a..abf26b7 100644 --- a/src/export.c +++ b/src/export.c @@ -25,6 +25,7 @@ #include #include "export.h" +#include "comic.h" #include "tbo-file-dialog.h" #include "tbo-drawing.h" #include "tbo-ui-utils.h" @@ -45,34 +46,149 @@ struct export_file_args { GtkEntry *entry; }; -struct export_dialog_data { - GMainLoop *loop; - gint response; -}; - -static gboolean -export_close_request_cb (GtkWindow *dialog, struct export_dialog_data *data) +static void +show_export_error (TboWindow *tbo, const gchar *message) { - if (data->response == GTK_RESPONSE_NONE) - data->response = GTK_RESPONSE_CANCEL; - g_main_loop_quit (data->loop); - return TRUE; + tbo_alert_show (GTK_WINDOW (tbo->window), message, NULL); } -static void -export_button_cb (GtkButton *button, GtkWindow *dialog) +gboolean +tbo_export_file (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height) { - struct export_dialog_data *data = g_object_get_data (G_OBJECT (dialog), "tbo-export-data"); - gint response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tbo-response")); + cairo_surface_t *surface = NULL; + cairo_t *cr = NULL; + gchar *base_filename = NULL; + gchar *format_pages = NULL; + GList *page_list; + gchar *export_to = NULL; + gint i, n, n2; + gboolean exported = FALSE; + gboolean success = TRUE; - data->response = response; - gtk_window_close (dialog); -} + if (filename == NULL || *filename == '\0' || width <= 0 || height <= 0) + return FALSE; -static void -show_export_error (TboWindow *tbo, const gchar *message) -{ - tbo_alert_show (GTK_WINDOW (tbo->window), message, NULL); + if (format_hint != NULL && *format_hint != '\0') + { + export_to = g_ascii_strdown (format_hint, -1); + base_filename = g_strdup (filename); + } + else + { + gchar *dot = strrchr (filename, '.'); + + if (dot != NULL && dot[1] != '\0') + { + export_to = g_ascii_strdown (dot + 1, -1); + base_filename = g_strndup (filename, dot - filename); + } + else + { + base_filename = g_strdup (filename); + export_to = g_strdup ("png"); + } + } + + if (g_strcmp0 (export_to, "png") != 0 && + g_strcmp0 (export_to, "pdf") != 0 && + g_strcmp0 (export_to, "svg") != 0) + { + g_free (export_to); + export_to = g_strdup ("png"); + } + + n = tbo_comic_len (tbo->comic); + n2 = n; + for (i = 0; n; n = n / 10, i++); + format_pages = g_strdup_printf ("%%s%%0%dd.%%s", i); + + for (i = 0, page_list = tbo_comic_get_pages (tbo->comic); page_list; i++, page_list = page_list->next) + { + gchar *rpath = g_strdup_printf (format_pages, base_filename, i, export_to); + + if (n2 == 1) + { + g_free (rpath); + rpath = g_strdup_printf ("%s.%s", base_filename, export_to); + } + + if (strcmp (export_to, "pdf") == 0) + { + if (!surface) + { + g_free (rpath); + rpath = g_strdup_printf ("%s.%s", base_filename, export_to); + surface = cairo_pdf_surface_create (rpath, width, height); + cr = cairo_create (surface); + } + } + else if (strcmp (export_to, "svg") == 0) + { + surface = cairo_svg_surface_create (rpath, width, height); + cr = cairo_create (surface); + } + else + { + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + } + + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS || cairo_status (cr) != CAIRO_STATUS_SUCCESS) + { + show_export_error (tbo, cairo_status_to_string (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS ? + cairo_surface_status (surface) : + cairo_status (cr))); + success = FALSE; + g_free (rpath); + break; + } + + tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, (Page *) page_list->data, width, height); + + if (strcmp (export_to, "pdf") == 0) + cairo_show_page (cr); + else if (strcmp (export_to, "png") == 0) + { + cairo_status_t status = cairo_surface_write_to_png (surface, rpath); + if (status != CAIRO_STATUS_SUCCESS) + { + show_export_error (tbo, cairo_status_to_string (status)); + success = FALSE; + } + } + + if (success) + exported = TRUE; + + if (strcmp (export_to, "pdf") != 0) + { + cairo_surface_destroy (surface); + cairo_destroy (cr); + surface = NULL; + cr = NULL; + } + + g_free (rpath); + + if (!success) + break; + } + + if (surface != NULL) + { + cairo_surface_destroy (surface); + cairo_destroy (cr); + } + + g_free (format_pages); + g_free (base_filename); + g_free (export_to); + + return success && exported; } static gboolean @@ -115,18 +231,11 @@ filedialog_cb (GtkWidget *widget, gpointer data) gboolean tbo_export (TboWindow *tbo) { - cairo_surface_t *surface = NULL; - cairo_t *cr; - gint width = tbo->comic->width; - gint height = tbo->comic->height; + gint width = tbo_comic_get_width (tbo->comic); + gint height = tbo_comic_get_height (tbo->comic); gchar *filename = NULL; - gchar *base_filename = NULL; - gchar *format_pages = NULL; - GList *page_list; - gint i, n, n2; gint response; gdouble scale = 1.0; - gchar *export_to = NULL; gint export_to_index; struct export_spin_args spin_args; struct export_spin_args spin_args2; @@ -153,7 +262,8 @@ tbo_export (TboWindow *tbo) NULL, }; - struct export_dialog_data data; + TboDialogRunData data; + const gchar *format_hint = NULL; dialog = gtk_window_new (); gtk_window_set_title (GTK_WINDOW (dialog), _("Export as")); @@ -180,24 +290,24 @@ tbo_export (TboWindow *tbo) } else { - gtk_editable_set_text (GTK_EDITABLE (fileinput), tbo->comic->title); + gtk_editable_set_text (GTK_EDITABLE (fileinput), tbo_comic_get_title (tbo->comic)); } tbo_widget_add_child (hbox, filelabel); tbo_widget_add_child (hbox, fileinput); tbo_widget_add_child (hbox, filebutton); tbo_widget_add_child (vbox, hbox); - spinw = add_spin_with_label (vbox, _("width: "), tbo->comic->width); - spinh = add_spin_with_label (vbox, _("height: "), tbo->comic->height); + spinw = add_spin_with_label (vbox, _("width: "), tbo_comic_get_width (tbo->comic)); + spinh = add_spin_with_label (vbox, _("height: "), tbo_comic_get_height (tbo->comic)); - spin_args.current_size = tbo->comic->width; - spin_args.current_size2 = tbo->comic->height; + spin_args.current_size = tbo_comic_get_width (tbo->comic); + spin_args.current_size2 = tbo_comic_get_height (tbo->comic); spin_args.spin2 = spinh; spin_args.scale = &scale; g_signal_connect (spinw, "value-changed", G_CALLBACK (export_size_cb), &spin_args); - spin_args2.current_size = tbo->comic->height; - spin_args2.current_size2 = tbo->comic->width; + spin_args2.current_size = tbo_comic_get_height (tbo->comic); + spin_args2.current_size2 = tbo_comic_get_width (tbo->comic); spin_args2.spin2 = spinw; spin_args2.scale = &scale; g_signal_connect (spinh, "value-changed", G_CALLBACK (export_size_cb), &spin_args2); @@ -211,13 +321,13 @@ tbo_export (TboWindow *tbo) button = gtk_button_new_with_mnemonic (_("_Cancel")); g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_CANCEL)); - g_signal_connect (button, "clicked", G_CALLBACK (export_button_cb), dialog); + g_signal_connect (button, "clicked", G_CALLBACK (tbo_dialog_button_cb), dialog); tbo_widget_add_child (actions, button); button = gtk_button_new_with_mnemonic (_("_Save")); gtk_widget_add_css_class (button, "suggested-action"); g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (GTK_RESPONSE_ACCEPT)); - g_signal_connect (button, "clicked", G_CALLBACK (export_button_cb), dialog); + g_signal_connect (button, "clicked", G_CALLBACK (tbo_dialog_button_cb), dialog); tbo_widget_add_child (actions, button); tbo_widget_add_child (vbox, actions); @@ -228,12 +338,9 @@ tbo_export (TboWindow *tbo) file_args.entry = GTK_ENTRY (fileinput); g_signal_connect (filebutton, "clicked", G_CALLBACK (filedialog_cb), &file_args); - data.loop = g_main_loop_new (NULL, FALSE); - data.response = GTK_RESPONSE_NONE; - g_object_set_data (G_OBJECT (dialog), "tbo-export-data", &data); - g_signal_connect (dialog, "close-request", G_CALLBACK (export_close_request_cb), &data); - gtk_window_present (GTK_WINDOW (dialog)); - g_main_loop_run (data.loop); + tbo_dialog_run_data_init (&data, GTK_RESPONSE_CANCEL); + g_signal_connect (dialog, "close-request", G_CALLBACK (tbo_dialog_close_request_cb), &data); + tbo_dialog_run (GTK_WINDOW (dialog), &data); response = data.response; @@ -255,127 +362,29 @@ tbo_export (TboWindow *tbo) /* 0 guess, 1 png, 2 pdf, 3 svg */ export_to_index = gtk_drop_down_get_selected (GTK_DROP_DOWN (dropdown)); - switch (export_to_index) - { - case 0: - { - gchar *dot = strrchr (filename, '.'); - - if (dot != NULL && dot[1] != '\0') - { - export_to = g_ascii_strdown (dot + 1, -1); - base_filename = g_strndup (filename, dot - filename); - } - else - { - base_filename = g_strdup (filename); - export_to = g_strdup ("png"); - } - break; - } - case 1: - export_to = g_strdup ("png"); - base_filename = g_strdup (filename); - break; - case 2: - export_to = g_strdup ("pdf"); - base_filename = g_strdup (filename); - break; - case 3: - export_to = g_strdup ("svg"); - base_filename = g_strdup (filename); - break; - default: - export_to = g_strdup ("png"); - base_filename = g_strdup (filename); - break; - } - - if (g_strcmp0 (export_to, "png") != 0 && - g_strcmp0 (export_to, "pdf") != 0 && - g_strcmp0 (export_to, "svg") != 0) - { - g_free (export_to); - export_to = g_strdup ("png"); - } + if (export_to_index == 1) + format_hint = "png"; + else if (export_to_index == 2) + format_hint = "pdf"; + else if (export_to_index == 3) + format_hint = "svg"; - n = g_list_length (tbo->comic->pages); - n2 = n; - for (i=0; n; n=n/10, i++); - format_pages = g_strdup_printf ("%%s%%0%dd.%%s", i); - for (i=0, page_list = g_list_first (tbo->comic->pages); page_list; i++, page_list = page_list->next) - { - gchar *rpath = g_strdup_printf (format_pages, base_filename, i, export_to); - if (n2 == 1) - { - g_free (rpath); - rpath = g_strdup_printf ("%s.%s", base_filename, export_to); - } - // PDF - if (strcmp (export_to, "pdf") == 0) - { - if (!surface) - { - g_free (rpath); - rpath = g_strdup_printf ("%s.%s", base_filename, export_to); - surface = cairo_pdf_surface_create (rpath, width, height); - cr = cairo_create (surface); - } - } - // SVG - else if (strcmp (export_to, "svg") == 0) - { - surface = cairo_svg_surface_create (rpath, width, height); - cr = cairo_create (surface); - } - // PNG or unknown format... default is png - else - { - surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); - cr = cairo_create (surface); - } - - cairo_scale (cr, scale, scale); - - // drawing the stuff - tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, (Page *)page_list->data, width/scale, height/scale); - - if (strcmp (export_to, "pdf") == 0) - cairo_show_page (cr); - else if (strcmp (export_to, "png") == 0) - { - cairo_status_t status = cairo_surface_write_to_png (surface, rpath); - if (status != CAIRO_STATUS_SUCCESS) - show_export_error (tbo, cairo_status_to_string (status)); - } - - cairo_scale (cr, 1/scale, 1/scale); - - // Not destroying for multipage - if (strcmp (export_to, "pdf") != 0) - { - cairo_surface_destroy (surface); - cairo_destroy (cr); - surface = NULL; - } - - g_free (rpath); - } + width = (gint) (width * scale); + height = (gint) (height * scale); - if (surface) + if (!tbo_export_file (tbo, filename, format_hint, width, height)) { - cairo_surface_destroy (surface); - cairo_destroy (cr); + gtk_window_destroy (GTK_WINDOW (dialog)); + tbo_dialog_run_data_clear (&data); + g_free (filename); + return FALSE; } } - g_free (format_pages); - g_free (base_filename); - g_free (export_to); g_free (filename); gtk_window_destroy (GTK_WINDOW (dialog)); - g_main_loop_unref (data.loop); + tbo_dialog_run_data_clear (&data); - return FALSE; + return response == GTK_RESPONSE_ACCEPT; } diff --git a/src/export.h b/src/export.h index 0af7d93..2998325 100644 --- a/src/export.h +++ b/src/export.h @@ -25,5 +25,10 @@ #include "tbo-window.h" gboolean tbo_export (TboWindow *tbo); +gboolean tbo_export_file (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height); #endif diff --git a/src/frame.c b/src/frame.c index 7c57491..be9cfd8 100644 --- a/src/frame.c +++ b/src/frame.c @@ -20,26 +20,95 @@ #include #include #include -#include #include #include #include "frame.h" #include "tbo-types.h" #include "tbo-object-base.h" +#include "tbo-utils.h" + +struct _Frame +{ + GObject parent_instance; + + gint x; + gint y; + gint width; + gint height; + gboolean border; + Color color; + GList *objects; +}; + +struct _FrameClass +{ + GObjectClass parent_class; +}; + +typedef struct +{ + cairo_t *cr; + Frame *frame; +} DrawObjectsData; + +G_DEFINE_TYPE (Frame, tbo_frame, G_TYPE_OBJECT); static int BASE_X = 0; static int BASE_Y = 0; static float SCALE_FACTOR = 0; static Color BASE_COLOR = {1, 1, 1}; +static void +draw_objects (gpointer data, gpointer user_data) +{ + DrawObjectsData *draw_data = user_data; + TboObjectBase *obj = TBO_OBJECT_BASE (data); + + obj->draw (obj, draw_data->frame, draw_data->cr); +} + +static void +tbo_frame_dispose (GObject *object) +{ + Frame *self = TBO_FRAME (object); + + if (self->objects != NULL) + { + g_list_free_full (self->objects, g_object_unref); + self->objects = NULL; + } + + G_OBJECT_CLASS (tbo_frame_parent_class)->dispose (object); +} + +static void +tbo_frame_init (Frame *self) +{ + self->x = 0; + self->y = 0; + self->width = 0; + self->height = 0; + self->border = TRUE; + self->color = BASE_COLOR; + self->objects = NULL; +} + +static void +tbo_frame_class_init (FrameClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = tbo_frame_dispose; +} + void tbo_frame_set_scale_factor (Frame *frame, int width, int height) { float scale_factor = 1.; float scale_factor2 = 1.; - scale_factor = (width-20) / (float)frame->width; - scale_factor2 = (height-20) / (float)frame->height; + scale_factor = (width - 20) / (float) frame->width; + scale_factor2 = (height - 20) / (float) frame->height; scale_factor = scale_factor > scale_factor2 ? scale_factor2 : scale_factor; @@ -50,7 +119,8 @@ int tbo_frame_get_x_centered (Frame *frame, int width) { int x; - x = (width/2.0) - (frame->width*SCALE_FACTOR / 2.0); + + x = (width / 2.0) - (frame->width * SCALE_FACTOR / 2.0); x = x / SCALE_FACTOR; return x; } @@ -59,108 +129,193 @@ int tbo_frame_get_y_centered (Frame *frame, int height) { int y; - y = (height/2.0) - (frame->height*SCALE_FACTOR / 2.0); + + y = (height / 2.0) - (frame->height * SCALE_FACTOR / 2.0); y = y / SCALE_FACTOR; return y; } Frame * -tbo_frame_new (int x, int y, - int width, int height) +tbo_frame_new (int x, int y, int width, int height) { - Frame *new_frame; - - new_frame = malloc (sizeof(Frame)); - new_frame->objects = NULL; + Frame *new_frame = g_object_new (TBO_TYPE_FRAME, NULL); new_frame->x = x; new_frame->y = y; new_frame->width = width; new_frame->height = height; - new_frame->border = TRUE; - new_frame->color = malloc (sizeof (Color)); - new_frame->color->r = BASE_COLOR.r; - new_frame->color->g = BASE_COLOR.g; - new_frame->color->b = BASE_COLOR.b; return new_frame; } -static void -free_objects (gpointer data, - gpointer user_data) +void +tbo_frame_free (Frame *frame) { - TboObjectBase *obj = TBO_OBJECT_BASE (data); - g_object_unref (obj); + if (frame != NULL) + g_object_unref (frame); +} + +int +tbo_frame_get_x (Frame *frame) +{ + return frame->x; +} + +int +tbo_frame_get_y (Frame *frame) +{ + return frame->y; +} + +int +tbo_frame_get_width (Frame *frame) +{ + return frame->width; +} + +int +tbo_frame_get_height (Frame *frame) +{ + return frame->height; } void -tbo_frame_free (Frame *frame) +tbo_frame_get_bounds (Frame *frame, int *x, int *y, int *width, int *height) { - g_list_foreach (g_list_first (frame->objects), free_objects, NULL); - g_list_free (frame->objects); - free (frame->color); - free (frame); + if (x != NULL) + *x = frame->x; + if (y != NULL) + *y = frame->y; + if (width != NULL) + *width = frame->width; + if (height != NULL) + *height = frame->height; } -static void -draw_objects (gpointer data, - gpointer user_data) +void +tbo_frame_set_position (Frame *frame, int x, int y) { - void **pdata = user_data; - TboObjectBase *obj = TBO_OBJECT_BASE (data); - cairo_t *cr = (cairo_t *)pdata[0]; - Frame *frame = (Frame *)pdata[1]; - obj->draw (obj, frame, cr); + frame->x = x; + frame->y = y; +} + +void +tbo_frame_set_size (Frame *frame, int width, int height) +{ + frame->width = width; + frame->height = height; +} + +void +tbo_frame_set_bounds (Frame *frame, int x, int y, int width, int height) +{ + frame->x = x; + frame->y = y; + frame->width = width; + frame->height = height; +} + +gboolean +tbo_frame_get_border (Frame *frame) +{ + return frame->border; +} + +void +tbo_frame_set_border (Frame *frame, gboolean border) +{ + frame->border = border; +} + +void +tbo_frame_get_color (Frame *frame, GdkRGBA *color) +{ + if (color == NULL) + return; + + color->red = frame->color.r; + color->green = frame->color.g; + color->blue = frame->color.b; + color->alpha = 1.0; +} + +void +tbo_frame_set_color_rgb (Frame *frame, gdouble red, gdouble green, gdouble blue) +{ + frame->color.r = red; + frame->color.g = green; + frame->color.b = blue; + BASE_COLOR = frame->color; +} + +GList * +tbo_frame_get_objects (Frame *frame) +{ + return frame->objects; +} + +gint +tbo_frame_object_count (Frame *frame) +{ + return g_list_length (frame->objects); +} + +gint +tbo_frame_object_nth (Frame *frame, TboObjectBase *obj) +{ + return g_list_index (frame->objects, obj); +} + +gboolean +tbo_frame_has_obj (Frame *frame, TboObjectBase *obj) +{ + return g_list_find (frame->objects, obj) != NULL; } void tbo_frame_draw_complete (Frame *frame, cairo_t *cr, - float fill_r, float fill_g, float fill_b, - float border_r, float border_g, float border_b, - int line_width) + float fill_r, float fill_g, float fill_b, + float border_r, float border_g, float border_b, + int line_width) { - cairo_set_source_rgb(cr, fill_r, fill_g, fill_b); - cairo_rectangle(cr, frame->x, frame->y, - frame->width, frame->height); - cairo_fill(cr); + DrawObjectsData draw_data = { + .cr = cr, + .frame = frame, + }; + + cairo_set_source_rgb (cr, fill_r, fill_g, fill_b); + cairo_rectangle (cr, frame->x, frame->y, frame->width, frame->height); + cairo_fill (cr); cairo_set_line_width (cr, line_width); if (frame->border) { - cairo_set_source_rgb(cr, border_r, border_g, border_b); - cairo_rectangle (cr, frame->x, frame->y, - frame->width, frame->height); + cairo_set_source_rgb (cr, border_r, border_g, border_b); + cairo_rectangle (cr, frame->x, frame->y, frame->width, frame->height); cairo_stroke (cr); } - void **crframe = malloc (sizeof(void*)*2); - crframe[0] = (void*)cr; - crframe[1] = (void*)frame; - - g_list_foreach (g_list_first (frame->objects), draw_objects, crframe); - - free (crframe); + g_list_foreach (frame->objects, draw_objects, &draw_data); } void tbo_frame_draw (Frame *frame, cairo_t *cr) { Color border = {0, 0, 0}; - Color *fill = frame->color; + tbo_frame_draw_complete (frame, cr, - fill->r, fill->g, fill->b, - border.r, border.g, border.b, - 4); + frame->color.r, frame->color.g, frame->color.b, + border.r, border.g, border.b, + 4); } int tbo_frame_point_inside (Frame *frame, int x, int y) { if ((x >= frame->x) && - (x <= (frame->x + frame->width)) && - (y >= frame->y) && - (y <= (frame->y + frame->height))) + (x <= (frame->x + frame->width)) && + (y >= frame->y) && + (y <= (frame->y + frame->height))) return 1; else return 0; @@ -170,16 +325,16 @@ int tbo_frame_point_inside_obj (TboObjectBase *obj, int x, int y) { int ox, oy, ow, oh; + int xnew1, ynew1, xnew2, ynew2, xnew3, ynew3; + int xmax, ymax, xmin, ymin; tbo_frame_get_obj_relative (obj, &ox, &oy, &ow, &oh); - int xnew1 = ox + (ow * cos (obj->angle)); - int ynew1 = oy + (ow * sin (obj->angle)); - int xnew2 = ox + (-oh * sin (obj->angle)); - int ynew2 = oy + (oh * cos (obj->angle)); - int xnew3 = ox + (ow * cos(obj->angle) - oh * sin(obj->angle)); - int ynew3 = oy + (oh * cos(obj->angle) + ow * sin(obj->angle)); - - int xmax, ymax, xmin, ymin; + xnew1 = ox + (ow * cos (obj->angle)); + ynew1 = oy + (ow * sin (obj->angle)); + xnew2 = ox + (-oh * sin (obj->angle)); + ynew2 = oy + (oh * cos (obj->angle)); + xnew3 = ox + (ow * cos (obj->angle) - oh * sin (obj->angle)); + ynew3 = oy + (oh * cos (obj->angle) + ow * sin (obj->angle)); xmax = ox; if (xnew1 > xmax) @@ -212,9 +367,9 @@ tbo_frame_point_inside_obj (TboObjectBase *obj, int x, int y) ymin = ynew3; if ((x >= xmin) && - (x <= xmax) && - (y >= ymin) && - (y <= ymax)) + (x <= xmax) && + (y >= ymin) && + (y <= ymax)) return 1; else return 0; @@ -241,28 +396,40 @@ tbo_frame_get_base_y (int y) return (y / SCALE_FACTOR) - BASE_Y; } -void tbo_frame_draw_scaled (Frame *frame, cairo_t *cr, int width, int height) +void +tbo_frame_draw_scaled (Frame *frame, cairo_t *cr, int width, int height) { - int RX, RY; + int previous_x; + int previous_y; + tbo_frame_set_scale_factor (frame, width, height); cairo_scale (cr, SCALE_FACTOR, SCALE_FACTOR); - RX = frame->x; - RY = frame->y; + previous_x = frame->x; + previous_y = frame->y; frame->x = tbo_frame_get_x_centered (frame, width); frame->y = tbo_frame_get_y_centered (frame, height); BASE_X = frame->x; BASE_Y = frame->y; tbo_frame_draw (frame, cr); - cairo_scale (cr, 1/SCALE_FACTOR, 1/SCALE_FACTOR); - frame->x = RX; - frame->y = RY; + cairo_scale (cr, 1 / SCALE_FACTOR, 1 / SCALE_FACTOR); + frame->x = previous_x; + frame->y = previous_y; } void tbo_frame_add_obj (Frame *frame, TboObjectBase *obj) { - frame->objects = g_list_append (frame->objects, obj); + tbo_frame_insert_obj (frame, obj, -1); +} + +void +tbo_frame_insert_obj (Frame *frame, TboObjectBase *obj, int nth) +{ + if (nth < 0) + frame->objects = g_list_append (frame->objects, obj); + else + frame->objects = g_list_insert (frame->objects, obj, nth); } float @@ -274,44 +441,52 @@ tbo_frame_get_scale_factor (void) void tbo_frame_del_obj (Frame *frame, TboObjectBase *obj) { - frame->objects = g_list_remove (g_list_first (frame->objects), obj); + frame->objects = g_list_remove (frame->objects, obj); g_object_unref (obj); } +void +tbo_frame_reorder_obj (Frame *frame, TboObjectBase *obj, int nth) +{ + if (!tbo_frame_has_obj (frame, obj)) + return; + + frame->objects = g_list_remove (frame->objects, obj); + tbo_frame_insert_obj (frame, obj, nth); +} + void tbo_frame_set_color (Frame *frame, GdkRGBA *color) { - frame->color->r = color->red; - frame->color->g = color->green; - frame->color->b = color->blue; - BASE_COLOR.r = frame->color->r; - BASE_COLOR.g = frame->color->g; - BASE_COLOR.b = frame->color->b; + tbo_frame_set_color_rgb (frame, color->red, color->green, color->blue); } void tbo_frame_save (Frame *frame, FILE *file) { - char buffer[255]; GList *o; TboObjectBase *obj; - - snprintf (buffer, 255, " \n", - frame->x, frame->y, frame->width, - frame->height, frame->border, - frame->color->r, frame->color->g, frame->color->b); - fwrite (buffer, sizeof (char), strlen (buffer), file); - - for (o=g_list_first (frame->objects); o; o = g_list_next(o)) + GString *xml; + + xml = g_string_new (" x); + tbo_xml_append_attr_int (xml, "y", frame->y); + tbo_xml_append_attr_int (xml, "width", frame->width); + tbo_xml_append_attr_int (xml, "height", frame->height); + tbo_xml_append_attr_int (xml, "border", frame->border); + tbo_xml_append_attr_double (xml, "r", frame->color.r); + tbo_xml_append_attr_double (xml, "g", frame->color.g); + tbo_xml_append_attr_double (xml, "b", frame->color.b); + g_string_append (xml, ">\n"); + tbo_xml_write (file, xml); + + for (o = frame->objects; o; o = g_list_next (o)) { obj = TBO_OBJECT_BASE (o->data); obj->save (obj, file); } - snprintf (buffer, 255, " \n"); - fwrite (buffer, sizeof (char), strlen (buffer), file); + fputs (" \n", file); } Frame * @@ -319,18 +494,15 @@ tbo_frame_clone (Frame *frame) { GList *o; TboObjectBase *cur_object; - Frame *newframe = tbo_frame_new (frame->x, frame->y, - frame->width, frame->height); + Frame *newframe = tbo_frame_new (frame->x, frame->y, frame->width, frame->height); - for (o=g_list_first (frame->objects); o; o = g_list_next(o)) + for (o = frame->objects; o; o = g_list_next (o)) { cur_object = TBO_OBJECT_BASE (o->data); tbo_frame_add_obj (newframe, cur_object->clone (cur_object)); } newframe->border = frame->border; - newframe->color->r = frame->color->r; - newframe->color->g = frame->color->g; - newframe->color->b = frame->color->b; + newframe->color = frame->color; return newframe; } diff --git a/src/frame.h b/src/frame.h index c5a2b02..d1563d3 100644 --- a/src/frame.h +++ b/src/frame.h @@ -20,14 +20,42 @@ #ifndef __TBO_FRAME__ #define __TBO_FRAME__ +#include #include #include #include #include "tbo-types.h" #include "tbo-object-base.h" +#define TBO_TYPE_FRAME (tbo_frame_get_type ()) +#define TBO_FRAME(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TBO_TYPE_FRAME, Frame)) +#define TBO_IS_FRAME(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TBO_TYPE_FRAME)) +#define TBO_FRAME_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TBO_TYPE_FRAME, FrameClass)) +#define TBO_IS_FRAME_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TBO_TYPE_FRAME)) +#define TBO_FRAME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TBO_TYPE_FRAME, FrameClass)) + +typedef struct _FrameClass FrameClass; + +GType tbo_frame_get_type (void); + Frame *tbo_frame_new (int x, int y, int witdh, int heigth); void tbo_frame_free (Frame *frame); +int tbo_frame_get_x (Frame *frame); +int tbo_frame_get_y (Frame *frame); +int tbo_frame_get_width (Frame *frame); +int tbo_frame_get_height (Frame *frame); +void tbo_frame_get_bounds (Frame *frame, int *x, int *y, int *width, int *height); +void tbo_frame_set_position (Frame *frame, int x, int y); +void tbo_frame_set_size (Frame *frame, int width, int height); +void tbo_frame_set_bounds (Frame *frame, int x, int y, int width, int height); +gboolean tbo_frame_get_border (Frame *frame); +void tbo_frame_set_border (Frame *frame, gboolean border); +void tbo_frame_get_color (Frame *frame, GdkRGBA *color); +void tbo_frame_set_color_rgb (Frame *frame, gdouble red, gdouble green, gdouble blue); +GList *tbo_frame_get_objects (Frame *frame); +gint tbo_frame_object_count (Frame *frame); +gint tbo_frame_object_nth (Frame *frame, TboObjectBase *obj); +gboolean tbo_frame_has_obj (Frame *frame, TboObjectBase *obj); void tbo_frame_draw_complete (Frame *frame, cairo_t *cr, float fill_r, float fill_g, float fill_b, @@ -39,7 +67,9 @@ void tbo_frame_draw_scaled (Frame *frame, cairo_t *cr, int width, int height); int tbo_frame_point_inside (Frame *frame, int x, int y); int tbo_frame_point_inside_obj (TboObjectBase *obj, int x, int y); void tbo_frame_add_obj (Frame *frame, TboObjectBase *obj); +void tbo_frame_insert_obj (Frame *frame, TboObjectBase *obj, int nth); void tbo_frame_del_obj (Frame *frame, TboObjectBase *obj); +void tbo_frame_reorder_obj (Frame *frame, TboObjectBase *obj, int nth); void tbo_frame_get_obj_relative (TboObjectBase *obj, int *x, int *y, int *w, int *h); float tbo_frame_get_scale_factor (void); int tbo_frame_get_base_y (int y); diff --git a/src/page.c b/src/page.c index 5bc9f6f..74c2e41 100644 --- a/src/page.c +++ b/src/page.c @@ -25,40 +25,90 @@ #include "page.h" #include "frame.h" -Page * -tbo_page_new (Comic *comic) +struct _Page +{ + GObject parent_instance; + + GList *frames; + Frame *current_frame; +}; + +struct _PageClass +{ + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (Page, tbo_page, G_TYPE_OBJECT); + +static GList * +page_frame_link (Page *page, Frame *frame) { - Page *new_page; - new_page = malloc(sizeof(Page)); - new_page->comic = comic; - new_page->frames = NULL; + return g_list_find (page->frames, frame); +} - return new_page; +static void +page_set_current_frame_fallback (Page *page, GList *hint) +{ + if (hint != NULL) + page->current_frame = hint->data; + else if (page->frames != NULL) + page->current_frame = page->frames->data; + else + page->current_frame = NULL; } -void tbo_page_free (Page *page) +static void +tbo_page_dispose (GObject *object) { - GList *f; + Page *self = TBO_PAGE (object); - if (tbo_page_len (page) > 0) + self->current_frame = NULL; + + if (self->frames != NULL) { - for (f=tbo_page_get_frames (page); f; f=g_list_next(f)) - { - tbo_frame_free ((Frame *) f->data); - } + g_list_free_full (self->frames, (GDestroyNotify) tbo_frame_free); + self->frames = NULL; } - g_list_free (tbo_page_get_frames (page)); - free (page); + + G_OBJECT_CLASS (tbo_page_parent_class)->dispose (object); +} + +static void +tbo_page_init (Page *self) +{ + self->frames = NULL; + self->current_frame = NULL; +} + +static void +tbo_page_class_init (PageClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = tbo_page_dispose; +} + +Page * +tbo_page_new (Comic *comic) +{ + (void) comic; + return g_object_new (TBO_TYPE_PAGE, NULL); +} + +void +tbo_page_free (Page *page) +{ + if (page != NULL) + g_object_unref (page); } Frame * -tbo_page_new_frame (Page *page, int x, int y, - int w, int h) +tbo_page_new_frame (Page *page, int x, int y, int w, int h) { Frame *frame; frame = tbo_frame_new (x, y, w, h); - page->frames = g_list_append (page->frames, frame); + tbo_page_insert_frame (page, frame, -1); return frame; } @@ -66,7 +116,19 @@ tbo_page_new_frame (Page *page, int x, int y, void tbo_page_add_frame (Page *page, Frame *frame) { - page->frames = g_list_append (page->frames, frame); + tbo_page_insert_frame (page, frame, -1); +} + +void +tbo_page_insert_frame (Page *page, Frame *frame, int nth) +{ + if (nth < 0) + page->frames = g_list_append (page->frames, frame); + else + page->frames = g_list_insert (page->frames, frame, nth); + + if (page->current_frame == NULL) + page->current_frame = frame; } void @@ -74,28 +136,53 @@ tbo_page_del_frame_by_index (Page *page, int nth) { Frame *frame; - frame = (Frame *) g_list_nth_data (g_list_first (page->frames), nth); + frame = (Frame *) g_list_nth_data (page->frames, nth); tbo_page_del_frame (page, frame); } void tbo_page_del_frame (Page *page, Frame *frame) { - page->frames = g_list_remove (g_list_first (page->frames), frame); + GList *link; + GList *next_link; + GList *prev_link; + + if (frame == NULL) + return; + + link = page_frame_link (page, frame); + if (link == NULL) + return; + + next_link = link->next; + prev_link = link->prev; + page->frames = g_list_remove (page->frames, frame); + + if (page->current_frame == frame) + page_set_current_frame_fallback (page, next_link != NULL ? next_link : prev_link); + tbo_frame_free (frame); } int tbo_page_len (Page *page) { - return g_list_length (g_list_first (page->frames)); + return g_list_length (page->frames); } int tbo_page_frame_index (Page *page) { - return g_list_position (g_list_first (page->frames), - page->frames) + 1; + if (page->current_frame == NULL) + return 0; + + return g_list_index (page->frames, page->current_frame) + 1; +} + +int +tbo_page_frame_nth (Page *page, Frame *frame) +{ + return g_list_index (page->frames, frame); } gboolean @@ -117,9 +204,15 @@ tbo_page_frame_last (Page *page) Frame * tbo_page_next_frame (Page *page) { - if (page->frames->next) + GList *current_link; + + if (page->current_frame == NULL) + return NULL; + + current_link = page_frame_link (page, page->current_frame); + if (current_link != NULL && current_link->next != NULL) { - page->frames = page->frames->next; + page->current_frame = current_link->next->data; return tbo_page_get_current_frame (page); } return NULL; @@ -128,9 +221,15 @@ tbo_page_next_frame (Page *page) Frame * tbo_page_prev_frame (Page *page) { - if (page->frames->prev) + GList *current_link; + + if (page->current_frame == NULL) + return NULL; + + current_link = page_frame_link (page, page->current_frame); + if (current_link != NULL && current_link->prev != NULL) { - page->frames = page->frames->prev; + page->current_frame = current_link->prev->data; return tbo_page_get_current_frame (page); } return NULL; @@ -139,28 +238,38 @@ tbo_page_prev_frame (Page *page) Frame * tbo_page_get_current_frame (Page *page) { - return (Frame *)page->frames->data; + return page->current_frame; } void tbo_page_set_current_frame (Page *page, Frame *frame) { - page->frames = g_list_find (g_list_first (page->frames), frame); + if (frame == NULL) + { + page->current_frame = NULL; + return; + } + + page->current_frame = page_frame_link (page, frame) != NULL ? frame : NULL; } Frame * tbo_page_first_frame (Page *page) { - page->frames = g_list_first (page->frames); if (page->frames != NULL) - return page->frames->data; + { + page->current_frame = page->frames->data; + return page->current_frame; + } + + page->current_frame = NULL; return NULL; } GList * tbo_page_get_frames (Page *page) { - return g_list_first (page->frames); + return page->frames; } void @@ -168,10 +277,11 @@ tbo_page_save (Page *page, FILE *file) { char buffer[255]; GList *f; + snprintf (buffer, 255, " \n"); fwrite (buffer, sizeof (char), strlen (buffer), file); - for (f=g_list_first (page->frames); f; f = g_list_next(f)) + for (f = page->frames; f; f = g_list_next (f)) { tbo_frame_save ((Frame *) f->data, file); } diff --git a/src/page.h b/src/page.h index f4cde48..8a71e07 100644 --- a/src/page.h +++ b/src/page.h @@ -20,18 +20,32 @@ #ifndef __TBO_PAGE__ #define __TBO_PAGE__ +#include #include #include #include "tbo-types.h" +#define TBO_TYPE_PAGE (tbo_page_get_type ()) +#define TBO_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TBO_TYPE_PAGE, Page)) +#define TBO_IS_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TBO_TYPE_PAGE)) +#define TBO_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TBO_TYPE_PAGE, PageClass)) +#define TBO_IS_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TBO_TYPE_PAGE)) +#define TBO_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TBO_TYPE_PAGE, PageClass)) + +typedef struct _PageClass PageClass; + +GType tbo_page_get_type (void); + Page *tbo_page_new (Comic *comic); void tbo_page_free (Page *page); Frame *tbo_page_new_frame (Page *page, int x, int y, int w, int h); void tbo_page_add_frame (Page *page, Frame *frame); +void tbo_page_insert_frame (Page *page, Frame *frame, int nth); void tbo_page_del_frame_by_index (Page *page, int nth); void tbo_page_del_frame (Page *page, Frame *frame); int tbo_page_len (Page *page); int tbo_page_frame_index (Page *page); +int tbo_page_frame_nth (Page *page, Frame *frame); gboolean tbo_page_frame_first (Page *page); gboolean tbo_page_frame_last (Page *page); Frame *tbo_page_first_frame (Page *page); @@ -43,4 +57,3 @@ GList *tbo_page_get_frames (Page *page); void tbo_page_save (Page *page, FILE *file); #endif - diff --git a/src/tbo-drawing.c b/src/tbo-drawing.c index 949c82e..eea353c 100644 --- a/src/tbo-drawing.c +++ b/src/tbo-drawing.c @@ -33,6 +33,57 @@ G_DEFINE_TYPE (TboDrawing, tbo_drawing, GTK_TYPE_DRAWING_AREA); +static void tbo_drawing_set_window_pointer (TboDrawing *self, TboWindow *tbo); +static void tbo_drawing_set_comic_pointer (TboDrawing *self, Comic *comic); + +static void +tbo_drawing_set_current_frame_pointer (TboDrawing *self, Frame *frame) +{ + if (self->current_frame == frame) + return; + + if (self->current_frame != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (self->current_frame), + (gpointer *) &self->current_frame); + } + + self->current_frame = frame; + + if (self->current_frame != NULL) + { + g_object_add_weak_pointer (G_OBJECT (self->current_frame), + (gpointer *) &self->current_frame); + } +} + +static void +tbo_drawing_set_window_pointer (TboDrawing *self, TboWindow *tbo) +{ + self->tbo = tbo; +} + +static void +tbo_drawing_set_comic_pointer (TboDrawing *self, Comic *comic) +{ + if (self->comic == comic) + return; + + if (self->comic != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (self->comic), + (gpointer *) &self->comic); + } + + self->comic = comic; + + if (self->comic != NULL) + { + g_object_add_weak_pointer (G_OBJECT (self->comic), + (gpointer *) &self->comic); + } +} + static gboolean queue_redraw_cb (gpointer data) { @@ -73,7 +124,7 @@ draw_func (GtkDrawingArea *area, cairo_t *cr, gint width, gint height, gpointer tbo_drawing_draw (TBO_DRAWING (area), cr); - tbo_tooltip_draw (cr); + tbo_tooltip_draw (cr, self); // Update drawing helpers if (self->tool) @@ -146,6 +197,12 @@ tbo_drawing_init (TboDrawing *self) self->current_frame = NULL; self->zoom = 1; self->comic = NULL; + self->tbo = NULL; + self->tooltip = NULL; + self->tooltip_x = 0; + self->tooltip_y = 0; + self->tooltip_alpha = 0.0; + self->tooltip_timeout_id = 0; self->tool = NULL; self->redraw_source_id = 0; gtk_widget_set_focusable (GTK_WIDGET (self), TRUE); @@ -165,8 +222,19 @@ tbo_drawing_init (TboDrawing *self) static void tbo_drawing_finalize (GObject *self) { - if (TBO_DRAWING (self)->redraw_source_id != 0) - g_source_remove (TBO_DRAWING (self)->redraw_source_id); + TboDrawing *drawing = TBO_DRAWING (self); + + if (drawing->redraw_source_id != 0) + g_source_remove (drawing->redraw_source_id); + + if (drawing->tooltip_timeout_id != 0) + g_source_remove (drawing->tooltip_timeout_id); + if (drawing->tooltip != NULL) + g_string_free (drawing->tooltip, TRUE); + + tbo_drawing_set_window_pointer (drawing, NULL); + tbo_drawing_set_comic_pointer (drawing, NULL); + tbo_drawing_set_current_frame_pointer (drawing, NULL); /* Chain up to the parent class */ G_OBJECT_CLASS (tbo_drawing_parent_class)->finalize (self); @@ -194,12 +262,26 @@ GtkWidget * tbo_drawing_new_with_params (Comic *comic) { GtkWidget *drawing = tbo_drawing_new (); - TBO_DRAWING (drawing)->comic = comic; - gtk_widget_set_size_request (drawing, comic->width + 2, comic->height + 2); + tbo_drawing_set_comic (TBO_DRAWING (drawing), comic); + gtk_widget_set_size_request (drawing, + tbo_comic_get_width (comic) + 2, + tbo_comic_get_height (comic) + 2); return drawing; } +void +tbo_drawing_set_comic (TboDrawing *self, Comic *comic) +{ + tbo_drawing_set_comic_pointer (self, comic); +} + +Comic * +tbo_drawing_get_comic (TboDrawing *self) +{ + return self->comic; +} + void tbo_drawing_update (TboDrawing *self) { @@ -215,7 +297,7 @@ tbo_drawing_update (TboDrawing *self) void tbo_drawing_set_current_frame (TboDrawing *self, Frame *frame) { - self->current_frame = frame; + tbo_drawing_set_current_frame_pointer (self, frame); } Frame * @@ -233,8 +315,11 @@ tbo_drawing_draw (TboDrawing *self, cairo_t *cr) int w, h; - w = self->comic->width; - h = self->comic->height; + if (self->comic == NULL) + return; + + w = tbo_comic_get_width (self->comic); + h = tbo_comic_get_height (self->comic); // white background if (tbo_drawing_get_current_frame (self)) cairo_set_source_rgb(cr, 0, 0, 0); @@ -311,8 +396,8 @@ tbo_drawing_zoom_fit (TboDrawing *self) get_view_size (self, &w, &h); - z1 = fabs ((float)w / (float)self->comic->width); - z2 = fabs ((float)h / (float)self->comic->height); + z1 = fabs ((float)w / (float)tbo_comic_get_width (self->comic)); + z2 = fabs ((float)h / (float)tbo_comic_get_height (self->comic)); self->zoom = z1 < z2 ? z1 : z2; tbo_drawing_adjust_scroll (self); } @@ -332,8 +417,8 @@ tbo_drawing_adjust_scroll (TboDrawing *self) if (!self->comic) return; - width = MAX (1, ceil (self->comic->width * self->zoom)); - height = MAX (1, ceil (self->comic->height * self->zoom)); + width = MAX (1, ceil (tbo_comic_get_width (self->comic) * self->zoom)); + height = MAX (1, ceil (tbo_comic_get_height (self->comic) * self->zoom)); gtk_widget_set_size_request (GTK_WIDGET (self), width, height); tbo_drawing_update (self); } @@ -341,5 +426,6 @@ tbo_drawing_adjust_scroll (TboDrawing *self) void tbo_drawing_init_dnd (TboDrawing *self, TboWindow *tbo) { + tbo_drawing_set_window_pointer (self, tbo); tbo_dnd_setup_drawing_dest (self, tbo); } diff --git a/src/tbo-drawing.h b/src/tbo-drawing.h index 1bcfd85..8d987c5 100644 --- a/src/tbo-drawing.h +++ b/src/tbo-drawing.h @@ -47,6 +47,12 @@ struct _TboDrawing Frame *current_frame; gdouble zoom; Comic *comic; + TboWindow *tbo; + GString *tooltip; + gint tooltip_x; + gint tooltip_y; + gdouble tooltip_alpha; + guint tooltip_timeout_id; guint redraw_source_id; }; @@ -67,6 +73,8 @@ GType tbo_drawing_get_type (void); GtkWidget * tbo_drawing_new (void); GtkWidget * tbo_drawing_new_with_params (Comic *comic); void tbo_drawing_update (TboDrawing *self); +void tbo_drawing_set_comic (TboDrawing *self, Comic *comic); +Comic * tbo_drawing_get_comic (TboDrawing *self); void tbo_drawing_set_current_frame (TboDrawing *self, Frame *frame); Frame * tbo_drawing_get_current_frame (TboDrawing *self); void tbo_drawing_draw (TboDrawing *self, cairo_t *cr); diff --git a/src/tbo-file-dialog.c b/src/tbo-file-dialog.c index 6702d80..76ddd2c 100644 --- a/src/tbo-file-dialog.c +++ b/src/tbo-file-dialog.c @@ -49,6 +49,30 @@ create_dialog (const gchar *title, TboWindow *window, const gchar *accept_label) return dialog; } +static gchar *finish_dialog (struct file_dialog_data *data); + +static gchar * +run_open_dialog (GtkFileDialog *dialog, TboWindow *window) +{ + struct file_dialog_data data = {0}; + + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_open (dialog, GTK_WINDOW (window->window), NULL, file_open_cb, &data); + g_main_loop_run (data.loop); + return finish_dialog (&data); +} + +static gchar * +run_save_dialog (GtkFileDialog *dialog, TboWindow *window) +{ + struct file_dialog_data data = {0}; + + data.loop = g_main_loop_new (NULL, FALSE); + gtk_file_dialog_save (dialog, GTK_WINDOW (window->window), NULL, file_save_cb, &data); + g_main_loop_run (data.loop); + return finish_dialog (&data); +} + static gchar * finish_dialog (struct file_dialog_data *data) { @@ -101,17 +125,15 @@ tbo_file_dialog_open_project (TboWindow *window) { GtkFileDialog *dialog = create_dialog (_("Open"), window, _("_Open")); GListStore *filters = create_project_filters (); - struct file_dialog_data data = {0}; + gchar *path; gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); set_initial_folder (dialog, tbo_window_get_open_dir (window)); - data.loop = g_main_loop_new (NULL, FALSE); - gtk_file_dialog_open (dialog, GTK_WINDOW (window->window), NULL, file_open_cb, &data); - g_main_loop_run (data.loop); + path = run_open_dialog (dialog, window); g_object_unref (filters); g_object_unref (dialog); - return finish_dialog (&data); + return path; } gchar * @@ -119,19 +141,17 @@ tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) { GtkFileDialog *dialog = create_dialog (_("Save as"), window, _("_Save")); GListStore *filters = create_project_filters (); - struct file_dialog_data data = {0}; + gchar *path; gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); set_initial_folder (dialog, tbo_window_get_open_dir (window)); if (suggested_name != NULL && *suggested_name != '\0') gtk_file_dialog_set_initial_name (dialog, suggested_name); - data.loop = g_main_loop_new (NULL, FALSE); - gtk_file_dialog_save (dialog, GTK_WINDOW (window->window), NULL, file_save_cb, &data); - g_main_loop_run (data.loop); + path = run_save_dialog (dialog, window); g_object_unref (filters); g_object_unref (dialog); - return finish_dialog (&data); + return path; } gchar * @@ -140,7 +160,7 @@ tbo_file_dialog_open_image (TboWindow *window) GtkFileDialog *dialog = create_dialog (_("Add an Image"), window, _("_Open")); GListStore *filters = g_list_store_new (GTK_TYPE_FILE_FILTER); GtkFileFilter *filter = gtk_file_filter_new (); - struct file_dialog_data data = {0}; + gchar *path; gtk_file_filter_set_name (filter, _("Image files")); gtk_file_filter_add_mime_type (filter, "image/*"); @@ -155,20 +175,18 @@ tbo_file_dialog_open_image (TboWindow *window) gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); set_initial_folder (dialog, tbo_window_get_open_dir (window)); - data.loop = g_main_loop_new (NULL, FALSE); - gtk_file_dialog_open (dialog, GTK_WINDOW (window->window), NULL, file_open_cb, &data); - g_main_loop_run (data.loop); + path = run_open_dialog (dialog, window); g_object_unref (filters); g_object_unref (dialog); - return finish_dialog (&data); + return path; } gchar * tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) { GtkFileDialog *dialog = create_dialog (_("Export as"), window, _("_Save")); - struct file_dialog_data data = {0}; + gchar *path; set_initial_folder (dialog, tbo_window_get_export_dir (window)); @@ -186,10 +204,8 @@ tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) } } - data.loop = g_main_loop_new (NULL, FALSE); - gtk_file_dialog_save (dialog, GTK_WINDOW (window->window), NULL, file_save_cb, &data); - g_main_loop_run (data.loop); + path = run_save_dialog (dialog, window); g_object_unref (dialog); - return finish_dialog (&data); + return path; } diff --git a/src/tbo-object-base.c b/src/tbo-object-base.c index 8b7a0b2..a179681 100644 --- a/src/tbo-object-base.c +++ b/src/tbo-object-base.c @@ -21,6 +21,7 @@ #include #include #include "tbo-types.h" +#include "frame.h" #include "tbo-object-base.h" G_DEFINE_TYPE (TboObjectBase, tbo_object_base, G_TYPE_OBJECT); @@ -163,7 +164,7 @@ tbo_object_base_get_flip_matrix (TboObjectBase *self, cairo_matrix_t *mx) void tbo_object_base_order_down (TboObjectBase *self, Frame *frame) { - GList *list = g_list_find (frame->objects, self); + GList *list = g_list_find (tbo_frame_get_objects (frame), self); GList *prev = g_list_previous (list); TboObjectBase *tmp; if (prev) @@ -177,7 +178,7 @@ tbo_object_base_order_down (TboObjectBase *self, Frame *frame) void tbo_object_base_order_up (TboObjectBase *self, Frame *frame) { - GList *list = g_list_find (frame->objects, self); + GList *list = g_list_find (tbo_frame_get_objects (frame), self); GList *next = g_list_next (list); TboObjectBase *tmp; if (next) diff --git a/src/tbo-object-pixmap.c b/src/tbo-object-pixmap.c index 0b5ae38..a9b929a 100644 --- a/src/tbo-object-pixmap.c +++ b/src/tbo-object-pixmap.c @@ -24,7 +24,9 @@ #include #include #include "tbo-types.h" +#include "frame.h" #include "tbo-files.h" +#include "tbo-utils.h" #include "tbo-object-pixmap.h" G_DEFINE_TYPE (TboObjectPixmap, tbo_object_pixmap, TBO_TYPE_OBJECT_BASE); @@ -175,15 +177,20 @@ static void draw (TboObjectBase *self, Frame *frame, cairo_t *cr) { TboObjectPixmap *pixmap = TBO_OBJECT_PIXMAP (self); + int frame_x = tbo_frame_get_x (frame); + int frame_y = tbo_frame_get_y (frame); + int frame_width = tbo_frame_get_width (frame); + int frame_height = tbo_frame_get_height (frame); + if (!ensure_scaled_pixbuf (self, pixmap)) return; cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; tbo_object_base_get_flip_matrix (self, &mx); - cairo_rectangle(cr, frame->x+2, frame->y+2, frame->width-4, frame->height-4); + cairo_rectangle (cr, frame_x + 2, frame_y + 2, frame_width - 4, frame_height - 4); cairo_clip (cr); - cairo_translate (cr, frame->x+self->x, frame->y+self->y); + cairo_translate (cr, frame_x + self->x, frame_y + self->y); cairo_rotate (cr, self->angle); cairo_transform (cr, &mx); @@ -192,25 +199,20 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_transform (cr, &mx); cairo_rotate (cr, -self->angle); - cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); + cairo_translate (cr, -(frame_x + self->x), -(frame_y + self->y)); cairo_reset_clip (cr); } static void save (TboObjectBase *self, FILE *file) { - char buffer[1024]; - - snprintf (buffer, 1024, " \n ", - self->x, self->y, self->width, self->height, - self->angle, self->flipv, self->fliph, TBO_OBJECT_PIXMAP (self)->path->str); - fwrite (buffer, sizeof (char), strlen (buffer), file); - - snprintf (buffer, 1024, " \n"); - fwrite (buffer, sizeof (char), strlen (buffer), file); + GString *xml = g_string_new (" path->str); + g_string_append (xml, ">\n "); + tbo_xml_write (file, xml); + fputs (" \n", file); } static TboObjectBase * diff --git a/src/tbo-object-svg.c b/src/tbo-object-svg.c index 5885971..a794052 100644 --- a/src/tbo-object-svg.c +++ b/src/tbo-object-svg.c @@ -25,7 +25,9 @@ #include #include #include "tbo-types.h" +#include "frame.h" #include "tbo-files.h" +#include "tbo-utils.h" #include "tbo-object-svg.h" G_DEFINE_TYPE (TboObjectSvg, tbo_object_svg, TBO_TYPE_OBJECT_BASE); @@ -130,6 +132,10 @@ static void draw (TboObjectBase *self, Frame *frame, cairo_t *cr) { TboObjectSvg *svg = TBO_OBJECT_SVG (self); + int frame_x = tbo_frame_get_x (frame); + int frame_y = tbo_frame_get_y (frame); + int frame_width = tbo_frame_get_width (frame); + int frame_height = tbo_frame_get_height (frame); if (!ensure_surface (self, svg)) return; @@ -137,9 +143,9 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; tbo_object_base_get_flip_matrix (self, &mx); - cairo_rectangle(cr, frame->x+2, frame->y+2, frame->width-4, frame->height-4); + cairo_rectangle (cr, frame_x + 2, frame_y + 2, frame_width - 4, frame_height - 4); cairo_clip (cr); - cairo_translate (cr, frame->x+self->x, frame->y+self->y); + cairo_translate (cr, frame_x + self->x, frame_y + self->y); cairo_rotate (cr, self->angle); cairo_transform (cr, &mx); cairo_set_source_surface (cr, svg->surface, 0, 0); @@ -147,26 +153,20 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_transform (cr, &mx); cairo_rotate (cr, -self->angle); - cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); + cairo_translate (cr, -(frame_x + self->x), -(frame_y + self->y)); cairo_reset_clip (cr); } static void save (TboObjectBase *self, FILE *file) { - char buffer[1024]; - - snprintf (buffer, 1024, " \n ", - self->x, self->y, self->width, self->height, - self->angle, self->flipv, self->fliph, - TBO_OBJECT_SVG (self)->path->str); - fwrite (buffer, sizeof (char), strlen (buffer), file); - - snprintf (buffer, 1024, " \n"); - fwrite (buffer, sizeof (char), strlen (buffer), file); + GString *xml = g_string_new (" path->str); + g_string_append (xml, ">\n "); + tbo_xml_write (file, xml); + fputs (" \n", file); } static TboObjectBase * diff --git a/src/tbo-object-text.c b/src/tbo-object-text.c index 0661d3e..2a68928 100644 --- a/src/tbo-object-text.c +++ b/src/tbo-object-text.c @@ -23,6 +23,8 @@ #include #include #include "tbo-types.h" +#include "frame.h" +#include "tbo-utils.h" #include "tbo-object-text.h" G_DEFINE_TYPE (TboObjectText, tbo_object_text, TBO_TYPE_OBJECT_BASE); @@ -36,6 +38,10 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) { TboObjectText *textobj = TBO_OBJECT_TEXT (self); gchar *text = textobj->text->str; + int frame_x = tbo_frame_get_x (frame); + int frame_y = tbo_frame_get_y (frame); + int frame_width = tbo_frame_get_width (frame); + int frame_height = tbo_frame_get_height (frame); PangoLayout *layout; PangoFontDescription *desc = textobj->description; @@ -71,9 +77,9 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_matrix_t mx = {1, 0, 0, 1, 0, 0}; tbo_object_base_get_flip_matrix (self, &mx); - cairo_rectangle(cr, frame->x+2, frame->y+2, frame->width-4, frame->height-4); + cairo_rectangle (cr, frame_x + 2, frame_y + 2, frame_width - 4, frame_height - 4); cairo_clip (cr); - cairo_translate (cr, frame->x+self->x, frame->y+self->y); + cairo_translate (cr, frame_x + self->x, frame_y + self->y); cairo_rotate (cr, self->angle); cairo_transform (cr, &mx); cairo_scale (cr, factorw, factorh); @@ -83,38 +89,35 @@ draw (TboObjectBase *self, Frame *frame, cairo_t *cr) cairo_scale (cr, 1/factorw, 1/factorh); cairo_transform (cr, &mx); cairo_rotate (cr, -self->angle); - cairo_translate (cr, -(frame->x+self->x), -(frame->y+self->y)); + cairo_translate (cr, -(frame_x + self->x), -(frame_y + self->y)); cairo_reset_clip (cr); + g_object_unref (layout); } static void save (TboObjectBase *self, FILE *file) { - char buffer[1024]; - float r, g, b; + gchar *font; + gchar *text_escaped; + GString *xml; TboObjectText *text = TBO_OBJECT_TEXT (self); - r = text->font_color->red; - g = text->font_color->green; - b = text->font_color->blue; - - snprintf (buffer, 1024, " \n", - self->x, self->y, self->width, self->height, - self->angle, self->flipv, self->fliph, - pango_font_description_to_string (text->description), - r, g, b); - fwrite (buffer, sizeof (char), strlen (buffer), file); - - snprintf (buffer, 1024, "%s", g_markup_escape_text (text->text->str, strlen (text->text->str))); - fwrite (buffer, sizeof (char), strlen (buffer), file); - - snprintf (buffer, 1024, "\n \n"); - fwrite (buffer, sizeof (char), strlen (buffer), file); + font = pango_font_description_to_string (text->description); + text_escaped = g_markup_escape_text (text->text->str, -1); + xml = g_string_new (" font_color->red); + tbo_xml_append_attr_double (xml, "g", text->font_color->green); + tbo_xml_append_attr_double (xml, "b", text->font_color->blue); + g_string_append (xml, ">\n"); + tbo_xml_write (file, xml); + fputs (text_escaped, file); + fputs ("\n \n", file); + + g_free (text_escaped); + g_free (font); } static TboObjectBase * diff --git a/src/tbo-tool-frame.c b/src/tbo-tool-frame.c index 7a9542c..48b9994 100644 --- a/src/tbo-tool-frame.c +++ b/src/tbo-tool-frame.c @@ -23,6 +23,7 @@ #include "comic.h" #include "tbo-tool-frame.h" #include "tbo-drawing.h" +#include "tbo-undo.h" G_DEFINE_TYPE (TboToolFrame, tbo_tool_frame, TBO_TYPE_TOOL_BASE); @@ -33,6 +34,7 @@ static void on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *even static void on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); static void on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event); static void drawing (TboToolBase *tool, cairo_t *cr); +static void finalize (GObject *object); /* Definitions */ @@ -57,10 +59,11 @@ on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) { x = (int)event->x; y = (int)event->y; - self->tmp_frame->width = (int)fabs (x - self->n_frame_x); - self->tmp_frame->height = (int)fabs (y - self->n_frame_y); - self->tmp_frame->x = MINIMUM (self->n_frame_x, x); - self->tmp_frame->y = MINIMUM (self->n_frame_y, y); + tbo_frame_set_bounds (self->tmp_frame, + MINIMUM (self->n_frame_x, x), + MINIMUM (self->n_frame_y, y), + (int)fabs (x - self->n_frame_x), + (int)fabs (y - self->n_frame_y)); } } tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); @@ -86,10 +89,14 @@ on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) if (w != 0 && h != 0) { - tbo_page_new_frame (tbo_comic_get_current_page (tbo->comic), + Page *page = tbo_comic_get_current_page (tbo->comic); + Frame *frame = tbo_page_new_frame (page, MINIMUM (self->n_frame_x, event->x), MINIMUM (self->n_frame_y, event->y), w, h); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_add_new (page, frame)); tbo_window_mark_dirty (tbo); + tbo_window_refresh_status (tbo); + tbo_toolbar_update (tbo->toolbar); } self->n_frame_x = -1; @@ -130,6 +137,16 @@ tbo_tool_frame_init (TboToolFrame *self) static void tbo_tool_frame_class_init (TboToolFrameClass *klass) { + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = finalize; +} + +static void +finalize (GObject *object) +{ + tbo_tool_frame_reset_state (TBO_TOOL_FRAME (object)); + G_OBJECT_CLASS (tbo_tool_frame_parent_class)->finalize (object); } /* object functions */ @@ -152,3 +169,19 @@ tbo_tool_frame_new_with_params (TboWindow *tbo) tbo_tool_base->tbo = tbo; return tbo_tool; } + +void +tbo_tool_frame_reset_state (TboToolFrame *self) +{ + if (self == NULL) + return; + + self->n_frame_x = -1; + self->n_frame_y = -1; + + if (self->tmp_frame != NULL) + { + tbo_frame_free (self->tmp_frame); + self->tmp_frame = NULL; + } +} diff --git a/src/tbo-tool-frame.h b/src/tbo-tool-frame.h index 3435513..15845f6 100644 --- a/src/tbo-tool-frame.h +++ b/src/tbo-tool-frame.h @@ -62,5 +62,6 @@ GType tbo_tool_frame_get_type (void); */ GObject * tbo_tool_frame_new (void); GObject * tbo_tool_frame_new_with_params (TboWindow *tbo); +void tbo_tool_frame_reset_state (TboToolFrame *self); #endif /* __TBO_TOOL_FRAME_H__ */ diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index 97bf78c..44ca721 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -56,6 +56,49 @@ static void frame_view_on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent static gboolean delete_selected (TboToolSelector *self); static void open_text_editor (TboToolSelector *self, TboObjectText *text); static void finalize (GObject *object); +static void tbo_tool_selector_set_selected_frame_pointer (TboToolSelector *self, Frame *frame); +static void tbo_tool_selector_set_selected_object_pointer (TboToolSelector *self, TboObjectBase *obj); + +#define MIN_FRAME_DIMENSION 1 +#define MIN_OBJECT_DIMENSION 1 +#define ANGLE_EPSILON 1e-9 + +static gint +clamp_frame_dimension (gint value) +{ + return MAX (MIN_FRAME_DIMENSION, value); +} + +static gint +clamp_object_dimension (gint value) +{ + return MAX (MIN_OBJECT_DIMENSION, value); +} + +static gboolean +frame_geometry_changed (TboToolSelector *tool) +{ + Frame *frame = tool->selected_frame; + + return frame != NULL && + (tool->start_m_x != tbo_frame_get_x (frame) || + tool->start_m_y != tbo_frame_get_y (frame) || + tool->start_m_w != tbo_frame_get_width (frame) || + tool->start_m_h != tbo_frame_get_height (frame)); +} + +static gboolean +object_geometry_changed (TboToolSelector *tool) +{ + TboObjectBase *obj = tool->selected_object; + + return obj != NULL && + (tool->start_m_x != obj->x || + tool->start_m_y != obj->y || + tool->start_m_w != obj->width || + tool->start_m_h != obj->height || + fabs (tool->start_m_angle - obj->angle) > ANGLE_EPSILON); +} /* Definitions */ @@ -64,27 +107,149 @@ static gboolean update_selected_cb (GtkSpinButton *widget, TboToolSelector *tool) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (tool)->tbo->drawing); + TboWindow *tbo = TBO_TOOL_BASE (tool)->tbo; + GdkRGBA old_color; + gint old_x; + gint old_y; + gint old_width; + gint old_height; + gboolean old_border; + gint x; + gint y; + gint width; + gint height; + if (tool->resizing || tool->clicked || tool->selected_frame == NULL || tool->spin_x == NULL) return FALSE; - tool->selected_frame->x = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_x)); - tool->selected_frame->y = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_y)); - tool->selected_frame->width = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_w)); - tool->selected_frame->height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_h)); + x = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_x)); + y = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_y)); + width = clamp_frame_dimension (gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_w))); + height = clamp_frame_dimension (gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (tool->spin_h))); + if (x == tbo_frame_get_x (tool->selected_frame) && + y == tbo_frame_get_y (tool->selected_frame) && + width == tbo_frame_get_width (tool->selected_frame) && + height == tbo_frame_get_height (tool->selected_frame)) + return FALSE; + + old_x = tbo_frame_get_x (tool->selected_frame); + old_y = tbo_frame_get_y (tool->selected_frame); + old_width = tbo_frame_get_width (tool->selected_frame); + old_height = tbo_frame_get_height (tool->selected_frame); + old_border = tbo_frame_get_border (tool->selected_frame); + tbo_frame_get_color (tool->selected_frame, &old_color); + + tbo_frame_set_bounds (tool->selected_frame, + x, + y, + width, + height); + + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_frame_state_new (tool->selected_frame, + old_x, + old_y, + old_width, + old_height, + old_border, + old_color.red, + old_color.green, + old_color.blue, + x, + y, + width, + height, + old_border, + old_color.red, + old_color.green, + old_color.blue)); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (drawing); return FALSE; } +static void +tbo_tool_selector_set_selected_frame_pointer (TboToolSelector *self, Frame *frame) +{ + if (self->selected_frame == frame) + return; + + if (self->selected_frame != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (self->selected_frame), + (gpointer *) &self->selected_frame); + } + + self->selected_frame = frame; + + if (self->selected_frame != NULL) + { + g_object_add_weak_pointer (G_OBJECT (self->selected_frame), + (gpointer *) &self->selected_frame); + } +} + +static void +tbo_tool_selector_set_selected_object_pointer (TboToolSelector *self, TboObjectBase *obj) +{ + if (self->selected_object == obj) + return; + + if (self->selected_object != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (self->selected_object), + (gpointer *) &self->selected_object); + } + + self->selected_object = obj; + + if (self->selected_object != NULL) + { + g_object_add_weak_pointer (G_OBJECT (self->selected_object), + (gpointer *) &self->selected_object); + } +} + static void update_color_cb (GtkWidget *button, GParamSpec *pspec, TboToolSelector *tool) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (tool)->tbo->drawing); + TboWindow *tbo = TBO_TOOL_BASE (tool)->tbo; + GdkRGBA current_color; + gboolean border; if (tool->resizing || tool->clicked || tool->selected_frame == NULL) return; const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (button)); + tbo_frame_get_color (tool->selected_frame, ¤t_color); + if (gdk_rgba_equal (¤t_color, color)) + return; + + border = tbo_frame_get_border (tool->selected_frame); + tbo_frame_set_color (tool->selected_frame, (GdkRGBA *) color); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_frame_state_new (tool->selected_frame, + tbo_frame_get_x (tool->selected_frame), + tbo_frame_get_y (tool->selected_frame), + tbo_frame_get_width (tool->selected_frame), + tbo_frame_get_height (tool->selected_frame), + border, + current_color.red, + current_color.green, + current_color.blue, + tbo_frame_get_x (tool->selected_frame), + tbo_frame_get_y (tool->selected_frame), + tbo_frame_get_width (tool->selected_frame), + tbo_frame_get_height (tool->selected_frame), + border, + color->red, + color->green, + color->blue)); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (drawing); } @@ -92,10 +257,39 @@ static gboolean update_border_cb (GtkCheckButton *button, TboToolSelector *tool) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (tool)->tbo->drawing); + TboWindow *tbo = TBO_TOOL_BASE (tool)->tbo; + gboolean border; + GdkRGBA color; if (tool->resizing || tool->clicked || tool->selected_frame == NULL) return FALSE; - tool->selected_frame->border = gtk_check_button_get_active (button); + border = gtk_check_button_get_active (button); + if (tbo_frame_get_border (tool->selected_frame) == border) + return FALSE; + + tbo_frame_get_color (tool->selected_frame, &color); + + tbo_frame_set_border (tool->selected_frame, border); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_frame_state_new (tool->selected_frame, + tbo_frame_get_x (tool->selected_frame), + tbo_frame_get_y (tool->selected_frame), + tbo_frame_get_width (tool->selected_frame), + tbo_frame_get_height (tool->selected_frame), + !border, + color.red, + color.green, + color.blue, + tbo_frame_get_x (tool->selected_frame), + tbo_frame_get_y (tool->selected_frame), + tbo_frame_get_width (tool->selected_frame), + tbo_frame_get_height (tool->selected_frame), + border, + color.red, + color.green, + color.blue)); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (drawing); return FALSE; } @@ -108,13 +302,21 @@ update_tool_area (TboToolSelector *self) GtkWidget *label; GdkRGBA gdk_color = { 0, 0, 0, 1 }; GtkColorDialog *color_dialog; + int frame_x, frame_y, frame_width, frame_height; + + tbo_frame_get_bounds (self->selected_frame, &frame_x, &frame_y, &frame_width, &frame_height); + tbo_frame_get_color (self->selected_frame, &gdk_color); if (!self->spin_x) { - self->spin_x = add_spin_with_label (toolarea, "x: ", self->selected_frame->x); - self->spin_y = add_spin_with_label (toolarea, "y: ", self->selected_frame->y); - self->spin_w = add_spin_with_label (toolarea, "w: ", self->selected_frame->width); - self->spin_h = add_spin_with_label (toolarea, "h: ", self->selected_frame->height); + self->spin_x = add_spin_with_label (toolarea, "x: ", frame_x); + self->spin_y = add_spin_with_label (toolarea, "y: ", frame_y); + self->spin_w = add_spin_with_label (toolarea, "w: ", frame_width); + self->spin_h = add_spin_with_label (toolarea, "h: ", frame_height); + gtk_spin_button_set_range (GTK_SPIN_BUTTON (self->spin_x), -10000, 10000); + gtk_spin_button_set_range (GTK_SPIN_BUTTON (self->spin_y), -10000, 10000); + gtk_spin_button_set_range (GTK_SPIN_BUTTON (self->spin_w), MIN_FRAME_DIMENSION, 10000); + gtk_spin_button_set_range (GTK_SPIN_BUTTON (self->spin_h), MIN_FRAME_DIMENSION, 10000); g_signal_connect (self->spin_x, "value-changed", G_CALLBACK (update_selected_cb), self); g_signal_connect (self->spin_y, "value-changed", G_CALLBACK (update_selected_cb), self); @@ -127,9 +329,6 @@ update_tool_area (TboToolSelector *self) gtk_label_set_yalign (GTK_LABEL (label), 0.5); color_dialog = gtk_color_dialog_new (); self->color_button = gtk_color_dialog_button_new (color_dialog); - gdk_color.red = self->selected_frame->color->r; - gdk_color.green = self->selected_frame->color->g; - gdk_color.blue = self->selected_frame->color->b; gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); tbo_box_pack_start (hpanel, label, TRUE, TRUE, 5); @@ -138,21 +337,18 @@ update_tool_area (TboToolSelector *self) g_signal_connect (self->color_button, "notify::rgba", G_CALLBACK (update_color_cb), self); self->border_button = gtk_check_button_new_with_label (_("border")); - gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), self->selected_frame->border); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), tbo_frame_get_border (self->selected_frame)); tbo_box_pack_start (toolarea, self->border_button, FALSE, FALSE, 5); g_signal_connect (self->border_button, "toggled", G_CALLBACK (update_border_cb), self); tbo_widget_show_all (toolarea); } - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_x), self->selected_frame->x); - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_y), self->selected_frame->y); - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_w), self->selected_frame->width); - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_h), self->selected_frame->height); - gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), self->selected_frame->border); - gdk_color.red = self->selected_frame->color->r; - gdk_color.green = self->selected_frame->color->g; - gdk_color.blue = self->selected_frame->color->b; + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_x), frame_x); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_y), frame_y); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_w), frame_width); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_h), frame_height); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), tbo_frame_get_border (self->selected_frame)); gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); } @@ -192,8 +388,8 @@ static gboolean over_resizer (TboToolSelector *self, Frame *frame, int x, int y) { int rx, ry; - rx = frame->x + frame->width; - ry = frame->y + frame->height; + rx = tbo_frame_get_x (frame) + tbo_frame_get_width (frame); + ry = tbo_frame_get_y (frame) + tbo_frame_get_height (frame); float r_size; r_size = R_SIZE / tbo_drawing_get_zoom (TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing)); @@ -261,13 +457,6 @@ over_rotater_obj (TboToolSelector *self, TboObjectBase *obj, int x, int y) } } -static gboolean -moved_frame (TboToolSelector *tool) -{ - Frame *obj = tool->selected_frame; - return (tool->start_m_x != obj->x || tool->start_m_y != obj->y); -} - static gboolean moved_object (TboToolSelector *tool) { @@ -309,23 +498,35 @@ on_release (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) TboWindow *tbo = tool->tbo; gboolean should_open_text_editor = FALSE; // TODO create undo actions for movements / resizing and rotating - if (self->selected_object && moved_object (self)) { + if (object_geometry_changed (self)) { tbo_undo_stack_insert (tbo->undo_stack, - tbo_action_object_move_new (self->selected_object, - self->start_m_x, - self->start_m_y, - self->selected_object->x, - self->selected_object->y)); + tbo_action_object_transform_new (self->selected_object, + self->start_m_x, + self->start_m_y, + self->start_m_w, + self->start_m_h, + self->start_m_angle, + self->selected_object->x, + self->selected_object->y, + self->selected_object->width, + self->selected_object->height, + self->selected_object->angle)); tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); } - else if (self->selected_frame && moved_frame (self)) { + else if (frame_geometry_changed (self)) { tbo_undo_stack_insert (tbo->undo_stack, - tbo_action_frame_move_new (self->selected_frame, - self->start_m_x, - self->start_m_y, - self->selected_frame->x, - self->selected_frame->y)); + tbo_action_frame_transform_new (self->selected_frame, + self->start_m_x, + self->start_m_y, + self->start_m_w, + self->start_m_h, + tbo_frame_get_x (self->selected_frame), + tbo_frame_get_y (self->selected_frame), + tbo_frame_get_width (self->selected_frame), + tbo_frame_get_height (self->selected_frame))); tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); } should_open_text_editor = self->edit_text_on_release && @@ -378,18 +579,25 @@ delete_selected (TboToolSelector *self) if (obj != NULL && tbo_drawing_get_current_frame (drawing) != NULL) { + gint index = tbo_frame_object_nth (frame, obj); + tbo_tool_selector_set_selected_object_pointer (self, NULL); tbo_frame_del_obj (frame, obj); - self->selected_object = NULL; + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_remove_new (frame, obj, index)); tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); update_menubar (tbo); return TRUE; } if (frame != NULL && tbo_drawing_get_current_frame (drawing) == NULL) { + gint index = tbo_page_frame_nth (page, frame); tbo_page_del_frame (page, frame); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_remove_new (page, frame, index)); tbo_tool_selector_set_selected (self, NULL); tbo_window_mark_dirty (tbo); + tbo_window_refresh_status (tbo); + tbo_toolbar_update (tbo->toolbar); return TRUE; } @@ -442,8 +650,8 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event // resizing object if (self->resizing) { - self->selected_object->width = self->start_m_w - offset_x; - self->selected_object->height = self->start_m_h - offset_y; + self->selected_object->width = clamp_object_dimension (self->start_m_w - offset_x); + self->selected_object->height = clamp_object_dimension (self->start_m_h - offset_y); } else if (self->rotating) { @@ -468,6 +676,7 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event self->start_m_y = self->selected_object->y; self->start_m_w = self->selected_object->width; self->start_m_h = self->selected_object->height; + self->start_m_angle = self->selected_object->angle; } tbo_object_group_set_vars (self->selected_object); @@ -524,7 +733,7 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *even { frame = tbo_drawing_get_current_frame (drawing); - for (obj_list = g_list_first (frame->objects); obj_list; obj_list = obj_list->next) + for (obj_list = tbo_frame_get_objects (frame); obj_list; obj_list = obj_list->next) { obj = TBO_OBJECT_BASE (obj_list->data); tbo_object_group_set_vars (obj); @@ -572,6 +781,7 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *even self->start_m_y = self->selected_object->y; self->start_m_w = self->selected_object->width; self->start_m_h = self->selected_object->height; + self->start_m_angle = self->selected_object->angle; } self->clicked = TRUE; } @@ -596,13 +806,13 @@ frame_view_drawing (TboToolBase *tool, cairo_t *cr) if (current_obj != NULL && !G_IS_OBJECT (current_obj)) { - self->selected_object = NULL; + tbo_tool_selector_set_selected_object_pointer (self, NULL); current_obj = NULL; } - if (current_obj != NULL && frame != NULL && g_list_find (frame->objects, current_obj) == NULL) + if (current_obj != NULL && frame != NULL && !tbo_frame_has_obj (frame, current_obj)) { - self->selected_object = NULL; + tbo_tool_selector_set_selected_object_pointer (self, NULL); current_obj = NULL; } @@ -705,6 +915,12 @@ frame_view_on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event) if (current_obj != NULL) { + int x1 = current_obj->x; + int y1 = current_obj->y; + int width1 = current_obj->width; + int height1 = current_obj->height; + gdouble angle1 = current_obj->angle; + switch (event.keyval) { case GDK_KEY_less: @@ -728,6 +944,28 @@ frame_view_on_key (TboToolBase *tool, GtkWidget *widget, TboKeyEvent event) default: break; } + + if (x1 != current_obj->x || + y1 != current_obj->y || + width1 != current_obj->width || + height1 != current_obj->height || + fabs (angle1 - current_obj->angle) > ANGLE_EPSILON) + { + tbo_undo_stack_insert (tool->tbo->undo_stack, + tbo_action_object_transform_new (current_obj, + x1, + y1, + width1, + height1, + angle1, + current_obj->x, + current_obj->y, + current_obj->width, + current_obj->height, + current_obj->angle)); + tbo_window_mark_dirty (tool->tbo); + tbo_toolbar_update (tool->tbo->toolbar); + } } tbo_drawing_update (drawing); } @@ -750,12 +988,16 @@ page_view_drawing (TboToolBase *tool, cairo_t *cr) if (selected != NULL) { + int selected_x = tbo_frame_get_x (selected); + int selected_y = tbo_frame_get_y (selected); + int selected_width = tbo_frame_get_width (selected); + int selected_height = tbo_frame_get_height (selected); + cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); cairo_set_line_width (cr, 1); cairo_set_dash (cr, dashes, G_N_ELEMENTS (dashes), 0); cairo_set_source_rgb (cr, border.r, border.g, border.b); - cairo_rectangle (cr, selected->x, selected->y, - selected->width, selected->height); + cairo_rectangle (cr, selected_x, selected_y, selected_width, selected_height); cairo_stroke (cr); // resizer @@ -773,8 +1015,8 @@ page_view_drawing (TboToolBase *tool, cairo_t *cr) cairo_set_line_width (cr, 1); cairo_set_dash (cr, dashes, 0, 0); - x = selected->x + selected->width; - y = selected->y + selected->height; + x = selected_x + selected_width; + y = selected_y + selected_height; r_size = R_SIZE / tbo_drawing_get_zoom (drawing); cairo_set_line_width (cr, 1 / tbo_drawing_get_zoom (drawing)); @@ -843,10 +1085,10 @@ page_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event if (selected) { - self->start_m_x = selected->x; - self->start_m_y = selected->y; - self->start_m_w = selected->width; - self->start_m_h = selected->height; + self->start_m_x = tbo_frame_get_x (selected); + self->start_m_y = tbo_frame_get_y (selected); + self->start_m_w = tbo_frame_get_width (selected); + self->start_m_h = tbo_frame_get_height (selected); tbo_page_set_current_frame (page, selected); } self->clicked = TRUE; @@ -873,16 +1115,18 @@ page_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) // resizing frame if (self->resizing) { - selected->width = abs (self->start_m_w - offset_x); - selected->height = abs (self->start_m_h - offset_y); + tbo_frame_set_size (selected, + clamp_frame_dimension (abs (self->start_m_w - offset_x)), + clamp_frame_dimension (abs (self->start_m_h - offset_y))); update_tool_area (self); } // moving frame else { - selected->x = self->start_m_x - offset_x; - selected->y = self->start_m_y - offset_y; + tbo_frame_set_position (selected, + self->start_m_x - offset_x, + self->start_m_y - offset_y); update_tool_area (self); } @@ -910,8 +1154,8 @@ page_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) if (tbo_frame_point_inside ((Frame*)frame_list->data, x, y)) { frame = (Frame*)frame_list->data; - x1 = frame->x + (frame->width / 2); - y1 = frame->y + (frame->height / 2); + x1 = tbo_frame_get_x (frame) + (tbo_frame_get_width (frame) / 2); + y1 = tbo_frame_get_y (frame) + (tbo_frame_get_height (frame) / 2); tbo_tooltip_set (_("double click or press Enter"), x1, y1, tbo); found = TRUE; } @@ -933,6 +1177,7 @@ tbo_tool_selector_init (TboToolSelector *self) self->start_m_y = 0; self->start_m_w = 0; self->start_m_h = 0; + self->start_m_angle = 0.0; self->clicked = FALSE; self->edit_text_on_release = FALSE; self->over_resizer = FALSE; @@ -969,6 +1214,9 @@ finalize (GObject *object) { TboToolSelector *self = TBO_TOOL_SELECTOR (object); + tbo_tool_selector_set_selected_frame_pointer (self, NULL); + tbo_tool_selector_set_selected_object_pointer (self, NULL); + if (self->toolarea_widget != NULL) g_object_unref (self->toolarea_widget); @@ -1017,7 +1265,7 @@ tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame) return; } - self->selected_frame = frame; + tbo_tool_selector_set_selected_frame_pointer (self, frame); if (self->selected_frame != NULL) update_tool_area (self); update_menubar (TBO_TOOL_BASE (self)->tbo); @@ -1030,10 +1278,10 @@ tbo_tool_selector_set_selected_obj (TboToolSelector *self, TboObjectBase *obj) { TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing); Frame *frame = tbo_drawing_get_current_frame (drawing); - if (frame != NULL && g_list_find (frame->objects, self->selected_object) != NULL) + if (frame != NULL && tbo_frame_has_obj (frame, self->selected_object)) tbo_frame_del_obj (frame, self->selected_object); } - self->selected_object = obj; + tbo_tool_selector_set_selected_object_pointer (self, obj); update_menubar (TBO_TOOL_BASE (self)->tbo); } @@ -1043,21 +1291,106 @@ tbo_tool_selector_delete_selected (TboToolSelector *self) return delete_selected (self); } +void +tbo_tool_selector_reset_state (TboToolSelector *self) +{ + if (self == NULL) + return; + + tbo_tool_selector_set_selected_frame_pointer (self, NULL); + tbo_tool_selector_set_selected_object_pointer (self, NULL); + self->x = 0; + self->y = 0; + self->start_x = 0; + self->start_y = 0; + self->start_m_x = 0; + self->start_m_y = 0; + self->start_m_w = 0; + self->start_m_h = 0; + self->start_m_angle = 0.0; + self->clicked = FALSE; + self->edit_text_on_release = FALSE; + self->over_resizer = FALSE; + self->over_rotater = FALSE; + self->resizing = FALSE; + self->rotating = FALSE; +} + static void frame_move_do (TboAction *act) { TboActionFrameMove *action = (TboActionFrameMove*)act; - action->frame->x = action->x2; - action->frame->y = action->y2; + + if (action->frame == NULL) + return; + + tbo_frame_set_position (action->frame, action->x2, action->y2); +} + +static void +frame_transform_do (TboAction *act) +{ + TboActionFrameTransform *action = (TboActionFrameTransform *) act; + + if (action->frame == NULL) + return; + + tbo_frame_set_bounds (action->frame, + action->x2, + action->y2, + action->width2, + action->height2); +} + +static void +frame_transform_undo (TboAction *act) +{ + TboActionFrameTransform *action = (TboActionFrameTransform *) act; + + if (action->frame == NULL) + return; + + tbo_frame_set_bounds (action->frame, + action->x1, + action->y1, + action->width1, + action->height1); +} + +static void +frame_transform_free (TboAction *act) +{ + TboActionFrameTransform *action = (TboActionFrameTransform *) act; + + if (action->frame != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (action->frame), + (gpointer *) &action->frame); + } } static void frame_move_undo (TboAction *act) { TboActionFrameMove *action = (TboActionFrameMove*)act; - action->frame->x = action->x1; - action->frame->y = action->y1; + + if (action->frame == NULL) + return; + + tbo_frame_set_position (action->frame, action->x1, action->y1); +} + +static void +frame_move_free (TboAction *act) +{ + TboActionFrameMove *action = (TboActionFrameMove*)act; + + if (action->frame != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (action->frame), + (gpointer *) &action->frame); + } } TboAction * @@ -1071,25 +1404,130 @@ tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2) action->y2 = y2; action->action_do = frame_move_do; action->action_undo = frame_move_undo; + action->action_free = frame_move_free; + + if (action->frame != NULL) + { + g_object_add_weak_pointer (G_OBJECT (action->frame), + (gpointer *) &action->frame); + } + return (TboAction*)action; } +TboAction * +tbo_action_frame_transform_new (Frame *frame, + int x1, + int y1, + int width1, + int height1, + int x2, + int y2, + int width2, + int height2) +{ + TboActionFrameTransform *action = (TboActionFrameTransform *) tbo_action_new (TboActionFrameTransform); + + action->frame = frame; + action->x1 = x1; + action->y1 = y1; + action->width1 = width1; + action->height1 = height1; + action->x2 = x2; + action->y2 = y2; + action->width2 = width2; + action->height2 = height2; + action->action_do = frame_transform_do; + action->action_undo = frame_transform_undo; + action->action_free = frame_transform_free; + + if (action->frame != NULL) + { + g_object_add_weak_pointer (G_OBJECT (action->frame), + (gpointer *) &action->frame); + } + + return (TboAction *) action; +} + static void obj_move_do (TboAction *act) { TboActionObjMove *action = (TboActionObjMove*)act; + + if (action->obj == NULL) + return; + + action->obj->x = action->x2; + action->obj->y = action->y2; +} + +static void +obj_transform_do (TboAction *act) +{ + TboActionObjTransform *action = (TboActionObjTransform *) act; + + if (action->obj == NULL) + return; + action->obj->x = action->x2; action->obj->y = action->y2; + action->obj->width = action->width2; + action->obj->height = action->height2; + action->obj->angle = action->angle2; +} + +static void +obj_transform_undo (TboAction *act) +{ + TboActionObjTransform *action = (TboActionObjTransform *) act; + + if (action->obj == NULL) + return; + + action->obj->x = action->x1; + action->obj->y = action->y1; + action->obj->width = action->width1; + action->obj->height = action->height1; + action->obj->angle = action->angle1; +} + +static void +obj_transform_free (TboAction *act) +{ + TboActionObjTransform *action = (TboActionObjTransform *) act; + + if (action->obj != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (action->obj), + (gpointer *) &action->obj); + } } static void obj_move_undo (TboAction *act) { TboActionObjMove *action = (TboActionObjMove*)act; + + if (action->obj == NULL) + return; + action->obj->x = action->x1; action->obj->y = action->y1; } +static void +obj_move_free (TboAction *act) +{ + TboActionObjMove *action = (TboActionObjMove*)act; + + if (action->obj != NULL) + { + g_object_remove_weak_pointer (G_OBJECT (action->obj), + (gpointer *) &action->obj); + } +} + TboAction * tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2) { @@ -1101,5 +1539,52 @@ tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y action->y2 = y2; action->action_do = obj_move_do; action->action_undo = obj_move_undo; + action->action_free = obj_move_free; + + if (action->obj != NULL) + { + g_object_add_weak_pointer (G_OBJECT (action->obj), + (gpointer *) &action->obj); + } + return (TboAction*)action; } + +TboAction * +tbo_action_object_transform_new (TboObjectBase *object, + int x1, + int y1, + int width1, + int height1, + gdouble angle1, + int x2, + int y2, + int width2, + int height2, + gdouble angle2) +{ + TboActionObjTransform *action = (TboActionObjTransform *) tbo_action_new (TboActionObjTransform); + + action->obj = object; + action->x1 = x1; + action->y1 = y1; + action->width1 = width1; + action->height1 = height1; + action->angle1 = angle1; + action->x2 = x2; + action->y2 = y2; + action->width2 = width2; + action->height2 = height2; + action->angle2 = angle2; + action->action_do = obj_transform_do; + action->action_undo = obj_transform_undo; + action->action_free = obj_transform_free; + + if (action->obj != NULL) + { + g_object_add_weak_pointer (G_OBJECT (action->obj), + (gpointer *) &action->obj); + } + + return (TboAction *) action; +} diff --git a/src/tbo-tool-selector.h b/src/tbo-tool-selector.h index 39de908..5011351 100644 --- a/src/tbo-tool-selector.h +++ b/src/tbo-tool-selector.h @@ -55,6 +55,7 @@ struct _TboToolSelector gint start_m_y; gint start_m_w; gint start_m_h; + gdouble start_m_angle; gboolean clicked; gboolean edit_text_on_release; gboolean over_resizer; @@ -87,6 +88,7 @@ Frame * tbo_tool_selector_get_selected_frame (TboToolSelector *self); TboObjectBase * tbo_tool_selector_get_selected_obj (TboToolSelector *self); void tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame); void tbo_tool_selector_set_selected_obj (TboToolSelector *self, TboObjectBase *obj); +void tbo_tool_selector_reset_state (TboToolSelector *self); gboolean tbo_tool_selector_delete_selected (TboToolSelector *self); GObject * tbo_tool_selector_new (void); GObject * tbo_tool_selector_new_with_params (TboWindow *tbo); @@ -95,11 +97,14 @@ GObject * tbo_tool_selector_new_with_params (TboWindow *tbo); * TboActionFrameMove for undo and redo frame movements */ typedef struct _TboActionFrameMove TboActionFrameMove; +typedef struct _TboActionFrameTransform TboActionFrameTransform; typedef struct _TboActionObjMove TboActionObjMove; +typedef struct _TboActionObjTransform TboActionObjTransform; struct _TboActionFrameMove { void (*action_do) (TboAction *action); void (*action_undo) (TboAction *action); + void (*action_free) (TboAction *action); Frame *frame; int x1; int y1; @@ -108,9 +113,34 @@ struct _TboActionFrameMove { }; TboAction * tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2); +struct _TboActionFrameTransform { + void (*action_do) (TboAction *action); + void (*action_undo) (TboAction *action); + void (*action_free) (TboAction *action); + Frame *frame; + int x1; + int y1; + int width1; + int height1; + int x2; + int y2; + int width2; + int height2; +}; +TboAction * tbo_action_frame_transform_new (Frame *frame, + int x1, + int y1, + int width1, + int height1, + int x2, + int y2, + int width2, + int height2); + struct _TboActionObjMove { void (*action_do) (TboAction *action); void (*action_undo) (TboAction *action); + void (*action_free) (TboAction *action); TboObjectBase *obj; int x1; int y1; @@ -119,5 +149,33 @@ struct _TboActionObjMove { }; TboAction * tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2); +struct _TboActionObjTransform { + void (*action_do) (TboAction *action); + void (*action_undo) (TboAction *action); + void (*action_free) (TboAction *action); + TboObjectBase *obj; + int x1; + int y1; + int width1; + int height1; + gdouble angle1; + int x2; + int y2; + int width2; + int height2; + gdouble angle2; +}; +TboAction * tbo_action_object_transform_new (TboObjectBase *object, + int x1, + int y1, + int width1, + int height1, + gdouble angle1, + int x2, + int y2, + int width2, + int height2, + gdouble angle2); + #endif /* __TBO_TOOL_SELECTOR_H__ */ diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index bd27a85..dde0c70 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -24,6 +24,7 @@ #include "tbo-object-base.h" #include "tbo-tool-selector.h" #include "tbo-tool-text.h" +#include "tbo-undo.h" G_DEFINE_TYPE (TboToolText, tbo_tool_text, TBO_TYPE_TOOL_BASE); @@ -39,6 +40,71 @@ static void finalize (GObject *object); static gint tbo_tool_text_get_font_size (TboToolText *self); static gchar *tbo_tool_text_build_font (TboToolText *self); static void tbo_tool_text_sync_font_controls (TboToolText *self, const gchar *font_string); +static void on_text_begin_user_action (GtkTextBuffer *buf, TboToolText *self); +static void on_text_end_user_action (GtkTextBuffer *buf, TboToolText *self); +static void tbo_tool_text_capture_state (TboToolText *self); +static void tbo_tool_text_clear_capture_state (TboToolText *self); +static gboolean flush_pending_text_change (gpointer data); + +static void +tbo_tool_text_capture_state (TboToolText *self) +{ + if (self->text_selected == NULL || self->captured_text != NULL) + return; + + self->captured_text = g_strdup (tbo_object_text_get_text (self->text_selected)); + self->captured_font = tbo_object_text_get_string (self->text_selected); + self->captured_color = *self->text_selected->font_color; +} + +static void +tbo_tool_text_clear_capture_state (TboToolText *self) +{ + g_clear_pointer (&self->captured_text, g_free); + g_clear_pointer (&self->captured_font, g_free); +} + +static gboolean +flush_pending_text_change (gpointer data) +{ + TboToolText *self = TBO_TOOL_TEXT (data); + TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + gchar *text; + gchar *font; + GtkTextIter start, end; + + self->pending_text_change_id = 0; + + if (self->text_selected == NULL || self->captured_text == NULL) + { + tbo_tool_text_clear_capture_state (self); + return G_SOURCE_REMOVE; + } + + gtk_text_buffer_get_start_iter (self->text_buffer, &start); + gtk_text_buffer_get_end_iter (self->text_buffer, &end); + text = gtk_text_buffer_get_text (self->text_buffer, &start, &end, FALSE); + font = tbo_object_text_get_string (self->text_selected); + + if (g_strcmp0 (self->captured_text, text) != 0) + { + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_text_state_new (self->text_selected, + self->captured_text, + self->captured_font, + &self->captured_color, + text, + font, + self->text_selected->font_color)); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } + + g_free (text); + g_free (font); + tbo_tool_text_clear_capture_state (self); + return G_SOURCE_REMOVE; +} /* Definitions */ @@ -59,27 +125,132 @@ on_text_change (GtkTextBuffer *buf, TboToolText *self) gtk_text_buffer_get_start_iter (buf, &start); gtk_text_buffer_get_end_iter (buf, &end); + if (self->syncing_controls) + return FALSE; + if (self->text_selected) { + gchar *old_text = g_strdup (tbo_object_text_get_text (self->text_selected)); + gchar *old_font = tbo_object_text_get_string (self->text_selected); gchar *text = gtk_text_buffer_get_text (buf, &start, &end, FALSE); + + if (g_strcmp0 (old_text, text) == 0) + { + g_free (old_text); + g_free (old_font); + g_free (text); + return FALSE; + } + + if (!self->text_capture_active) + tbo_tool_text_capture_state (self); + tbo_object_text_set_text (self->text_selected, text); + if (!self->text_capture_active) + { + if (self->pending_text_change_id == 0) + self->pending_text_change_id = g_idle_add (flush_pending_text_change, self); + } + g_free (old_text); + g_free (old_font); g_free (text); - tbo_window_mark_dirty (tbo); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } return FALSE; } +static void +on_text_begin_user_action (GtkTextBuffer *buf, TboToolText *self) +{ + (void) buf; + + if (self->syncing_controls || self->text_selected == NULL || self->text_capture_active) + return; + + self->text_capture_active = TRUE; + tbo_tool_text_capture_state (self); +} + +static void +on_text_end_user_action (GtkTextBuffer *buf, TboToolText *self) +{ + TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + GtkTextIter start, end; + gchar *text; + gchar *font; + + (void) buf; + + if (!self->text_capture_active || self->text_selected == NULL) + return; + + if (self->pending_text_change_id != 0) + { + g_source_remove (self->pending_text_change_id); + self->pending_text_change_id = 0; + } + + gtk_text_buffer_get_start_iter (self->text_buffer, &start); + gtk_text_buffer_get_end_iter (self->text_buffer, &end); + text = gtk_text_buffer_get_text (self->text_buffer, &start, &end, FALSE); + font = tbo_object_text_get_string (self->text_selected); + + if (g_strcmp0 (self->captured_text, text) != 0) + { + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_text_state_new (self->text_selected, + self->captured_text, + self->captured_font, + &self->captured_color, + text, + font, + self->text_selected->font_color)); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } + + g_free (text); + g_free (font); + tbo_tool_text_clear_capture_state (self); + self->text_capture_active = FALSE; +} + static void on_font_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + if (self->syncing_controls) + return; + if (self->text_selected) { + gchar *old_text = g_strdup (tbo_object_text_get_text (self->text_selected)); + gchar *old_font = tbo_object_text_get_string (self->text_selected); + GdkRGBA old_color = *self->text_selected->font_color; gchar *font = tbo_tool_text_build_font (self); + + if (g_strcmp0 (old_font, font) == 0) + { + g_free (old_text); + g_free (old_font); + g_free (font); + return; + } + tbo_object_text_change_font (self->text_selected, font); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_text_state_new (self->text_selected, + old_text, + old_font, + &old_color, + old_text, + font, + &old_color)); + g_free (old_text); + g_free (old_font); g_free (font); tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } } @@ -95,11 +266,36 @@ static void on_color_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) { TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; + if (self->syncing_controls) + return; + if (self->text_selected) { + gchar *old_text = g_strdup (tbo_object_text_get_text (self->text_selected)); + gchar *old_font = tbo_object_text_get_string (self->text_selected); + GdkRGBA old_color = *self->text_selected->font_color; const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color)); + + if (gdk_rgba_equal (&old_color, color)) + { + g_free (old_text); + g_free (old_font); + return; + } + tbo_object_text_change_color (self->text_selected, (GdkRGBA *) color); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_text_state_new (self->text_selected, + old_text, + old_font, + &old_color, + old_text, + old_font, + color)); + g_free (old_text); + g_free (old_font); tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } } @@ -170,6 +366,8 @@ setup_toolarea (TboToolText *self) self->text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); gtk_text_buffer_set_text (self->text_buffer, "", -1); g_signal_connect (self->text_buffer, "changed", G_CALLBACK (on_text_change), self); + g_signal_connect (self->text_buffer, "begin-user-action", G_CALLBACK (on_text_begin_user_action), self); + g_signal_connect (self->text_buffer, "end-user-action", G_CALLBACK (on_text_end_user_action), self); tbo_scrolled_window_set_child (scroll, view); tbo_box_pack_start (vbox, scroll, FALSE, FALSE, 5); @@ -222,7 +420,7 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) return; } - for (obj_list = g_list_first (frame->objects); obj_list; obj_list = obj_list->next) + for (obj_list = tbo_frame_get_objects (frame); obj_list; obj_list = obj_list->next) { obj = TBO_OBJECT_BASE (obj_list->data); if (TBO_IS_OBJECT_TEXT (obj) && tbo_frame_point_inside_obj (obj, x, y)) @@ -236,7 +434,7 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) x = tbo_frame_get_base_x (x); y = tbo_frame_get_base_y (y); - if (x < 0 || y < 0 || x > frame->width || y > frame->height) + if (x < 0 || y < 0 || x > tbo_frame_get_width (frame) || y > tbo_frame_get_height (frame)) return; gchar *font = tbo_tool_text_build_font (self); @@ -247,7 +445,10 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) &color)); g_free (font); tbo_frame_add_obj (frame, TBO_OBJECT_BASE (text)); + tbo_undo_stack_insert (tool->tbo->undo_stack, + tbo_action_object_add_new (frame, TBO_OBJECT_BASE (text))); tbo_window_mark_dirty (tool->tbo); + tbo_toolbar_update (tool->tbo->toolbar); } tbo_tool_text_set_selected (self, text); tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); @@ -288,6 +489,11 @@ tbo_tool_text_init (TboToolText *self) self->font_color = NULL; self->text_selected = NULL; self->text_buffer = NULL; + self->syncing_controls = FALSE; + self->text_capture_active = FALSE; + self->pending_text_change_id = 0; + self->captured_text = NULL; + self->captured_font = NULL; self->parent_instance.on_select = on_select; self->parent_instance.on_unselect = on_unselect; @@ -306,6 +512,12 @@ tbo_tool_text_class_init (TboToolTextClass *klass) static void finalize (GObject *object) { + TboToolText *self = TBO_TOOL_TEXT (object); + + if (self->pending_text_change_id != 0) + g_source_remove (self->pending_text_change_id); + tbo_tool_text_clear_capture_state (self); + tbo_tool_text_reset_state (TBO_TOOL_TEXT (object)); G_OBJECT_CLASS (tbo_tool_text_parent_class)->finalize (object); } @@ -412,6 +624,15 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) { char *str; + if (self->pending_text_change_id != 0) + { + g_source_remove (self->pending_text_change_id); + self->pending_text_change_id = 0; + } + self->text_capture_active = FALSE; + tbo_tool_text_clear_capture_state (self); + self->syncing_controls = TRUE; + if (self->text_selected) { g_object_unref (self->text_selected); self->text_selected = NULL; @@ -427,6 +648,7 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) GdkRGBA default_color = { 0, 0, 0, 1 }; gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), &default_color); } + self->syncing_controls = FALSE; return; } @@ -438,4 +660,27 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), text->font_color); gtk_text_buffer_set_text (self->text_buffer, str, -1); self->text_selected = g_object_ref (text); + self->syncing_controls = FALSE; +} + +void +tbo_tool_text_reset_state (TboToolText *self) +{ + if (self == NULL) + return; + + if (self->pending_text_change_id != 0) + { + g_source_remove (self->pending_text_change_id); + self->pending_text_change_id = 0; + } + + self->text_capture_active = FALSE; + tbo_tool_text_clear_capture_state (self); + + if (self->text_selected == NULL) + return; + + g_object_unref (self->text_selected); + self->text_selected = NULL; } diff --git a/src/tbo-tool-text.h b/src/tbo-tool-text.h index 0242fa7..4dab37f 100644 --- a/src/tbo-tool-text.h +++ b/src/tbo-tool-text.h @@ -47,6 +47,12 @@ struct _TboToolText GtkWidget *font_color; TboObjectText *text_selected; GtkTextBuffer *text_buffer; + gboolean syncing_controls; + gboolean text_capture_active; + guint pending_text_change_id; + gchar *captured_text; + gchar *captured_font; + GdkRGBA captured_color; }; struct _TboToolTextClass @@ -67,5 +73,6 @@ GObject * tbo_tool_text_new_with_params (TboWindow *tbo); gchar * tbo_tool_text_get_pango_font (TboToolText *self); gchar * tbo_tool_text_get_font_name (TboToolText *self); void tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text); +void tbo_tool_text_reset_state (TboToolText *self); #endif /* __TBO_TOOL_TEXT_H__ */ diff --git a/src/tbo-toolbar.c b/src/tbo-toolbar.c index 5ddcfa8..a0e3651 100644 --- a/src/tbo-toolbar.c +++ b/src/tbo-toolbar.c @@ -127,12 +127,14 @@ static gboolean add_new_page (GtkWidget *widget, TboWindow *tbo) { Page *page = tbo_comic_new_page (tbo->comic); + gint index = tbo_comic_page_nth (tbo->comic, page); tbo_window_add_page_widget (tbo, create_darea (tbo)); tbo_comic_set_current_page (tbo->comic, page); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_page_add_new (tbo->comic, page, index)); tbo_window_set_current_tab_page (tbo, TRUE); tbo_window_mark_dirty (tbo); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); return FALSE; } @@ -141,12 +143,20 @@ static gboolean del_current_page (GtkWidget *widget, TboWindow *tbo) { int nth = tbo_comic_page_index (tbo->comic); + Page *page = tbo_comic_get_current_page (tbo->comic); + + if (page == NULL) + return FALSE; + + g_object_ref (page); tbo_comic_del_current_page (tbo->comic); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_page_remove_new (tbo->comic, page, nth)); tbo_window_remove_page_widget (tbo, nth); tbo_window_set_current_tab_page (tbo, TRUE); tbo_window_mark_dirty (tbo); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); + g_object_unref (page); return FALSE; } @@ -156,7 +166,7 @@ next_page (GtkWidget *widget, TboWindow *tbo) tbo_comic_next_page (tbo->comic); tbo_window_set_current_tab_page (tbo, TRUE); tbo_toolbar_update (tbo->toolbar); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_drawing_adjust_scroll (TBO_DRAWING (tbo->drawing)); return FALSE; @@ -168,7 +178,7 @@ prev_page (GtkWidget *widget, TboWindow *tbo) tbo_comic_prev_page (tbo->comic); tbo_window_set_current_tab_page (tbo, TRUE); tbo_toolbar_update (tbo->toolbar); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_drawing_adjust_scroll (TBO_DRAWING (tbo->drawing)); return FALSE; @@ -247,10 +257,10 @@ generate_toolbar (TboToolbar *self) section = create_section_box (); self->button_new = create_button (create_project_icon ("icons/new.svg"), _("New comic")); self->button_open = create_button (create_icon_from_name ("document-open-symbolic"), _("Open comic")); - self->button_save = create_button (create_icon_from_name ("document-save-as-symbolic"), _("Save comic as")); + self->button_save = create_button (create_icon_from_name ("document-save-symbolic"), _("Save comic")); g_signal_connect (self->button_new, "clicked", G_CALLBACK (tbo_comic_new_dialog), self->tbo); g_signal_connect (self->button_open, "clicked", G_CALLBACK (tbo_comic_open_dialog), self->tbo); - g_signal_connect (self->button_save, "clicked", G_CALLBACK (tbo_comic_saveas_dialog), self->tbo); + g_signal_connect (self->button_save, "clicked", G_CALLBACK (tbo_comic_save_dialog), self->tbo); tbo_box_pack_start (section, self->button_new, FALSE, FALSE, 0); tbo_box_pack_start (section, self->button_open, FALSE, FALSE, 0); tbo_box_pack_start (section, self->button_save, FALSE, FALSE, 0); diff --git a/src/tbo-tooltip.c b/src/tbo-tooltip.c index d48b6df..c8eb8b0 100644 --- a/src/tbo-tooltip.c +++ b/src/tbo-tooltip.c @@ -28,9 +28,37 @@ #define TOOLTIP_ALPHA 0.7 -static GString *TOOLTIP = NULL; -static int X=0, Y=0; -static double ALPHA = 0.0; +static TboDrawing * +get_tooltip_drawing (TboWindow *tbo) +{ + if (tbo == NULL || tbo->drawing == NULL || !TBO_IS_DRAWING (tbo->drawing)) + return NULL; + + return TBO_DRAWING (tbo->drawing); +} + +static void +clear_tooltip (TboDrawing *drawing, gboolean clear_timeout) +{ + if (drawing == NULL) + return; + + if (clear_timeout && drawing->tooltip_timeout_id != 0) + { + g_source_remove (drawing->tooltip_timeout_id); + drawing->tooltip_timeout_id = 0; + } + + if (drawing->tooltip != NULL) + { + g_string_free (drawing->tooltip, TRUE); + drawing->tooltip = NULL; + } + + drawing->tooltip_x = 0; + drawing->tooltip_y = 0; + drawing->tooltip_alpha = 0.0; +} void cairo_rounded_rectangle (cairo_t *cr, int xx, int yy, int w, int h) @@ -56,16 +84,20 @@ cairo_rounded_rectangle (cairo_t *cr, int xx, int yy, int w, int h) gboolean quit_tooltip_cb (gpointer p) { - tbo_tooltip_set (NULL, 0, 0, (TboWindow*) p); + TboDrawing *drawing = TBO_DRAWING (p); + + drawing->tooltip_timeout_id = 0; + clear_tooltip (drawing, FALSE); + tbo_drawing_update (drawing); return FALSE; } void -tbo_tooltip_draw_background (cairo_t *cr, int w, int h) +tbo_tooltip_draw_background (cairo_t *cr, int w, int h, TboDrawing *drawing) { int margin = 5; - cairo_set_source_rgba (cr, 0, 0, 0, ALPHA); + cairo_set_source_rgba (cr, 0, 0, 0, drawing->tooltip_alpha); cairo_rounded_rectangle (cr, -margin, -margin, w + margin * 2, h + margin * 2); cairo_fill (cr); } @@ -73,59 +105,67 @@ tbo_tooltip_draw_background (cairo_t *cr, int w, int h) void tbo_tooltip_set (const char *tooltip, int x, int y, TboWindow *tbo) { + TboDrawing *drawing = get_tooltip_drawing (tbo); + + if (drawing == NULL) + return; + + if (drawing->tooltip_timeout_id != 0) + { + g_source_remove (drawing->tooltip_timeout_id); + drawing->tooltip_timeout_id = 0; + } - if (!TOOLTIP) + if (drawing->tooltip == NULL) { - if (tooltip) + if (tooltip != NULL) { - TOOLTIP = g_string_new (tooltip); - ALPHA = TOOLTIP_ALPHA; - X = x; - Y = y; + drawing->tooltip = g_string_new (tooltip); + drawing->tooltip_alpha = TOOLTIP_ALPHA; + drawing->tooltip_x = x; + drawing->tooltip_y = y; } } else { - // if it's the same passing - if (tooltip != NULL && x == X && y == Y && !strcmp (tooltip, TOOLTIP->str)) + if (tooltip != NULL && + x == drawing->tooltip_x && + y == drawing->tooltip_y && + !strcmp (tooltip, drawing->tooltip->str)) return; - // removing tooltip if tooltip == NULL - if (!tooltip && TOOLTIP) + if (tooltip == NULL) { - ALPHA = 0.0; - g_string_free (TOOLTIP, TRUE); - TOOLTIP = NULL; + clear_tooltip (drawing, FALSE); } else { - g_string_free (TOOLTIP, TRUE); - - TOOLTIP = g_string_new (tooltip); - ALPHA = TOOLTIP_ALPHA; - X = x; - Y = y; + g_string_free (drawing->tooltip, TRUE); + drawing->tooltip = g_string_new (tooltip); + drawing->tooltip_alpha = TOOLTIP_ALPHA; + drawing->tooltip_x = x; + drawing->tooltip_y = y; } } - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_drawing_update (drawing); } -GString * -tbo_tooltip_get (void) +void +tbo_tooltip_reset (TboWindow *tbo) { - return TOOLTIP; + clear_tooltip (get_tooltip_drawing (tbo), TRUE); } void -tbo_tooltip_draw (cairo_t *cr) +tbo_tooltip_draw (cairo_t *cr, TboDrawing *drawing) { - if (!TOOLTIP) + if (drawing == NULL || drawing->tooltip == NULL) return; int w=0, h=0; int posx, posy; - gchar *text = TOOLTIP->str; + gchar *text = drawing->tooltip->str; PangoLayout *layout; layout = pango_cairo_create_layout (cr); @@ -137,25 +177,31 @@ tbo_tooltip_draw (cairo_t *cr) //pango_layout_set_font_description (layout, desc); pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER); - posx = X - w / 2; - posy = Y - h / 2; + posx = drawing->tooltip_x - w / 2; + posy = drawing->tooltip_y - h / 2; cairo_translate (cr, posx, posy); - tbo_tooltip_draw_background (cr, w, h); + tbo_tooltip_draw_background (cr, w, h, drawing); - cairo_set_source_rgba (cr, 1, 1, 1, ALPHA); + cairo_set_source_rgba (cr, 1, 1, 1, drawing->tooltip_alpha); pango_cairo_show_layout (cr, layout); cairo_translate (cr, -posx, -posy); + g_object_unref (layout); } void tbo_tooltip_set_center_timeout (const char *tooltip, int timeout, TboWindow *tbo) { + TboDrawing *drawing = get_tooltip_drawing (tbo); int x, y; + + if (drawing == NULL) + return; + x = gtk_widget_get_width (tbo->drawing) / 2; y = gtk_widget_get_height (tbo->drawing) / 2; tbo_tooltip_set (tooltip, x, y, tbo); - g_timeout_add (timeout, quit_tooltip_cb, tbo); + drawing->tooltip_timeout_id = g_timeout_add (timeout, quit_tooltip_cb, drawing); } diff --git a/src/tbo-tooltip.h b/src/tbo-tooltip.h index 446e532..1f8781c 100644 --- a/src/tbo-tooltip.h +++ b/src/tbo-tooltip.h @@ -21,12 +21,13 @@ #define __TBO_TOOLTIP_H__ #include -#include -#include "tbo-window.h" +#include "tbo-types.h" + +typedef struct _TboDrawing TboDrawing; void tbo_tooltip_set (const char *tooltip, int x, int y, TboWindow *tbo); void tbo_tooltip_set_center_timeout (const char *tooltip, int timeout, TboWindow *tbo); -GString *tbo_tooltip_get (void); -void tbo_tooltip_draw (cairo_t *cr); +void tbo_tooltip_reset (TboWindow *tbo); +void tbo_tooltip_draw (cairo_t *cr, TboDrawing *drawing); #endif diff --git a/src/tbo-types.h b/src/tbo-types.h index 9ec37e6..0016837 100644 --- a/src/tbo-types.h +++ b/src/tbo-types.h @@ -31,33 +31,9 @@ typedef struct double b; } Color; -typedef struct -{ - char title[255]; - int width; - int height; - GList *pages; - -} Comic; - -typedef struct -{ - Comic *comic; - GList *frames; - -} Page; - -typedef struct -{ - int x; - int y; - int width; - int height; - gboolean border; - Color *color; - GList *objects; - -} Frame; +typedef struct _Comic Comic; +typedef struct _Page Page; +typedef struct _Frame Frame; typedef struct { diff --git a/src/tbo-undo.c b/src/tbo-undo.c index 0fd86f1..8ef1e72 100644 --- a/src/tbo-undo.c +++ b/src/tbo-undo.c @@ -19,8 +19,466 @@ */ #include +#include +#include "comic.h" +#include "page.h" +#include "frame.h" +#include "tbo-object-base.h" +#include "tbo-object-text.h" #include "tbo-undo.h" +typedef struct +{ + TboAction base; + Page *page; + Frame *frame; + gint index; +} TboActionFrameAdd; + +typedef struct +{ + TboAction base; + Comic *comic; + Page *page; + gint index; +} TboActionPageChange; + +typedef struct +{ + TboAction base; + Frame *frame; + TboObjectBase *obj; + gint index; +} TboActionObjectAdd; + +typedef struct +{ + TboAction base; + Page *page; + Frame *frame; + gint index; +} TboActionFrameChange; + +typedef struct +{ + TboAction base; + Frame *frame; + TboObjectBase *obj; + gint index; +} TboActionObjectChange; + +typedef struct +{ + TboAction base; + Frame *frame; + gint x1, y1, width1, height1; + gboolean border1; + gdouble r1, g1, b1; + gint x2, y2, width2, height2; + gboolean border2; + gdouble r2, g2, b2; +} TboActionFrameState; + +typedef struct +{ + TboAction base; + TboObjectBase *obj; + gboolean flipv1; + gboolean fliph1; + gboolean flipv2; + gboolean fliph2; +} TboActionObjectFlags; + +typedef struct +{ + TboAction base; + Frame *frame; + TboObjectBase *obj; + gint index1; + gint index2; +} TboActionObjectOrder; + +typedef struct +{ + TboAction base; + TboObjectText *obj; + gchar *text1; + gchar *font1; + GdkRGBA color1; + gchar *text2; + gchar *font2; + GdkRGBA color2; +} TboActionTextState; + +static void +free_action_link (GList *link) +{ + if (link == NULL) + return; + + tbo_action_del ((TboAction *) link->data); + g_list_free_1 (link); +} + +static gboolean +page_has_frame (Page *page, Frame *frame) +{ + return page != NULL && frame != NULL && g_list_find (tbo_page_get_frames (page), frame) != NULL; +} + +static gboolean +comic_has_page (Comic *comic, Page *page) +{ + return comic != NULL && page != NULL && g_list_find (tbo_comic_get_pages (comic), page) != NULL; +} + +static void +frame_add_do (TboAction *action) +{ + TboActionFrameAdd *frame_action = (TboActionFrameAdd *) action; + + if (frame_action->page == NULL || frame_action->frame == NULL || page_has_frame (frame_action->page, frame_action->frame)) + return; + + g_object_ref (frame_action->frame); + tbo_page_insert_frame (frame_action->page, frame_action->frame, frame_action->index); +} + +static void +frame_add_undo (TboAction *action) +{ + TboActionFrameAdd *frame_action = (TboActionFrameAdd *) action; + + if (frame_action->page == NULL || frame_action->frame == NULL || !page_has_frame (frame_action->page, frame_action->frame)) + return; + + tbo_page_del_frame (frame_action->page, frame_action->frame); +} + +static void +frame_add_free (TboAction *action) +{ + TboActionFrameAdd *frame_action = (TboActionFrameAdd *) action; + + if (frame_action->page != NULL) + g_object_remove_weak_pointer (G_OBJECT (frame_action->page), (gpointer *) &frame_action->page); + if (frame_action->frame != NULL) + g_object_unref (frame_action->frame); +} + +static void +object_add_do (TboAction *action) +{ + TboActionObjectAdd *obj_action = (TboActionObjectAdd *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + g_object_ref (obj_action->obj); + tbo_frame_insert_obj (obj_action->frame, obj_action->obj, obj_action->index); +} + +static void +object_add_undo (TboAction *action) +{ + TboActionObjectAdd *obj_action = (TboActionObjectAdd *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || !tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + tbo_frame_del_obj (obj_action->frame, obj_action->obj); +} + +static void +object_add_free (TboAction *action) +{ + TboActionObjectAdd *obj_action = (TboActionObjectAdd *) action; + + if (obj_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->frame), (gpointer *) &obj_action->frame); + if (obj_action->obj != NULL) + g_object_unref (obj_action->obj); +} + +static void +page_add_do (TboAction *action) +{ + TboActionPageChange *page_action = (TboActionPageChange *) action; + + if (page_action->comic == NULL || page_action->page == NULL || comic_has_page (page_action->comic, page_action->page)) + return; + + g_object_ref (page_action->page); + tbo_comic_insert_page (page_action->comic, page_action->page, page_action->index); + tbo_comic_set_current_page (page_action->comic, page_action->page); +} + +static void +page_add_undo (TboAction *action) +{ + TboActionPageChange *page_action = (TboActionPageChange *) action; + gint index; + + if (page_action->comic == NULL || page_action->page == NULL || !comic_has_page (page_action->comic, page_action->page)) + return; + + index = tbo_comic_page_nth (page_action->comic, page_action->page); + if (index >= 0) + tbo_comic_del_page (page_action->comic, index); +} + +static void +page_remove_do (TboAction *action) +{ + page_add_undo (action); +} + +static void +page_remove_undo (TboAction *action) +{ + page_add_do (action); +} + +static void +page_change_free (TboAction *action) +{ + TboActionPageChange *page_action = (TboActionPageChange *) action; + + if (page_action->comic != NULL) + g_object_remove_weak_pointer (G_OBJECT (page_action->comic), (gpointer *) &page_action->comic); + if (page_action->page != NULL) + g_object_unref (page_action->page); +} + +static void +frame_remove_do (TboAction *action) +{ + TboActionFrameChange *frame_action = (TboActionFrameChange *) action; + + if (frame_action->page == NULL || frame_action->frame == NULL || !page_has_frame (frame_action->page, frame_action->frame)) + return; + + tbo_page_del_frame (frame_action->page, frame_action->frame); +} + +static void +frame_remove_undo (TboAction *action) +{ + TboActionFrameChange *frame_action = (TboActionFrameChange *) action; + + if (frame_action->page == NULL || frame_action->frame == NULL || page_has_frame (frame_action->page, frame_action->frame)) + return; + + g_object_ref (frame_action->frame); + tbo_page_insert_frame (frame_action->page, frame_action->frame, frame_action->index); +} + +static void +frame_change_free (TboAction *action) +{ + TboActionFrameChange *frame_action = (TboActionFrameChange *) action; + + if (frame_action->page != NULL) + g_object_remove_weak_pointer (G_OBJECT (frame_action->page), (gpointer *) &frame_action->page); + if (frame_action->frame != NULL) + g_object_unref (frame_action->frame); +} + +static void +object_remove_do (TboAction *action) +{ + TboActionObjectChange *obj_action = (TboActionObjectChange *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || !tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + tbo_frame_del_obj (obj_action->frame, obj_action->obj); +} + +static void +object_remove_undo (TboAction *action) +{ + TboActionObjectChange *obj_action = (TboActionObjectChange *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + g_object_ref (obj_action->obj); + tbo_frame_insert_obj (obj_action->frame, obj_action->obj, obj_action->index); +} + +static void +object_change_free (TboAction *action) +{ + TboActionObjectChange *obj_action = (TboActionObjectChange *) action; + + if (obj_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->frame), (gpointer *) &obj_action->frame); + if (obj_action->obj != NULL) + g_object_unref (obj_action->obj); +} + +static void +apply_frame_state (TboActionFrameState *action, + gint x, + gint y, + gint width, + gint height, + gboolean border, + gdouble r, + gdouble g, + gdouble b) +{ + if (action->frame == NULL) + return; + + tbo_frame_set_bounds (action->frame, x, y, width, height); + tbo_frame_set_border (action->frame, border); + tbo_frame_set_color_rgb (action->frame, r, g, b); +} + +static void +frame_state_do (TboAction *action) +{ + TboActionFrameState *frame_action = (TboActionFrameState *) action; + + apply_frame_state (frame_action, + frame_action->x2, frame_action->y2, + frame_action->width2, frame_action->height2, + frame_action->border2, + frame_action->r2, frame_action->g2, frame_action->b2); +} + +static void +frame_state_undo (TboAction *action) +{ + TboActionFrameState *frame_action = (TboActionFrameState *) action; + + apply_frame_state (frame_action, + frame_action->x1, frame_action->y1, + frame_action->width1, frame_action->height1, + frame_action->border1, + frame_action->r1, frame_action->g1, frame_action->b1); +} + +static void +frame_state_free (TboAction *action) +{ + TboActionFrameState *frame_action = (TboActionFrameState *) action; + + if (frame_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (frame_action->frame), (gpointer *) &frame_action->frame); +} + +static void +object_flags_do (TboAction *action) +{ + TboActionObjectFlags *obj_action = (TboActionObjectFlags *) action; + + if (obj_action->obj == NULL) + return; + + obj_action->obj->flipv = obj_action->flipv2; + obj_action->obj->fliph = obj_action->fliph2; +} + +static void +object_flags_undo (TboAction *action) +{ + TboActionObjectFlags *obj_action = (TboActionObjectFlags *) action; + + if (obj_action->obj == NULL) + return; + + obj_action->obj->flipv = obj_action->flipv1; + obj_action->obj->fliph = obj_action->fliph1; +} + +static void +object_flags_free (TboAction *action) +{ + TboActionObjectFlags *obj_action = (TboActionObjectFlags *) action; + + if (obj_action->obj != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->obj), (gpointer *) &obj_action->obj); +} + +static void +object_order_do (TboAction *action) +{ + TboActionObjectOrder *obj_action = (TboActionObjectOrder *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || !tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + tbo_frame_reorder_obj (obj_action->frame, obj_action->obj, obj_action->index2); +} + +static void +object_order_undo (TboAction *action) +{ + TboActionObjectOrder *obj_action = (TboActionObjectOrder *) action; + + if (obj_action->frame == NULL || obj_action->obj == NULL || !tbo_frame_has_obj (obj_action->frame, obj_action->obj)) + return; + + tbo_frame_reorder_obj (obj_action->frame, obj_action->obj, obj_action->index1); +} + +static void +object_order_free (TboAction *action) +{ + TboActionObjectOrder *obj_action = (TboActionObjectOrder *) action; + + if (obj_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->frame), (gpointer *) &obj_action->frame); + if (obj_action->obj != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->obj), (gpointer *) &obj_action->obj); +} + +static void +apply_text_state (TboActionTextState *action, + const gchar *text, + const gchar *font, + const GdkRGBA *color) +{ + if (action->obj == NULL) + return; + + tbo_object_text_set_text (action->obj, text); + tbo_object_text_change_font (action->obj, (gchar *) font); + tbo_object_text_change_color (action->obj, (GdkRGBA *) color); +} + +static void +text_state_do (TboAction *action) +{ + TboActionTextState *text_action = (TboActionTextState *) action; + + apply_text_state (text_action, text_action->text2, text_action->font2, &text_action->color2); +} + +static void +text_state_undo (TboAction *action) +{ + TboActionTextState *text_action = (TboActionTextState *) action; + + apply_text_state (text_action, text_action->text1, text_action->font1, &text_action->color1); +} + +static void +text_state_free (TboAction *action) +{ + TboActionTextState *text_action = (TboActionTextState *) action; + + if (text_action->obj != NULL) + g_object_remove_weak_pointer (G_OBJECT (text_action->obj), (gpointer *) &text_action->obj); + g_free (text_action->text1); + g_free (text_action->font1); + g_free (text_action->text2); + g_free (text_action->font2); +} + void tbo_action_set (TboAction *action, gpointer action_do, @@ -33,13 +491,16 @@ tbo_action_set (TboAction *action, void tbo_action_del (TboAction *action) { + if (action->action_free != NULL) + action->action_free (action); + free (action); } void tbo_action_del_data (TboAction *action, gpointer user_data) { - free (action); + tbo_action_del (action); } TboUndoStack * @@ -56,10 +517,17 @@ void tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action) { // Removing each element before the actual one - if (stack->first) { - while (stack->first != stack->list) { - tbo_action_del ((TboAction*)((stack->first)->data)); - stack->first = g_list_remove_link (stack->first, stack->first); + if (stack->first) + { + while (stack->first != stack->list) + { + GList *link = stack->first; + + stack->first = stack->first->next; + if (stack->first != NULL) + stack->first->prev = NULL; + + free_action_link (link); } } @@ -68,6 +536,28 @@ tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action) stack->first = stack->list; } +void +tbo_undo_stack_clear (TboUndoStack *stack) +{ + GList *link; + + if (stack == NULL) + return; + + link = stack->first; + while (link != NULL) + { + GList *next = link->next; + + free_action_link (link); + link = next; + } + + stack->first = NULL; + stack->list = NULL; + stack->last_flag = TRUE; +} + void tbo_undo_stack_undo (TboUndoStack *stack) { @@ -111,7 +601,7 @@ tbo_undo_stack_redo (TboUndoStack *stack) void tbo_undo_stack_del (TboUndoStack *stack) { - g_list_foreach (stack->first, (GFunc)tbo_action_del_data, NULL); + tbo_undo_stack_clear (stack); free (stack); } @@ -127,3 +617,211 @@ tbo_undo_active_redo (TboUndoStack *stack) { return stack->first != stack->list; } + +TboAction * +tbo_action_frame_add_new (Page *page, Frame *frame) +{ + TboActionFrameAdd *action = (TboActionFrameAdd *) tbo_action_new (TboActionFrameAdd); + + action->page = page; + action->frame = g_object_ref (frame); + action->index = tbo_page_frame_nth (page, frame); + action->base.action_do = frame_add_do; + action->base.action_undo = frame_add_undo; + action->base.action_free = frame_add_free; + + if (action->page != NULL) + g_object_add_weak_pointer (G_OBJECT (action->page), (gpointer *) &action->page); + + return (TboAction *) action; +} + +TboAction * +tbo_action_object_add_new (Frame *frame, TboObjectBase *object) +{ + TboActionObjectAdd *action = (TboActionObjectAdd *) tbo_action_new (TboActionObjectAdd); + + action->frame = frame; + action->obj = g_object_ref (object); + action->index = tbo_frame_object_nth (frame, object); + action->base.action_do = object_add_do; + action->base.action_undo = object_add_undo; + action->base.action_free = object_add_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + + return (TboAction *) action; +} + +TboAction * +tbo_action_page_add_new (Comic *comic, Page *page, int index) +{ + TboActionPageChange *action = (TboActionPageChange *) tbo_action_new (TboActionPageChange); + + action->comic = comic; + action->page = g_object_ref (page); + action->index = index; + action->base.action_do = page_add_do; + action->base.action_undo = page_add_undo; + action->base.action_free = page_change_free; + + if (action->comic != NULL) + g_object_add_weak_pointer (G_OBJECT (action->comic), (gpointer *) &action->comic); + + return (TboAction *) action; +} + +TboAction * +tbo_action_page_remove_new (Comic *comic, Page *page, int index) +{ + TboActionPageChange *action = (TboActionPageChange *) tbo_action_new (TboActionPageChange); + + action->comic = comic; + action->page = g_object_ref (page); + action->index = index; + action->base.action_do = page_remove_do; + action->base.action_undo = page_remove_undo; + action->base.action_free = page_change_free; + + if (action->comic != NULL) + g_object_add_weak_pointer (G_OBJECT (action->comic), (gpointer *) &action->comic); + + return (TboAction *) action; +} + +TboAction * +tbo_action_frame_remove_new (Page *page, Frame *frame, int index) +{ + TboActionFrameChange *action = (TboActionFrameChange *) tbo_action_new (TboActionFrameChange); + + action->page = page; + action->frame = g_object_ref (frame); + action->index = index; + action->base.action_do = frame_remove_do; + action->base.action_undo = frame_remove_undo; + action->base.action_free = frame_change_free; + + if (action->page != NULL) + g_object_add_weak_pointer (G_OBJECT (action->page), (gpointer *) &action->page); + + return (TboAction *) action; +} + +TboAction * +tbo_action_object_remove_new (Frame *frame, TboObjectBase *object, int index) +{ + TboActionObjectChange *action = (TboActionObjectChange *) tbo_action_new (TboActionObjectChange); + + action->frame = frame; + action->obj = g_object_ref (object); + action->index = index; + action->base.action_do = object_remove_do; + action->base.action_undo = object_remove_undo; + action->base.action_free = object_change_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + + return (TboAction *) action; +} + +TboAction * +tbo_action_frame_state_new (Frame *frame, + int x1, int y1, int width1, int height1, + gboolean border1, gdouble r1, gdouble g1, gdouble b1, + int x2, int y2, int width2, int height2, + gboolean border2, gdouble r2, gdouble g2, gdouble b2) +{ + TboActionFrameState *action = (TboActionFrameState *) tbo_action_new (TboActionFrameState); + + action->frame = frame; + action->x1 = x1; action->y1 = y1; action->width1 = width1; action->height1 = height1; + action->border1 = border1; action->r1 = r1; action->g1 = g1; action->b1 = b1; + action->x2 = x2; action->y2 = y2; action->width2 = width2; action->height2 = height2; + action->border2 = border2; action->r2 = r2; action->g2 = g2; action->b2 = b2; + action->base.action_do = frame_state_do; + action->base.action_undo = frame_state_undo; + action->base.action_free = frame_state_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + + return (TboAction *) action; +} + +TboAction * +tbo_action_object_flags_new (TboObjectBase *object, + gboolean flipv1, + gboolean fliph1, + gboolean flipv2, + gboolean fliph2) +{ + TboActionObjectFlags *action = (TboActionObjectFlags *) tbo_action_new (TboActionObjectFlags); + + action->obj = object; + action->flipv1 = flipv1; + action->fliph1 = fliph1; + action->flipv2 = flipv2; + action->fliph2 = fliph2; + action->base.action_do = object_flags_do; + action->base.action_undo = object_flags_undo; + action->base.action_free = object_flags_free; + + if (action->obj != NULL) + g_object_add_weak_pointer (G_OBJECT (action->obj), (gpointer *) &action->obj); + + return (TboAction *) action; +} + +TboAction * +tbo_action_object_order_new (Frame *frame, + TboObjectBase *object, + int index1, + int index2) +{ + TboActionObjectOrder *action = (TboActionObjectOrder *) tbo_action_new (TboActionObjectOrder); + + action->frame = frame; + action->obj = object; + action->index1 = index1; + action->index2 = index2; + action->base.action_do = object_order_do; + action->base.action_undo = object_order_undo; + action->base.action_free = object_order_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + if (action->obj != NULL) + g_object_add_weak_pointer (G_OBJECT (action->obj), (gpointer *) &action->obj); + + return (TboAction *) action; +} + +TboAction * +tbo_action_text_state_new (TboObjectText *object, + const gchar *text1, + const gchar *font1, + const GdkRGBA *color1, + const gchar *text2, + const gchar *font2, + const GdkRGBA *color2) +{ + TboActionTextState *action = (TboActionTextState *) tbo_action_new (TboActionTextState); + + action->obj = object; + action->text1 = g_strdup (text1); + action->font1 = g_strdup (font1); + action->color1 = *color1; + action->text2 = g_strdup (text2); + action->font2 = g_strdup (font2); + action->color2 = *color2; + action->base.action_do = text_state_do; + action->base.action_undo = text_state_undo; + action->base.action_free = text_state_free; + + if (action->obj != NULL) + g_object_add_weak_pointer (G_OBJECT (action->obj), (gpointer *) &action->obj); + + return (TboAction *) action; +} diff --git a/src/tbo-undo.h b/src/tbo-undo.h index a8bf17a..ee7b29f 100644 --- a/src/tbo-undo.h +++ b/src/tbo-undo.h @@ -23,8 +23,12 @@ #include #include +#include "tbo-types.h" -#define tbo_action_new(action_type) (TboAction*) malloc (sizeof (action_type)) +typedef struct _TboObjectBase TboObjectBase; +typedef struct _TboObjectText TboObjectText; + +#define tbo_action_new(action_type) (TboAction*) calloc (1, sizeof (action_type)) #define tbo_action_do(action) ((TboAction*) action)->action_do ((TboAction*)action) #define tbo_action_undo(action) ((TboAction*) action)->action_undo ((TboAction*)action) @@ -35,6 +39,7 @@ typedef struct _TboUndoStack TboUndoStack; struct _TboAction { void (*action_do) (TboAction *action); void (*action_undo) (TboAction *action); + void (*action_free) (TboAction *action); }; void tbo_action_set (TboAction *action, gpointer action_do, gpointer action_undo); @@ -48,6 +53,7 @@ struct _TboUndoStack { TboUndoStack * tbo_undo_stack_new (void); void tbo_undo_stack_del (TboUndoStack *stack); +void tbo_undo_stack_clear (TboUndoStack *stack); void tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action); void tbo_undo_stack_undo (TboUndoStack *stack); void tbo_undo_stack_redo (TboUndoStack *stack); @@ -55,4 +61,32 @@ void tbo_undo_stack_redo (TboUndoStack *stack); gboolean tbo_undo_active_undo (TboUndoStack *stack); gboolean tbo_undo_active_redo (TboUndoStack *stack); +TboAction * tbo_action_frame_add_new (Page *page, Frame *frame); +TboAction * tbo_action_page_add_new (Comic *comic, Page *page, int index); +TboAction * tbo_action_page_remove_new (Comic *comic, Page *page, int index); +TboAction * tbo_action_frame_remove_new (Page *page, Frame *frame, int index); +TboAction * tbo_action_object_add_new (Frame *frame, TboObjectBase *object); +TboAction * tbo_action_object_remove_new (Frame *frame, TboObjectBase *object, int index); +TboAction * tbo_action_frame_state_new (Frame *frame, + int x1, int y1, int width1, int height1, + gboolean border1, gdouble r1, gdouble g1, gdouble b1, + int x2, int y2, int width2, int height2, + gboolean border2, gdouble r2, gdouble g2, gdouble b2); +TboAction * tbo_action_object_flags_new (TboObjectBase *object, + gboolean flipv1, + gboolean fliph1, + gboolean flipv2, + gboolean fliph2); +TboAction * tbo_action_object_order_new (Frame *frame, + TboObjectBase *object, + int index1, + int index2); +TboAction * tbo_action_text_state_new (TboObjectText *object, + const gchar *text1, + const gchar *font1, + const GdkRGBA *color1, + const gchar *text2, + const gchar *font2, + const GdkRGBA *color2); + #endif diff --git a/src/tbo-utils.c b/src/tbo-utils.c index a55c55c..9cc9f62 100644 --- a/src/tbo-utils.c +++ b/src/tbo-utils.c @@ -17,8 +17,13 @@ */ +#include #include #include +#include +#include +#include "config.h" +#include "tbo-object-base.h" #include "tbo-utils.h" @@ -46,3 +51,149 @@ tbo_get_data_path (const gchar *relative_path) g_free (installed_path); return g_build_filename (SOURCE_DATA_DIR, relative_path, NULL); } + +gchar * +tbo_get_locale_path (void) +{ + gchar *exe_path; + gchar *exe_dir; + gchar *build_locale_dir; + + exe_path = g_file_read_link ("/proc/self/exe", NULL); + if (exe_path == NULL) + return g_strdup (GNOMELOCALEDIR); + + exe_dir = g_path_get_dirname (exe_path); + build_locale_dir = g_build_filename (exe_dir, "po", NULL); + + g_free (exe_path); + g_free (exe_dir); + + if (g_file_test (build_locale_dir, G_FILE_TEST_IS_DIR)) + return build_locale_dir; + + g_free (build_locale_dir); + return g_strdup (GNOMELOCALEDIR); +} + +void +tbo_init_i18n (void) +{ + gchar *locale_dir; + + setlocale (LC_ALL, ""); + +#ifdef ENABLE_NLS + locale_dir = tbo_get_locale_path (); + bindtextdomain (GETTEXT_PACKAGE, locale_dir); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + g_free (locale_dir); +#endif +} + +const gchar * +tbo_ascii_formatd (gchar *buffer, gsize buffer_len, gdouble value) +{ + return g_ascii_formatd (buffer, buffer_len, "%.17g", value); +} + +gboolean +tbo_ascii_parse_int (const gchar *text, gint *value) +{ + gchar *endptr = NULL; + gint64 parsed; + + if (text == NULL || *text == '\0') + return FALSE; + + errno = 0; + parsed = g_ascii_strtoll (text, &endptr, 10); + if (endptr == text || *endptr != '\0' || errno == ERANGE || + parsed < G_MININT || parsed > G_MAXINT) + return FALSE; + + if (value != NULL) + *value = (gint) parsed; + + return TRUE; +} + +gboolean +tbo_ascii_parse_double (const gchar *text, gdouble *value) +{ + gchar *normalized = NULL; + gchar *endptr = NULL; + gdouble parsed; + + if (text == NULL || *text == '\0') + return FALSE; + + errno = 0; + parsed = g_ascii_strtod (text, &endptr); + if (endptr == text || *endptr != '\0' || errno == ERANGE) + { + if (strchr (text, ',') == NULL || strchr (text, '.') != NULL) + return FALSE; + + normalized = g_strdup (text); + g_strdelimit (normalized, ",", '.'); + endptr = NULL; + errno = 0; + parsed = g_ascii_strtod (normalized, &endptr); + if (endptr == normalized || *endptr != '\0' || errno == ERANGE) + { + g_free (normalized); + return FALSE; + } + g_free (normalized); + } + + if (value != NULL) + *value = parsed; + + return TRUE; +} + +void +tbo_xml_append_attr_int (GString *xml, const gchar *name, gint value) +{ + g_string_append_printf (xml, " %s=\"%d\"", name, value); +} + +void +tbo_xml_append_attr_double (GString *xml, const gchar *name, gdouble value) +{ + gchar buffer[G_ASCII_DTOSTR_BUF_SIZE]; + + tbo_ascii_formatd (buffer, sizeof (buffer), value); + g_string_append_printf (xml, " %s=\"%s\"", name, buffer); +} + +void +tbo_xml_append_attr_string (GString *xml, const gchar *name, const gchar *value) +{ + gchar *escaped = g_markup_escape_text (value != NULL ? value : "", -1); + + g_string_append_printf (xml, " %s=\"%s\"", name, escaped); + g_free (escaped); +} + +void +tbo_xml_append_object_attrs (GString *xml, TboObjectBase *self) +{ + tbo_xml_append_attr_int (xml, "x", self->x); + tbo_xml_append_attr_int (xml, "y", self->y); + tbo_xml_append_attr_int (xml, "width", self->width); + tbo_xml_append_attr_int (xml, "height", self->height); + tbo_xml_append_attr_double (xml, "angle", self->angle); + tbo_xml_append_attr_int (xml, "flipv", self->flipv); + tbo_xml_append_attr_int (xml, "fliph", self->fliph); +} + +void +tbo_xml_write (FILE *file, GString *xml) +{ + fputs (xml->str, file); + g_string_free (xml, TRUE); +} diff --git a/src/tbo-utils.h b/src/tbo-utils.h index d84cd83..8dcd036 100644 --- a/src/tbo-utils.h +++ b/src/tbo-utils.h @@ -22,7 +22,19 @@ #include +typedef struct _TboObjectBase TboObjectBase; + void get_base_name (gchar *str, gchar *ret, int size); gchar *tbo_get_data_path (const gchar *relative_path); +gchar *tbo_get_locale_path (void); +void tbo_init_i18n (void); +const gchar *tbo_ascii_formatd (gchar *buffer, gsize buffer_len, gdouble value); +gboolean tbo_ascii_parse_int (const gchar *text, gint *value); +gboolean tbo_ascii_parse_double (const gchar *text, gdouble *value); +void tbo_xml_append_attr_int (GString *xml, const gchar *name, gint value); +void tbo_xml_append_attr_double (GString *xml, const gchar *name, gdouble value); +void tbo_xml_append_attr_string (GString *xml, const gchar *name, const gchar *value); +void tbo_xml_append_object_attrs (GString *xml, TboObjectBase *self); +void tbo_xml_write (FILE *file, GString *xml); #endif diff --git a/src/tbo-widget.c b/src/tbo-widget.c index 64d8276..a5540b0 100644 --- a/src/tbo-widget.c +++ b/src/tbo-widget.c @@ -10,6 +10,8 @@ #include "tbo-widget.h" +#define TBO_DIALOG_RUN_DATA_KEY "tbo-dialog-run-data" + struct alert_run_data { GMainLoop *loop; gint response; @@ -143,6 +145,56 @@ tbo_scrolled_window_set_child (GtkWidget *scrolled, GtkWidget *child) gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled), child); } +void +tbo_dialog_run_data_init (TboDialogRunData *data, gint close_response) +{ + data->loop = g_main_loop_new (NULL, FALSE); + data->response = GTK_RESPONSE_NONE; + data->close_response = close_response; +} + +void +tbo_dialog_run_data_clear (TboDialogRunData *data) +{ + if (data->loop != NULL) + { + g_main_loop_unref (data->loop); + data->loop = NULL; + } +} + +gboolean +tbo_dialog_close_request_cb (GtkWindow *dialog, TboDialogRunData *data) +{ + if (data->response == GTK_RESPONSE_NONE) + data->response = data->close_response; + + g_main_loop_quit (data->loop); + return TRUE; +} + +void +tbo_dialog_button_cb (GtkButton *button, GtkWindow *dialog) +{ + TboDialogRunData *data = g_object_get_data (G_OBJECT (dialog), TBO_DIALOG_RUN_DATA_KEY); + gint response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "tbo-response")); + + if (data != NULL) + data->response = response; + + gtk_window_close (dialog); +} + +gint +tbo_dialog_run (GtkWindow *dialog, TboDialogRunData *data) +{ + g_object_set_data (G_OBJECT (dialog), TBO_DIALOG_RUN_DATA_KEY, data); + tbo_widget_show_all (GTK_WIDGET (dialog)); + gtk_window_present (dialog); + g_main_loop_run (data->loop); + return data->response; +} + gint tbo_alert_choose (GtkWindow *parent, const gchar *message, diff --git a/src/tbo-widget.h b/src/tbo-widget.h index 77a2915..11145b2 100644 --- a/src/tbo-widget.h +++ b/src/tbo-widget.h @@ -14,6 +14,13 @@ #include #include +typedef struct +{ + GMainLoop *loop; + gint response; + gint close_response; +} TboDialogRunData; + GtkWidget *tbo_widget_get_first_child (GtkWidget *widget); gint tbo_widget_get_child_count (GtkWidget *widget); void tbo_widget_add_child (GtkWidget *parent, GtkWidget *child); @@ -24,6 +31,11 @@ void tbo_paned_pack_start (GtkWidget *paned, GtkWidget *child, gboolean resize, void tbo_paned_pack_end (GtkWidget *paned, GtkWidget *child, gboolean resize, gboolean shrink); GtkWidget *tbo_scrolled_window_get_child (GtkWidget *scrolled); void tbo_scrolled_window_set_child (GtkWidget *scrolled, GtkWidget *child); +void tbo_dialog_run_data_init (TboDialogRunData *data, gint close_response); +void tbo_dialog_run_data_clear (TboDialogRunData *data); +gboolean tbo_dialog_close_request_cb (GtkWindow *dialog, TboDialogRunData *data); +void tbo_dialog_button_cb (GtkButton *button, GtkWindow *dialog); +gint tbo_dialog_run (GtkWindow *dialog, TboDialogRunData *data); gint tbo_alert_choose (GtkWindow *parent, const gchar *message, const gchar *detail, diff --git a/src/tbo-window.c b/src/tbo-window.c index a225793..3b6459b 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -25,11 +25,18 @@ #include "tbo-types.h" #include "tbo-window.h" #include "comic.h" +#include "frame.h" #include "page.h" +#include "tbo-object-group.h" +#include "tbo-object-pixmap.h" +#include "tbo-object-svg.h" +#include "tbo-object-text.h" #include "ui-menu.h" #include "tbo-toolbar.h" #include "tbo-drawing.h" +#include "tbo-tool-frame.h" #include "tbo-tool-selector.h" +#include "tbo-tool-text.h" #include "tbo-tooltip.h" #include "tbo-utils.h" #include "tbo-widget.h" @@ -37,6 +44,42 @@ static gboolean KEY_BINDER = TRUE; +static gboolean on_key_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + TboWindow *tbo); + +static void +setup_darea_controllers (GtkWidget *darea, TboWindow *tbo) +{ + GtkEventController *key; + + tbo_drawing_init_dnd (TBO_DRAWING (darea), tbo); + + key = gtk_event_controller_key_new (); + g_signal_connect (key, "key-pressed", G_CALLBACK (on_key_cb), tbo); + gtk_widget_add_controller (darea, key); +} + +static void +detach_document_state (TboWindow *tbo) +{ + if (tbo == NULL) + return; + + if (tbo->toolbar != NULL && tbo->toolbar->tools != NULL) + { + tbo_tool_selector_reset_state (TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR])); + tbo_tool_frame_reset_state (TBO_TOOL_FRAME (tbo->toolbar->tools[TBO_TOOLBAR_FRAME])); + tbo_tool_text_reset_state (TBO_TOOL_TEXT (tbo->toolbar->tools[TBO_TOOLBAR_TEXT])); + tbo->toolbar->selected_tool = NULL; + } + + tbo_undo_stack_clear (tbo->undo_stack); + tbo_tooltip_reset (tbo); +} + static void apply_theme_preferences (void) { @@ -94,16 +137,44 @@ refresh_page_tab_labels (TboWindow *tbo) } } +static void +sync_page_widgets_with_comic (TboWindow *tbo) +{ + gint widget_count; + gint comic_count; + + if (tbo == NULL) + return; + + widget_count = tbo_window_get_page_count (tbo); + comic_count = tbo_comic_len (tbo->comic); + + while (widget_count < comic_count) + { + tbo_window_add_page_widget (tbo, create_darea (tbo)); + widget_count++; + } + + while (widget_count > comic_count) + { + tbo_window_remove_page_widget (tbo, widget_count - 1); + widget_count--; + } +} + static gboolean notebook_switch_page_cb (GtkNotebook *notebook, GtkWidget *page, guint page_num, TboWindow *tbo) { + if (tbo == NULL || tbo->destroying) + return FALSE; + tbo_comic_set_current_page_nth (tbo->comic, page_num); tbo_window_set_current_tab_page (tbo, FALSE); tbo_toolbar_update (tbo->toolbar); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_drawing_adjust_scroll (TBO_DRAWING (tbo->drawing)); return FALSE; } @@ -203,24 +274,123 @@ confirm_close (TboWindow *tbo) } static void -update_statusbar (TboWindow *tbo, int x, int y) +append_status_segment (GString *status, const gchar *segment) { - char buffer[200]; - gboolean in_frame_view = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL; + if (segment == NULL || *segment == '\0') + return; + + if (status->len > 0) + g_string_append (status, " | "); + + g_string_append (status, segment); +} - if (in_frame_view) +static gint +frame_index_for_status (Page *page, Frame *frame) +{ + GList *frames; + gint index = 1; + + if (page == NULL || frame == NULL) + return 0; + + for (frames = tbo_page_get_frames (page); frames != NULL; frames = frames->next, index++) + { + if (frames->data == frame) + return index; + } + + return 0; +} + +static const gchar * +object_label_for_status (TboObjectBase *obj) +{ + if (obj == NULL) + return NULL; + if (TBO_IS_OBJECT_TEXT (obj)) + return _("Text"); + if (TBO_IS_OBJECT_SVG (obj)) + return _("SVG image"); + if (TBO_IS_OBJECT_PIXMAP (obj)) + return _("Image"); + if (TBO_IS_OBJECT_GROUP (obj)) + return _("Group"); + + return _("Object"); +} + +static void +update_statusbar (TboWindow *tbo) +{ + GString *status; + Page *page; + TboToolSelector *selector = NULL; + Frame *selected_frame = NULL; + Frame *current_frame; + TboObjectBase *selected_object = NULL; + gint frame_index; + gchar *segment; + + page = tbo_comic_get_current_page (tbo->comic); + current_frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); + if (tbo->toolbar != NULL && tbo->toolbar->tools != NULL) + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + if (selector != NULL) { - snprintf (buffer, 200, _("editing frame [ %5d,%5d ] | Esc: back to page"), x, y); + selected_frame = tbo_tool_selector_get_selected_frame (selector); + selected_object = tbo_tool_selector_get_selected_obj (selector); + } + + status = g_string_new (NULL); + + segment = g_strdup_printf (_("Page %d of %d"), + tbo_comic_page_index (tbo->comic) + 1, + tbo_comic_len (tbo->comic)); + append_status_segment (status, segment); + g_free (segment); + + segment = g_strdup_printf (_("Frames: %d"), page != NULL ? tbo_page_len (page) : 0); + append_status_segment (status, segment); + g_free (segment); + + if (current_frame != NULL) + { + frame_index = frame_index_for_status (page, current_frame); + if (frame_index > 0) + segment = g_strdup_printf (_("Editing frame %d"), frame_index); + else + segment = g_strdup (_("Editing frame")); + append_status_segment (status, segment); + g_free (segment); + + if (selected_object != NULL) + { + segment = g_strdup_printf (_("Object: %s"), object_label_for_status (selected_object)); + append_status_segment (status, segment); + g_free (segment); + } + + append_status_segment (status, _("Esc: back to page")); } else { - snprintf (buffer, 200, _("page: %d of %d [ %5d,%5d ] | frames: %d | Enter: frame"), - tbo_comic_page_index (tbo->comic), - tbo_comic_len (tbo->comic), - x, y, - tbo_page_len (tbo_comic_get_current_page (tbo->comic))); + if (selected_frame != NULL) + { + frame_index = frame_index_for_status (page, selected_frame); + if (frame_index > 0) + segment = g_strdup_printf (_("Frame %d selected"), frame_index); + else + segment = g_strdup (_("Frame selected")); + append_status_segment (status, segment); + g_free (segment); + } + + append_status_segment (status, _("Enter: frame")); } - gtk_label_set_text (GTK_LABEL (tbo->status), buffer); + + gtk_label_set_text (GTK_LABEL (tbo->status), status->str); + g_string_free (status, TRUE); } static void @@ -281,7 +451,7 @@ on_key_cb (GtkEventControllerKey *controller, if (tool) tool->on_key (tool, GTK_WIDGET (tbo->window), event); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); if (KEY_BINDER && (state & (GDK_CONTROL_MASK | GDK_ALT_MASK | GDK_META_MASK)) == 0) { @@ -328,6 +498,8 @@ global_key_cb (GtkEventControllerKey *controller, GdkModifierType state, TboWindow *tbo) { + GtkWidget *focus; + if (keyval == GDK_KEY_Escape && tbo->drawing != NULL && tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL) @@ -336,16 +508,28 @@ global_key_cb (GtkEventControllerKey *controller, return TRUE; } - return FALSE; -} + focus = gtk_window_get_focus (GTK_WINDOW (tbo->window)); + if ((keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter) && + tbo->drawing != NULL && + tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) == NULL) + { + TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + Frame *selected_frame = tbo_tool_selector_get_selected_frame (selector); + + if (selected_frame != NULL && + (focus == NULL || + focus == tbo->drawing || + focus == tbo->dw_scroll || + focus == tbo->notebook || + gtk_widget_is_ancestor (focus, tbo->dw_scroll) || + gtk_widget_is_ancestor (focus, tbo->notebook))) + { + tbo_window_enter_frame (tbo, selected_frame); + return TRUE; + } + } -static void -on_move_cb (GtkEventControllerMotion *controller, - gdouble x, - gdouble y, - TboWindow *tbo) -{ - update_statusbar (tbo, (int)x, (int)y); + return FALSE; } TboWindow * @@ -380,9 +564,13 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, void tbo_window_free (TboWindow *tbo) { - tbo_comic_free (tbo->comic); + detach_document_state (tbo); if (tbo->toolbar) + { g_object_unref (tbo->toolbar); + tbo->toolbar = NULL; + } + tbo_comic_free (tbo->comic); g_free (tbo->path); g_free (tbo->browse_path); g_free (tbo->export_path); @@ -492,6 +680,7 @@ tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo) { if (confirm_close (tbo)) { + tbo_window_reset_document_state (tbo); tbo->destroying = TRUE; return FALSE; } @@ -503,8 +692,6 @@ tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo) GtkWidget * create_darea (TboWindow *tbo) { - GtkEventController *key; - GtkEventController *motion; GtkWidget *scrolled; GtkWidget *darea; @@ -512,15 +699,7 @@ create_darea (TboWindow *tbo) gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); darea = tbo_drawing_new_with_params (tbo->comic); tbo_scrolled_window_set_child (scrolled, darea); - tbo_drawing_init_dnd (TBO_DRAWING (darea), tbo); - - motion = gtk_event_controller_motion_new (); - g_signal_connect (motion, "motion", G_CALLBACK (on_move_cb), tbo); - gtk_widget_add_controller (darea, motion); - - key = gtk_event_controller_key_new (); - g_signal_connect (key, "key-pressed", G_CALLBACK (on_key_cb), tbo); - gtk_widget_add_controller (darea, key); + setup_darea_controllers (darea, tbo); tbo_widget_show_all (scrolled); return scrolled; @@ -566,7 +745,7 @@ tbo_new_tbo (GtkApplication *app, int width, int height) tbo_widget_add_child (window, container); comic = tbo_comic_new (_("Untitled"), width, height); - gtk_window_set_title (GTK_WINDOW (window), comic->title); + gtk_window_set_title (GTK_WINDOW (window), tbo_comic_get_title (comic)); scrolled = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); darea = tbo_drawing_new_with_params (comic); @@ -604,8 +783,7 @@ tbo_new_tbo (GtkApplication *app, int width, int height) toolbar = TBO_TOOLBAR (tbo_toolbar_new_with_params (tbo)); tbo->toolbar = toolbar; - // drag & drop - tbo_drawing_init_dnd (TBO_DRAWING (darea), tbo); + setup_darea_controllers (darea, tbo); // key press event g_signal_connect (tbo->notebook, "switch-page", G_CALLBACK (notebook_switch_page_cb), tbo); @@ -628,17 +806,17 @@ tbo_new_tbo (GtkApplication *app, int width, int height) apply_window_icon (window); tbo_toolbar_set_selected_tool (toolbar, TBO_TOOLBAR_SELECTOR); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); return tbo; } void -tbo_window_update_status (TboWindow *tbo, int x, int y) +tbo_window_refresh_status (TboWindow *tbo) { if (tbo == NULL || tbo->destroying) return; - update_statusbar (tbo, x, y); + update_statusbar (tbo); tbo_toolbar_update (tbo->toolbar); } @@ -663,6 +841,9 @@ tbo_window_set_current_tab_page (TboWindow *tbo, gboolean setit) { int nth; + if (tbo == NULL || tbo->destroying) + return; + nth = tbo_comic_page_index (tbo->comic); if (setit) gtk_notebook_set_current_page (GTK_NOTEBOOK (tbo->notebook), nth); @@ -695,10 +876,32 @@ tbo_window_enter_frame (TboWindow *tbo, Frame *frame) gtk_widget_grab_focus (tbo->drawing); tbo_tooltip_set (NULL, 0, 0, tbo); tbo_tooltip_set_center_timeout (_("press Esc to go back"), 3000, tbo); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_drawing_adjust_scroll (drawing); } +void +tbo_window_reset_document_state (TboWindow *tbo) +{ + if (tbo == NULL) + return; + + if (tbo->toolbar != NULL) + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_NONE); + + detach_document_state (tbo); + + if (tbo->drawing != NULL) + { + TBO_DRAWING (tbo->drawing)->tool = NULL; + tbo_drawing_set_comic (TBO_DRAWING (tbo->drawing), NULL); + tbo_drawing_set_current_frame (TBO_DRAWING (tbo->drawing), NULL); + } + + tbo_window_set_key_binder (tbo, TRUE); + tbo_tooltip_set (NULL, 0, 0, tbo); +} + void tbo_window_leave_frame (TboWindow *tbo) { @@ -718,26 +921,38 @@ tbo_window_leave_frame (TboWindow *tbo) tbo_tool_selector_set_selected_obj (selector, NULL); gtk_widget_grab_focus (tbo->drawing); tbo_tooltip_set (NULL, 0, 0, tbo); - tbo_window_update_status (tbo, 0, 0); + tbo_window_refresh_status (tbo); tbo_drawing_adjust_scroll (drawing); } gboolean tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo) { + gint old_page_count = tbo_window_get_page_count (tbo); + tbo_undo_stack_undo (tbo->undo_stack); tbo_window_mark_dirty (tbo); + sync_page_widgets_with_comic (tbo); + if (old_page_count != tbo_window_get_page_count (tbo)) + tbo_window_set_current_tab_page (tbo, TRUE); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); return FALSE; } gboolean tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo) { + gint old_page_count = tbo_window_get_page_count (tbo); + tbo_undo_stack_redo (tbo->undo_stack); tbo_window_mark_dirty (tbo); + sync_page_widgets_with_comic (tbo); + if (old_page_count != tbo_window_get_page_count (tbo)) + tbo_window_set_current_tab_page (tbo, TRUE); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); return FALSE; } diff --git a/src/tbo-window.h b/src/tbo-window.h index 4ebbe3b..68c165d 100644 --- a/src/tbo-window.h +++ b/src/tbo-window.h @@ -50,7 +50,7 @@ void tbo_window_free (TboWindow *tbo); gboolean tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, TboWindow *tbo); gboolean tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo); TboWindow * tbo_new_tbo (GtkApplication *app, int width, int height); -void tbo_window_update_status (TboWindow *tbo, int x, int y); +void tbo_window_refresh_status (TboWindow *tbo); void tbo_empty_tool_area (TboWindow *tbo); void tbo_window_set_path (TboWindow *tbo, const gchar *path); void tbo_window_set_browse_path (TboWindow *tbo, const gchar *path); @@ -68,6 +68,7 @@ GtkWidget *create_darea (TboWindow *tbo); void tbo_window_set_key_binder (TboWindow *tbo, gboolean keyb); void tbo_window_enter_frame (TboWindow *tbo, Frame *frame); void tbo_window_leave_frame (TboWindow *tbo); +void tbo_window_reset_document_state (TboWindow *tbo); gboolean tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo); gboolean tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo); diff --git a/src/tbo.c b/src/tbo.c index c8a6c8a..f2c349e 100644 --- a/src/tbo.c +++ b/src/tbo.c @@ -23,6 +23,7 @@ #include "tbo-window.h" #include "comic.h" +#include "tbo-utils.h" static void present_window (TboWindow *tbo) @@ -74,13 +75,7 @@ int main (int argc, char**argv){ int status; g_set_application_name ("TBO"); - -#ifdef ENABLE_NLS - /* Initialize the i18n stuff */ - bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR); - bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); - textdomain (GETTEXT_PACKAGE); -#endif + tbo_init_i18n (); app = gtk_application_new ("net.danigm.tbo", G_APPLICATION_HANDLES_OPEN); g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL); diff --git a/src/ui-menu.c b/src/ui-menu.c index f158875..255f29b 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -134,14 +134,19 @@ clone_selection (TboWindow *tbo) Page *page = tbo_comic_get_current_page (tbo->comic); TboDrawing *drawing = TBO_DRAWING (tbo->drawing); + gboolean cloned = FALSE; + if (!tbo_drawing_get_current_frame (drawing) && frame) { Frame *cloned_frame = tbo_frame_clone (frame); - cloned_frame->x += 10; - cloned_frame->y -= 10; + tbo_frame_set_position (cloned_frame, + tbo_frame_get_x (cloned_frame) + 10, + tbo_frame_get_y (cloned_frame) - 10); tbo_page_add_frame (page, cloned_frame); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_add_new (page, cloned_frame)); tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); tbo_tool_selector_set_selected (selector, cloned_frame); + cloned = TRUE; } else if (obj && tbo_drawing_get_current_frame (drawing)) { @@ -149,11 +154,18 @@ clone_selection (TboWindow *tbo) cloned_obj->x += 10; cloned_obj->y -= 10; tbo_frame_add_obj (frame, cloned_obj); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, cloned_obj)); tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); tbo_tool_selector_set_selected_obj (selector, cloned_obj); + cloned = TRUE; } - tbo_window_mark_dirty (tbo); + if (cloned) + { + tbo_window_mark_dirty (tbo); + if (!tbo_drawing_get_current_frame (drawing)) + tbo_window_refresh_status (tbo); + } tbo_drawing_update (drawing); } @@ -174,12 +186,27 @@ flip_selection_h (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; + Frame *frame = selector->selected_frame; + gint index1; + gint index2; - if (obj != NULL) + if (obj != NULL && frame != NULL) + { + index1 = tbo_frame_object_nth (frame, obj); tbo_object_base_fliph (obj); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_object_flags_new (obj, + obj->flipv, + !obj->fliph, + obj->flipv, + obj->fliph)); + } if (obj != NULL) + { tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } @@ -188,12 +215,24 @@ flip_selection_v (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; + Frame *frame = selector->selected_frame; - if (obj != NULL) + if (obj != NULL && frame != NULL) + { tbo_object_base_flipv (obj); + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_object_flags_new (obj, + !obj->flipv, + obj->fliph, + obj->flipv, + obj->fliph)); + } if (obj != NULL) + { tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } @@ -202,12 +241,24 @@ order_selection_up (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; + Frame *frame = selector->selected_frame; + gint index1; + gint index2; - if (obj != NULL) - tbo_object_base_order_up (obj, selector->selected_frame); + if (obj != NULL && frame != NULL) + { + index1 = tbo_frame_object_nth (frame, obj); + tbo_object_base_order_up (obj, frame); + index2 = tbo_frame_object_nth (frame, obj); + if (index1 != index2) + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_order_new (frame, obj, index1, index2)); + } if (obj != NULL) + { tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } @@ -216,12 +267,24 @@ order_selection_down (TboWindow *tbo) { TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; + Frame *frame = selector->selected_frame; + gint index1; + gint index2; - if (obj != NULL) - tbo_object_base_order_down (obj, selector->selected_frame); + if (obj != NULL && frame != NULL) + { + index1 = tbo_frame_object_nth (frame, obj); + tbo_object_base_order_down (obj, frame); + index2 = tbo_frame_object_nth (frame, obj); + if (index1 != index2) + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_order_new (frame, obj, index1, index2)); + } if (obj != NULL) + { tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } diff --git a/tests/ascii_locale_roundtrip_check.c b/tests/ascii_locale_roundtrip_check.c new file mode 100644 index 0000000..c0c0dea --- /dev/null +++ b/tests/ascii_locale_roundtrip_check.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include + +#include "comic.h" +#include "comic-load.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-window.h" + +static gboolean +set_decimal_comma_locale (void) +{ + const char *locales[] = { + "es_ES.utf8", + "es_ES", + "spanish", + }; + guint i; + + for (i = 0; i < G_N_ELEMENTS (locales); i++) + { + if (setlocale (LC_NUMERIC, locales[i]) != NULL) + return TRUE; + } + + return FALSE; +} + +int +main (int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *tmpname; + gint fd; + gchar *contents = NULL; + gsize length = 0; + Page *page; + Frame *frame; + GdkRGBA frame_color; + GdkRGBA text_color = {0.1, 0.2, 0.3, 1.0}; + TboObjectBase *obj; + TboObjectText *text; + Comic *reloaded; + + if (argc != 1) + return 2; + + gtk_init (); + + if (!set_decimal_comma_locale ()) + return 77; + + app = gtk_application_new ("net.danigm.tbo.asciilocale", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 10, 20, 100, 80); + tbo_frame_set_color_rgb (frame, 0.25, 0.5, 0.75); + + obj = TBO_OBJECT_BASE (tbo_object_text_new_with_params (5, 6, 70, 25, "hola", "Sans 12", &text_color)); + obj->angle = 1.25; + tbo_frame_add_obj (frame, obj); + + tmpname = g_build_filename (g_get_tmp_dir (), "tbo-ascii-locale-XXXXXX.tbo", NULL); + fd = g_mkstemp (tmpname); + if (fd < 0) + return 4; + close (fd); + + if (!tbo_comic_save (tbo, tmpname)) + return 5; + + if (!g_file_get_contents (tmpname, &contents, &length, NULL)) + return 6; + + if (strstr (contents, "angle=\"1.25\"") == NULL) + return 7; + if (strstr (contents, "angle=\"1,25\"") != NULL) + return 8; + if (strstr (contents, "r=\"0.25\"") == NULL) + return 9; + if (strstr (contents, "r=\"0,25\"") != NULL) + return 10; + + reloaded = tbo_comic_load (tmpname); + if (reloaded == NULL) + return 11; + + page = tbo_comic_get_current_page (reloaded); + frame = tbo_page_get_frames (page)->data; + tbo_frame_get_color (frame, &frame_color); + if (fabs (frame_color.red - 0.25) > 1e-9 || + fabs (frame_color.green - 0.5) > 1e-9 || + fabs (frame_color.blue - 0.75) > 1e-9) + return 12; + + obj = tbo_frame_get_objects (frame)->data; + text = TBO_OBJECT_TEXT (obj); + if (fabs (obj->angle - 1.25) > 1e-9) + return 13; + if (fabs (text->font_color->red - text_color.red) > 1e-6 || + fabs (text->font_color->green - text_color.green) > 1e-6 || + fabs (text->font_color->blue - text_color.blue) > 1e-6) + return 14; + + g_free (contents); + g_remove (tmpname); + g_free (tmpname); + tbo_comic_free (reloaded); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/asset_bounds_check.c b/tests/asset_bounds_check.c index 1ade239..4ff4ee1 100644 --- a/tests/asset_bounds_check.c +++ b/tests/asset_bounds_check.c @@ -37,18 +37,24 @@ int main(int argc, char **argv) frame = frames->data; tbo_window_enter_frame (tbo, frame); - before = g_list_length (frame->objects); - if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", frame->width / 2, frame->height / 2) == NULL) + before = tbo_frame_object_count (frame); + if (tbo_dnd_insert_asset (tbo, + "tbo/logo/tbo.svg", + tbo_frame_get_width (frame) / 2, + tbo_frame_get_height (frame) / 2) == NULL) return 5; - after_inside = g_list_length (frame->objects); + after_inside = tbo_frame_object_count (frame); if (after_inside != before + 1) return 6; - if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", frame->width + 50, frame->height + 50) != NULL) + if (tbo_dnd_insert_asset (tbo, + "tbo/logo/tbo.svg", + tbo_frame_get_width (frame) + 50, + tbo_frame_get_height (frame) + 50) != NULL) return 7; - after_outside = g_list_length (frame->objects); + after_outside = tbo_frame_object_count (frame); if (after_outside != after_inside) return 8; diff --git a/tests/clone_dirty_check.c b/tests/clone_dirty_check.c new file mode 100644 index 0000000..d7d5991 --- /dev/null +++ b/tests/clone_dirty_check.c @@ -0,0 +1,48 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + gint frames_before; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.clonedirty", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 100, 80); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_window_mark_clean (tbo); + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "clone", NULL); + if (tbo_window_has_unsaved_changes (tbo)) + return 3; + + frames_before = tbo_page_len (page); + tbo_tool_selector_set_selected (selector, frame); + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "clone", NULL); + if (!tbo_window_has_unsaved_changes (tbo) || tbo_page_len (page) != frames_before + 1) + return 4; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/comic_gobject_lifetime_check.c b/tests/comic_gobject_lifetime_check.c new file mode 100644 index 0000000..08b7a81 --- /dev/null +++ b/tests/comic_gobject_lifetime_check.c @@ -0,0 +1,38 @@ +#include + +#include "comic.h" +#include "tbo-drawing.h" + +int +main (void) +{ + Comic *comic; + GtkWidget *drawing_widget; + TboDrawing *drawing; + + gtk_init (); + + comic = tbo_comic_new ("Test", 800, 600); + if (comic == NULL) + return 1; + + if (!G_IS_OBJECT (comic) || !TBO_IS_COMIC (comic)) + return 2; + + drawing_widget = tbo_drawing_new_with_params (comic); + drawing = TBO_DRAWING (drawing_widget); + if (tbo_drawing_get_comic (drawing) != comic) + return 3; + + g_object_ref (comic); + tbo_comic_free (comic); + if (tbo_comic_len (comic) != 1) + return 4; + + g_object_unref (comic); + if (tbo_drawing_get_comic (drawing) != NULL) + return 5; + + g_object_unref (drawing_widget); + return 0; +} diff --git a/tests/create_undo_check.c b/tests/create_undo_check.c new file mode 100644 index 0000000..5394e9a --- /dev/null +++ b/tests/create_undo_check.c @@ -0,0 +1,71 @@ +#include + +#include "comic.h" +#include "dnd.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-tool-base.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolBase *frame_tool; + Page *page; + Frame *frame; + TboPointerEvent click_event = { .x = 10, .y = 10 }; + TboPointerEvent release_event = { .x = 110, .y = 90 }; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.createundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_FRAME); + frame_tool = tbo_toolbar_get_selected_tool (tbo->toolbar); + frame_tool->on_click (frame_tool, tbo->drawing, &click_event); + frame_tool->on_release (frame_tool, tbo->drawing, &release_event); + + if (!gtk_widget_is_sensitive (tbo->toolbar->button_undo) || tbo_page_len (page) != 1) + return 3; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_page_len (page) != 0) + return 4; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_page_len (page) != 1) + return 5; + + frame = tbo_page_get_frames (page)->data; + tbo_window_enter_frame (tbo, frame); + tbo_undo_stack_clear (tbo->undo_stack); + tbo_toolbar_update (tbo->toolbar); + + if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", 20, 20) == NULL) + return 6; + if (!gtk_widget_is_sensitive (tbo->toolbar->button_undo) || tbo_frame_object_count (frame) != 1) + return 7; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_frame_object_count (frame) != 0) + return 8; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_frame_object_count (frame) != 1) + return 9; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/dirty_state_check.c b/tests/dirty_state_check.c new file mode 100644 index 0000000..144f6eb --- /dev/null +++ b/tests/dirty_state_check.c @@ -0,0 +1,90 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-tool-base.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-undo.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *obj; + TboKeyEvent event = { 0 }; + GdkRGBA color = { 0.2, 0.4, 0.6, 1.0 }; + GdkRGBA current_color; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.dirtystate", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + drain_events (); + + tbo_window_mark_clean (tbo); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (selector->spin_w), tbo_frame_get_width (frame) + 10); + drain_events (); + if (!tbo_window_has_unsaved_changes (tbo) || tbo_frame_get_width (frame) != 130) + return 3; + + tbo_window_mark_clean (tbo); + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (selector->color_button), &color); + drain_events (); + tbo_frame_get_color (frame, ¤t_color); + if (!tbo_window_has_unsaved_changes (tbo) || !gdk_rgba_equal (¤t_color, &color)) + return 4; + + tbo_window_mark_clean (tbo); + gtk_check_button_set_active (GTK_CHECK_BUTTON (selector->border_button), FALSE); + drain_events (); + if (!tbo_window_has_unsaved_changes (tbo) || tbo_frame_get_border (frame)) + return 5; + + obj = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 60, 20, "dirty", "Sans 12", &color)); + tbo_frame_add_obj (frame, obj); + tbo_window_enter_frame (tbo, frame); + tbo_tool_selector_set_selected_obj (selector, obj); + tbo_window_mark_clean (tbo); + tbo_undo_stack_clear (tbo->undo_stack); + + event.keyval = GDK_KEY_Right; + TBO_TOOL_BASE (selector)->on_key (TBO_TOOL_BASE (selector), tbo->drawing, event); + if (!tbo_window_has_unsaved_changes (tbo) || + !tbo_undo_active_undo (tbo->undo_stack) || + !gtk_widget_is_sensitive (tbo->toolbar->button_undo) || + obj->x != 20) + return 6; + + tbo_window_undo_cb (NULL, tbo); + if (obj->x != 10) + return 7; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/document_state_reset_check.c b/tests/document_state_reset_check.c new file mode 100644 index 0000000..400539f --- /dev/null +++ b/tests/document_state_reset_check.c @@ -0,0 +1,120 @@ +#include + +#include "tbo-window.h" +#include "comic.h" +#include "page.h" +#include "frame.h" +#include "tbo-toolbar.h" +#include "tbo-drawing.h" +#include "tbo-tool-frame.h" +#include "tbo-tool-selector.h" +#include "tbo-tool-text.h" + +static TboObjectText * +find_first_text (Frame *frame) +{ + GList *objects; + + for (objects = tbo_frame_get_objects (frame); objects != NULL; objects = objects->next) + { + if (TBO_IS_OBJECT_TEXT (objects->data)) + return TBO_OBJECT_TEXT (objects->data); + } + + return NULL; +} + +int main(int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + GList *frames; + TboObjectText *text; + TboToolText *text_tool; + TboToolFrame *frame_tool; + TboToolSelector *selector; + + if (argc != 2) + return 2; + + gtk_init(); + + app = gtk_application_new ("net.danigm.tbo.documentstate", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_open (tbo, argv[1]); + + page = tbo_comic_get_current_page (tbo->comic); + frames = tbo_page_get_frames (page); + if (frames == NULL) + return 4; + + frame = frames->data; + text = find_first_text (frame); + if (text == NULL) + return 5; + + tbo_window_enter_frame (tbo, frame); + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) == NULL) + return 6; + + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + frame_tool = TBO_TOOL_FRAME (tbo->toolbar->tools[TBO_TOOLBAR_FRAME]); + text_tool = TBO_TOOL_TEXT (tbo->toolbar->tools[TBO_TOOLBAR_TEXT]); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + tbo_tool_text_set_selected (text_tool, text); + if (text_tool->text_selected == NULL) + return 7; + + frame_tool->tmp_frame = tbo_frame_new (0, 0, 10, 10); + frame_tool->n_frame_x = 0; + frame_tool->n_frame_y = 0; + + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_frame_move_new (frame, + tbo_frame_get_x (frame), + tbo_frame_get_y (frame), + tbo_frame_get_x (frame) + 10, + tbo_frame_get_y (frame) + 10)); + if (!tbo_undo_active_undo (tbo->undo_stack)) + return 8; + + tbo_comic_open (tbo, argv[1]); + + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != NULL) + return 9; + if (text_tool->text_selected != NULL) + return 10; + if (frame_tool->tmp_frame != NULL) + return 11; + if (selector->selected_frame != NULL || selector->selected_object != NULL) + return 12; + if (tbo_undo_active_undo (tbo->undo_stack) || tbo_undo_active_redo (tbo->undo_stack)) + return 13; + if (tbo_toolbar_get_selected_tool (tbo->toolbar) != tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]) + return 14; + + page = tbo_comic_get_current_page (tbo->comic); + frames = tbo_page_get_frames (page); + if (frames == NULL) + return 15; + + frame = frames->data; + text = find_first_text (frame); + if (text == NULL) + return 16; + + tbo_window_enter_frame (tbo, frame); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + tbo_tool_text_set_selected (text_tool, text); + + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/enter_frame_key_check.c b/tests/enter_frame_key_check.c new file mode 100644 index 0000000..32efabe --- /dev/null +++ b/tests/enter_frame_key_check.c @@ -0,0 +1,78 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static GtkEventControllerKey * +find_window_key_controller (GtkWidget *window) +{ + GListModel *controllers; + guint i; + + controllers = gtk_widget_observe_controllers (window); + for (i = 0; i < g_list_model_get_n_items (controllers); i++) + { + GtkEventController *controller = g_list_model_get_item (controllers, i); + + if (GTK_IS_EVENT_CONTROLLER_KEY (controller) && + gtk_event_controller_get_propagation_phase (controller) == GTK_PHASE_CAPTURE) + { + g_object_unref (controllers); + return GTK_EVENT_CONTROLLER_KEY (controller); + } + + g_object_unref (controller); + } + + g_object_unref (controllers); + return NULL; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + GtkEventControllerKey *controller; + gboolean handled = FALSE; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.enterframe", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 100, 80); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + gtk_widget_grab_focus (tbo->notebook); + + controller = find_window_key_controller (tbo->window); + if (controller == NULL) + return 3; + + g_signal_emit_by_name (controller, "key-pressed", GDK_KEY_Return, 0u, 0u, &handled); + if (!handled) + return 4; + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != frame) + return 5; + + g_object_unref (controller); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_formats_check.c b/tests/export_formats_check.c new file mode 100644 index 0000000..88932d2 --- /dev/null +++ b/tests/export_formats_check.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +#include "comic.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page1; + Page *page2; + gchar *svg_base; + gchar *png_base; + gchar *pdf_base; + gchar *file0 = NULL; + gchar *file1 = NULL; + gchar *svg0 = NULL; + gchar *svg1 = NULL; + gchar *pdffile = NULL; + gchar *contents = NULL; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportformats", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page1 = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page1, 10, 10, 120, 90); + page2 = tbo_comic_new_page (tbo->comic); + tbo_page_new_frame (page2, 20, 20, 140, 100); + + svg_base = make_tmp_base ("tbo-export-svg-XXXXXX"); + if (!tbo_export_file (tbo, svg_base, "svg", 800, 450)) + return 3; + svg0 = g_strdup_printf ("%s0.svg", svg_base); + svg1 = g_strdup_printf ("%s1.svg", svg_base); + if (!g_file_test (svg0, G_FILE_TEST_EXISTS) || !g_file_test (svg1, G_FILE_TEST_EXISTS)) + return 4; + if (!g_file_get_contents (svg0, &contents, NULL, NULL) || strstr (contents, "window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_result_check.c b/tests/export_result_check.c new file mode 100644 index 0000000..0fc10fc --- /dev/null +++ b/tests/export_result_check.c @@ -0,0 +1,56 @@ +#include +#include + +#include "comic.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + gchar *tmpbase; + gchar *pngfile; + gint fd; + gboolean exported; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportresult", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 10, 10, 100, 80); + tbo_frame_set_color_rgb (frame, 0.4, 0.5, 0.6); + + tmpbase = g_build_filename (g_get_tmp_dir (), "tbo-export-result-XXXXXX", NULL); + fd = g_mkstemp (tmpbase); + if (fd < 0) + return 3; + close (fd); + g_remove (tmpbase); + + exported = tbo_export_file (tbo, tmpbase, "png", 800, 450); + pngfile = g_strdup_printf ("%s.png", tmpbase); + if (!exported || !g_file_test (pngfile, G_FILE_TEST_EXISTS)) + return 4; + + if (tbo_export_file (tbo, "", "png", 800, 450)) + return 5; + + g_remove (pngfile); + g_free (pngfile); + g_free (tmpbase); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/frame_count_status_check.c b/tests/frame_count_status_check.c new file mode 100644 index 0000000..28f85c0 --- /dev/null +++ b/tests/frame_count_status_check.c @@ -0,0 +1,44 @@ +#include +#include + +#include "tbo-tool-base.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolBase *tool; + TboPointerEvent click_event = { .x = 10, .y = 10 }; + TboPointerEvent release_event = { .x = 110, .y = 90 }; + const gchar *status; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.framecountstatus", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_window_refresh_status (tbo); + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Frames: 0") == NULL) + return 3; + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_FRAME); + tool = tbo_toolbar_get_selected_tool (tbo->toolbar); + tool->on_click (tool, tbo->drawing, &click_event); + tool->on_release (tool, tbo->drawing, &release_event); + + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Frames: 1") == NULL) + return 4; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/frame_gobject_lifetime_check.c b/tests/frame_gobject_lifetime_check.c new file mode 100644 index 0000000..50988fb --- /dev/null +++ b/tests/frame_gobject_lifetime_check.c @@ -0,0 +1,100 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static Frame * +find_frame_with_objects (Page *page) +{ + GList *frames; + + for (frames = tbo_page_get_frames (page); frames != NULL; frames = frames->next) + { + Frame *frame = frames->data; + + if (tbo_frame_object_count (frame) > 0) + return frame; + } + + return NULL; +} + +int +main (int argc, char **argv) +{ + GtkApplication *app; + TboWindow *tbo; + TboDrawing *drawing; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *obj; + + if (argc != 2) + return 2; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.framegobject", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_open (tbo, argv[1]); + + page = tbo_comic_get_current_page (tbo->comic); + frame = find_frame_with_objects (page); + if (frame == NULL) + return 4; + + if (!G_IS_OBJECT (frame) || !TBO_IS_FRAME (frame)) + return 5; + + drawing = TBO_DRAWING (tbo->drawing); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + + tbo_window_enter_frame (tbo, frame); + if (tbo_drawing_get_current_frame (drawing) != frame) + return 6; + if (tbo_tool_selector_get_selected_frame (selector) != frame) + return 7; + + obj = tbo_frame_get_objects (frame)->data; + tbo_tool_selector_set_selected_obj (selector, obj); + if (tbo_tool_selector_get_selected_obj (selector) != obj) + return 8; + + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_move_new (obj, obj->x, obj->y, obj->x + 10, obj->y + 10)); + tbo_frame_del_obj (frame, obj); + if (tbo_tool_selector_get_selected_obj (selector) != NULL) + return 9; + + tbo_undo_stack_undo (tbo->undo_stack); + tbo_undo_stack_redo (tbo->undo_stack); + + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_frame_move_new (frame, + tbo_frame_get_x (frame), + tbo_frame_get_y (frame), + tbo_frame_get_x (frame) + 10, + tbo_frame_get_y (frame) + 10)); + tbo_page_del_frame (page, frame); + + if (tbo_drawing_get_current_frame (drawing) != NULL) + return 10; + if (tbo_tool_selector_get_selected_frame (selector) != NULL) + return 11; + + tbo_undo_stack_undo (tbo->undo_stack); + tbo_undo_stack_redo (tbo->undo_stack); + + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/i18n_check.c b/tests/i18n_check.c new file mode 100644 index 0000000..98a1776 --- /dev/null +++ b/tests/i18n_check.c @@ -0,0 +1,38 @@ +#include +#include +#include + +#include "tbo-utils.h" + +int +main (void) +{ + const gchar *translated; + const gchar *translated_status; + gchar *locale_dir; + + g_setenv ("LC_ALL", "es_ES.UTF-8", TRUE); + g_setenv ("LANGUAGE", "es_ES:es", TRUE); + + tbo_init_i18n (); + + locale_dir = tbo_get_locale_path (); + if (g_file_test (locale_dir, G_FILE_TEST_IS_DIR) == FALSE) + return 2; + + translated = _("Untitled"); + g_free (locale_dir); + + if (strcmp (translated, "Untitled") == 0) + return 3; + if (g_str_has_prefix (translated, "Sin") == FALSE) + return 4; + + translated_status = _("Page %d of %d"); + if (strcmp (translated_status, "Page %d of %d") == 0) + return 5; + if (g_str_has_prefix (translated_status, "Pá") == FALSE) + return 6; + + return 0; +} diff --git a/tests/invalid_tbo_check.c b/tests/invalid_tbo_check.c new file mode 100644 index 0000000..23c3b29 --- /dev/null +++ b/tests/invalid_tbo_check.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +#include "comic-load.h" +#include "comic.h" + +int +main (int argc, char **argv) +{ + gchar *tmpname; + gint fd; + Comic *comic; + const gchar *invalid = + "" + " " + " " + " broken" + " " + " " + ""; + + if (argc != 2) + return 2; + + gtk_init (); + + tmpname = g_build_filename (g_get_tmp_dir (), "tbo-invalid-XXXXXX.tbo", NULL); + fd = g_mkstemp (tmpname); + if (fd < 0) + return 3; + if (write (fd, invalid, strlen (invalid)) < 0) + return 4; + close (fd); + + comic = tbo_comic_load_with_alerts (tmpname, FALSE); + g_remove (tmpname); + g_free (tmpname); + if (comic != NULL) + { + tbo_comic_free (comic); + return 5; + } + + comic = tbo_comic_load (argv[1]); + if (comic == NULL) + return 6; + + tbo_comic_free (comic); + return 0; +} diff --git a/tests/invalid_tbo_variants_check.c b/tests/invalid_tbo_variants_check.c new file mode 100644 index 0000000..2cfe74a --- /dev/null +++ b/tests/invalid_tbo_variants_check.c @@ -0,0 +1,68 @@ +#include +#include +#include +#include + +#include "comic-load.h" +#include "comic.h" + +static gboolean +invalid_case_fails (const gchar *xml) +{ + gchar *tmpname = g_build_filename (g_get_tmp_dir (), "tbo-invalid-variant-XXXXXX.tbo", NULL); + gint fd = g_mkstemp (tmpname); + Comic *comic; + + if (fd < 0) + return FALSE; + if (write (fd, xml, strlen (xml)) < 0) + { + close (fd); + g_remove (tmpname); + g_free (tmpname); + return FALSE; + } + close (fd); + + comic = tbo_comic_load_with_alerts (tmpname, FALSE); + g_remove (tmpname); + g_free (tmpname); + if (comic != NULL) + { + tbo_comic_free (comic); + return FALSE; + } + + return TRUE; +} + +int +main (int argc, char **argv) +{ + const gchar *cases[] = { + "", + "", + "", + "broken", + "", + }; + guint i; + Comic *comic; + + if (argc != 2) + return 2; + + gtk_init (); + + for (i = 0; i < G_N_ELEMENTS (cases); i++) + { + if (!invalid_case_fails (cases[i])) + return 3 + i; + } + + comic = tbo_comic_load (argv[1]); + if (comic == NULL) + return 10; + tbo_comic_free (comic); + return 0; +} diff --git a/tests/legacy_decimal_load_check.c b/tests/legacy_decimal_load_check.c new file mode 100644 index 0000000..0a83c55 --- /dev/null +++ b/tests/legacy_decimal_load_check.c @@ -0,0 +1,60 @@ +#include + +#include "comic-load.h" +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" + +int +main (int argc, char **argv) +{ + Comic *comic; + Page *page; + GList *frames; + GList *objects; + Frame *frame; + TboObjectText *text = NULL; + GdkRGBA color; + + if (argc != 2) + return 2; + + gtk_init (); + + comic = tbo_comic_load (argv[1]); + if (comic == NULL) + return 3; + + page = tbo_comic_get_current_page (comic); + if (page == NULL) + return 4; + + frames = tbo_page_get_frames (page); + if (frames == NULL) + return 5; + + frame = frames->data; + tbo_frame_get_color (frame, &color); + if (color.red < 0.99 || color.green < 0.99 || color.blue < 0.99) + return 6; + + for (objects = tbo_frame_get_objects (frame); objects != NULL; objects = objects->next) + { + if (TBO_IS_OBJECT_TEXT (objects->data)) + { + text = TBO_OBJECT_TEXT (objects->data); + break; + } + } + + if (text == NULL) + return 7; + if (g_strcmp0 (tbo_object_text_get_text (text), "Tutorial") != 0) + return 8; + if (text->font_color->red > 0.01 || text->font_color->green > 0.01 || text->font_color->blue > 0.01) + return 9; + + tbo_comic_free (comic); + return 0; +} diff --git a/tests/load_render_check.c b/tests/load_render_check.c index 2bb4b48..a27a616 100644 --- a/tests/load_render_check.c +++ b/tests/load_render_check.c @@ -31,9 +31,13 @@ int main(int argc, char **argv) return 5; drawing = tbo_drawing_new_with_params (comic); - surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, comic->width, comic->height); + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + tbo_comic_get_width (comic), + tbo_comic_get_height (comic)); cr = cairo_create (surface); - tbo_drawing_draw_page (TBO_DRAWING (drawing), cr, page, comic->width, comic->height); + tbo_drawing_draw_page (TBO_DRAWING (drawing), cr, page, + tbo_comic_get_width (comic), + tbo_comic_get_height (comic)); if (cairo_status (cr) != CAIRO_STATUS_SUCCESS || cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) diff --git a/tests/model_current_state_check.c b/tests/model_current_state_check.c new file mode 100644 index 0000000..126c1fe --- /dev/null +++ b/tests/model_current_state_check.c @@ -0,0 +1,81 @@ +#include "comic.h" +#include "page.h" +#include "frame.h" + +int +main (void) +{ + Comic *comic = tbo_comic_new ("Test", 800, 600); + Page *page1; + Page *page2; + Page *page3; + Page *page4; + Frame *frame1; + Frame *frame2; + Frame *frame3; + + if (comic == NULL) + return 1; + + page1 = tbo_comic_get_current_page (comic); + if (page1 == NULL) + return 2; + + page2 = tbo_comic_new_page (comic); + if (tbo_comic_get_current_page (comic) != page1) + return 3; + + tbo_comic_set_current_page (comic, page2); + page3 = tbo_comic_new_page (comic); + if (tbo_comic_get_current_page (comic) != page2) + return 4; + + if (tbo_comic_next_page (comic) != page3) + return 5; + if (tbo_comic_get_current_page (comic) != page3) + return 6; + + tbo_comic_del_page (comic, 0); + if (tbo_comic_get_current_page (comic) != page3) + return 7; + + if (!tbo_comic_del_current_page (comic)) + return 8; + if (tbo_comic_get_current_page (comic) != page2) + return 9; + + page4 = tbo_comic_new_page (comic); + if (tbo_comic_get_current_page (comic) != page2) + return 10; + + frame1 = tbo_page_new_frame (page2, 0, 0, 20, 20); + if (tbo_page_get_current_frame (page2) != frame1) + return 11; + + frame2 = tbo_page_new_frame (page2, 10, 10, 30, 30); + if (tbo_page_get_current_frame (page2) != frame1) + return 12; + + tbo_page_set_current_frame (page2, frame2); + frame3 = tbo_page_new_frame (page2, 20, 20, 40, 40); + if (tbo_page_get_current_frame (page2) != frame2) + return 13; + + tbo_page_del_frame (page2, frame1); + if (tbo_page_get_current_frame (page2) != frame2) + return 14; + + tbo_page_del_frame (page2, frame2); + if (tbo_page_get_current_frame (page2) != frame3) + return 15; + + tbo_page_set_current_frame (page2, frame3); + if (tbo_page_frame_index (page2) != 1) + return 16; + + if (tbo_comic_page_nth (comic, page4) != 1) + return 17; + + tbo_comic_free (comic); + return 0; +} diff --git a/tests/page_gobject_lifetime_check.c b/tests/page_gobject_lifetime_check.c new file mode 100644 index 0000000..687e8a6 --- /dev/null +++ b/tests/page_gobject_lifetime_check.c @@ -0,0 +1,45 @@ +#include + +#include "comic.h" +#include "page.h" +#include "frame.h" + +int +main (void) +{ + Comic *comic; + Page *page; + Frame *frame; + + comic = tbo_comic_new ("Test", 800, 600); + if (comic == NULL) + return 1; + + page = tbo_comic_get_current_page (comic); + if (page == NULL) + return 2; + + if (!G_IS_OBJECT (page) || !TBO_IS_PAGE (page)) + return 3; + + frame = tbo_page_new_frame (page, 10, 20, 30, 40); + if (frame == NULL) + return 4; + + g_object_ref (page); + tbo_comic_free (comic); + + if (tbo_page_len (page) != 1) + return 5; + if (tbo_page_get_current_frame (page) != frame) + return 6; + if (tbo_frame_get_width (frame) != 30 || tbo_frame_get_height (frame) != 40) + return 7; + + tbo_page_del_frame (page, frame); + if (tbo_page_len (page) != 0) + return 8; + + g_object_unref (page); + return 0; +} diff --git a/tests/page_status_check.c b/tests/page_status_check.c new file mode 100644 index 0000000..d62e9b4 --- /dev/null +++ b/tests/page_status_check.c @@ -0,0 +1,38 @@ +#include +#include + +#include "comic.h" +#include "page.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page2; + const gchar *status; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.pagestatus", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page2 = tbo_comic_new_page (tbo->comic); + tbo_window_add_page_widget (tbo, create_darea (tbo)); + tbo_comic_set_current_page (tbo->comic, page2); + tbo_window_set_current_tab_page (tbo, TRUE); + tbo_window_refresh_status (tbo); + + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (g_str_has_prefix (status, "Page 2 of 2 | Frames: 0 | Enter: frame") == FALSE) + return 3; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/page_undo_check.c b/tests/page_undo_check.c new file mode 100644 index 0000000..f447461 --- /dev/null +++ b/tests/page_undo_check.c @@ -0,0 +1,48 @@ +#include + +#include "comic.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.pageundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + if (tbo_window_get_page_count (tbo) != 1) + return 3; + + g_signal_emit_by_name (tbo->toolbar->button_new_page, "clicked"); + if (tbo_comic_len (tbo->comic) != 2 || tbo_window_get_page_count (tbo) != 2) + return 4; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 1 || tbo_window_get_page_count (tbo) != 1) + return 5; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 2 || tbo_window_get_page_count (tbo) != 2) + return 6; + + g_signal_emit_by_name (tbo->toolbar->button_delete_page, "clicked"); + if (tbo_comic_len (tbo->comic) != 1 || tbo_window_get_page_count (tbo) != 1) + return 7; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 2 || tbo_window_get_page_count (tbo) != 2) + return 8; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/resize_rotate_undo_check.c b/tests/resize_rotate_undo_check.c new file mode 100644 index 0000000..bf67b6e --- /dev/null +++ b/tests/resize_rotate_undo_check.c @@ -0,0 +1,96 @@ +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-tool-base.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-undo.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *obj; + TboPointerEvent move_event = { 0 }; + TboPointerEvent release_event = { 0 }; + GdkRGBA color = { 0, 0, 0, 1 }; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.resizeundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 100, 80); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + selector->clicked = TRUE; + selector->resizing = TRUE; + selector->start_x = 100; + selector->start_y = 80; + selector->start_m_x = tbo_frame_get_x (frame); + selector->start_m_y = tbo_frame_get_y (frame); + selector->start_m_w = tbo_frame_get_width (frame); + selector->start_m_h = tbo_frame_get_height (frame); + move_event.x = 0; + move_event.y = 0; + TBO_TOOL_BASE (selector)->on_move (TBO_TOOL_BASE (selector), tbo->drawing, &move_event); + if (tbo_frame_get_width (frame) != 1 || tbo_frame_get_height (frame) != 1) + return 3; + + TBO_TOOL_BASE (selector)->on_release (TBO_TOOL_BASE (selector), tbo->drawing, &release_event); + if (!tbo_undo_active_undo (tbo->undo_stack)) + return 4; + + tbo_undo_stack_undo (tbo->undo_stack); + if (tbo_frame_get_width (frame) != 100 || tbo_frame_get_height (frame) != 80) + return 5; + tbo_undo_stack_redo (tbo->undo_stack); + if (tbo_frame_get_width (frame) != 1 || tbo_frame_get_height (frame) != 1) + return 6; + + tbo_undo_stack_clear (tbo->undo_stack); + + obj = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 60, 20, "rotate", "Sans 12", &color)); + tbo_frame_add_obj (frame, obj); + tbo_window_enter_frame (tbo, frame); + tbo_tool_selector_set_selected_obj (selector, obj); + selector->clicked = TRUE; + selector->rotating = TRUE; + selector->start_m_x = obj->x; + selector->start_m_y = obj->y; + selector->start_m_w = obj->width; + selector->start_m_h = obj->height; + selector->start_m_angle = obj->angle; + obj->angle = 0.75; + + TBO_TOOL_BASE (selector)->on_release (TBO_TOOL_BASE (selector), tbo->drawing, &release_event); + if (!tbo_undo_active_undo (tbo->undo_stack)) + return 7; + + tbo_undo_stack_undo (tbo->undo_stack); + if (fabs (obj->angle) > 1e-9) + return 8; + tbo_undo_stack_redo (tbo->undo_stack); + if (fabs (obj->angle - 0.75) > 1e-9) + return 9; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/status_hierarchy_check.c b/tests/status_hierarchy_check.c new file mode 100644 index 0000000..efb901d --- /dev/null +++ b/tests/status_hierarchy_check.c @@ -0,0 +1,62 @@ +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *obj; + GdkRGBA color = { 0, 0, 0, 1 }; + const gchar *status; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.statushierarchy", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 100, 80); + obj = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 60, 20, "hello", "Sans 12", &color)); + tbo_frame_add_obj (frame, obj); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + tbo_window_refresh_status (tbo); + + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Page 1 of 1") == NULL || + strstr (status, "Frames: 1") == NULL || + strstr (status, "Frame 1 selected") == NULL) + return 3; + + tbo_window_enter_frame (tbo, frame); + tbo_tool_selector_set_selected_obj (selector, obj); + tbo_window_refresh_status (tbo); + + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Page 1 of 1") == NULL || + strstr (status, "Editing frame 1") == NULL || + strstr (status, "Object: Text") == NULL) + return 4; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/text_undo_check.c b/tests/text_undo_check.c new file mode 100644 index 0000000..dfeb373 --- /dev/null +++ b/tests/text_undo_check.c @@ -0,0 +1,76 @@ +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-tool-text.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + TboObjectText *text; + TboToolText *tool; + GdkRGBA color = { 0, 0, 0, 1 }; + gchar *original_font; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.textundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (10, 10, 60, 20, "old", "Sans 12", &color)); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (text)); + tbo_window_enter_frame (tbo, frame); + tool = TBO_TOOL_TEXT (tbo->toolbar->tools[TBO_TOOLBAR_TEXT]); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + tbo_tool_text_set_selected (tool, text); + original_font = tbo_object_text_get_string (text); + + tbo_undo_stack_clear (tbo->undo_stack); + gtk_text_buffer_set_text (tool->text_buffer, "new text", -1); + drain_events (); + if (strcmp (tbo_object_text_get_text (text), "new text") != 0) + return 3; + + tbo_window_undo_cb (NULL, tbo); + if (strcmp (tbo_object_text_get_text (text), "old") != 0) + return 4; + tbo_window_redo_cb (NULL, tbo); + if (strcmp (tbo_object_text_get_text (text), "new text") != 0) + return 5; + + tbo_undo_stack_clear (tbo->undo_stack); + gtk_font_dialog_button_set_font_desc (GTK_FONT_DIALOG_BUTTON (tool->font), + pango_font_description_from_string ("Sans Bold 18")); + drain_events (); + if (strcmp (tbo_object_text_get_string (text), original_font) == 0) + return 6; + tbo_window_undo_cb (NULL, tbo); + if (strcmp (tbo_object_text_get_string (text), original_font) != 0) + return 7; + + g_free (original_font); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/toolbar_save_button_check.c b/tests/toolbar_save_button_check.c new file mode 100644 index 0000000..573a579 --- /dev/null +++ b/tests/toolbar_save_button_check.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + gchar *tmpname; + gint fd; + gchar *contents = NULL; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.toolbarsave", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 10, 10, 100, 80); + tbo_frame_set_color_rgb (frame, 0.4, 0.5, 0.6); + tbo_window_mark_dirty (tbo); + + tmpname = g_build_filename (g_get_tmp_dir (), "tbo-toolbar-save-XXXXXX.tbo", NULL); + fd = g_mkstemp (tmpname); + if (fd < 0) + return 3; + close (fd); + + tbo_window_set_path (tbo, tmpname); + g_signal_emit_by_name (tbo->toolbar->button_save, "clicked"); + while (g_main_context_iteration (NULL, FALSE)); + + if (tbo_window_has_unsaved_changes (tbo)) + return 4; + if (!g_file_test (tmpname, G_FILE_TEST_EXISTS)) + return 5; + if (!g_file_get_contents (tmpname, &contents, NULL, NULL) || strstr (contents, "window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/tooltip_scope_check.c b/tests/tooltip_scope_check.c new file mode 100644 index 0000000..aac4110 --- /dev/null +++ b/tests/tooltip_scope_check.c @@ -0,0 +1,55 @@ +#include + +#include "tbo-drawing.h" +#include "tbo-tooltip.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo1; + TboWindow *tbo2; + TboDrawing *drawing1; + TboDrawing *drawing2; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.tooltipscope", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo1 = tbo_new_tbo (app, 800, 450); + tbo2 = tbo_new_tbo (app, 800, 450); + drawing1 = TBO_DRAWING (tbo1->drawing); + drawing2 = TBO_DRAWING (tbo2->drawing); + + tbo_tooltip_set ("one", 10, 20, tbo1); + if (drawing1->tooltip == NULL || strcmp (drawing1->tooltip->str, "one") != 0) + return 3; + if (drawing2->tooltip != NULL) + return 4; + + tbo_tooltip_set_center_timeout ("two", 1000, tbo2); + if (drawing2->tooltip == NULL || strcmp (drawing2->tooltip->str, "two") != 0) + return 5; + if (drawing2->tooltip_timeout_id == 0) + return 6; + if (drawing1->tooltip == NULL || strcmp (drawing1->tooltip->str, "one") != 0) + return 7; + + tbo_tooltip_reset (tbo1); + tbo_tooltip_reset (tbo2); + if (drawing1->tooltip != NULL || drawing2->tooltip != NULL) + return 8; + if (drawing2->tooltip_timeout_id != 0) + return 9; + + tbo_window_mark_clean (tbo1); + tbo_window_mark_clean (tbo2); + gtk_window_close (GTK_WINDOW (tbo1->window)); + gtk_window_close (GTK_WINDOW (tbo2->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/xml_roundtrip_check.c b/tests/xml_roundtrip_check.c new file mode 100644 index 0000000..2a81eca --- /dev/null +++ b/tests/xml_roundtrip_check.c @@ -0,0 +1,122 @@ +#include +#include +#include + +#include "comic-load.h" +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-pixmap.h" +#include "tbo-object-svg.h" +#include "tbo-object-text.h" +#include "tbo-window.h" + +static gchar * +build_long_text (void) +{ + GString *text = g_string_new ("Header <&> \"quoted\"\n"); + gint i; + + for (i = 0; i < 300; i++) + g_string_append_printf (text, "Line %d &\"value\"\n", i); + + return g_string_free (text, FALSE); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + TboObjectText *text; + TboObjectSvg *svg; + TboObjectPixmap *pixmap; + GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 }; + gchar *long_text; + gchar *expected_text; + gchar *tmpname; + gint fd; + gchar *contents = NULL; + Comic *reloaded; + GList *objects; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.xmlroundtrip", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 0, 0, 300, 200); + long_text = build_long_text (); + expected_text = g_strdup (long_text); + g_strstrip (expected_text); + + text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (10, 10, 200, 100, long_text, "Sans 12", &color)); + svg = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (5, 5, 20, 20, "assets/& weird \"quote\" .svg")); + pixmap = TBO_OBJECT_PIXMAP (tbo_object_pixmap_new_with_params (15, 15, 25, 25, "assets/& weird \"quote\" .png")); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (text)); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (svg)); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (pixmap)); + + tmpname = g_build_filename (g_get_tmp_dir (), "tbo-xml-roundtrip-XXXXXX.tbo", NULL); + fd = g_mkstemp (tmpname); + if (fd < 0) + return 3; + close (fd); + + if (!tbo_comic_save (tbo, tmpname)) + return 4; + if (!g_file_get_contents (tmpname, &contents, NULL, NULL)) + return 5; + if (strstr (contents, "assets/& weird") != NULL) + return 6; + if (strstr (contents, "& weird "quote" <svg>.svg") == NULL) + return 7; + if (strstr (contents, "Header <&>") != NULL) + return 8; + if (strstr (contents, "Header <&> "quoted"") == NULL) + return 9; + + reloaded = tbo_comic_load (tmpname); + if (reloaded == NULL) + return 10; + + page = tbo_comic_get_current_page (reloaded); + frame = tbo_page_get_frames (page)->data; + text = NULL; + svg = NULL; + pixmap = NULL; + for (objects = tbo_frame_get_objects (frame); objects != NULL; objects = objects->next) + { + if (TBO_IS_OBJECT_TEXT (objects->data)) + text = TBO_OBJECT_TEXT (objects->data); + else if (TBO_IS_OBJECT_SVG (objects->data)) + svg = TBO_OBJECT_SVG (objects->data); + else if (TBO_IS_OBJECT_PIXMAP (objects->data)) + pixmap = TBO_OBJECT_PIXMAP (objects->data); + } + + if (text == NULL || svg == NULL || pixmap == NULL) + return 11; + if (strcmp (tbo_object_text_get_text (text), expected_text) != 0) + return 12; + if (strcmp (svg->path->str, "assets/& weird \"quote\" .svg") != 0) + return 13; + if (strcmp (pixmap->path->str, "assets/& weird \"quote\" .png") != 0) + return 14; + + g_free (contents); + g_remove (tmpname); + g_free (tmpname); + g_free (long_text); + g_free (expected_text); + tbo_comic_free (reloaded); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} From 0eb39a6914cfce47630ea454160afe12ba21a5d3 Mon Sep 17 00:00:00 2001 From: jaime Date: Tue, 21 Apr 2026 09:26:21 +0200 Subject: [PATCH 15/22] Prevent frame-view bugs and harden export and asset loading --- NEXT-STEPS.md | 224 ++++++++++++++------ meson.build | 126 +++++++++++ src/dnd.c | 20 +- src/dnd.h | 1 + src/doodle-treeview.c | 24 ++- src/export.c | 24 ++- src/frame.c | 7 +- src/tbo-drawing.c | 166 ++++++++++++++- src/tbo-drawing.h | 5 + src/tbo-tool-selector.c | 42 ++-- src/tbo-tool-text.c | 11 +- src/tbo-window.c | 93 ++++---- src/tbo-window.h | 2 + src/ui-menu.c | 61 +++++- tests/doodle_dir_error_check.c | 38 ++++ tests/export_extension_hint_check.c | 83 ++++++++ tests/export_scale_check.c | 133 ++++++++++++ tests/frame_default_color_check.c | 26 +++ tests/frame_view_coordinate_mapping_check.c | 58 +++++ tests/group_clone_check.c | 75 +++++++ tests/group_selection_cleanup_check.c | 89 ++++++++ tests/key_binder_scope_check.c | 41 ++++ tests/zoom_bounds_check.c | 41 ++++ 23 files changed, 1239 insertions(+), 151 deletions(-) create mode 100644 tests/doodle_dir_error_check.c create mode 100644 tests/export_extension_hint_check.c create mode 100644 tests/export_scale_check.c create mode 100644 tests/frame_default_color_check.c create mode 100644 tests/frame_view_coordinate_mapping_check.c create mode 100644 tests/group_clone_check.c create mode 100644 tests/group_selection_cleanup_check.c create mode 100644 tests/key_binder_scope_check.c create mode 100644 tests/zoom_bounds_check.c diff --git a/NEXT-STEPS.md b/NEXT-STEPS.md index 5860b73..eb8dbbc 100644 --- a/NEXT-STEPS.md +++ b/NEXT-STEPS.md @@ -1,92 +1,196 @@ # Next Steps -Actualizado: 2026-04-20 +Actualizado: 2026-04-21 + +## Objetivo de este documento + +Este archivo es el roadmap operativo del agente para continuar el trabajo en `TBO`. +Debe usarse para retomar contexto rapido, priorizar bien y evitar mezclar tareas. ## Estado actual -- Refactor grande de modelo/ownership: hecho -- Calidad: hecha -- Patrones detectados: hechos -- Cobertura final: hecha -- Bugs extra resueltos durante la sesión: hechos -- Suite actual: `29/29` tests OK - -## Cambios importantes ya consolidados - -- `Comic`, `Page` y `Frame` ya son `GObject` -- `undo/redo` cubre ya altas/borrados y la mayoría de mutaciones principales -- El botón principal del toolbar es `Save` -- La barra baja ahora expresa mejor la jerarquía y es traducible -- Las coordenadas del panel inferior se eliminaron por rendimiento - -## Lista global pendiente, en orden recomendado - -1. Plantillas al crear cómic -2. Autosave + recientes + reabrir último proyecto -3. Inspector contextual persistente + jerarquía `página -> frame -> objeto` -4. Miniaturas de páginas -5. Reordenación de páginas por drag&drop + duplicar página -6. Búsqueda instantánea y mejor clasificación de assets -7. Favoritos y recientes de assets -8. Presets de bocadillos y estilos de texto -9. Exportación guiada con preview + rango de páginas -10. Exportar página actual o selección -11. Guía integrada de atajos -12. Modo presentación / visor -13. Identidad visual y consistencia estética -14. Packaging/metainfo +- Proyecto en C con GTK4 y Meson. +- `Comic`, `Page` y `Frame` ya son `GObject`. +- La refactorizacion grande de modelo y ownership ya quedo consolidada. +- `undo/redo` cubre altas, borrados y la mayoria de mutaciones principales. +- El boton principal del toolbar es `Save`. +- La barra inferior ya expresa mejor la jerarquia y es traducible. +- Las coordenadas del panel inferior se eliminaron por rendimiento. +- Suite actual: `38/38` tests OK. + +## Trabajo ya cerrado + +Ya no debe tratarse como pendiente en este roadmap: + +- crash al clonar selecciones agrupadas, +- limpieza del `TboObjectGroup` temporal, +- zoom minimo valido, +- color base de frame sin estado global compartido, +- doble escalado en exportacion, +- conversiones de coordenadas de frame view sin depender de globals fragiles, +- `KEY_BINDER` por ventana, +- duplicacion de extensiones en exportacion, +- manejo seguro de errores al abrir directorios de assets. + +## Principio de trabajo + +Orden recomendado: +1. Priorizar primero mejoras con mucho valor de usuario y bajo riesgo. +2. Despues atacar claridad de flujo, feedback y consistencia UX. +3. Dejar para mas tarde refactors amplios, identidad visual grande y packaging. +4. Mantener cambios minimos, locales y faciles de verificar. +5. No mezclar en una misma sesion una feature de producto con una refactorizacion amplia si no es necesario. + +## Prioridad actual + +No hay bugs criticos abiertos confirmados en esta lista. +La prioridad pasa a ser un roadmap combinado de producto + UX, ordenado por importancia real. + +## Tareas restantes ordenadas por importancia + +1. Plantillas al crear comic. +- Mejor siguiente tarea: alto valor, alcance corto y punto de entrada claro. + +2. Autosave + recientes + reabrir ultimo proyecto. +- Mucho impacto en seguridad percibida y continuidad de trabajo. + +3. Mostrar estado `dirty` de forma visible en titulo o UI persistente. +- Quick win UX muy importante. + +4. Hacer persistente y claro el modo actual: `Page` vs `Frame`. +- Mejora fuerte de claridad y evita errores de contexto. + +5. Exportar pagina actual o seleccion. +- Mejora de uso muy practica y contenida. + +6. Mejorar feedback de insercion de imagen y drag and drop cuando falla. +- El usuario debe saber por que una insercion no se realizo. + +7. Guia integrada de atajos. +- Mucho retorno con poco riesgo. + +8. Inspector contextual persistente con jerarquia `pagina -> frame -> objeto`. +- Mejora fuerte de flujo y discoverability. + +9. Reordenacion de paginas por drag and drop + duplicar pagina. +- Muy util para trabajo real con documentos largos. + +10. Busqueda instantanea y mejor clasificacion de assets. +- Sube mucho el valor del panel lateral cuando el catalogo crece. + +11. Favoritos y recientes de assets. +- Complementa bien la tarea anterior. + +12. Exportacion guiada con preview + rango de paginas. +- Valiosa, pero despues de cerrar lo basico de export. + +13. Presets de bocadillos y estilos de texto. +- Util, pero menos prioritaria que navegacion, contexto y export. + +14. Miniaturas de paginas. +- Interesante, aunque de mayor coste estructural. + +15. Mejorar accesibilidad por teclado en boton de menu y assets. +- Importante, pero despues de las mejoras principales de flujo. + +16. Dejar de forzar `Adwaita-dark`. +- Buena mejora de integracion con el sistema y consistencia visual. + +17. Unificar labels, capitalizacion, copy y tono textual. +- Pulido valioso, no urgente. + +18. Reforzar consistencia visual entre toolbar, sidebar, dialogs e iconografia. +- Trabajo de polish visual progresivo. + +19. Revisar si conviene simplificar la doble barra superior. +- Mejor hacerlo cuando la UX este mas asentada. + +20. Identidad visual y consistencia estetica. +- Linea importante, pero no antes de cerrar lo funcional y la claridad base. + +21. Modo presentacion / visor. +- Interesante como feature, menos importante ahora. + +22. Packaging/metainfo. +- Importante para distribucion, no para el nucleo del flujo de trabajo. + +## Deuda tecnica restante + +No es prioritaria frente al roadmap anterior, pero sigue pendiente: + +- patron repetido entre `Comic`, `Page` y `Frame` para lista + elemento actual, +- undo repartido entre `src/tbo-undo.c` y logica adicional en `src/tbo-tool-selector.c`, +- inconsistencias menores de APIs publicas y contratos de indices, +- include duplicado de `` en `src/comic.c`. + +## Cobertura pendiente + +Faltan tests especificos para: + +- plantillas con layout inicial, +- autosave y recuperacion al arrancar, +- recientes + reabrir ultimo proyecto, +- exportacion de pagina actual o seleccion, +- feedback y rutas de error de insercion por DnD, +- accesibilidad basica de flujos principales por teclado. ## Siguiente tarea a retomar -### 1. Plantillas al crear cómic +### 1. Plantillas al crear comic Objetivo: - -- Añadir presets útiles en `New Comic` -- Crear el documento inicial con tamaño y layout base acordes a la plantilla +- Anadir presets utiles en `New Comic`. +- Crear el documento inicial con tamano y layout base acordes a la plantilla. Plantillas previstas: - - Storyboard `16:9` -- Tira cómica +- Tira comica - `A4` -- Presentación +- Presentacion Punto de entrada principal: - - `src/comic-new-dialog.c` Archivos previsibles a tocar: - - `src/comic-new-dialog.c` - `src/tbo-window.c` - `src/tbo-window.h` -- quizá un helper nuevo pequeño si compensa, pero mejor cambio mínimo +- Solo anadir un helper nuevo si compensa claramente. -Implementación recomendada: +Implementacion recomendada: +1. Anadir selector de plantilla en el dialogo `New Comic`. +2. Al cambiar plantilla, autocompletar `width` y `height`. +3. Al aceptar, crear el comic con el tamano elegido. +4. Aplicar layout inicial solo si la plantilla no es la opcion vacia. +5. Anadir al menos un test de regresion para una plantilla con layout inicial. -1. Añadir selector de plantilla en el diálogo `New Comic` -2. Al cambiar plantilla, autocompletar `width/height` -3. Al aceptar, crear el cómic con el tamaño elegido -4. Aplicar layout inicial solo si la plantilla no es "vacía" implícita -5. Añadir test de regresión para al menos una plantilla con layout inicial +Notas de diseno: +- Mantener simple el dialogo. +- No crear aun un sistema grande de presets persistentes. +- Empezar con layouts estaticos y claros. +- No mezclar esta tarea con miniaturas de paginas ni con una reforma general de UI. -Notas de diseño: +## Decisiones abiertas a vigilar -- Mantener simple el diálogo -- No crear aún un sistema grande de presets persistentes -- Empezar con layouts estáticos y claros +- Si una plantilla crea varios frames iniciales, decidir si eso forma parte del estado base del documento o si entra en el stack de `undo/redo`. +- No mezclar todavia `GtkNotebook` con la futura tarea de miniaturas. +- Si se mejora exportacion, separar claramente correcciones de bugs de mejoras de UX. +- Si se toca multiventana, evitar introducir mas estado global compartido. +- Si se implementa autosave, definir antes el formato, frecuencia y politica de recuperacion. -## Riesgos / cosas a vigilar al retomar - -- `undo/redo` ahora cubre mucho más, pero si una feature nueva crea contenido automáticamente debe decidir si entra o no en el stack -- Si una plantilla crea varios frames iniciales, conviene decidir si se considera estado base del documento o una acción deshacible -- La UI actual usa `GtkNotebook`; las miniaturas de páginas vendrán después, no mezclar ambas tareas - -## Punto de comprobación rápido al volver +## Comprobacion rapida al retomar Ejecutar: - ```bash meson compile -C build && meson test -C build --no-rebuild --print-errorlogs --num-processes 1 ``` + +## Regla practica para el agente + +Antes de empezar una tarea: +1. Confirmar si toca producto, UX o deuda tecnica. +2. Leer los archivos exactos implicados. +3. Hacer el cambio minimo correcto. +4. Anadir o ajustar tests si el cambio altera comportamiento. +5. Verificar compile + tests. +6. Actualizar este documento si cambia la prioridad real del roadmap. diff --git a/meson.build b/meson.build index f491a36..5112e7b 100644 --- a/meson.build +++ b/meson.build @@ -231,6 +231,78 @@ clone_dirty_check = executable( install: false ) +group_clone_check = executable( + 'group-clone-check', + common_sources + files('tests/group_clone_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +group_selection_cleanup_check = executable( + 'group-selection-cleanup-check', + common_sources + files('tests/group_selection_cleanup_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +zoom_bounds_check = executable( + 'zoom-bounds-check', + common_sources + files('tests/zoom_bounds_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_default_color_check = executable( + 'frame-default-color-check', + common_sources + files('tests/frame_default_color_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_scale_check = executable( + 'export-scale-check', + common_sources + files('tests/export_scale_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_view_coordinate_mapping_check = executable( + 'frame-view-coordinate-mapping-check', + common_sources + files('tests/frame_view_coordinate_mapping_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +key_binder_scope_check = executable( + 'key-binder-scope-check', + common_sources + files('tests/key_binder_scope_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_extension_hint_check = executable( + 'export-extension-hint-check', + common_sources + files('tests/export_extension_hint_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +doodle_dir_error_check = executable( + 'doodle-dir-error-check', + common_sources + files('tests/doodle_dir_error_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + enter_frame_key_check = executable( 'enter-frame-key-check', common_sources + files('tests/enter_frame_key_check.c'), @@ -433,6 +505,60 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'group-clone-check', + group_clone_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'group-selection-cleanup-check', + group_selection_cleanup_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'zoom-bounds-check', + zoom_bounds_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-default-color-check', + frame_default_color_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-scale-check', + export_scale_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-view-coordinate-mapping-check', + frame_view_coordinate_mapping_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'key-binder-scope-check', + key_binder_scope_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-extension-hint-check', + export_extension_hint_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'doodle-dir-error-check', + doodle_dir_error_check, + env: ['G_DEBUG=fatal-criticals'] +) + test( 'enter-frame-key-check', enter_frame_key_check, diff --git a/src/dnd.c b/src/dnd.c index 569480c..6707742 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -112,18 +112,28 @@ drop_handl (GtkDropTarget *target, GtkAdjustment *adj; gdouble zoom = tbo_drawing_get_zoom (TBO_DRAWING (tbo->drawing)); const gchar *asset_path = g_value_get_string (value); - gint rx; - gint ry; if (asset_path == NULL) return FALSE; adj = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); - rx = tbo_frame_get_base_x ((x + gtk_adjustment_get_value (adj)) / zoom); + x = (x + gtk_adjustment_get_value (adj)) / zoom; adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); - ry = tbo_frame_get_base_y ((y + gtk_adjustment_get_value (adj)) / zoom); + y = (y + gtk_adjustment_get_value (adj)) / zoom; - return tbo_dnd_insert_asset (tbo, asset_path, rx, ry) != NULL; + return tbo_dnd_insert_asset_at_view_coords (tbo, asset_path, x, y) != NULL; +} + +TboObjectBase * +tbo_dnd_insert_asset_at_view_coords (TboWindow *tbo, const gchar *asset_path, gdouble x, gdouble y) +{ + gint frame_x; + gint frame_y; + + if (!tbo_drawing_view_to_frame (TBO_DRAWING (tbo->drawing), x, y, &frame_x, &frame_y)) + return NULL; + + return tbo_dnd_insert_asset (tbo, asset_path, frame_x, frame_y); } TboObjectBase * diff --git a/src/dnd.h b/src/dnd.h index 196698b..5900dfd 100644 --- a/src/dnd.h +++ b/src/dnd.h @@ -27,6 +27,7 @@ void tbo_dnd_setup_asset_source (GtkWidget *widget, const gchar *full_path, const gchar *relative_path); void tbo_dnd_setup_drawing_dest (TboDrawing *drawing, TboWindow *tbo); +TboObjectBase *tbo_dnd_insert_asset_at_view_coords (TboWindow *tbo, const gchar *asset_path, gdouble x, gdouble y); TboObjectBase *tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y); #endif diff --git a/src/doodle-treeview.c b/src/doodle-treeview.c index 8099dbb..3e63072 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -135,20 +135,36 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) { GError *error = NULL; const gchar *filename; + GDir *dir; struct stat filestat; int st; GArray *array = g_array_new (FALSE, FALSE, sizeof(GString*)); st = stat (base_dir, &filestat); if (st) + { + g_array_free (array, TRUE); return NULL; + } - GDir *dir = g_dir_open (base_dir, 0, &error); + dir = g_dir_open (base_dir, 0, &error); + if (dir == NULL) + { + if (error != NULL) + g_error_free (error); + g_array_free (array, TRUE); + return NULL; + } while ((filename = g_dir_read_name (dir))) { gchar *complete_dir = g_build_filename (base_dir, filename, NULL); st = stat (complete_dir, &filestat); + if (st) + { + g_free (complete_dir); + continue; + } if (isdir && bubble_mode && strcmp (filename, "bubble")) { @@ -202,6 +218,12 @@ doodle_add_images (gchar *dir) gtk_grid_set_row_spacing (GTK_GRID (grid), 8); gtk_grid_set_column_spacing (GTK_GRID (grid), 8); + if (arr == NULL) + { + tbo_widget_show_all (GTK_WIDGET (grid)); + return grid; + } + GString *mystr; for (i=0; ilen; i++) { diff --git a/src/export.c b/src/export.c index abf26b7..32edd07 100644 --- a/src/export.c +++ b/src/export.c @@ -46,6 +46,21 @@ struct export_file_args { GtkEntry *entry; }; +static gchar * +strip_matching_extension (const gchar *filename, const gchar *extension) +{ + const gchar *dot; + + if (filename == NULL || extension == NULL) + return g_strdup (filename); + + dot = strrchr (filename, '.'); + if (dot != NULL && g_ascii_strcasecmp (dot + 1, extension) == 0) + return g_strndup (filename, dot - filename); + + return g_strdup (filename); +} + static void show_export_error (TboWindow *tbo, const gchar *message) { @@ -75,7 +90,7 @@ tbo_export_file (TboWindow *tbo, if (format_hint != NULL && *format_hint != '\0') { export_to = g_ascii_strdown (format_hint, -1); - base_filename = g_strdup (filename); + base_filename = strip_matching_extension (filename, export_to); } else { @@ -346,8 +361,8 @@ tbo_export (TboWindow *tbo) if (response == GTK_RESPONSE_ACCEPT) { - width = (gint) (width * scale); - height = (gint) (height * scale); + width = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spinw)); + height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spinh)); filename = g_strdup (gtk_editable_get_text (GTK_EDITABLE (fileinput))); if (filename == NULL || *filename == '\0') @@ -369,9 +384,6 @@ tbo_export (TboWindow *tbo) else if (export_to_index == 3) format_hint = "svg"; - width = (gint) (width * scale); - height = (gint) (height * scale); - if (!tbo_export_file (tbo, filename, format_hint, width, height)) { gtk_window_destroy (GTK_WINDOW (dialog)); diff --git a/src/frame.c b/src/frame.c index be9cfd8..6989596 100644 --- a/src/frame.c +++ b/src/frame.c @@ -53,10 +53,10 @@ typedef struct G_DEFINE_TYPE (Frame, tbo_frame, G_TYPE_OBJECT); +static const Color DEFAULT_FRAME_COLOR = {1, 1, 1}; static int BASE_X = 0; static int BASE_Y = 0; -static float SCALE_FACTOR = 0; -static Color BASE_COLOR = {1, 1, 1}; +static float SCALE_FACTOR = 1; static void draw_objects (gpointer data, gpointer user_data) @@ -89,7 +89,7 @@ tbo_frame_init (Frame *self) self->width = 0; self->height = 0; self->border = TRUE; - self->color = BASE_COLOR; + self->color = DEFAULT_FRAME_COLOR; self->objects = NULL; } @@ -245,7 +245,6 @@ tbo_frame_set_color_rgb (Frame *frame, gdouble red, gdouble green, gdouble blue) frame->color.r = red; frame->color.g = green; frame->color.b = blue; - BASE_COLOR = frame->color; } GList * diff --git a/src/tbo-drawing.c b/src/tbo-drawing.c index eea353c..45e294e 100644 --- a/src/tbo-drawing.c +++ b/src/tbo-drawing.c @@ -36,6 +36,54 @@ G_DEFINE_TYPE (TboDrawing, tbo_drawing, GTK_TYPE_DRAWING_AREA); static void tbo_drawing_set_window_pointer (TboDrawing *self, TboWindow *tbo); static void tbo_drawing_set_comic_pointer (TboDrawing *self, Comic *comic); +static gdouble +clamp_zoom (gdouble zoom) +{ + if (!isfinite (zoom) || zoom < ZOOM_STEP) + return ZOOM_STEP; + + return zoom; +} + +static gboolean +get_frame_view_transform (TboDrawing *self, Frame *frame, gdouble *scale, gint *base_x, gint *base_y) +{ + gint width; + gint height; + gint centered_x; + gint centered_y; + gdouble scale_x; + gdouble scale_y; + gdouble current_scale; + + if (self == NULL || self->comic == NULL || frame == NULL) + return FALSE; + + width = tbo_comic_get_width (self->comic); + height = tbo_comic_get_height (self->comic); + if (width <= 20 || height <= 20 || + tbo_frame_get_width (frame) <= 0 || tbo_frame_get_height (frame) <= 0) + return FALSE; + + scale_x = (width - 20) / (gdouble) tbo_frame_get_width (frame); + scale_y = (height - 20) / (gdouble) tbo_frame_get_height (frame); + current_scale = MIN (scale_x, scale_y); + if (!isfinite (current_scale) || current_scale <= 0.0) + return FALSE; + + centered_x = (gint) ((width / 2.0) - (tbo_frame_get_width (frame) * current_scale / 2.0)); + centered_y = (gint) ((height / 2.0) - (tbo_frame_get_height (frame) * current_scale / 2.0)); + + if (scale != NULL) + *scale = current_scale; + if (base_x != NULL) + *base_x = centered_x / current_scale; + if (base_y != NULL) + *base_y = centered_y / current_scale; + + return TRUE; +} + static void tbo_drawing_set_current_frame_pointer (TboDrawing *self, Frame *frame) { @@ -135,9 +183,10 @@ static void motion_notify_cb (GtkEventControllerMotion *controller, gdouble x, gdouble y, gpointer user_data) { TboDrawing *self = TBO_DRAWING (user_data); + gdouble zoom = clamp_zoom (self->zoom); TboPointerEvent event = { - .x = x / self->zoom, - .y = y / self->zoom, + .x = x / zoom, + .y = y / zoom, .button = 0, .n_press = 0, .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (controller)), @@ -151,9 +200,10 @@ static void click_pressed_cb (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { TboDrawing *self = TBO_DRAWING (user_data); + gdouble zoom = clamp_zoom (self->zoom); TboPointerEvent event = { - .x = x / self->zoom, - .y = y / self->zoom, + .x = x / zoom, + .y = y / zoom, .button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)), .n_press = n_press, .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)), @@ -174,9 +224,10 @@ static void click_released_cb (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { TboDrawing *self = TBO_DRAWING (user_data); + gdouble zoom = clamp_zoom (self->zoom); TboPointerEvent event = { - .x = x / self->zoom, - .y = y / self->zoom, + .x = x / zoom, + .y = y / zoom, .button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)), .n_press = n_press, .state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)), @@ -370,21 +421,21 @@ tbo_drawing_draw_page (TboDrawing *self, cairo_t *cr, Page *page, gint w, gint h void tbo_drawing_zoom_in (TboDrawing *self) { - self->zoom += ZOOM_STEP; + self->zoom = clamp_zoom (self->zoom + ZOOM_STEP); tbo_drawing_adjust_scroll (self); } void tbo_drawing_zoom_out (TboDrawing *self) { - self->zoom -= ZOOM_STEP; + self->zoom = clamp_zoom (self->zoom - ZOOM_STEP); tbo_drawing_adjust_scroll (self); } void tbo_drawing_zoom_100 (TboDrawing *self) { - self->zoom = 1; + self->zoom = clamp_zoom (1); tbo_drawing_adjust_scroll (self); } @@ -398,7 +449,7 @@ tbo_drawing_zoom_fit (TboDrawing *self) z1 = fabs ((float)w / (float)tbo_comic_get_width (self->comic)); z2 = fabs ((float)h / (float)tbo_comic_get_height (self->comic)); - self->zoom = z1 < z2 ? z1 : z2; + self->zoom = clamp_zoom (z1 < z2 ? z1 : z2); tbo_drawing_adjust_scroll (self); } @@ -408,6 +459,99 @@ tbo_drawing_get_zoom (TboDrawing *self) return self->zoom; } +gdouble +tbo_drawing_get_current_frame_scale (TboDrawing *self) +{ + gdouble scale; + + if (!get_frame_view_transform (self, self->current_frame, &scale, NULL, NULL)) + return 1.0; + + return scale; +} + +gboolean +tbo_drawing_view_to_frame (TboDrawing *self, gdouble view_x, gdouble view_y, gint *frame_x, gint *frame_y) +{ + gdouble scale; + gint base_x; + gint base_y; + + if (!get_frame_view_transform (self, self->current_frame, &scale, &base_x, &base_y)) + return FALSE; + + if (frame_x != NULL) + *frame_x = (view_x / scale) - base_x; + if (frame_y != NULL) + *frame_y = (view_y / scale) - base_y; + + return TRUE; +} + +void +tbo_drawing_get_object_relative (TboDrawing *self, TboObjectBase *obj, gint *x, gint *y, gint *w, gint *h) +{ + gdouble scale; + gint base_x; + gint base_y; + + if (!get_frame_view_transform (self, self->current_frame, &scale, &base_x, &base_y)) + { + if (x != NULL) + *x = 0; + if (y != NULL) + *y = 0; + if (w != NULL) + *w = 0; + if (h != NULL) + *h = 0; + return; + } + + if (x != NULL) + *x = (base_x + obj->x) * scale; + if (y != NULL) + *y = (base_y + obj->y) * scale; + if (w != NULL) + *w = obj->width * scale; + if (h != NULL) + *h = obj->height * scale; +} + +gboolean +tbo_drawing_point_inside_object (TboDrawing *self, TboObjectBase *obj, gint x, gint y) +{ + gint ox; + gint oy; + gint ow; + gint oh; + gint xnew1; + gint ynew1; + gint xnew2; + gint ynew2; + gint xnew3; + gint ynew3; + gint xmax; + gint ymax; + gint xmin; + gint ymin; + + tbo_drawing_get_object_relative (self, obj, &ox, &oy, &ow, &oh); + xnew1 = ox + (ow * cos (obj->angle)); + ynew1 = oy + (ow * sin (obj->angle)); + xnew2 = ox + (-oh * sin (obj->angle)); + ynew2 = oy + (oh * cos (obj->angle)); + xnew3 = ox + (ow * cos (obj->angle) - oh * sin (obj->angle)); + ynew3 = oy + (oh * cos (obj->angle) + ow * sin (obj->angle)); + + xmax = MAX (MAX (ox, xnew1), MAX (xnew2, xnew3)); + ymax = MAX (MAX (oy, ynew1), MAX (ynew2, ynew3)); + xmin = MIN (MIN (ox, xnew1), MIN (xnew2, xnew3)); + ymin = MIN (MIN (oy, ynew1), MIN (ynew2, ynew3)); + + return x >= xmin && x <= xmax && y >= ymin && y <= ymax; +} + void tbo_drawing_adjust_scroll (TboDrawing *self) { @@ -417,6 +561,8 @@ tbo_drawing_adjust_scroll (TboDrawing *self) if (!self->comic) return; + self->zoom = clamp_zoom (self->zoom); + width = MAX (1, ceil (tbo_comic_get_width (self->comic) * self->zoom)); height = MAX (1, ceil (tbo_comic_get_height (self->comic) * self->zoom)); gtk_widget_set_size_request (GTK_WIDGET (self), width, height); diff --git a/src/tbo-drawing.h b/src/tbo-drawing.h index 8d987c5..ff0cf11 100644 --- a/src/tbo-drawing.h +++ b/src/tbo-drawing.h @@ -24,6 +24,7 @@ #include #include #include "tbo-types.h" +#include "tbo-object-base.h" #include "tbo-tool-base.h" #define TBO_TYPE_DRAWING (tbo_drawing_get_type ()) @@ -84,6 +85,10 @@ void tbo_drawing_zoom_out (TboDrawing *self); void tbo_drawing_zoom_100 (TboDrawing *self); void tbo_drawing_zoom_fit (TboDrawing *self); gdouble tbo_drawing_get_zoom (TboDrawing *self); +gdouble tbo_drawing_get_current_frame_scale (TboDrawing *self); +gboolean tbo_drawing_view_to_frame (TboDrawing *self, gdouble view_x, gdouble view_y, gint *frame_x, gint *frame_y); +void tbo_drawing_get_object_relative (TboDrawing *self, TboObjectBase *obj, gint *x, gint *y, gint *w, gint *h); +gboolean tbo_drawing_point_inside_object (TboDrawing *self, TboObjectBase *obj, gint x, gint y); void tbo_drawing_adjust_scroll (TboDrawing *self); void tbo_drawing_init_dnd (TboDrawing *self, TboWindow *tbo); diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index 44ca721..7686413 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -58,6 +58,7 @@ static void open_text_editor (TboToolSelector *self, TboObjectText *text); static void finalize (GObject *object); static void tbo_tool_selector_set_selected_frame_pointer (TboToolSelector *self, Frame *frame); static void tbo_tool_selector_set_selected_object_pointer (TboToolSelector *self, TboObjectBase *obj); +static void clear_selected_group (TboToolSelector *self); #define MIN_FRAME_DIMENSION 1 #define MIN_OBJECT_DIMENSION 1 @@ -212,6 +213,16 @@ tbo_tool_selector_set_selected_object_pointer (TboToolSelector *self, TboObjectB } } +static void +clear_selected_group (TboToolSelector *self) +{ + if (!TBO_IS_OBJECT_GROUP (self->selected_object) || self->selected_frame == NULL) + return; + + if (tbo_frame_has_obj (self->selected_frame, self->selected_object)) + tbo_frame_del_obj (self->selected_frame, self->selected_object); +} + static void update_color_cb (GtkWidget *button, GParamSpec *pspec, TboToolSelector *tool) { @@ -412,7 +423,9 @@ over_resizer_obj (TboToolSelector *self, TboObjectBase *obj, int x, int y) { int rx, ry; int ox, oy, ow, oh; - tbo_frame_get_obj_relative (obj, &ox, &oy, &ow, &oh); + TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing); + + tbo_drawing_get_object_relative (drawing, obj, &ox, &oy, &ow, &oh); rx = ox + (ow * cos(obj->angle) - oh * sin(obj->angle)); ry = oy + (oh * cos(obj->angle) + ow * sin(obj->angle)); @@ -437,7 +450,9 @@ over_rotater_obj (TboToolSelector *self, TboObjectBase *obj, int x, int y) { int rx, ry; int ox, oy, ow, oh; - tbo_frame_get_obj_relative (obj, &ox, &oy, &ow, &oh); + TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing); + + tbo_drawing_get_object_relative (drawing, obj, &ox, &oy, &ow, &oh); rx = ox; ry = oy; @@ -633,6 +648,7 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event { int x, y, offset_x, offset_y; TboToolSelector *self = TBO_TOOL_SELECTOR (tool); + TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); x = (int)event->x; y = (int)event->y; @@ -644,8 +660,10 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event { if (self->clicked) { - offset_x = (self->start_x - x) / tbo_frame_get_scale_factor (); - offset_y = (self->start_y - y) / tbo_frame_get_scale_factor (); + gdouble scale = tbo_drawing_get_current_frame_scale (drawing); + + offset_x = (self->start_x - x) / scale; + offset_y = (self->start_y - y) / scale; // resizing object if (self->resizing) @@ -737,7 +755,7 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *even { obj = TBO_OBJECT_BASE (obj_list->data); tbo_object_group_set_vars (obj); - if (tbo_frame_point_inside_obj (obj, x, y)) + if (tbo_drawing_point_inside_object (drawing, obj, x, y)) { // Selecting last occurrence. obj2 = obj; @@ -826,7 +844,7 @@ frame_view_drawing (TboToolBase *tool, cairo_t *cr) cairo_set_dash (cr, dashes, G_N_ELEMENTS (dashes), 0); cairo_set_source_rgb (cr, border.r, border.g, border.b); int ox, oy, ow, oh; - tbo_frame_get_obj_relative (current_obj, &ox, &oy, &ow, &oh); + tbo_drawing_get_object_relative (drawing, current_obj, &ox, &oy, &ow, &oh); cairo_translate (cr, ox, oy); cairo_rotate (cr, current_obj->angle); @@ -1214,6 +1232,7 @@ finalize (GObject *object) { TboToolSelector *self = TBO_TOOL_SELECTOR (object); + clear_selected_group (self); tbo_tool_selector_set_selected_frame_pointer (self, NULL); tbo_tool_selector_set_selected_object_pointer (self, NULL); @@ -1274,13 +1293,9 @@ tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame) void tbo_tool_selector_set_selected_obj (TboToolSelector *self, TboObjectBase *obj) { - if (!obj && TBO_IS_OBJECT_GROUP (self->selected_object)) - { - TboDrawing *drawing = TBO_DRAWING (TBO_TOOL_BASE (self)->tbo->drawing); - Frame *frame = tbo_drawing_get_current_frame (drawing); - if (frame != NULL && tbo_frame_has_obj (frame, self->selected_object)) - tbo_frame_del_obj (frame, self->selected_object); - } + if (obj != self->selected_object) + clear_selected_group (self); + tbo_tool_selector_set_selected_object_pointer (self, obj); update_menubar (TBO_TOOL_BASE (self)->tbo); } @@ -1297,6 +1312,7 @@ tbo_tool_selector_reset_state (TboToolSelector *self) if (self == NULL) return; + clear_selected_group (self); tbo_tool_selector_set_selected_frame_pointer (self, NULL); tbo_tool_selector_set_selected_object_pointer (self, NULL); self->x = 0; diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index dde0c70..6cc3fa1 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -405,7 +405,8 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) TboObjectText *text; GdkRGBA color; TboToolText *self = TBO_TOOL_TEXT (tool); - Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tool->tbo->drawing)); + TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); + Frame *frame = tbo_drawing_get_current_frame (drawing); if (self->text_selected != NULL) { @@ -423,7 +424,7 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) for (obj_list = tbo_frame_get_objects (frame); obj_list; obj_list = obj_list->next) { obj = TBO_OBJECT_BASE (obj_list->data); - if (TBO_IS_OBJECT_TEXT (obj) && tbo_frame_point_inside_obj (obj, x, y)) + if (TBO_IS_OBJECT_TEXT (obj) && tbo_drawing_point_inside_object (drawing, obj, x, y)) { text = TBO_OBJECT_TEXT (obj); found = TRUE; @@ -431,8 +432,8 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) } if (!found) { - x = tbo_frame_get_base_x (x); - y = tbo_frame_get_base_y (y); + if (!tbo_drawing_view_to_frame (drawing, x, y, &x, &y)) + return; if (x < 0 || y < 0 || x > tbo_frame_get_width (frame) || y > tbo_frame_get_height (frame)) return; @@ -468,7 +469,7 @@ drawing (TboToolBase *tool, cairo_t *cr) cairo_set_dash (cr, dashes, G_N_ELEMENTS (dashes), 0); cairo_set_source_rgb (cr, 0.9, 0, 0); int ox, oy, ow, oh; - tbo_frame_get_obj_relative (obj, &ox, &oy, &ow, &oh); + tbo_drawing_get_object_relative (TBO_DRAWING (tool->tbo->drawing), obj, &ox, &oy, &ow, &oh); cairo_translate (cr, ox, oy); cairo_rotate (cr, obj->angle); diff --git a/src/tbo-window.c b/src/tbo-window.c index 3b6459b..770fba6 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -42,8 +42,6 @@ #include "tbo-widget.h" #include "comic-saveas-dialog.h" -static gboolean KEY_BINDER = TRUE; - static gboolean on_key_cb (GtkEventControllerKey *controller, guint keyval, guint keycode, @@ -433,6 +431,57 @@ load_app_css (void) loaded = TRUE; } +static gboolean +tbo_window_apply_unmodified_key (TboWindow *tbo, guint keyval) +{ + TboDrawing *drawing = TBO_DRAWING (tbo->drawing); + + switch (keyval) + { + case GDK_KEY_plus: + tbo_drawing_zoom_in (drawing); + return TRUE; + case GDK_KEY_minus: + tbo_drawing_zoom_out (drawing); + return TRUE; + case GDK_KEY_1: + tbo_drawing_zoom_100 (drawing); + return TRUE; + case GDK_KEY_2: + tbo_drawing_zoom_fit (drawing); + return TRUE; + case GDK_KEY_s: + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + return TRUE; + case GDK_KEY_t: + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + return TRUE; + case GDK_KEY_d: + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_DOODLE); + return TRUE; + case GDK_KEY_b: + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_BUBBLE); + return TRUE; + case GDK_KEY_f: + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_FRAME); + return TRUE; + default: + return FALSE; + } +} + +gboolean +tbo_window_handle_unmodified_key (TboWindow *tbo, guint keyval, GdkModifierType state) +{ + if (tbo == NULL || tbo->drawing == NULL) + return FALSE; + + if (!tbo->key_binder || (state & (GDK_CONTROL_MASK | GDK_ALT_MASK | GDK_META_MASK)) != 0) + return FALSE; + + return tbo_window_apply_unmodified_key (tbo, keyval); +} + static gboolean on_key_cb (GtkEventControllerKey *controller, guint keyval, @@ -441,7 +490,6 @@ on_key_cb (GtkEventControllerKey *controller, TboWindow *tbo) { TboToolBase *tool; - TboDrawing *drawing = TBO_DRAWING (tbo->drawing); TboKeyEvent event = { .keyval = keyval, .state = state }; if (tbo->drawing == NULL || !gtk_widget_has_focus (GTK_WIDGET (tbo->drawing))) @@ -453,41 +501,7 @@ on_key_cb (GtkEventControllerKey *controller, tbo_window_refresh_status (tbo); - if (KEY_BINDER && (state & (GDK_CONTROL_MASK | GDK_ALT_MASK | GDK_META_MASK)) == 0) - { - switch (keyval) - { - case GDK_KEY_plus: - tbo_drawing_zoom_in (drawing); - break; - case GDK_KEY_minus: - tbo_drawing_zoom_out (drawing); - break; - case GDK_KEY_1: - tbo_drawing_zoom_100 (drawing); - break; - case GDK_KEY_2: - tbo_drawing_zoom_fit (drawing); - break; - case GDK_KEY_s: - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); - break; - case GDK_KEY_t: - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); - break; - case GDK_KEY_d: - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_DOODLE); - break; - case GDK_KEY_b: - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_BUBBLE); - break; - case GDK_KEY_f: - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_FRAME); - break; - default: - break; - } - } + tbo_window_handle_unmodified_key (tbo, keyval, state); return FALSE; } @@ -555,6 +569,7 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, tbo->path = NULL; tbo->browse_path = NULL; tbo->export_path = NULL; + tbo->key_binder = TRUE; tbo->dirty = FALSE; tbo->destroying = FALSE; @@ -829,7 +844,7 @@ tbo_empty_tool_area (TboWindow *tbo) void tbo_window_set_key_binder (TboWindow *tbo, gboolean keyb) { - KEY_BINDER = keyb; + tbo->key_binder = keyb; if (keyb) tbo_menu_enable_accel_keys (tbo); else diff --git a/src/tbo-window.h b/src/tbo-window.h index 68c165d..f5a4879 100644 --- a/src/tbo-window.h +++ b/src/tbo-window.h @@ -41,6 +41,7 @@ struct _TboWindow gchar *path; gchar *browse_path; gchar *export_path; + gboolean key_binder; gboolean dirty; gboolean destroying; }; @@ -69,6 +70,7 @@ void tbo_window_set_key_binder (TboWindow *tbo, gboolean keyb); void tbo_window_enter_frame (TboWindow *tbo, Frame *frame); void tbo_window_leave_frame (TboWindow *tbo); void tbo_window_reset_document_state (TboWindow *tbo); +gboolean tbo_window_handle_unmodified_key (TboWindow *tbo, guint keyval, GdkModifierType state); gboolean tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo); gboolean tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo); diff --git a/src/ui-menu.c b/src/ui-menu.c index 255f29b..6a4d5fa 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -33,6 +33,7 @@ #include "frame.h" #include "page.h" #include "tbo-object-base.h" +#include "tbo-object-group.h" #include "tbo-undo.h" #include "tbo-utils.h" @@ -125,6 +126,39 @@ set_accels_enabled (TboWindow *tbo, gboolean enabled) } } +static void +clone_group_selection (TboWindow *tbo, TboToolSelector *selector, Frame *frame, TboObjectGroup *group, gboolean *cloned) +{ + GList *objects; + TboObjectBase *last_cloned = NULL; + + for (objects = group->objs; objects != NULL; objects = objects->next) + { + TboObjectBase *object = TBO_OBJECT_BASE (objects->data); + TboObjectBase *cloned_obj; + + if (object == NULL || object->clone == NULL) + continue; + + cloned_obj = object->clone (object); + if (cloned_obj == NULL) + continue; + + cloned_obj->x += 10; + cloned_obj->y -= 10; + tbo_frame_add_obj (frame, cloned_obj); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, cloned_obj)); + last_cloned = cloned_obj; + *cloned = TRUE; + } + + if (last_cloned != NULL) + { + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected_obj (selector, last_cloned); + } +} + static void clone_selection (TboWindow *tbo) { @@ -150,14 +184,25 @@ clone_selection (TboWindow *tbo) } else if (obj && tbo_drawing_get_current_frame (drawing)) { - TboObjectBase *cloned_obj = obj->clone (obj); - cloned_obj->x += 10; - cloned_obj->y -= 10; - tbo_frame_add_obj (frame, cloned_obj); - tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, cloned_obj)); - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); - tbo_tool_selector_set_selected_obj (selector, cloned_obj); - cloned = TRUE; + if (TBO_IS_OBJECT_GROUP (obj)) + { + clone_group_selection (tbo, selector, frame, TBO_OBJECT_GROUP (obj), &cloned); + } + else if (obj->clone != NULL) + { + TboObjectBase *cloned_obj = obj->clone (obj); + + if (cloned_obj != NULL) + { + cloned_obj->x += 10; + cloned_obj->y -= 10; + tbo_frame_add_obj (frame, cloned_obj); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, cloned_obj)); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected_obj (selector, cloned_obj); + cloned = TRUE; + } + } } if (cloned) diff --git a/tests/doodle_dir_error_check.c b/tests/doodle_dir_error_check.c new file mode 100644 index 0000000..efb79b0 --- /dev/null +++ b/tests/doodle_dir_error_check.c @@ -0,0 +1,38 @@ +#include +#include +#include + +#include "doodle-treeview.h" + +GArray *get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode); +GtkWidget *doodle_add_images (gchar *dir); + +int +main (void) +{ + gchar *dir; + GtkWidget *grid; + + gtk_init (); + + dir = g_dir_make_tmp ("tbo-doodle-noaccess-XXXXXX", NULL); + if (dir == NULL) + return 2; + + if (g_chmod (dir, 0) != 0) + return 3; + + if (get_files (dir, FALSE, FALSE) != NULL) + return 4; + + grid = doodle_add_images (dir); + if (!GTK_IS_GRID (grid)) + return 5; + if (gtk_widget_get_first_child (grid) != NULL) + return 6; + + g_chmod (dir, 0700); + g_rmdir (dir); + g_free (dir); + return 0; +} diff --git a/tests/export_extension_hint_check.c b/tests/export_extension_hint_check.c new file mode 100644 index 0000000..7803bb5 --- /dev/null +++ b/tests/export_extension_hint_check.c @@ -0,0 +1,83 @@ +#include +#include +#include + +#include "comic.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + gchar *base; + gchar *filename; + gchar *expected; + gchar *unexpected; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportextensionhint", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page, 10, 10, 120, 90); + + base = make_tmp_base ("tbo-export-hint-XXXXXX"); + filename = g_strdup_printf ("%s.png", base); + if (!tbo_export_file (tbo, filename, "png", 800, 450)) + return 3; + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + return 4; + unexpected = g_strdup_printf ("%s.png", filename); + if (g_file_test (unexpected, G_FILE_TEST_EXISTS)) + return 5; + + g_remove (filename); + g_free (unexpected); + + tbo_comic_new_page (tbo->comic); + filename = g_strdup_printf ("%s-pages.png", base); + if (!tbo_export_file (tbo, filename, "png", 800, 450)) + return 6; + expected = g_strdup_printf ("%s-pages0.png", base); + if (!g_file_test (expected, G_FILE_TEST_EXISTS)) + return 7; + g_remove (expected); + g_free (expected); + expected = g_strdup_printf ("%s-pages1.png", base); + if (!g_file_test (expected, G_FILE_TEST_EXISTS)) + return 8; + g_remove (expected); + g_free (expected); + unexpected = g_strdup_printf ("%s0.png", filename); + if (g_file_test (unexpected, G_FILE_TEST_EXISTS)) + return 9; + + g_free (unexpected); + g_free (filename); + g_free (base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_scale_check.c b/tests/export_scale_check.c new file mode 100644 index 0000000..e3aee0e --- /dev/null +++ b/tests/export_scale_check.c @@ -0,0 +1,133 @@ +#include +#include +#include + +#include "export.h" +#include "tbo-window.h" + +typedef struct +{ + TboWindow *tbo; + const gchar *filename; +} ExportDialogState; + +static void +collect_export_controls (GtkWidget *widget, + GtkEntry **entry, + GtkSpinButton **width_spin, + GtkButton **save_button) +{ + GtkWidget *child; + + if (GTK_IS_ENTRY (widget) && *entry == NULL) + *entry = GTK_ENTRY (widget); + + if (GTK_IS_SPIN_BUTTON (widget) && *width_spin == NULL) + *width_spin = GTK_SPIN_BUTTON (widget); + + if (GTK_IS_BUTTON (widget) && gtk_widget_has_css_class (widget, "suggested-action")) + *save_button = GTK_BUTTON (widget); + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + collect_export_controls (child, entry, width_spin, save_button); +} + +static GtkWindow * +find_export_dialog (ExportDialogState *state) +{ + GListModel *toplevels; + guint i; + + toplevels = gtk_window_get_toplevels (); + + for (i = 0; i < g_list_model_get_n_items (toplevels); i++) + { + GtkWindow *window = GTK_WINDOW (g_list_model_get_item (toplevels, i)); + + if (window != GTK_WINDOW (state->tbo->window) && + gtk_window_get_transient_for (window) == GTK_WINDOW (state->tbo->window)) + return window; + + g_object_unref (window); + } + + return NULL; +} + +static gboolean +respond_export_dialog (gpointer data) +{ + ExportDialogState *state = data; + GtkWindow *dialog; + GtkEntry *entry = NULL; + GtkSpinButton *width_spin = NULL; + GtkButton *save_button = NULL; + + dialog = find_export_dialog (state); + if (dialog == NULL) + return G_SOURCE_CONTINUE; + + collect_export_controls (GTK_WIDGET (dialog), &entry, &width_spin, &save_button); + if (entry == NULL || width_spin == NULL || save_button == NULL) + return G_SOURCE_CONTINUE; + + gtk_editable_set_text (GTK_EDITABLE (entry), state->filename); + gtk_spin_button_set_value (width_spin, 1600); + g_signal_emit_by_name (save_button, "clicked"); + g_object_unref (dialog); + return G_SOURCE_REMOVE; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *tmpbase; + gchar *pngfile; + gint fd; + gboolean exported; + GdkPixbuf *pixbuf; + GError *error = NULL; + ExportDialogState state; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportscale", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + tmpbase = g_build_filename (g_get_tmp_dir (), "tbo-export-scale-XXXXXX", NULL); + fd = g_mkstemp (tmpbase); + if (fd < 0) + return 3; + close (fd); + g_remove (tmpbase); + + state.tbo = tbo; + state.filename = tmpbase; + g_idle_add (respond_export_dialog, &state); + + exported = tbo_export (tbo); + if (!exported) + return 4; + + pngfile = g_strdup_printf ("%s.png", tmpbase); + pixbuf = gdk_pixbuf_new_from_file (pngfile, &error); + if (pixbuf == NULL) + return 5; + + if (gdk_pixbuf_get_width (pixbuf) != 1600 || gdk_pixbuf_get_height (pixbuf) != 900) + return 6; + + g_object_unref (pixbuf); + g_remove (pngfile); + g_free (pngfile); + g_free (tmpbase); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/frame_default_color_check.c b/tests/frame_default_color_check.c new file mode 100644 index 0000000..1ea0ef8 --- /dev/null +++ b/tests/frame_default_color_check.c @@ -0,0 +1,26 @@ +#include + +#include "frame.h" + +int +main (void) +{ + Frame *first; + Frame *second; + GdkRGBA color; + + first = tbo_frame_new (0, 0, 100, 100); + second = tbo_frame_new (0, 0, 100, 100); + + tbo_frame_set_color_rgb (first, 0.2, 0.4, 0.6); + tbo_frame_get_color (second, &color); + + if (fabs (color.red - 1.0) > 1e-9 || + fabs (color.green - 1.0) > 1e-9 || + fabs (color.blue - 1.0) > 1e-9) + return 2; + + tbo_frame_free (first); + tbo_frame_free (second); + return 0; +} diff --git a/tests/frame_view_coordinate_mapping_check.c b/tests/frame_view_coordinate_mapping_check.c new file mode 100644 index 0000000..a50350d --- /dev/null +++ b/tests/frame_view_coordinate_mapping_check.c @@ -0,0 +1,58 @@ +#include + +#include "config.h" +#include "comic.h" +#include "dnd.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-object-text.h" +#include "tbo-tool-base.h" +#include "tbo-tool-text.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + TboToolBase *text_tool; + TboToolText *text_state; + TboPointerEvent click_event = { .x = 400, .y = 225 }; + gchar *asset_path; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.frameviewcoords", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 100, 80); + tbo_window_enter_frame (tbo, frame); + + text_tool = TBO_TOOL_BASE (tbo->toolbar->tools[TBO_TOOLBAR_TEXT]); + text_state = TBO_TOOL_TEXT (text_tool); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); + text_tool->on_click (text_tool, tbo->drawing, &click_event); + if (tbo_frame_object_count (frame) != 1 || text_state->text_selected == NULL) + return 3; + + asset_path = g_build_filename (SOURCE_DATA_DIR, "bar", "body", "left-hand.svg", NULL); + if (tbo_dnd_insert_asset_at_view_coords (tbo, asset_path, 410, 235) == NULL) + return 4; + g_free (asset_path); + + if (tbo_frame_object_count (frame) != 2) + return 5; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/group_clone_check.c b/tests/group_clone_check.c new file mode 100644 index 0000000..dc1ab4f --- /dev/null +++ b/tests/group_clone_check.c @@ -0,0 +1,75 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-object-group.h" +#include "tbo-object-text.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectGroup *group; + TboObjectBase *first; + TboObjectBase *second; + GdkRGBA color = { 0, 0, 0, 1 }; + gint count_before; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.groupclone", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + first = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 40, 20, "one", "Sans 12", &color)); + second = TBO_OBJECT_BASE (tbo_object_text_new_with_params (50, 20, 40, 20, "two", "Sans 12", &color)); + tbo_frame_add_obj (frame, first); + tbo_frame_add_obj (frame, second); + + group = TBO_OBJECT_GROUP (tbo_object_group_new ()); + tbo_object_group_add (group, first); + tbo_object_group_add (group, second); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (group)); + + tbo_window_enter_frame (tbo, frame); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + tbo_tool_selector_set_selected_obj (selector, TBO_OBJECT_BASE (group)); + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != frame) + return 3; + + count_before = tbo_frame_object_count (frame); + tbo_window_mark_clean (tbo); + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "clone", NULL); + drain_events (); + + if (!tbo_window_has_unsaved_changes (tbo)) + return 4; + if (tbo_frame_object_count (frame) != count_before + 1) + return 5; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/group_selection_cleanup_check.c b/tests/group_selection_cleanup_check.c new file mode 100644 index 0000000..a0e940d --- /dev/null +++ b/tests/group_selection_cleanup_check.c @@ -0,0 +1,89 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-object-group.h" +#include "tbo-object-text.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +static TboObjectGroup * +add_group (Frame *frame, TboObjectBase *first, TboObjectBase *second) +{ + TboObjectGroup *group = TBO_OBJECT_GROUP (tbo_object_group_new ()); + + tbo_object_group_add (group, first); + tbo_object_group_add (group, second); + tbo_frame_add_obj (frame, TBO_OBJECT_BASE (group)); + return group; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *first; + TboObjectBase *second; + TboObjectGroup *group; + GdkRGBA color = { 0, 0, 0, 1 }; + gint count_with_group; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.groupcleanup", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + first = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 40, 20, "one", "Sans 12", &color)); + second = TBO_OBJECT_BASE (tbo_object_text_new_with_params (50, 20, 40, 20, "two", "Sans 12", &color)); + tbo_frame_add_obj (frame, first); + tbo_frame_add_obj (frame, second); + + tbo_window_enter_frame (tbo, frame); + if (tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)) != frame) + return 3; + + group = add_group (frame, first, second); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + tbo_tool_selector_set_selected_obj (selector, TBO_OBJECT_BASE (group)); + count_with_group = tbo_frame_object_count (frame); + + tbo_tool_selector_set_selected_obj (selector, first); + if (tbo_frame_object_count (frame) != count_with_group - 1) + return 4; + if (tbo_tool_selector_get_selected_obj (selector) != first) + return 5; + + group = add_group (frame, first, second); + tbo_tool_selector_set_selected_obj (selector, TBO_OBJECT_BASE (group)); + count_with_group = tbo_frame_object_count (frame); + + tbo_tool_selector_reset_state (selector); + if (tbo_frame_object_count (frame) != count_with_group - 1) + return 6; + if (selector->selected_frame != NULL || selector->selected_object != NULL) + return 7; + + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/key_binder_scope_check.c b/tests/key_binder_scope_check.c new file mode 100644 index 0000000..835aea8 --- /dev/null +++ b/tests/key_binder_scope_check.c @@ -0,0 +1,41 @@ +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo1; + TboWindow *tbo2; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.keybinderscope", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo1 = tbo_new_tbo (app, 800, 450); + tbo2 = tbo_new_tbo (app, 800, 450); + + tbo_window_set_key_binder (tbo1, FALSE); + + tbo_toolbar_set_selected_tool (tbo1->toolbar, TBO_TOOLBAR_FRAME); + if (tbo_window_handle_unmodified_key (tbo1, GDK_KEY_s, 0)) + return 3; + if (tbo_toolbar_get_selected_tool (tbo1->toolbar) != tbo1->toolbar->tools[TBO_TOOLBAR_FRAME]) + return 4; + + tbo_toolbar_set_selected_tool (tbo2->toolbar, TBO_TOOLBAR_FRAME); + if (!tbo_window_handle_unmodified_key (tbo2, GDK_KEY_s, 0)) + return 5; + if (tbo_toolbar_get_selected_tool (tbo2->toolbar) != tbo2->toolbar->tools[TBO_TOOLBAR_SELECTOR]) + return 6; + + tbo_window_mark_clean (tbo1); + tbo_window_mark_clean (tbo2); + gtk_window_close (GTK_WINDOW (tbo1->window)); + gtk_window_close (GTK_WINDOW (tbo2->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/zoom_bounds_check.c b/tests/zoom_bounds_check.c new file mode 100644 index 0000000..747483a --- /dev/null +++ b/tests/zoom_bounds_check.c @@ -0,0 +1,41 @@ +#include +#include + +#include "tbo-drawing.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboDrawing *drawing; + gdouble zoom; + gint i; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.zoombounds", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + drawing = TBO_DRAWING (tbo->drawing); + + for (i = 0; i < 100; i++) + tbo_drawing_zoom_out (drawing); + + zoom = tbo_drawing_get_zoom (drawing); + if (!isfinite (zoom) || zoom < ZOOM_STEP) + return 3; + + tbo_drawing_zoom_fit (drawing); + zoom = tbo_drawing_get_zoom (drawing); + if (!isfinite (zoom) || zoom < ZOOM_STEP) + return 4; + + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} From 720774cecdbf103816038694e864c429969635d7 Mon Sep 17 00:00:00 2001 From: jaime Date: Tue, 21 Apr 2026 21:34:23 +0200 Subject: [PATCH 16/22] Polish editing workflow, export, and desktop integration --- NEXT-STEPS.md | 196 ---- data/applications/net.danigm.tbo.desktop | 5 +- data/metainfo/net.danigm.tbo.metainfo.xml | 40 + meson.build | 225 +++++ po/es.po | 192 ++++ src/comic-load.c | 23 +- src/comic-load.h | 4 +- src/comic-new-dialog.c | 66 +- src/comic-open-dialog.c | 12 +- src/comic-saveas-dialog.c | 3 + src/comic.c | 190 ++-- src/comic.h | 16 +- src/dnd.c | 124 ++- src/dnd.h | 1 + src/doodle-treeview.c | 585 +++++++++--- src/export.c | 1000 +++++++++++++++++---- src/export.h | 21 + src/frame.c | 16 +- src/page.c | 110 +-- src/page.h | 2 + src/tbo-drawing.c | 15 +- src/tbo-drawing.h | 2 +- src/tbo-file-dialog.c | 6 +- src/tbo-list-utils.h | 142 +++ src/tbo-tool-selector.c | 273 ------ src/tbo-tool-selector.h | 84 -- src/tbo-toolbar.c | 79 +- src/tbo-toolbar.h | 1 + src/tbo-undo.c | 385 ++++++++ src/tbo-undo.h | 23 + src/tbo-utils.c | 2 +- src/tbo-utils.h | 2 +- src/tbo-widget.c | 4 + src/tbo-window.c | 989 +++++++++++++++++++- src/tbo-window.h | 40 +- src/tbo.c | 46 +- src/ui-menu.c | 323 ++++++- src/ui-menu.h | 1 + tests/a4_pdf_export_check.c | 82 ++ tests/assets_browser_search_check.c | 111 +++ tests/autosave_recovery_check.c | 82 ++ tests/comic_template_check.c | 112 +++ tests/dirty_title_check.c | 72 ++ tests/dnd_feedback_check.c | 72 ++ tests/export_guided_dialog_check.c | 183 ++++ tests/export_page_range_check.c | 121 +++ tests/export_scope_check.c | 154 ++++ tests/keyboard_accessibility_check.c | 154 ++++ tests/mode_status_check.c | 48 + tests/model_current_state_check.c | 2 +- tests/page_duplicate_check.c | 71 ++ tests/page_reorder_check.c | 65 ++ tests/page_status_check.c | 4 +- tests/recent_projects_check.c | 87 ++ tests/shortcuts_guide_check.c | 129 +++ tests/status_hierarchy_check.c | 6 +- tests/theme_respect_check.c | 106 +++ 57 files changed, 5795 insertions(+), 1114 deletions(-) delete mode 100644 NEXT-STEPS.md create mode 100644 data/metainfo/net.danigm.tbo.metainfo.xml create mode 100644 src/tbo-list-utils.h create mode 100644 tests/a4_pdf_export_check.c create mode 100644 tests/assets_browser_search_check.c create mode 100644 tests/autosave_recovery_check.c create mode 100644 tests/comic_template_check.c create mode 100644 tests/dirty_title_check.c create mode 100644 tests/dnd_feedback_check.c create mode 100644 tests/export_guided_dialog_check.c create mode 100644 tests/export_page_range_check.c create mode 100644 tests/export_scope_check.c create mode 100644 tests/keyboard_accessibility_check.c create mode 100644 tests/mode_status_check.c create mode 100644 tests/page_duplicate_check.c create mode 100644 tests/page_reorder_check.c create mode 100644 tests/recent_projects_check.c create mode 100644 tests/shortcuts_guide_check.c create mode 100644 tests/theme_respect_check.c diff --git a/NEXT-STEPS.md b/NEXT-STEPS.md deleted file mode 100644 index eb8dbbc..0000000 --- a/NEXT-STEPS.md +++ /dev/null @@ -1,196 +0,0 @@ -# Next Steps - -Actualizado: 2026-04-21 - -## Objetivo de este documento - -Este archivo es el roadmap operativo del agente para continuar el trabajo en `TBO`. -Debe usarse para retomar contexto rapido, priorizar bien y evitar mezclar tareas. - -## Estado actual - -- Proyecto en C con GTK4 y Meson. -- `Comic`, `Page` y `Frame` ya son `GObject`. -- La refactorizacion grande de modelo y ownership ya quedo consolidada. -- `undo/redo` cubre altas, borrados y la mayoria de mutaciones principales. -- El boton principal del toolbar es `Save`. -- La barra inferior ya expresa mejor la jerarquia y es traducible. -- Las coordenadas del panel inferior se eliminaron por rendimiento. -- Suite actual: `38/38` tests OK. - -## Trabajo ya cerrado - -Ya no debe tratarse como pendiente en este roadmap: - -- crash al clonar selecciones agrupadas, -- limpieza del `TboObjectGroup` temporal, -- zoom minimo valido, -- color base de frame sin estado global compartido, -- doble escalado en exportacion, -- conversiones de coordenadas de frame view sin depender de globals fragiles, -- `KEY_BINDER` por ventana, -- duplicacion de extensiones en exportacion, -- manejo seguro de errores al abrir directorios de assets. - -## Principio de trabajo - -Orden recomendado: -1. Priorizar primero mejoras con mucho valor de usuario y bajo riesgo. -2. Despues atacar claridad de flujo, feedback y consistencia UX. -3. Dejar para mas tarde refactors amplios, identidad visual grande y packaging. -4. Mantener cambios minimos, locales y faciles de verificar. -5. No mezclar en una misma sesion una feature de producto con una refactorizacion amplia si no es necesario. - -## Prioridad actual - -No hay bugs criticos abiertos confirmados en esta lista. -La prioridad pasa a ser un roadmap combinado de producto + UX, ordenado por importancia real. - -## Tareas restantes ordenadas por importancia - -1. Plantillas al crear comic. -- Mejor siguiente tarea: alto valor, alcance corto y punto de entrada claro. - -2. Autosave + recientes + reabrir ultimo proyecto. -- Mucho impacto en seguridad percibida y continuidad de trabajo. - -3. Mostrar estado `dirty` de forma visible en titulo o UI persistente. -- Quick win UX muy importante. - -4. Hacer persistente y claro el modo actual: `Page` vs `Frame`. -- Mejora fuerte de claridad y evita errores de contexto. - -5. Exportar pagina actual o seleccion. -- Mejora de uso muy practica y contenida. - -6. Mejorar feedback de insercion de imagen y drag and drop cuando falla. -- El usuario debe saber por que una insercion no se realizo. - -7. Guia integrada de atajos. -- Mucho retorno con poco riesgo. - -8. Inspector contextual persistente con jerarquia `pagina -> frame -> objeto`. -- Mejora fuerte de flujo y discoverability. - -9. Reordenacion de paginas por drag and drop + duplicar pagina. -- Muy util para trabajo real con documentos largos. - -10. Busqueda instantanea y mejor clasificacion de assets. -- Sube mucho el valor del panel lateral cuando el catalogo crece. - -11. Favoritos y recientes de assets. -- Complementa bien la tarea anterior. - -12. Exportacion guiada con preview + rango de paginas. -- Valiosa, pero despues de cerrar lo basico de export. - -13. Presets de bocadillos y estilos de texto. -- Util, pero menos prioritaria que navegacion, contexto y export. - -14. Miniaturas de paginas. -- Interesante, aunque de mayor coste estructural. - -15. Mejorar accesibilidad por teclado en boton de menu y assets. -- Importante, pero despues de las mejoras principales de flujo. - -16. Dejar de forzar `Adwaita-dark`. -- Buena mejora de integracion con el sistema y consistencia visual. - -17. Unificar labels, capitalizacion, copy y tono textual. -- Pulido valioso, no urgente. - -18. Reforzar consistencia visual entre toolbar, sidebar, dialogs e iconografia. -- Trabajo de polish visual progresivo. - -19. Revisar si conviene simplificar la doble barra superior. -- Mejor hacerlo cuando la UX este mas asentada. - -20. Identidad visual y consistencia estetica. -- Linea importante, pero no antes de cerrar lo funcional y la claridad base. - -21. Modo presentacion / visor. -- Interesante como feature, menos importante ahora. - -22. Packaging/metainfo. -- Importante para distribucion, no para el nucleo del flujo de trabajo. - -## Deuda tecnica restante - -No es prioritaria frente al roadmap anterior, pero sigue pendiente: - -- patron repetido entre `Comic`, `Page` y `Frame` para lista + elemento actual, -- undo repartido entre `src/tbo-undo.c` y logica adicional en `src/tbo-tool-selector.c`, -- inconsistencias menores de APIs publicas y contratos de indices, -- include duplicado de `` en `src/comic.c`. - -## Cobertura pendiente - -Faltan tests especificos para: - -- plantillas con layout inicial, -- autosave y recuperacion al arrancar, -- recientes + reabrir ultimo proyecto, -- exportacion de pagina actual o seleccion, -- feedback y rutas de error de insercion por DnD, -- accesibilidad basica de flujos principales por teclado. - -## Siguiente tarea a retomar - -### 1. Plantillas al crear comic - -Objetivo: -- Anadir presets utiles en `New Comic`. -- Crear el documento inicial con tamano y layout base acordes a la plantilla. - -Plantillas previstas: -- Storyboard `16:9` -- Tira comica -- `A4` -- Presentacion - -Punto de entrada principal: -- `src/comic-new-dialog.c` - -Archivos previsibles a tocar: -- `src/comic-new-dialog.c` -- `src/tbo-window.c` -- `src/tbo-window.h` -- Solo anadir un helper nuevo si compensa claramente. - -Implementacion recomendada: -1. Anadir selector de plantilla en el dialogo `New Comic`. -2. Al cambiar plantilla, autocompletar `width` y `height`. -3. Al aceptar, crear el comic con el tamano elegido. -4. Aplicar layout inicial solo si la plantilla no es la opcion vacia. -5. Anadir al menos un test de regresion para una plantilla con layout inicial. - -Notas de diseno: -- Mantener simple el dialogo. -- No crear aun un sistema grande de presets persistentes. -- Empezar con layouts estaticos y claros. -- No mezclar esta tarea con miniaturas de paginas ni con una reforma general de UI. - -## Decisiones abiertas a vigilar - -- Si una plantilla crea varios frames iniciales, decidir si eso forma parte del estado base del documento o si entra en el stack de `undo/redo`. -- No mezclar todavia `GtkNotebook` con la futura tarea de miniaturas. -- Si se mejora exportacion, separar claramente correcciones de bugs de mejoras de UX. -- Si se toca multiventana, evitar introducir mas estado global compartido. -- Si se implementa autosave, definir antes el formato, frecuencia y politica de recuperacion. - -## Comprobacion rapida al retomar - -Ejecutar: -```bash -meson compile -C build && meson test -C build --no-rebuild --print-errorlogs --num-processes 1 -``` - -## Regla practica para el agente - -Antes de empezar una tarea: -1. Confirmar si toca producto, UX o deuda tecnica. -2. Leer los archivos exactos implicados. -3. Hacer el cambio minimo correcto. -4. Anadir o ajustar tests si el cambio altera comportamiento. -5. Verificar compile + tests. -6. Actualizar este documento si cambia la prioridad real del roadmap. diff --git a/data/applications/net.danigm.tbo.desktop b/data/applications/net.danigm.tbo.desktop index ebe82aa..de1a05f 100644 --- a/data/applications/net.danigm.tbo.desktop +++ b/data/applications/net.danigm.tbo.desktop @@ -2,10 +2,11 @@ Type=Application Name=TBO GenericName=Comic Editor -Comment=Create comic strips and illustrated presentations +Comment=Create comics and illustrated presentations Exec=tbo %f Icon=tbo Terminal=false StartupNotify=false StartupWMClass=tbo -Categories=Graphics;VectorGraphics;GTK; +Categories=Graphics;2DGraphics;VectorGraphics;GTK; +Keywords=comic;comics;storyboard;presentation;editor;gtk; diff --git a/data/metainfo/net.danigm.tbo.metainfo.xml b/data/metainfo/net.danigm.tbo.metainfo.xml new file mode 100644 index 0000000..68c382e --- /dev/null +++ b/data/metainfo/net.danigm.tbo.metainfo.xml @@ -0,0 +1,40 @@ + + + net.danigm.tbo + CC0-1.0 + GPL-3.0-or-later + TBO + Create comics and illustrated presentations + + TBO contributors + + net.danigm.tbo.desktop + + tbo + + https://github.com/j4imefoo/TBO + https://github.com/j4imefoo/TBO/issues + https://github.com/j4imefoo/TBO + +

TBO is a desktop editor for making comics and illustrated presentations with a simple page-and-frame workflow.

+

It combines page layout, frame editing, text, bubbles, doodles and export tools in a lightweight GTK4 application.

+
    +
  • Create comics page by page with editable frames
  • +
  • Add text, bubbles, doodles and image assets inside frames
  • +
  • Export the whole document, the current page or a selection to PNG, PDF or SVG
  • +
+ + + + + +

This GTK4 release modernizes TBO and improves its day-to-day workflow.

+
    +
  • Improved export workflow with preview, page range and selection support
  • +
  • Added comic templates, autosave recovery and recent projects
  • +
  • Polished the interface, keyboard accessibility and overall consistency
  • +
+
+
+
+ diff --git a/meson.build b/meson.build index 5112e7b..c9a958b 100644 --- a/meson.build +++ b/meson.build @@ -303,6 +303,134 @@ doodle_dir_error_check = executable( install: false ) +comic_template_check = executable( + 'comic-template-check', + common_sources + files('tests/comic_template_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +a4_pdf_export_check = executable( + 'a4-pdf-export-check', + common_sources + files('tests/a4_pdf_export_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +autosave_recovery_check = executable( + 'autosave-recovery-check', + common_sources + files('tests/autosave_recovery_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +recent_projects_check = executable( + 'recent-projects-check', + common_sources + files('tests/recent_projects_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +dirty_title_check = executable( + 'dirty-title-check', + common_sources + files('tests/dirty_title_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +mode_status_check = executable( + 'mode-status-check', + common_sources + files('tests/mode_status_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_scope_check = executable( + 'export-scope-check', + common_sources + files('tests/export_scope_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +dnd_feedback_check = executable( + 'dnd-feedback-check', + common_sources + files('tests/dnd_feedback_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +shortcuts_guide_check = executable( + 'shortcuts-guide-check', + common_sources + files('tests/shortcuts_guide_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +page_duplicate_check = executable( + 'page-duplicate-check', + common_sources + files('tests/page_duplicate_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +page_reorder_check = executable( + 'page-reorder-check', + common_sources + files('tests/page_reorder_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +assets_browser_search_check = executable( + 'assets-browser-search-check', + common_sources + files('tests/assets_browser_search_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_page_range_check = executable( + 'export-page-range-check', + common_sources + files('tests/export_page_range_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_guided_dialog_check = executable( + 'export-guided-dialog-check', + common_sources + files('tests/export_guided_dialog_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +keyboard_accessibility_check = executable( + 'keyboard-accessibility-check', + common_sources + files('tests/keyboard_accessibility_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +theme_respect_check = executable( + 'theme-respect-check', + common_sources + files('tests/theme_respect_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + enter_frame_key_check = executable( 'enter-frame-key-check', common_sources + files('tests/enter_frame_key_check.c'), @@ -559,6 +687,102 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'comic-template-check', + comic_template_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'a4-pdf-export-check', + a4_pdf_export_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'autosave-recovery-check', + autosave_recovery_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'recent-projects-check', + recent_projects_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'dirty-title-check', + dirty_title_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'mode-status-check', + mode_status_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-scope-check', + export_scope_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'dnd-feedback-check', + dnd_feedback_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'shortcuts-guide-check', + shortcuts_guide_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'page-duplicate-check', + page_duplicate_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'page-reorder-check', + page_reorder_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'assets-browser-search-check', + assets_browser_search_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-page-range-check', + export_page_range_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-guided-dialog-check', + export_guided_dialog_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'keyboard-accessibility-check', + keyboard_accessibility_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'theme-respect-check', + theme_respect_check, + env: ['G_DEBUG=fatal-criticals'] +) + test( 'enter-frame-key-check', enter_frame_key_check, @@ -632,6 +856,7 @@ install_subdir('data/icons', install_dir: join_paths(get_option('datadir'), 'tbo install_subdir('data/doodle', install_dir: join_paths(get_option('datadir'), 'tbo')) install_data('data/applications/net.danigm.tbo.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) +install_data('data/metainfo/net.danigm.tbo.metainfo.xml', install_dir: join_paths(get_option('datadir'), 'metainfo')) install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'pixmaps')) install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps')) install_data(configured_icon_png, install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '128x128', 'apps')) diff --git a/po/es.po b/po/es.po index 431e5fc..32bed8b 100644 --- a/po/es.po +++ b/po/es.po @@ -474,3 +474,195 @@ msgstr "No se pudo mezclar tbo-menu-ui.xml: %s" #~ msgid "Save current document as svg" #~ msgstr "Guardar el documento actual como svg" + +msgid "New Comic (Ctrl+N)" +msgstr "Cómic nuevo (Ctrl+N)" + +msgid "Open Comic (Ctrl+O)" +msgstr "Abrir cómic (Ctrl+O)" + +msgid "Save Comic (Ctrl+S)" +msgstr "Guardar cómic (Ctrl+S)" + +msgid "Undo (Ctrl+Z)" +msgstr "Deshacer (Ctrl+Z)" + +msgid "Redo (Ctrl+Y)" +msgstr "Rehacer (Ctrl+Y)" + +msgid "Duplicate Page" +msgstr "Duplicar página" + +msgid "Previous Page" +msgstr "Página anterior" + +msgid "New Frame (F)" +msgstr "Nueva viñeta (F)" + +msgid "Doodle (D)" +msgstr "Monigote (D)" + +msgid "Bubble (B)" +msgstr "Bocadillo (B)" + +msgid "Text (T)" +msgstr "Texto (T)" + +msgid "Insert Image" +msgstr "Insertar imagen" + +msgid "Zoom Out (-)" +msgstr "Reducir (-)" + +msgid "Zoom In (+)" +msgstr "Ampliar (+)" + +msgid "Zoom Fit (2)" +msgstr "Ajustar (2)" + +msgid "Template: " +msgstr "Plantilla: " + +msgid "Width: " +msgstr "Anchura: " + +msgid "Height: " +msgstr "Altura: " + +msgid "Empty" +msgstr "Vacío" + +msgid "Comic Strip" +msgstr "Tira cómica" + +msgid "Keyboard Shortcuts" +msgstr "Atajos de teclado" + +msgid "File" +msgstr "Archivo" + +msgid "Tools" +msgstr "Herramientas" + +msgid "View and Navigation" +msgstr "Vista y navegación" + +msgid "Arrange" +msgstr "Organización" + +msgid "Clone Selection" +msgstr "Clonar selección" + +msgid "Delete Selection" +msgstr "Eliminar selección" + +msgid "Zoom In" +msgstr "Ampliar" + +msgid "Zoom Out" +msgstr "Reducir" + +msgid "Zoom Fit" +msgstr "Ajustar" + +msgid "Enter Selected Frame" +msgstr "Entrar en la viñeta seleccionada" + +msgid "Back to Page" +msgstr "Volver a la página" + +msgid "Horizontal Flip" +msgstr "Reflejo horizontal" + +msgid "Vertical Flip" +msgstr "Reflejo vertical" + +msgid "Move to Front" +msgstr "Mover al frente" + +msgid "Move to Back" +msgstr "Mover atrás" + +msgid "TBO Comic Editor" +msgstr "Editor de cómics TBO" + +msgid "Reopen Last Project" +msgstr "Reabrir el último proyecto" + +msgid "Open Recent" +msgstr "Abrir reciente" + +msgid "Follow System Theme" +msgstr "Seguir el tema del sistema" + +msgid "Dark Theme" +msgstr "Tema oscuro" + +msgid "Light Theme" +msgstr "Tema claro" + +msgid "Theme" +msgstr "Tema" + +msgid "All Pages" +msgstr "Todas las páginas" + +msgid "Current Page" +msgstr "Página actual" + +msgid "Guess by Extension" +msgstr "Detectar por extensión" + +msgid "Choose File" +msgstr "Elegir archivo" + +msgid "File Name: " +msgstr "Nombre de archivo: " + +msgid "From Page: " +msgstr "Desde la página: " + +msgid "To Page: " +msgstr "Hasta la página: " + +msgid "Preview" +msgstr "Vista previa" + +msgid "Preview: Selection" +msgstr "Vista previa: selección" + +#, c-format +msgid "Preview: Current Page %d" +msgstr "Vista previa: página actual %d" + +#, c-format +msgid "Preview: Page %d" +msgstr "Vista previa: página %d" + +#, c-format +msgid "Preview: Page %d of Range %d-%d" +msgstr "Vista previa: página %d del rango %d-%d" + +msgid "Please select a frame to export." +msgstr "Selecciona una viñeta para exportar." + +msgid "Please choose a filename to export." +msgstr "Elige un nombre de archivo para exportar." + +msgid "Search Assets" +msgstr "Buscar recursos" + +msgid "Search Bubbles" +msgstr "Buscar bocadillos" + +msgid "No assets match this search." +msgstr "No hay recursos que coincidan con esta búsqueda." + +msgid "Enter a frame before inserting an image." +msgstr "Entra en una viñeta antes de insertar una imagen." + +msgid "Drop the image inside the current frame." +msgstr "Suelta la imagen dentro de la viñeta actual." + +msgid "Couldn't insert the image." +msgstr "No se pudo insertar la imagen." diff --git a/src/comic-load.c b/src/comic-load.c index 1f736fe..accf0c6 100644 --- a/src/comic-load.c +++ b/src/comic-load.c @@ -184,6 +184,8 @@ create_tbo_comic (TboLoadContext *context, { gint width = 0; gint height = 0; + const gchar *paper_value; + TboComicPaper paper = TBO_COMIC_PAPER_NONE; TboLoadAttr attrs[] = { {"width", ATTR_INT, &width, TRUE, FALSE}, {"height", ATTR_INT, &height, TRUE, FALSE}, @@ -210,7 +212,24 @@ create_tbo_comic (TboLoadContext *context, return FALSE; } + paper_value = find_attr_value (attribute_names, attribute_values, "paper"); + if (paper_value != NULL && *paper_value != '\0') + { + if (g_ascii_strcasecmp (paper_value, "a4") == 0) + paper = TBO_COMIC_PAPER_A4; + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Unsupported paper value '%s'", + paper_value); + return FALSE; + } + } + context->comic = tbo_comic_new (context->title, width, height); + tbo_comic_set_paper (context->comic, paper); tbo_comic_del_page (context->comic, 0); return TRUE; } @@ -559,7 +578,7 @@ static GMarkupParser parser = { }; Comic * -tbo_comic_load_with_alerts (char *filename, gboolean show_alerts) +tbo_comic_load_with_alerts (const gchar *filename, gboolean show_alerts) { TboLoadContext context = { 0 }; GMarkupParseContext *markup_context; @@ -623,7 +642,7 @@ tbo_comic_load_with_alerts (char *filename, gboolean show_alerts) } Comic * -tbo_comic_load (char *filename) +tbo_comic_load (const gchar *filename) { return tbo_comic_load_with_alerts (filename, TRUE); } diff --git a/src/comic-load.h b/src/comic-load.h index f4b5836..4ddf82f 100644 --- a/src/comic-load.h +++ b/src/comic-load.h @@ -22,7 +22,7 @@ #include "tbo-types.h" -Comic *tbo_comic_load_with_alerts (char *filename, gboolean show_alerts); -Comic *tbo_comic_load (char *filename); +Comic *tbo_comic_load_with_alerts (const gchar *filename, gboolean show_alerts); +Comic *tbo_comic_load (const gchar *filename); #endif diff --git a/src/comic-new-dialog.c b/src/comic-new-dialog.c index a1ada6e..31da0bd 100644 --- a/src/comic-new-dialog.c +++ b/src/comic-new-dialog.c @@ -24,10 +24,31 @@ #include "tbo-widget.h" #include "tbo-window.h" +typedef struct +{ + GtkWidget *spin_w; + GtkWidget *spin_h; +} TboComicTemplateControls; + +static void +template_selected_cb (GtkDropDown *dropdown, GParamSpec *pspec, gpointer user_data) +{ + TboComicTemplateControls *controls = user_data; + gint width; + gint height; + + (void) pspec; + + tbo_comic_template_get_default_size (gtk_drop_down_get_selected (dropdown), &width, &height); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (controls->spin_w), width); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (controls->spin_h), height); +} + gboolean tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) { GtkWidget *dialog; + GtkWidget *headerbar; GtkWidget *vbox; GtkWidget *hbox; GtkWidget *actions; @@ -35,11 +56,25 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) GtkWidget *label; GtkWidget *spin_w; GtkWidget *spin_h; + GtkWidget *dropdown; GtkAdjustment *adjustment; TboDialogRunData data; + TboComicTemplateControls template_controls; + gint default_width; + gint default_height; + const char *template_names[] = { + _("Empty"), + _("Comic Strip"), + "A4", + _("Storyboard"), + NULL + }; int width; int height; + TboComicTemplate template; + + tbo_comic_template_get_default_size (TBO_COMIC_TEMPLATE_EMPTY, &default_width, &default_height); dialog = gtk_window_new (); gtk_window_set_title (GTK_WINDOW (dialog), _("New Comic")); @@ -47,7 +82,12 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); + headerbar = gtk_header_bar_new (); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); + gtk_window_set_titlebar (GTK_WINDOW (dialog), headerbar); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class (vbox, "tbo-dialog-content"); gtk_widget_set_margin_start (vbox, 12); gtk_widget_set_margin_end (vbox, 12); gtk_widget_set_margin_top (vbox, 12); @@ -55,27 +95,42 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) tbo_widget_add_child (dialog, vbox); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); - label = gtk_label_new (_("width: ")); + label = gtk_label_new (_("Template: ")); + gtk_widget_set_size_request (label, 80, -1); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + dropdown = gtk_drop_down_new_from_strings (template_names); + gtk_drop_down_set_selected (GTK_DROP_DOWN (dropdown), TBO_COMIC_TEMPLATE_EMPTY); + tbo_box_pack_start (hbox, label, FALSE, FALSE, 0); + tbo_box_pack_start (hbox, dropdown, TRUE, TRUE, 0); + tbo_widget_add_child (vbox, hbox); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + label = gtk_label_new (_("Width: ")); gtk_widget_set_size_request (label, 60, -1); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_label_set_yalign (GTK_LABEL (label), 0.5); - adjustment = gtk_adjustment_new (800, 0, 10000, 100, 100, 0); + adjustment = gtk_adjustment_new (default_width, 0, 10000, 100, 100, 0); spin_w = gtk_spin_button_new (GTK_ADJUSTMENT (adjustment), 1, 0); tbo_box_pack_start (hbox, label, FALSE, FALSE, 0); tbo_box_pack_start (hbox, spin_w, TRUE, TRUE, 0); tbo_widget_add_child (vbox, hbox); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); - label = gtk_label_new (_("height: ")); + label = gtk_label_new (_("Height: ")); gtk_widget_set_size_request (label, 60, -1); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_label_set_yalign (GTK_LABEL (label), 0.5); - adjustment = gtk_adjustment_new (500, 0, 10000, 100, 100, 0); + adjustment = gtk_adjustment_new (default_height, 0, 10000, 100, 100, 0); spin_h = gtk_spin_button_new (GTK_ADJUSTMENT (adjustment), 1, 0); tbo_box_pack_start (hbox, label, FALSE, FALSE, 0); tbo_box_pack_start (hbox, spin_h, TRUE, TRUE, 0); tbo_widget_add_child (vbox, hbox); + template_controls.spin_w = spin_w; + template_controls.spin_h = spin_h; + g_signal_connect (dropdown, "notify::selected", G_CALLBACK (template_selected_cb), &template_controls); + actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_halign (actions, GTK_ALIGN_END); @@ -100,7 +155,8 @@ tbo_comic_new_dialog (GtkWidget *widget, TboWindow *window) { width = (int) gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin_w)); height = (int) gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin_h)); - tbo_new_tbo (gtk_window_get_application (GTK_WINDOW (window->window)), width, height); + template = gtk_drop_down_get_selected (GTK_DROP_DOWN (dropdown)); + tbo_new_tbo_with_template (gtk_window_get_application (GTK_WINDOW (window->window)), width, height, template); } gtk_window_destroy (GTK_WINDOW (dialog)); diff --git a/src/comic-open-dialog.c b/src/comic-open-dialog.c index ea4cb27..935bf31 100644 --- a/src/comic-open-dialog.c +++ b/src/comic-open-dialog.c @@ -25,6 +25,7 @@ #include "tbo-drawing.h" #include "tbo-window.h" #include "comic.h" +#include "ui-menu.h" gboolean @@ -35,9 +36,14 @@ tbo_comic_open_dialog (GtkWidget *widget, TboWindow *window) if (filename != NULL) { tbo_window_set_browse_path (window, filename); - tbo_comic_open (window, filename); - tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_refresh_status (window); + if (tbo_window_prepare_for_document_replace (window)) + { + tbo_comic_open (window, filename); + tbo_window_add_recent_project (filename); + tbo_menu_refresh (window); + tbo_drawing_update (TBO_DRAWING (window->drawing)); + tbo_window_refresh_status (window); + } g_free (filename); } diff --git a/src/comic-saveas-dialog.c b/src/comic-saveas-dialog.c index a9f9f90..c36ed2e 100644 --- a/src/comic-saveas-dialog.c +++ b/src/comic-saveas-dialog.c @@ -25,6 +25,7 @@ #include "tbo-file-dialog.h" #include "tbo-window.h" #include "comic.h" +#include "ui-menu.h" gboolean tbo_comic_save_dialog (GtkWidget *widget, TboWindow *window) @@ -52,6 +53,8 @@ tbo_comic_saveas_dialog (GtkWidget *widget, TboWindow *window) if (tbo_comic_save (window, filename)) { tbo_window_set_path (window, filename); + tbo_window_add_recent_project (filename); + tbo_menu_refresh (window); g_free (filename); return TRUE; } diff --git a/src/comic.c b/src/comic.c index 5c12f8b..bf8cfea 100644 --- a/src/comic.c +++ b/src/comic.c @@ -23,13 +23,13 @@ #include #include #include -#include #include "comic.h" #include "tbo-types.h" #include "tbo-window.h" #include "page.h" #include "comic-load.h" #include "tbo-drawing.h" +#include "tbo-list-utils.h" #include "tbo-utils.h" #include "tbo-widget.h" @@ -40,6 +40,7 @@ struct _Comic char title[255]; int width; int height; + TboComicPaper paper; GList *pages; Page *current_page; }; @@ -51,23 +52,6 @@ struct _ComicClass G_DEFINE_TYPE (Comic, tbo_comic, G_TYPE_OBJECT); -static GList * -comic_page_link (Comic *comic, Page *page) -{ - return g_list_find (comic->pages, page); -} - -static void -comic_set_current_page_fallback (Comic *comic, GList *hint) -{ - if (hint != NULL) - comic->current_page = hint->data; - else if (comic->pages != NULL) - comic->current_page = comic->pages->data; - else - comic->current_page = NULL; -} - static void tbo_comic_dispose (GObject *object) { @@ -90,6 +74,7 @@ tbo_comic_init (Comic *self) self->title[0] = '\0'; self->width = 0; self->height = 0; + self->paper = TBO_COMIC_PAPER_NONE; self->pages = NULL; self->current_page = NULL; } @@ -141,6 +126,46 @@ tbo_comic_get_height (Comic *comic) return comic->height; } +TboComicPaper +tbo_comic_get_paper (Comic *comic) +{ + return comic->paper; +} + +void +tbo_comic_set_paper (Comic *comic, TboComicPaper paper) +{ + comic->paper = paper; +} + +gboolean +tbo_comic_get_pdf_page_size (Comic *comic, gdouble *width, gdouble *height) +{ + gdouble page_width; + gdouble page_height; + + if (comic == NULL) + return FALSE; + + switch (comic->paper) + { + case TBO_COMIC_PAPER_A4: + page_width = 210.0 / 25.4 * 72.0; + page_height = 297.0 / 25.4 * 72.0; + break; + case TBO_COMIC_PAPER_NONE: + default: + return FALSE; + } + + if (width != NULL) + *width = page_width; + if (height != NULL) + *height = page_height; + + return TRUE; +} + GList * tbo_comic_get_pages (Comic *comic) { @@ -161,13 +186,7 @@ tbo_comic_new_page (Comic *comic) void tbo_comic_insert_page (Comic *comic, Page *page, int nth) { - if (nth < 0) - comic->pages = g_list_append (comic->pages, page); - else - comic->pages = g_list_insert (comic->pages, page, nth); - - if (comic->current_page == NULL) - comic->current_page = page; + tbo_current_list_insert (&comic->pages, (gpointer *) &comic->current_page, page, nth); } void @@ -182,14 +201,15 @@ tbo_comic_del_page (Comic *comic, int nth) if (page == NULL) return; - link = comic_page_link (comic, page); + link = tbo_list_utils_link (comic->pages, page); next_link = link != NULL ? link->next : NULL; prev_link = link != NULL ? link->prev : NULL; - comic->pages = g_list_remove (comic->pages, page); + if (!tbo_current_list_remove (&comic->pages, (gpointer *) &comic->current_page, page)) + return; - if (comic->current_page == page) - comic_set_current_page_fallback (comic, next_link != NULL ? next_link : prev_link); + if (comic->current_page == NULL && (next_link != NULL || prev_link != NULL)) + comic->current_page = (next_link != NULL ? next_link : prev_link)->data; tbo_page_free (page); } @@ -203,50 +223,39 @@ tbo_comic_len (Comic *comic) int tbo_comic_page_index (Comic *comic) { - if (comic->current_page == NULL) - return -1; + return tbo_current_list_index (comic->pages, comic->current_page); +} - return g_list_index (comic->pages, comic->current_page); +int +tbo_comic_page_position (Comic *comic) +{ + gint index = tbo_comic_page_index (comic); + + return index >= 0 ? index + 1 : 0; } int tbo_comic_page_nth (Comic *comic, Page *page) { - return g_list_index (comic->pages, page); + return tbo_current_list_index (comic->pages, page); } Page * tbo_comic_next_page (Comic *comic) { - GList *current_link; - if (comic->current_page == NULL) return NULL; - current_link = comic_page_link (comic, comic->current_page); - if (current_link != NULL && current_link->next != NULL) - { - comic->current_page = current_link->next->data; - return tbo_comic_get_current_page (comic); - } - return NULL; + return tbo_current_list_next (comic->pages, (gpointer *) &comic->current_page); } Page * tbo_comic_prev_page (Comic *comic) { - GList *current_link; - if (comic->current_page == NULL) return NULL; - current_link = comic_page_link (comic, comic->current_page); - if (current_link != NULL && current_link->prev != NULL) - { - comic->current_page = current_link->prev->data; - return tbo_comic_get_current_page (comic); - } - return NULL; + return tbo_current_list_prev (comic->pages, (gpointer *) &comic->current_page); } Page * @@ -264,15 +273,29 @@ tbo_comic_set_current_page (Comic *comic, Page *page) return; } - comic->current_page = comic_page_link (comic, page) != NULL ? page : NULL; + tbo_current_list_set (comic->pages, (gpointer *) &comic->current_page, page); } void tbo_comic_set_current_page_nth (Comic *comic, int nth) { - GList *link = g_list_nth (comic->pages, nth); + tbo_current_list_set_nth (comic->pages, (gpointer *) &comic->current_page, nth); +} + +void +tbo_comic_reorder_page (Comic *comic, Page *page, int nth) +{ + gint old_index; + + if (comic == NULL || page == NULL) + return; + + old_index = tbo_comic_page_nth (comic, page); + if (old_index < 0 || old_index == nth) + return; - comic->current_page = link != NULL ? link->data : NULL; + tbo_list_utils_remove (&comic->pages, page); + tbo_list_utils_insert (&comic->pages, page, nth); } gboolean @@ -310,7 +333,11 @@ tbo_comic_del_current_page (Comic *comic) } gboolean -tbo_comic_save (TboWindow *tbo, char *filename) +save_comic_to_file (TboWindow *tbo, + const gchar *filename, + gboolean update_window_state, + gboolean mark_clean, + gboolean show_errors) { GList *p; char buffer[255]; @@ -319,18 +346,30 @@ tbo_comic_save (TboWindow *tbo, char *filename) if (!file) { - perror (_("failed saving")); - tbo_alert_show (GTK_WINDOW (tbo->window), - _("Failed saving"), - strerror (errno)); + if (show_errors) + { + perror (_("failed saving")); + tbo_alert_show (GTK_WINDOW (tbo->window), + _("Failed saving"), + strerror (errno)); + } return FALSE; } - get_base_name (filename, comic->title, 255); - gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); - snprintf (buffer, 255, "\n", - comic->width, - comic->height); + if (update_window_state) + { + get_base_name (filename, comic->title, 255); + gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); + } + + if (comic->paper == TBO_COMIC_PAPER_A4) + snprintf (buffer, 255, "\n", + comic->width, + comic->height); + else + snprintf (buffer, 255, "\n", + comic->width, + comic->height); fwrite (buffer, sizeof (char), strlen (buffer), file); for (p = comic->pages; p; p = g_list_next (p)) @@ -341,12 +380,27 @@ tbo_comic_save (TboWindow *tbo, char *filename) snprintf (buffer, 255, "\n"); fwrite (buffer, sizeof (char), strlen (buffer), file); fclose (file); - tbo_window_mark_clean (tbo); + + if (mark_clean) + tbo_window_mark_clean (tbo); + return TRUE; } +gboolean +tbo_comic_save (TboWindow *tbo, const gchar *filename) +{ + return save_comic_to_file (tbo, filename, TRUE, TRUE, TRUE); +} + +gboolean +tbo_comic_save_snapshot (TboWindow *tbo, const gchar *filename) +{ + return save_comic_to_file (tbo, filename, FALSE, FALSE, FALSE); +} + void -tbo_comic_open (TboWindow *window, char *filename) +tbo_comic_open (TboWindow *window, const gchar *filename) { Comic *newcomic = tbo_comic_load (filename); Comic *oldcomic; @@ -370,7 +424,9 @@ tbo_comic_open (TboWindow *window, char *filename) for (nth = 0; nth < tbo_comic_len (window->comic); nth++) { - tbo_window_add_page_widget (window, create_darea (window)); + tbo_window_add_page_widget (window, + create_darea (window), + g_list_nth_data (tbo_comic_get_pages (window->comic), nth)); } tbo_window_set_path (window, filename); diff --git a/src/comic.h b/src/comic.h index eaeea18..9e97139 100644 --- a/src/comic.h +++ b/src/comic.h @@ -34,6 +34,12 @@ typedef struct _ComicClass ComicClass; +typedef enum +{ + TBO_COMIC_PAPER_NONE, + TBO_COMIC_PAPER_A4, +} TboComicPaper; + GType tbo_comic_get_type (void); Comic *tbo_comic_new (const char *title, int width, int height); @@ -41,6 +47,9 @@ void tbo_comic_free (Comic *comic); const gchar *tbo_comic_get_title (Comic *comic); gint tbo_comic_get_width (Comic *comic); gint tbo_comic_get_height (Comic *comic); +TboComicPaper tbo_comic_get_paper (Comic *comic); +void tbo_comic_set_paper (Comic *comic, TboComicPaper paper); +gboolean tbo_comic_get_pdf_page_size (Comic *comic, gdouble *width, gdouble *height); GList *tbo_comic_get_pages (Comic *comic); Page *tbo_comic_new_page (Comic *comic); void tbo_comic_insert_page (Comic *comic, Page *page, int nth); @@ -48,6 +57,7 @@ void tbo_comic_del_page (Comic *comic, int nth); gboolean tbo_comic_del_current_page (Comic *comic); int tbo_comic_len (Comic *comic); int tbo_comic_page_index (Comic *comic); +int tbo_comic_page_position (Comic *comic); int tbo_comic_page_nth (Comic *comic, Page *page); gboolean tbo_comic_page_first (Comic *comic); gboolean tbo_comic_page_last (Comic *comic); @@ -56,7 +66,9 @@ Page *tbo_comic_prev_page (Comic *comic); Page *tbo_comic_get_current_page (Comic *comic); void tbo_comic_set_current_page (Comic *comic, Page *page); void tbo_comic_set_current_page_nth (Comic *comic, int nth); -gboolean tbo_comic_save (TboWindow *tbo, char *filename); -void tbo_comic_open (TboWindow *window, char *filename); +void tbo_comic_reorder_page (Comic *comic, Page *page, int nth); +gboolean tbo_comic_save (TboWindow *tbo, const gchar *filename); +gboolean tbo_comic_save_snapshot (TboWindow *tbo, const gchar *filename); +void tbo_comic_open (TboWindow *window, const gchar *filename); #endif diff --git a/src/dnd.c b/src/dnd.c index 6707742..a56b820 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -19,17 +19,29 @@ #include #include +#include #include "dnd.h" #include "tbo-drawing.h" #include "frame.h" #include "tbo-object-svg.h" #include "tbo-object-pixmap.h" +#include "tbo-tooltip.h" #include "tbo-files.h" #include "tbo-window.h" #include "tbo-tool-selector.h" #include "tbo-undo.h" #include "tbo-widget.h" +typedef enum +{ + TBO_DND_INSERT_OK, + TBO_DND_INSERT_NO_FRAME, + TBO_DND_INSERT_OUTSIDE_FRAME, + TBO_DND_INSERT_INVALID_ASSET, +} TboDndInsertResult; + +#define TBO_DND_FEEDBACK_TIMEOUT_MS 2500 + static TboObjectBase * create_asset (const gchar *asset_path, gint x, gint y) { @@ -39,6 +51,71 @@ create_asset (const gchar *asset_path, gint x, gint y) return TBO_OBJECT_BASE (tbo_object_pixmap_new_with_params (x, y, 0, 0, (gchar *) asset_path)); } +static void +select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset); + +static void +show_insert_feedback (TboWindow *tbo, TboDndInsertResult result) +{ + const gchar *message = NULL; + + switch (result) + { + case TBO_DND_INSERT_NO_FRAME: + message = _("Enter a frame before inserting an image."); + break; + case TBO_DND_INSERT_OUTSIDE_FRAME: + message = _("Drop the image inside the current frame."); + break; + case TBO_DND_INSERT_INVALID_ASSET: + message = _("Couldn't insert the image."); + break; + case TBO_DND_INSERT_OK: + default: + break; + } + + if (message != NULL) + tbo_tooltip_set_center_timeout (message, TBO_DND_FEEDBACK_TIMEOUT_MS, tbo); +} + +static TboDndInsertResult +insert_asset_into_frame (TboWindow *tbo, + const gchar *asset_path, + gint x, + gint y, + TboObjectBase **inserted_asset) +{ + Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); + TboObjectBase *asset; + + if (inserted_asset != NULL) + *inserted_asset = NULL; + + if (frame == NULL) + return TBO_DND_INSERT_NO_FRAME; + if (asset_path == NULL || *asset_path == '\0') + return TBO_DND_INSERT_INVALID_ASSET; + if (x < 0 || y < 0 || x > tbo_frame_get_width (frame) || y > tbo_frame_get_height (frame)) + return TBO_DND_INSERT_OUTSIDE_FRAME; + + asset = create_asset (asset_path, x, y); + if (asset == NULL) + return TBO_DND_INSERT_INVALID_ASSET; + + tbo_frame_add_obj (frame, asset); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, asset)); + select_inserted_asset (tbo, frame, asset); + tbo_window_mark_dirty (tbo); + tbo_drawing_update (TBO_DRAWING (tbo->drawing)); + tbo_toolbar_update (tbo->toolbar); + + if (inserted_asset != NULL) + *inserted_asset = asset; + + return TBO_DND_INSERT_OK; +} + static void select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset) { @@ -129,33 +206,56 @@ tbo_dnd_insert_asset_at_view_coords (TboWindow *tbo, const gchar *asset_path, gd { gint frame_x; gint frame_y; + TboObjectBase *asset = NULL; + TboDndInsertResult result; if (!tbo_drawing_view_to_frame (TBO_DRAWING (tbo->drawing), x, y, &frame_x, &frame_y)) + { + show_insert_feedback (tbo, TBO_DND_INSERT_NO_FRAME); + return NULL; + } + + result = insert_asset_into_frame (tbo, asset_path, frame_x, frame_y, &asset); + if (result != TBO_DND_INSERT_OK) + { + show_insert_feedback (tbo, result); return NULL; + } - return tbo_dnd_insert_asset (tbo, asset_path, frame_x, frame_y); + return asset; } TboObjectBase * tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y) { - Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); TboObjectBase *asset; + TboDndInsertResult result; - if (frame == NULL || asset_path == NULL || *asset_path == '\0') + result = insert_asset_into_frame (tbo, asset_path, x, y, &asset); + if (result != TBO_DND_INSERT_OK) + { + show_insert_feedback (tbo, result); return NULL; + } - if (x < 0 || y < 0 || x > tbo_frame_get_width (frame) || y > tbo_frame_get_height (frame)) + return asset; +} + +TboObjectBase * +tbo_dnd_insert_asset_centered (TboWindow *tbo, const gchar *asset_path) +{ + Frame *frame = tbo_drawing_get_current_frame (TBO_DRAWING (tbo->drawing)); + + if (frame == NULL) + { + show_insert_feedback (tbo, TBO_DND_INSERT_NO_FRAME); return NULL; + } - asset = create_asset (asset_path, x, y); - tbo_frame_add_obj (frame, asset); - tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, asset)); - select_inserted_asset (tbo, frame, asset); - tbo_window_mark_dirty (tbo); - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - tbo_toolbar_update (tbo->toolbar); - return asset; + return tbo_dnd_insert_asset (tbo, + asset_path, + tbo_frame_get_width (frame) / 2, + tbo_frame_get_height (frame) / 2); } void diff --git a/src/dnd.h b/src/dnd.h index 5900dfd..fcdee0d 100644 --- a/src/dnd.h +++ b/src/dnd.h @@ -29,5 +29,6 @@ void tbo_dnd_setup_asset_source (GtkWidget *widget, const gchar *full_path, cons void tbo_dnd_setup_drawing_dest (TboDrawing *drawing, TboWindow *tbo); TboObjectBase *tbo_dnd_insert_asset_at_view_coords (TboWindow *tbo, const gchar *asset_path, gdouble x, gdouble y); TboObjectBase *tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y); +TboObjectBase *tbo_dnd_insert_asset_centered (TboWindow *tbo, const gchar *asset_path); #endif diff --git a/src/doodle-treeview.c b/src/doodle-treeview.c index 3e63072..c0c46eb 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -20,41 +20,53 @@ #include #include #include +#include #include -#include "tbo-object-svg.h" -#include "tbo-drawing.h" -#include "frame.h" +#include + #include "doodle-treeview.h" #include "dnd.h" -#include "tbo-utils.h" #include "tbo-files.h" #include "tbo-widget.h" -void free_gstring_array (GArray *arr); +typedef struct +{ + TboWindow *tbo; + GString *path; + gboolean top_level; +} DoodleExpanderData; + +typedef struct +{ + TboWindow *tbo; + GtkWidget *search_entry; + GtkWidget *content_box; + gboolean bubble_mode; +} DoodleBrowserState; -static GArray *TO_FREE = NULL; static GHashTable *THUMB_CACHE = NULL; -static TboWindow *TBO = NULL; -static void -free_gstring_data (gpointer data, GClosure *closure) -{ - if (data != NULL) - g_string_free ((GString *) data, TRUE); -} +static GdkPixbuf *get_thumbnail_pixbuf (const gchar *path, const gchar *relative_path); +static gint compare_gstrings (gconstpointer a, gconstpointer b); +static void sort_gstring_array (GArray *arr); +static gchar *normalize_search_text (const gchar *text); +static gboolean search_matches_text (const gchar *text, const gchar *query); +static gchar *humanize_label (const gchar *path); +static gchar *format_expander_label (const gchar *path, gint count, gboolean top_level); +static GtkWidget *create_dir_expander (const gchar *path, gint count, gboolean top_level); +static gint count_matching_assets_in_dir (const gchar *dir, const gchar *query); +static void asset_button_clicked_cb (GtkButton *button, gpointer user_data); +static GtkWidget *build_image_grid_internal (TboWindow *tbo, gchar *dir, const gchar *query, gboolean allow_empty); +static void free_expander_data (gpointer data, GClosure *closure); +static void on_expand_cb (GtkExpander *expander, GParamSpec *pspec, DoodleExpanderData *data); +static GtkWidget *doodle_create_no_results_label (void); +static gboolean populate_filtered_dir (TboWindow *tbo, const gchar *dir, GtkWidget *box, const gchar *query, gboolean top_level); +static void rebuild_browser_content (DoodleBrowserState *state); +static void search_changed_cb (GtkEditable *editable, gpointer user_data); void doodle_free_all (void) { - int i; - if (!TO_FREE) return; - for (i=0; ilen; i++) - { - free_gstring_array (g_array_index (TO_FREE, GArray*, i)); - } - g_array_free (TO_FREE, TRUE); - TO_FREE = NULL; - if (THUMB_CACHE != NULL) g_hash_table_remove_all (THUMB_CACHE); } @@ -108,28 +120,40 @@ get_thumbnail_pixbuf (const gchar *path, const gchar *relative_path) return pixbuf; } -void doodle_add_to_free (GArray *arr) -{ - if (!TO_FREE) - TO_FREE = g_array_new (FALSE, FALSE, sizeof(GArray*)); - - g_array_append_val (TO_FREE, arr); -} - void free_gstring_array (GArray *arr) { int i; - GString *mystr; - for (i=0; ilen; i++) + if (arr == NULL) + return; + + for (i = 0; i < (int) arr->len; i++) { - mystr = g_array_index (arr, GString*, i); + GString *mystr = g_array_index (arr, GString *, i); + g_string_free (mystr, TRUE); } + g_array_free (arr, TRUE); } +static gint +compare_gstrings (gconstpointer a, gconstpointer b) +{ + const GString *sa = *(const GString * const *) a; + const GString *sb = *(const GString * const *) b; + + return g_utf8_collate (sa->str, sb->str); +} + +static void +sort_gstring_array (GArray *arr) +{ + if (arr != NULL && arr->len > 1) + g_array_sort (arr, compare_gstrings); +} + GArray * get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) { @@ -138,7 +162,7 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) GDir *dir; struct stat filestat; int st; - GArray *array = g_array_new (FALSE, FALSE, sizeof(GString*)); + GArray *array = g_array_new (FALSE, FALSE, sizeof (GString *)); st = stat (base_dir, &filestat); if (st) @@ -156,9 +180,10 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) return NULL; } - while ((filename = g_dir_read_name (dir))) + while ((filename = g_dir_read_name (dir)) != NULL) { gchar *complete_dir = g_build_filename (base_dir, filename, NULL); + st = stat (complete_dir, &filestat); if (st) { @@ -166,7 +191,7 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) continue; } - if (isdir && bubble_mode && strcmp (filename, "bubble")) + if (isdir && bubble_mode && strcmp (filename, "bubble") != 0) { g_free (complete_dir); continue; @@ -192,183 +217,477 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) } g_dir_close (dir); - + sort_gstring_array (array); return array; } -GtkWidget * -doodle_add_images (gchar *dir) +static gchar * +normalize_search_text (const gchar *text) { - int i; - gchar *dirname; - GtkWidget *grid; - GtkWidget *image; - GtkWidget *button; - GdkPixbuf *pixbuf; - int left, top; - int columns = 2; - int thumb_width; - int thumb_height; + gchar *normalized; + gint i; + + if (text == NULL) + return g_strdup (""); + + normalized = g_utf8_casefold (text, -1); + for (i = 0; normalized[i] != '\0'; i++) + { + if (normalized[i] == '-' || normalized[i] == '_' || normalized[i] == '/' || normalized[i] == '.') + normalized[i] = ' '; + } - dirname = dir; + return normalized; +} - GArray *arr = get_files (dirname, FALSE, FALSE); +static gboolean +search_matches_text (const gchar *text, const gchar *query) +{ + gchar *normalized_text; + gchar *normalized_query; + gboolean matches; + + if (query == NULL || *query == '\0') + return TRUE; + + normalized_text = normalize_search_text (text); + normalized_query = normalize_search_text (query); + matches = g_strstr_len (normalized_text, -1, normalized_query) != NULL; + g_free (normalized_text); + g_free (normalized_query); + return matches; +} +static gchar * +humanize_label (const gchar *path) +{ + gchar *basename = g_path_get_basename (path); + gint i; + + for (i = 0; basename[i] != '\0'; i++) + { + if (basename[i] == '-' || basename[i] == '_') + basename[i] = ' '; + } + if (basename[0] != '\0') + basename[0] = g_ascii_toupper (basename[0]); + + return basename; +} + +static gchar * +format_expander_label (const gchar *path, gint count, gboolean top_level) +{ + gchar *title = humanize_label (path); + gchar *label; + + if (top_level) + label = g_strdup_printf ("%s (%d)", title, count); + else + label = g_strdup_printf ("%s (%d)", title, count); + + g_free (title); + return label; +} + +static GtkWidget * +create_dir_expander (const gchar *path, gint count, gboolean top_level) +{ + GtkWidget *expander; + gchar *label = format_expander_label (path, count, top_level); + + expander = gtk_expander_new (label); + gtk_widget_add_css_class (expander, top_level ? "tbo-sidebar-group" : "tbo-sidebar-subgroup"); + if (top_level) + gtk_expander_set_use_markup (GTK_EXPANDER (expander), TRUE); + g_free (label); + return expander; +} + +static gint +count_matching_assets_in_dir (const gchar *dir, const gchar *query) +{ + GArray *files; + GArray *subdirs; + gint count = 0; + gint i; + + files = get_files ((gchar *) dir, FALSE, FALSE); + if (files != NULL) + { + for (i = 0; i < (int) files->len; i++) + { + GString *file = g_array_index (files, GString *, i); + const gchar *relative_path; + gint prefix_len = tbo_files_prefix_len (file->str); + + relative_path = prefix_len > 0 ? file->str + prefix_len : file->str; + if (search_matches_text (relative_path, query)) + count++; + } + free_gstring_array (files); + } + + subdirs = get_files ((gchar *) dir, TRUE, FALSE); + if (subdirs != NULL) + { + for (i = 0; i < (int) subdirs->len; i++) + { + GString *subdir = g_array_index (subdirs, GString *, i); + + count += count_matching_assets_in_dir (subdir->str, query); + } + free_gstring_array (subdirs); + } + + return count; +} + +static void +asset_button_clicked_cb (GtkButton *button, gpointer user_data) +{ + TboWindow *tbo = user_data; + const gchar *asset_path = g_object_get_data (G_OBJECT (button), "tbo-asset-full-path"); + + if (tbo == NULL) + return; + + if (tbo_dnd_insert_asset_centered (tbo, asset_path) != NULL) + gtk_widget_grab_focus (tbo->drawing); +} + +static GtkWidget * +build_image_grid_internal (TboWindow *tbo, gchar *dir, const gchar *query, gboolean allow_empty) +{ + GArray *arr; + GtkWidget *grid; + gint visible = 0; + gint i; + + arr = get_files (dir, FALSE, FALSE); grid = gtk_grid_new (); + gtk_widget_add_css_class (grid, "tbo-asset-grid"); gtk_grid_set_row_spacing (GTK_GRID (grid), 8); gtk_grid_set_column_spacing (GTK_GRID (grid), 8); if (arr == NULL) { - tbo_widget_show_all (GTK_WIDGET (grid)); - return grid; + if (allow_empty) + { + tbo_widget_show_all (grid); + return grid; + } + return NULL; } - GString *mystr; - for (i=0; ilen; i++) + for (i = 0; i < (int) arr->len; i++) { + GString *mystr = g_array_index (arr, GString *, i); const gchar *relative_path; - int prefix_len; - - top = i / columns; - left = i % columns; + gint prefix_len = tbo_files_prefix_len (mystr->str); + GdkPixbuf *pixbuf; + GtkWidget *image; + GtkWidget *button; + gint thumb_width; + gint thumb_height; + gint left; + gint top; - mystr = g_array_index (arr, GString*, i); - prefix_len = tbo_files_prefix_len (mystr->str); relative_path = prefix_len > 0 ? mystr->str + prefix_len : mystr->str; + if (!search_matches_text (relative_path, query)) + continue; pixbuf = get_thumbnail_pixbuf (mystr->str, relative_path); if (pixbuf == NULL) continue; + top = visible / 2; + left = visible % 2; + visible++; + thumb_width = gdk_pixbuf_get_width (pixbuf); thumb_height = gdk_pixbuf_get_height (pixbuf); - image = tbo_picture_new_for_pixbuf (pixbuf); gtk_picture_set_can_shrink (GTK_PICTURE (image), FALSE); gtk_widget_set_size_request (image, thumb_width, thumb_height); + button = gtk_button_new (); gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); - gtk_widget_set_can_focus (button, FALSE); + gtk_widget_set_can_focus (button, TRUE); gtk_widget_set_size_request (button, thumb_width + 12, thumb_height + 12); - + gtk_widget_add_css_class (button, "tbo-asset-button"); + gtk_widget_set_tooltip_text (button, relative_path); tbo_dnd_setup_asset_source (button, mystr->str, relative_path); + g_signal_connect (button, "clicked", G_CALLBACK (asset_button_clicked_cb), tbo); tbo_widget_add_child (button, image); gtk_grid_attach (GTK_GRID (grid), button, left, top, 1, 1); g_object_unref (pixbuf); } - doodle_add_to_free (arr); + free_gstring_array (arr); - tbo_widget_show_all (GTK_WIDGET (grid)); + if (!allow_empty && visible == 0) + return NULL; + + tbo_widget_show_all (grid); return grid; } +GtkWidget * +doodle_add_images (gchar *dir) +{ + return build_image_grid_internal (NULL, dir, NULL, TRUE); +} + void doodle_add_dir_images (gchar *dir, GtkWidget *box) { - char base_name[255]; - get_base_name (dir, base_name, 255); - GtkWidget *expander = gtk_expander_new (base_name); - GtkWidget *grid = doodle_add_images (dir); + GtkWidget *expander = create_dir_expander (dir, count_matching_assets_in_dir (dir, NULL), FALSE); + GtkWidget *grid = build_image_grid_internal (NULL, dir, NULL, TRUE); + tbo_widget_add_child (expander, grid); gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); tbo_widget_add_child (box, expander); } -void -on_expand_cb (GtkExpander *expander, GParamSpec *pspec, GString *str) +static void +free_expander_data (gpointer data, GClosure *closure) +{ + DoodleExpanderData *expander_data = data; + + (void) closure; + + if (expander_data != NULL) + { + if (expander_data->path != NULL) + g_string_free (expander_data->path, TRUE); + g_free (expander_data); + } +} + +static void +on_expand_cb (GtkExpander *expander, GParamSpec *pspec, DoodleExpanderData *data) { - GString *mystr2; - int i; GtkWidget *vbox = gtk_expander_get_child (expander); - int numofchilds = 0; - if (vbox == NULL || !gtk_expander_get_expanded (expander)) + gint num_children; + GArray *subdirs; + GtkWidget *grid; + gint i; + + (void) pspec; + + if (vbox == NULL || !gtk_expander_get_expanded (expander) || data == NULL) return; - numofchilds = tbo_widget_get_child_count (vbox); + num_children = tbo_widget_get_child_count (vbox); + if (num_children > 0) + return; - if (numofchilds == 0) + subdirs = get_files (data->path->str, TRUE, FALSE); + if (subdirs != NULL) { - GArray *subdir = get_files (str->str, TRUE, FALSE); - - if (subdir != NULL && subdir->len > 0) - { - for (i=0; ilen; i++) - { - mystr2 = g_array_index (subdir, GString*, i); - doodle_add_dir_images (mystr2->str, vbox); - } - } - else + for (i = 0; i < (int) subdirs->len; i++) { - GtkWidget *grid = doodle_add_images (str->str); - tbo_widget_add_child (vbox, grid); + GString *subdir = g_array_index (subdirs, GString *, i); + gint count = count_matching_assets_in_dir (subdir->str, NULL); + GtkWidget *child_expander; + GtkWidget *child_box; + DoodleExpanderData *child_data; + + if (count == 0) + continue; + + child_expander = create_dir_expander (subdir->str, count, FALSE); + child_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + tbo_widget_add_child (child_expander, child_box); + tbo_box_pack_start (vbox, child_expander, FALSE, FALSE, 5); + + child_data = g_new0 (DoodleExpanderData, 1); + child_data->tbo = data->tbo; + child_data->path = g_string_new (subdir->str); + child_data->top_level = FALSE; + g_signal_connect_data (child_expander, + "notify::expanded", + G_CALLBACK (on_expand_cb), + child_data, + free_expander_data, + 0); } - if (subdir != NULL) - free_gstring_array (subdir); + free_gstring_array (subdirs); } - tbo_widget_show_all (GTK_WIDGET (vbox)); + + grid = build_image_grid_internal (data->tbo, data->path->str, NULL, FALSE); + if (grid != NULL) + tbo_widget_add_child (vbox, grid); + + tbo_widget_show_all (vbox); } -GtkWidget * -doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode) +static GtkWidget * +doodle_create_no_results_label (void) { + GtkWidget *label = gtk_label_new (_("No assets match this search.")); + + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_add_css_class (label, "dim-label"); + return label; +} + +static gboolean +populate_filtered_dir (TboWindow *tbo, const gchar *dir, GtkWidget *box, const gchar *query, gboolean top_level) +{ + GArray *subdirs; GtkWidget *expander; - GtkWidget *vbox; - GtkWidget *vbox2; - gchar *dirname; + GtkWidget *content; + GtkWidget *grid; + gint i; + gint count = count_matching_assets_in_dir (dir, query); - TBO = tbo; + if (count == 0) + return FALSE; - dirname = malloc (255*sizeof(char)); - char label_format[255]; - int i, k; + expander = create_dir_expander (dir, count, top_level); + content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + tbo_widget_add_child (expander, content); + gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); - vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + subdirs = get_files ((gchar *) dir, TRUE, FALSE); + if (subdirs != NULL) + { + for (i = 0; i < (int) subdirs->len; i++) + { + GString *subdir = g_array_index (subdirs, GString *, i); + + populate_filtered_dir (tbo, subdir->str, content, query, FALSE); + } + free_gstring_array (subdirs); + } - GArray *arr = NULL; - GString *mystr, *mystr2; + grid = build_image_grid_internal (tbo, (gchar *) dir, query, FALSE); + if (grid != NULL) + tbo_widget_add_child (content, grid); + + tbo_box_pack_start (box, expander, FALSE, FALSE, 5); + return TRUE; +} + +static void +rebuild_browser_content (DoodleBrowserState *state) +{ + gchar **possible_dirs; + const gchar *query; + gboolean added_any = FALSE; + gint k; - char **possible_dirs = tbo_files_get_dirs (); - for (k=0; possible_dirs[k]; k++) + if (state == NULL) + return; + + tbo_widget_destroy_all_children (state->content_box); + query = gtk_editable_get_text (GTK_EDITABLE (state->search_entry)); + possible_dirs = tbo_files_get_dirs (); + + for (k = 0; possible_dirs[k] != NULL; k++) { - arr = get_files (possible_dirs[k], TRUE, bubble_mode); - if (!arr) continue; + GArray *arr = get_files (possible_dirs[k], TRUE, state->bubble_mode); + gint i; + + if (arr == NULL) + continue; - for (i=0; ilen; i++) + for (i = 0; i < (int) arr->len; i++) { - mystr = g_array_index (arr, GString*, i); - - vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); - get_base_name (mystr->str, dirname, 255); - snprintf (label_format, 255, "%s", dirname); - expander = gtk_expander_new (label_format); - gtk_expander_set_use_markup (GTK_EXPANDER (expander), TRUE); - tbo_box_pack_start (vbox, expander, FALSE, FALSE, 5); - tbo_widget_add_child (expander, vbox2); - - mystr2 = g_string_new (mystr->str); - g_signal_connect_data (GTK_EXPANDER (expander), - "notify::expanded", - G_CALLBACK (on_expand_cb), - mystr2, - free_gstring_data, - 0); + GString *dir = g_array_index (arr, GString *, i); - if (bubble_mode) + if (query != NULL && *query != '\0') + { + if (populate_filtered_dir (state->tbo, dir->str, state->content_box, query, TRUE)) + added_any = TRUE; + } + else { - gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); - on_expand_cb (GTK_EXPANDER (expander), NULL, mystr2); + gint count = count_matching_assets_in_dir (dir->str, NULL); + GtkWidget *expander; + GtkWidget *vbox; + DoodleExpanderData *expander_data; + + if (count == 0) + continue; + + expander = create_dir_expander (dir->str, count, TRUE); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + tbo_widget_add_child (expander, vbox); + tbo_box_pack_start (state->content_box, expander, FALSE, FALSE, 5); + + expander_data = g_new0 (DoodleExpanderData, 1); + expander_data->tbo = state->tbo; + expander_data->path = g_string_new (dir->str); + expander_data->top_level = TRUE; + g_signal_connect_data (expander, + "notify::expanded", + G_CALLBACK (on_expand_cb), + expander_data, + free_expander_data, + 0); + + if (state->bubble_mode) + { + gtk_expander_set_expanded (GTK_EXPANDER (expander), TRUE); + on_expand_cb (GTK_EXPANDER (expander), NULL, expander_data); + } + + added_any = TRUE; } } + free_gstring_array (arr); } + + if (!added_any) + tbo_widget_add_child (state->content_box, doodle_create_no_results_label ()); + tbo_files_free (possible_dirs); + tbo_widget_show_all (state->content_box); +} - free (dirname); +static void +search_changed_cb (GtkEditable *editable, gpointer user_data) +{ + (void) editable; + rebuild_browser_content (user_data); +} - return vbox; +GtkWidget * +doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode) +{ + GtkWidget *root; + GtkWidget *search_entry; + GtkWidget *content; + DoodleBrowserState *state; + + root = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + search_entry = gtk_search_entry_new (); + gtk_widget_add_css_class (search_entry, "tbo-sidebar-search"); + gtk_widget_set_margin_start (search_entry, 4); + gtk_widget_set_margin_end (search_entry, 4); + gtk_widget_set_margin_top (search_entry, 4); + gtk_widget_set_margin_bottom (search_entry, 4); + gtk_search_entry_set_placeholder_text (GTK_SEARCH_ENTRY (search_entry), + bubble_mode ? _("Search Bubbles") : _("Search Assets")); + + content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); + tbo_widget_add_child (root, search_entry); + tbo_widget_add_child (root, content); + + state = g_new0 (DoodleBrowserState, 1); + state->tbo = tbo; + state->search_entry = search_entry; + state->content_box = content; + state->bubble_mode = bubble_mode; + g_object_set_data_full (G_OBJECT (root), "tbo-browser-state", state, g_free); + g_signal_connect (search_entry, "search-changed", G_CALLBACK (search_changed_cb), state); + + rebuild_browser_content (state); + return root; } diff --git a/src/export.c b/src/export.c index 32edd07..721288c 100644 --- a/src/export.c +++ b/src/export.c @@ -26,25 +26,50 @@ #include "export.h" #include "comic.h" +#include "frame.h" +#include "page.h" #include "tbo-file-dialog.h" #include "tbo-drawing.h" +#include "tbo-tool-selector.h" #include "tbo-ui-utils.h" #include "tbo-types.h" #include "tbo-widget.h" -static int LOCK = 0; - -struct export_spin_args { - gint current_size; - gint current_size2; - GtkWidget *spin2; - gdouble *scale; -}; +typedef struct +{ + GtkWidget *spinw; + GtkWidget *spinh; + gint base_width; + gint base_height; + gboolean updating; +} ExportSizeState; -struct export_file_args { +typedef struct +{ TboWindow *tbo; GtkEntry *entry; -}; +} ExportFileArgs; + +typedef struct +{ + TboWindow *tbo; + ExportSizeState size_state; + GtkWidget *scope_dropdown; + GtkWidget *format_dropdown; + GtkWidget *range_row; + GtkWidget *range_from_spin; + GtkWidget *range_to_spin; + GtkWidget *preview_box; + GtkWidget *preview_label; + gint page_width; + gint page_height; + gint selection_width; + gint selection_height; + gboolean has_selection; +} ExportDialogState; + +static TboExportScope dropdown_scope_to_export_scope (guint selected, gboolean has_selection); +static void draw_frame_export (cairo_t *cr, Frame *frame, gint width, gint height); static gchar * strip_matching_extension (const gchar *filename, const gchar *extension) @@ -67,130 +92,578 @@ show_export_error (TboWindow *tbo, const gchar *message) tbo_alert_show (GTK_WINDOW (tbo->window), message, NULL); } -gboolean -tbo_export_file (TboWindow *tbo, - const gchar *filename, - const gchar *format_hint, - gint width, - gint height) +static Frame * +get_export_selection_frame (TboWindow *tbo) { - cairo_surface_t *surface = NULL; - cairo_t *cr = NULL; - gchar *base_filename = NULL; - gchar *format_pages = NULL; - GList *page_list; - gchar *export_to = NULL; - gint i, n, n2; - gboolean exported = FALSE; - gboolean success = TRUE; + TboDrawing *drawing; + TboToolSelector *selector; + Frame *current_frame; - if (filename == NULL || *filename == '\0' || width <= 0 || height <= 0) - return FALSE; + if (tbo == NULL || tbo->drawing == NULL || tbo->toolbar == NULL || tbo->toolbar->tools == NULL) + return NULL; - if (format_hint != NULL && *format_hint != '\0') + drawing = TBO_DRAWING (tbo->drawing); + current_frame = tbo_drawing_get_current_frame (drawing); + if (current_frame != NULL) + return current_frame; + + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + return tbo_tool_selector_get_selected_frame (selector); +} + +static gboolean +has_export_selection (TboWindow *tbo) +{ + return get_export_selection_frame (tbo) != NULL; +} + +static void +get_export_scope_default_size (TboWindow *tbo, TboExportScope scope, gint *width, gint *height) +{ + Frame *selection; + + if (width == NULL || height == NULL) + return; + + *width = tbo_comic_get_width (tbo->comic); + *height = tbo_comic_get_height (tbo->comic); + + if (scope != TBO_EXPORT_SCOPE_SELECTION) + return; + + selection = get_export_selection_frame (tbo); + if (selection != NULL) { - export_to = g_ascii_strdown (format_hint, -1); - base_filename = strip_matching_extension (filename, export_to); + *width = tbo_frame_get_width (selection); + *height = tbo_frame_get_height (selection); + } +} + +static void +normalize_export_page_range (Comic *comic, gint *from_page, gint *to_page) +{ + gint page_count; + + if (comic == NULL || from_page == NULL || to_page == NULL) + return; + + page_count = MAX (1, tbo_comic_len (comic)); + *from_page = CLAMP (*from_page, 1, page_count); + *to_page = CLAMP (*to_page, 1, page_count); + + if (*from_page > *to_page) + *to_page = *from_page; +} + +static GList * +build_export_page_range (Comic *comic, gint from_page, gint to_page, gint *n_pages) +{ + GList *pages = NULL; + gint i; + + normalize_export_page_range (comic, &from_page, &to_page); + for (i = from_page - 1; i <= to_page - 1; i++) + pages = g_list_append (pages, g_list_nth_data (tbo_comic_get_pages (comic), i)); + + if (n_pages != NULL) + *n_pages = g_list_length (pages); + + return pages; +} + +static void +get_dialog_range (ExportDialogState *state, gint *from_page, gint *to_page) +{ + gint from_value; + gint to_value; + + if (state == NULL) + return; + + from_value = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_from_spin)); + to_value = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_to_spin)); + normalize_export_page_range (state->tbo->comic, &from_value, &to_value); + + if (from_page != NULL) + *from_page = from_value; + if (to_page != NULL) + *to_page = to_value; +} + +static Page * +get_preview_page_for_dialog (ExportDialogState *state, TboExportScope scope, gint from_page) +{ + if (state == NULL || state->tbo == NULL || state->tbo->comic == NULL) + return NULL; + + if (scope == TBO_EXPORT_SCOPE_CURRENT_PAGE) + return tbo_comic_get_current_page (state->tbo->comic); + if (scope == TBO_EXPORT_SCOPE_ALL_PAGES) + return g_list_nth_data (tbo_comic_get_pages (state->tbo->comic), MAX (0, from_page - 1)); + + return NULL; +} + +static GdkPixbuf * +create_page_preview_pixbuf (TboWindow *tbo, Page *page, gint width, gint height) +{ + cairo_surface_t *surface; + cairo_t *cr; + GdkPixbuf *pixbuf; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, page, width, height); + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, width, height); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return pixbuf; +} + +static GdkPixbuf * +create_frame_preview_pixbuf (Frame *frame, gint width, gint height) +{ + cairo_surface_t *surface; + cairo_t *cr; + GdkPixbuf *pixbuf; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + draw_frame_export (cr, frame, width, height); + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, width, height); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return pixbuf; +} + +static void +set_preview_pixbuf (ExportDialogState *state, GdkPixbuf *pixbuf) +{ + GtkWidget *picture = tbo_picture_new_for_pixbuf (pixbuf); + + gtk_picture_set_can_shrink (GTK_PICTURE (picture), TRUE); + gtk_widget_set_size_request (picture, 220, 160); + tbo_widget_destroy_all_children (state->preview_box); + tbo_widget_add_child (state->preview_box, picture); + tbo_widget_show_all (state->preview_box); +} + +static void +update_preview_and_range (ExportDialogState *state) +{ + TboExportScope scope; + gint from_page; + gint to_page; + gint width; + gint height; + gint preview_width; + gint preview_height; + GdkPixbuf *pixbuf = NULL; + gchar *label = NULL; + gboolean range_sensitive; + + if (state == NULL) + return; + + scope = dropdown_scope_to_export_scope (gtk_drop_down_get_selected (GTK_DROP_DOWN (state->scope_dropdown)), + state->has_selection); + get_dialog_range (state, &from_page, &to_page); + + range_sensitive = scope == TBO_EXPORT_SCOPE_ALL_PAGES && tbo_comic_len (state->tbo->comic) > 1; + gtk_widget_set_sensitive (state->range_row, range_sensitive); + + width = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->size_state.spinw)); + height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->size_state.spinh)); + preview_width = MAX (1, MIN (220, width)); + preview_height = MAX (1, MIN (160, (gint) ((preview_width * (gdouble) height) / MAX (1, width)))); + if (preview_height > 160) + { + preview_height = 160; + preview_width = MAX (1, (gint) ((preview_height * (gdouble) width) / MAX (1, height))); + } + + if (scope == TBO_EXPORT_SCOPE_SELECTION) + { + Frame *frame = get_export_selection_frame (state->tbo); + + if (frame != NULL) + pixbuf = create_frame_preview_pixbuf (frame, preview_width, preview_height); + label = g_strdup (_("Preview: Selection")); } else { - gchar *dot = strrchr (filename, '.'); + Page *page = get_preview_page_for_dialog (state, scope, from_page); - if (dot != NULL && dot[1] != '\0') - { - export_to = g_ascii_strdown (dot + 1, -1); - base_filename = g_strndup (filename, dot - filename); - } + if (page != NULL) + pixbuf = create_page_preview_pixbuf (state->tbo, page, preview_width, preview_height); + + if (scope == TBO_EXPORT_SCOPE_CURRENT_PAGE) + label = g_strdup_printf (_("Preview: Current Page %d"), tbo_comic_page_position (state->tbo->comic)); + else if (from_page == to_page) + label = g_strdup_printf (_("Preview: Page %d"), from_page); else + label = g_strdup_printf (_("Preview: Page %d of Range %d-%d"), from_page, from_page, to_page); + } + + gtk_label_set_text (GTK_LABEL (state->preview_label), label); + set_preview_pixbuf (state, pixbuf); + g_free (label); + if (pixbuf != NULL) + g_object_unref (pixbuf); +} + +static void +set_export_size_base (ExportSizeState *state, gint width, gint height) +{ + if (state == NULL) + return; + + state->base_width = MAX (1, width); + state->base_height = MAX (1, height); + + state->updating = TRUE; + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->spinw), state->base_width); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->spinh), state->base_height); + state->updating = FALSE; +} + +static gboolean +export_width_changed_cb (GtkWidget *widget, ExportSizeState *state) +{ + gint new_width; + gint new_height; + + if (state == NULL || state->updating || state->base_width <= 0 || state->base_height <= 0) + return FALSE; + + new_width = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (widget)); + if (new_width <= 0) + return FALSE; + + new_height = MAX (1, (gint) ((new_width * (gdouble) state->base_height) / state->base_width)); + state->updating = TRUE; + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->spinh), new_height); + state->updating = FALSE; + return FALSE; +} + +static gboolean +export_height_changed_cb (GtkWidget *widget, ExportSizeState *state) +{ + gint new_height; + gint new_width; + + if (state == NULL || state->updating || state->base_width <= 0 || state->base_height <= 0) + return FALSE; + + new_height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (widget)); + if (new_height <= 0) + return FALSE; + + new_width = MAX (1, (gint) ((new_height * (gdouble) state->base_width) / state->base_height)); + state->updating = TRUE; + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->spinw), new_width); + state->updating = FALSE; + return FALSE; +} + +static gboolean +dialog_width_changed_cb (GtkWidget *widget, gpointer user_data) +{ + ExportDialogState *state = user_data; + + export_width_changed_cb (widget, &state->size_state); + update_preview_and_range (state); + return FALSE; +} + +static gboolean +dialog_height_changed_cb (GtkWidget *widget, gpointer user_data) +{ + ExportDialogState *state = user_data; + + export_height_changed_cb (widget, &state->size_state); + update_preview_and_range (state); + return FALSE; +} + +static TboExportScope +dropdown_scope_to_export_scope (guint selected, gboolean has_selection) +{ + if (selected == 0) + return TBO_EXPORT_SCOPE_ALL_PAGES; + if (selected == 1) + return TBO_EXPORT_SCOPE_CURRENT_PAGE; + if (has_selection && selected == 2) + return TBO_EXPORT_SCOPE_SELECTION; + + return TBO_EXPORT_SCOPE_ALL_PAGES; +} + +static void +scope_selected_cb (GtkDropDown *dropdown, GParamSpec *pspec, gpointer user_data) +{ + ExportDialogState *args = user_data; + TboExportScope scope; + + (void) pspec; + + if (args == NULL) + return; + + scope = dropdown_scope_to_export_scope (gtk_drop_down_get_selected (dropdown), args->has_selection); + if (scope == TBO_EXPORT_SCOPE_SELECTION) + set_export_size_base (&args->size_state, args->selection_width, args->selection_height); + else + set_export_size_base (&args->size_state, args->page_width, args->page_height); + + update_preview_and_range (args); +} + +static gboolean +range_from_changed_cb (GtkWidget *widget, gpointer user_data) +{ + ExportDialogState *state = user_data; + gint from_page = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (widget)); + gint to_page = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_to_spin)); + + normalize_export_page_range (state->tbo->comic, &from_page, &to_page); + if (to_page != gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_to_spin))) + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->range_to_spin), to_page); + update_preview_and_range (state); + return FALSE; +} + +static gboolean +range_to_changed_cb (GtkWidget *widget, gpointer user_data) +{ + ExportDialogState *state = user_data; + gint from_page = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_from_spin)); + gint to_page = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (widget)); + + normalize_export_page_range (state->tbo->comic, &from_page, &to_page); + if (from_page != gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (state->range_from_spin))) + gtk_spin_button_set_value (GTK_SPIN_BUTTON (state->range_from_spin), from_page); + update_preview_and_range (state); + return FALSE; +} + +static void +format_selected_cb (GtkDropDown *dropdown, GParamSpec *pspec, gpointer user_data) +{ + (void) dropdown; + (void) pspec; + update_preview_and_range (user_data); +} + +static gboolean +filedialog_cb (GtkWidget *widget, gpointer data) +{ + ExportFileArgs *args = data; + const gchar *current_text = gtk_editable_get_text (GTK_EDITABLE (args->entry)); + gchar *filename = tbo_file_dialog_save_export (args->tbo, current_text); + + (void) widget; + + if (filename != NULL) + { + gtk_editable_set_text (GTK_EDITABLE (args->entry), filename); + tbo_window_set_export_path (args->tbo, filename); + g_free (filename); + } + return FALSE; +} + +static gboolean +begin_export_surface (TboWindow *tbo, + const gchar *export_to, + const gchar *path, + gint width, + gint height, + gboolean use_pdf_page_size, + cairo_surface_t **surface, + cairo_t **cr, + gdouble *draw_width, + gdouble *draw_height) +{ + if (g_strcmp0 (export_to, "pdf") == 0) + { + *draw_width = width; + *draw_height = height; + if (use_pdf_page_size && !tbo_comic_get_pdf_page_size (tbo->comic, draw_width, draw_height)) { - base_filename = g_strdup (filename); - export_to = g_strdup ("png"); + *draw_width = width; + *draw_height = height; } + *surface = cairo_pdf_surface_create (path, *draw_width, *draw_height); + } + else if (g_strcmp0 (export_to, "svg") == 0) + { + *draw_width = width; + *draw_height = height; + *surface = cairo_svg_surface_create (path, width, height); + } + else + { + *draw_width = width; + *draw_height = height; + *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); } - if (g_strcmp0 (export_to, "png") != 0 && - g_strcmp0 (export_to, "pdf") != 0 && - g_strcmp0 (export_to, "svg") != 0) + *cr = cairo_create (*surface); + if (cairo_surface_status (*surface) != CAIRO_STATUS_SUCCESS || cairo_status (*cr) != CAIRO_STATUS_SUCCESS) { - g_free (export_to); - export_to = g_strdup ("png"); + show_export_error (tbo, + cairo_status_to_string (cairo_surface_status (*surface) != CAIRO_STATUS_SUCCESS ? + cairo_surface_status (*surface) : + cairo_status (*cr))); + if (*surface != NULL) + cairo_surface_destroy (*surface); + if (*cr != NULL) + cairo_destroy (*cr); + *surface = NULL; + *cr = NULL; + return FALSE; } - n = tbo_comic_len (tbo->comic); - n2 = n; - for (i = 0; n; n = n / 10, i++); - format_pages = g_strdup_printf ("%%s%%0%dd.%%s", i); + return TRUE; +} - for (i = 0, page_list = tbo_comic_get_pages (tbo->comic); page_list; i++, page_list = page_list->next) +static gboolean +finish_export_surface (TboWindow *tbo, + const gchar *export_to, + const gchar *path, + cairo_surface_t *surface, + cairo_t *cr) +{ + if (g_strcmp0 (export_to, "pdf") == 0) + cairo_show_page (cr); + else if (g_strcmp0 (export_to, "png") == 0) { - gchar *rpath = g_strdup_printf (format_pages, base_filename, i, export_to); + cairo_status_t status = cairo_surface_write_to_png (surface, path); - if (n2 == 1) + if (status != CAIRO_STATUS_SUCCESS) { - g_free (rpath); - rpath = g_strdup_printf ("%s.%s", base_filename, export_to); + show_export_error (tbo, cairo_status_to_string (status)); + return FALSE; } + } - if (strcmp (export_to, "pdf") == 0) - { - if (!surface) - { - g_free (rpath); - rpath = g_strdup_printf ("%s.%s", base_filename, export_to); - surface = cairo_pdf_surface_create (rpath, width, height); - cr = cairo_create (surface); - } - } - else if (strcmp (export_to, "svg") == 0) - { - surface = cairo_svg_surface_create (rpath, width, height); - cr = cairo_create (surface); - } - else + return TRUE; +} + +static void +draw_frame_export (cairo_t *cr, Frame *frame, gint width, gint height) +{ + cairo_set_source_rgb (cr, 1, 1, 1); + cairo_rectangle (cr, 0, 0, width, height); + cairo_fill (cr); + tbo_frame_draw_scaled (frame, cr, width, height); +} + +static gboolean +export_page_list (TboWindow *tbo, + const gchar *base_filename, + const gchar *export_to, + gint width, + gint height, + GList *pages, + gint n_pages, + gboolean use_pdf_page_size) +{ + cairo_surface_t *surface = NULL; + cairo_t *cr = NULL; + gchar *format_pages = NULL; + gboolean exported = FALSE; + gboolean success = TRUE; + gint digits = 0; + gint count = n_pages; + gint index = 0; + + for (; count; count /= 10, digits++); + format_pages = g_strdup_printf ("%%s%%0%dd.%%s", MAX (1, digits)); + + for (; pages != NULL; pages = pages->next, index++) + { + gchar *path = g_strdup_printf (format_pages, base_filename, index, export_to); + gdouble draw_width; + gdouble draw_height; + + if (n_pages == 1 || g_strcmp0 (export_to, "pdf") == 0) { - surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); - cr = cairo_create (surface); + g_free (path); + path = g_strdup_printf ("%s.%s", base_filename, export_to); } - if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS || cairo_status (cr) != CAIRO_STATUS_SUCCESS) + if (g_strcmp0 (export_to, "pdf") == 0) { - show_export_error (tbo, cairo_status_to_string (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS ? - cairo_surface_status (surface) : - cairo_status (cr))); - success = FALSE; - g_free (rpath); - break; + if (surface == NULL) + { + if (!begin_export_surface (tbo, + export_to, + path, + width, + height, + use_pdf_page_size, + &surface, + &cr, + &draw_width, + &draw_height)) + { + g_free (path); + success = FALSE; + break; + } + } + else + { + if (use_pdf_page_size && !tbo_comic_get_pdf_page_size (tbo->comic, &draw_width, &draw_height)) + { + draw_width = width; + draw_height = height; + } + else if (!use_pdf_page_size) + { + draw_width = width; + draw_height = height; + } + } } - - tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, (Page *) page_list->data, width, height); - - if (strcmp (export_to, "pdf") == 0) - cairo_show_page (cr); - else if (strcmp (export_to, "png") == 0) + else { - cairo_status_t status = cairo_surface_write_to_png (surface, rpath); - if (status != CAIRO_STATUS_SUCCESS) + if (!begin_export_surface (tbo, + export_to, + path, + width, + height, + use_pdf_page_size, + &surface, + &cr, + &draw_width, + &draw_height)) { - show_export_error (tbo, cairo_status_to_string (status)); + g_free (path); success = FALSE; + break; } } - if (success) - exported = TRUE; + tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, TBO_PAGE (pages->data), draw_width, draw_height); + success = finish_export_surface (tbo, export_to, path, surface, cr); + g_free (path); + + if (!success) + break; + + exported = TRUE; - if (strcmp (export_to, "pdf") != 0) + if (g_strcmp0 (export_to, "pdf") != 0) { cairo_surface_destroy (surface); cairo_destroy (cr); surface = NULL; cr = NULL; } - - g_free (rpath); - - if (!success) - break; } if (surface != NULL) @@ -200,47 +673,170 @@ tbo_export_file (TboWindow *tbo, } g_free (format_pages); - g_free (base_filename); - g_free (export_to); - return success && exported; } static gboolean -export_size_cb (GtkWidget *widget, struct export_spin_args *args) +export_single_frame (TboWindow *tbo, + const gchar *base_filename, + const gchar *export_to, + gint width, + gint height, + Frame *frame) { - if (!LOCK) + cairo_surface_t *surface = NULL; + cairo_t *cr = NULL; + gchar *path; + gdouble draw_width; + gdouble draw_height; + gboolean success; + + path = g_strdup_printf ("%s.%s", base_filename, export_to); + if (!begin_export_surface (tbo, + export_to, + path, + width, + height, + FALSE, + &surface, + &cr, + &draw_width, + &draw_height)) { - LOCK = 1; - gint current_size = args->current_size; - gint current_size2 = args->current_size2; - gint new_size = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (widget)); - gint new_value; - if (new_size) - { - *(args->scale) = new_size / (gdouble) current_size; - new_value = (gint) (*(args->scale) * current_size2); - gtk_spin_button_set_value (GTK_SPIN_BUTTON (args->spin2), new_value); - } - LOCK = 0; + g_free (path); + return FALSE; } - return FALSE; + + draw_frame_export (cr, frame, (gint) draw_width, (gint) draw_height); + success = finish_export_surface (tbo, export_to, path, surface, cr); + + cairo_surface_destroy (surface); + cairo_destroy (cr); + g_free (path); + return success; } gboolean -filedialog_cb (GtkWidget *widget, gpointer data) +tbo_export_file_with_scope_range (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height, + TboExportScope scope, + gint from_page, + gint to_page) { - struct export_file_args *args = data; - const gchar *current_text = gtk_editable_get_text (GTK_EDITABLE (args->entry)); - gchar *filename = tbo_file_dialog_save_export (args->tbo, current_text); + gchar *base_filename = NULL; + gchar *export_to = NULL; + GList *pages = NULL; + gint n_pages = 0; + gboolean success = FALSE; - if (filename != NULL) + if (filename == NULL || *filename == '\0' || width <= 0 || height <= 0) + return FALSE; + + if (format_hint != NULL && *format_hint != '\0') { - gtk_editable_set_text (GTK_EDITABLE (args->entry), filename); - tbo_window_set_export_path (args->tbo, filename); - g_free (filename); + export_to = g_ascii_strdown (format_hint, -1); + base_filename = strip_matching_extension (filename, export_to); } - return FALSE; + else + { + gchar *dot = strrchr (filename, '.'); + + if (dot != NULL && dot[1] != '\0') + { + export_to = g_ascii_strdown (dot + 1, -1); + base_filename = g_strndup (filename, dot - filename); + } + else + { + base_filename = g_strdup (filename); + export_to = g_strdup ("png"); + } + } + + if (g_strcmp0 (export_to, "png") != 0 && + g_strcmp0 (export_to, "pdf") != 0 && + g_strcmp0 (export_to, "svg") != 0) + { + g_free (export_to); + export_to = g_strdup ("png"); + } + + switch (scope) + { + case TBO_EXPORT_SCOPE_CURRENT_PAGE: + pages = g_list_append (NULL, tbo_comic_get_current_page (tbo->comic)); + success = export_page_list (tbo, base_filename, export_to, width, height, pages, 1, TRUE); + g_list_free (pages); + break; + case TBO_EXPORT_SCOPE_SELECTION: + { + Frame *frame = get_export_selection_frame (tbo); + + if (frame == NULL) + { + show_export_error (tbo, _("Please select a frame to export.")); + success = FALSE; + } + else + { + success = export_single_frame (tbo, base_filename, export_to, width, height, frame); + } + break; + } + case TBO_EXPORT_SCOPE_ALL_PAGES: + default: + pages = build_export_page_range (tbo->comic, from_page, to_page, &n_pages); + success = export_page_list (tbo, + base_filename, + export_to, + width, + height, + pages, + n_pages, + TRUE); + g_list_free (pages); + break; + } + + g_free (base_filename); + g_free (export_to); + return success; +} + +gboolean +tbo_export_file_with_scope (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height, + TboExportScope scope) +{ + return tbo_export_file_with_scope_range (tbo, + filename, + format_hint, + width, + height, + scope, + 1, + tbo_comic_len (tbo->comic)); +} + +gboolean +tbo_export_file (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height) +{ + return tbo_export_file_with_scope (tbo, + filename, + format_hint, + width, + height, + TBO_EXPORT_SCOPE_ALL_PAGES); } gboolean @@ -248,15 +844,20 @@ tbo_export (TboWindow *tbo) { gint width = tbo_comic_get_width (tbo->comic); gint height = tbo_comic_get_height (tbo->comic); + gint selection_width = width; + gint selection_height = height; + gint page_count = tbo_comic_len (tbo->comic); gchar *filename = NULL; gint response; - gdouble scale = 1.0; gint export_to_index; - struct export_spin_args spin_args; - struct export_spin_args spin_args2; - struct export_file_args file_args; + gint from_page; + gint to_page; + TboExportScope scope; + ExportFileArgs file_args; + ExportDialogState dialog_state; GtkWidget *dialog; + GtkWidget *headerbar; GtkWidget *vbox; GtkWidget *hbox; GtkWidget *fileinput; @@ -264,30 +865,58 @@ tbo_export (TboWindow *tbo) GtkWidget *filebutton; GtkWidget *spinw; GtkWidget *spinh; - GtkWidget *dropdown; + GtkWidget *format_dropdown; + GtkWidget *scope_dropdown; + GtkWidget *range_row; + GtkWidget *range_from_label; + GtkWidget *range_from_spin; + GtkWidget *range_to_label; + GtkWidget *range_to_spin; + GtkWidget *preview_frame; + GtkWidget *preview_vbox; + GtkWidget *preview_label; + GtkWidget *preview_box; GtkWidget *actions; - GtkWidget *button; + GtkWidget *scope_label; gchar *basename = NULL; const char *export_formats[] = { - "guess by extension", + "Guess by Extension", ".png", ".pdf", ".svg", NULL, }; - + const char *export_scopes_with_selection[] = { + _("All Pages"), + _("Current Page"), + _("Selection"), + NULL, + }; + const char *export_scopes_without_selection[] = { + _("All Pages"), + _("Current Page"), + NULL, + }; TboDialogRunData data; const gchar *format_hint = NULL; + gboolean has_selection = has_export_selection (tbo); + + get_export_scope_default_size (tbo, TBO_EXPORT_SCOPE_SELECTION, &selection_width, &selection_height); dialog = gtk_window_new (); - gtk_window_set_title (GTK_WINDOW (dialog), _("Export as")); + gtk_window_set_title (GTK_WINDOW (dialog), _("Export")); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (tbo->window)); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); gtk_window_set_default_size (GTK_WINDOW (dialog), 420, -1); - filebutton = gtk_button_new_with_label (_("Choose file")); + headerbar = gtk_header_bar_new (); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); + gtk_window_set_titlebar (GTK_WINDOW (dialog), headerbar); + + filebutton = gtk_button_new_with_label (_("Choose File")); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class (vbox, "tbo-dialog-content"); gtk_widget_set_margin_start (vbox, 12); gtk_widget_set_margin_end (vbox, 12); gtk_widget_set_margin_top (vbox, 12); @@ -295,7 +924,7 @@ tbo_export (TboWindow *tbo) tbo_widget_add_child (dialog, vbox); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); - filelabel = gtk_label_new (_("Filename: ")); + filelabel = gtk_label_new (_("File Name: ")); fileinput = gtk_entry_new (); if (tbo->export_path != NULL) { @@ -312,24 +941,90 @@ tbo_export (TboWindow *tbo) tbo_widget_add_child (hbox, filebutton); tbo_widget_add_child (vbox, hbox); - spinw = add_spin_with_label (vbox, _("width: "), tbo_comic_get_width (tbo->comic)); - spinh = add_spin_with_label (vbox, _("height: "), tbo_comic_get_height (tbo->comic)); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + scope_label = gtk_label_new (_("Scope: ")); + gtk_widget_set_size_request (scope_label, 80, -1); + gtk_label_set_xalign (GTK_LABEL (scope_label), 0.0); + scope_dropdown = gtk_drop_down_new_from_strings (has_selection ? + export_scopes_with_selection : + export_scopes_without_selection); + gtk_widget_set_name (scope_dropdown, "export-scope"); + gtk_drop_down_set_selected (GTK_DROP_DOWN (scope_dropdown), 0); + tbo_widget_add_child (hbox, scope_label); + tbo_widget_add_child (hbox, scope_dropdown); + tbo_widget_add_child (vbox, hbox); + + spinw = add_spin_with_label (vbox, _("Width: "), width); + spinh = add_spin_with_label (vbox, _("Height: "), height); + gtk_widget_set_name (spinw, "export-width"); + gtk_widget_set_name (spinh, "export-height"); + + range_row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + range_from_label = gtk_label_new (_("From Page: ")); + gtk_widget_set_size_request (range_from_label, 80, -1); + gtk_label_set_xalign (GTK_LABEL (range_from_label), 0.0); + range_from_spin = gtk_spin_button_new (GTK_ADJUSTMENT (gtk_adjustment_new (1, 1, page_count, 1, 1, 0)), 1, 0); + gtk_widget_set_name (range_from_spin, "export-range-from"); + range_to_label = gtk_label_new (_("To Page: ")); + gtk_label_set_xalign (GTK_LABEL (range_to_label), 0.0); + range_to_spin = gtk_spin_button_new (GTK_ADJUSTMENT (gtk_adjustment_new (page_count, 1, page_count, 1, 1, 0)), 1, 0); + gtk_widget_set_name (range_to_spin, "export-range-to"); + tbo_widget_add_child (range_row, range_from_label); + tbo_widget_add_child (range_row, range_from_spin); + tbo_widget_add_child (range_row, range_to_label); + tbo_widget_add_child (range_row, range_to_spin); + tbo_widget_add_child (vbox, range_row); - spin_args.current_size = tbo_comic_get_width (tbo->comic); - spin_args.current_size2 = tbo_comic_get_height (tbo->comic); - spin_args.spin2 = spinh; - spin_args.scale = &scale; - g_signal_connect (spinw, "value-changed", G_CALLBACK (export_size_cb), &spin_args); + format_dropdown = gtk_drop_down_new_from_strings (export_formats); + gtk_widget_set_name (format_dropdown, "export-format"); + gtk_drop_down_set_selected (GTK_DROP_DOWN (format_dropdown), 0); + tbo_widget_add_child (vbox, format_dropdown); - spin_args2.current_size = tbo_comic_get_height (tbo->comic); - spin_args2.current_size2 = tbo_comic_get_width (tbo->comic); - spin_args2.spin2 = spinw; - spin_args2.scale = &scale; - g_signal_connect (spinh, "value-changed", G_CALLBACK (export_size_cb), &spin_args2); + preview_frame = gtk_frame_new (_("Preview")); + gtk_widget_add_css_class (preview_frame, "tbo-dialog-card"); + preview_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_set_margin_start (preview_vbox, 8); + gtk_widget_set_margin_end (preview_vbox, 8); + gtk_widget_set_margin_top (preview_vbox, 8); + gtk_widget_set_margin_bottom (preview_vbox, 8); + preview_label = gtk_label_new (NULL); + gtk_widget_set_name (preview_label, "export-preview-label"); + gtk_label_set_xalign (GTK_LABEL (preview_label), 0.0); + preview_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name (preview_box, "export-preview-box"); + gtk_widget_set_size_request (preview_box, 220, 160); + tbo_widget_add_child (preview_vbox, preview_label); + tbo_widget_add_child (preview_vbox, preview_box); + tbo_widget_add_child (preview_frame, preview_vbox); + tbo_widget_add_child (vbox, preview_frame); - dropdown = gtk_drop_down_new_from_strings (export_formats); - gtk_drop_down_set_selected (GTK_DROP_DOWN (dropdown), 0); - tbo_widget_add_child (vbox, dropdown); + dialog_state.tbo = tbo; + dialog_state.scope_dropdown = scope_dropdown; + dialog_state.format_dropdown = format_dropdown; + dialog_state.range_row = range_row; + dialog_state.range_from_spin = range_from_spin; + dialog_state.range_to_spin = range_to_spin; + dialog_state.preview_box = preview_box; + dialog_state.preview_label = preview_label; + dialog_state.page_width = width; + dialog_state.page_height = height; + dialog_state.selection_width = selection_width; + dialog_state.selection_height = selection_height; + dialog_state.has_selection = has_selection; + dialog_state.size_state.spinw = spinw; + dialog_state.size_state.spinh = spinh; + dialog_state.size_state.base_width = width; + dialog_state.size_state.base_height = height; + dialog_state.size_state.updating = FALSE; + + g_signal_connect (spinw, "value-changed", G_CALLBACK (dialog_width_changed_cb), &dialog_state); + g_signal_connect (spinh, "value-changed", G_CALLBACK (dialog_height_changed_cb), &dialog_state); + g_signal_connect (scope_dropdown, "notify::selected", G_CALLBACK (scope_selected_cb), &dialog_state); + g_signal_connect (format_dropdown, "notify::selected", G_CALLBACK (format_selected_cb), &dialog_state); + g_signal_connect (range_from_spin, "value-changed", G_CALLBACK (range_from_changed_cb), &dialog_state); + g_signal_connect (range_to_spin, "value-changed", G_CALLBACK (range_to_changed_cb), &dialog_state); + set_export_size_base (&dialog_state.size_state, width, height); + update_preview_and_range (&dialog_state); actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_halign (actions, GTK_ALIGN_END); @@ -346,7 +1041,6 @@ tbo_export (TboWindow *tbo) tbo_widget_add_child (actions, button); tbo_widget_add_child (vbox, actions); - tbo_widget_show_all (GTK_WIDGET (vbox)); file_args.tbo = tbo; @@ -363,6 +1057,8 @@ tbo_export (TboWindow *tbo) { width = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spinw)); height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spinh)); + scope = dropdown_scope_to_export_scope (gtk_drop_down_get_selected (GTK_DROP_DOWN (scope_dropdown)), has_selection); + get_dialog_range (&dialog_state, &from_page, &to_page); filename = g_strdup (gtk_editable_get_text (GTK_EDITABLE (fileinput))); if (filename == NULL || *filename == '\0') @@ -374,9 +1070,7 @@ tbo_export (TboWindow *tbo) } tbo_window_set_export_path (tbo, filename); - /* 0 guess, 1 png, 2 pdf, 3 svg */ - export_to_index = gtk_drop_down_get_selected (GTK_DROP_DOWN (dropdown)); - + export_to_index = gtk_drop_down_get_selected (GTK_DROP_DOWN (format_dropdown)); if (export_to_index == 1) format_hint = "png"; else if (export_to_index == 2) @@ -384,7 +1078,7 @@ tbo_export (TboWindow *tbo) else if (export_to_index == 3) format_hint = "svg"; - if (!tbo_export_file (tbo, filename, format_hint, width, height)) + if (!tbo_export_file_with_scope_range (tbo, filename, format_hint, width, height, scope, from_page, to_page)) { gtk_window_destroy (GTK_WINDOW (dialog)); tbo_dialog_run_data_clear (&data); @@ -394,9 +1088,7 @@ tbo_export (TboWindow *tbo) } g_free (filename); - gtk_window_destroy (GTK_WINDOW (dialog)); tbo_dialog_run_data_clear (&data); - return response == GTK_RESPONSE_ACCEPT; } diff --git a/src/export.h b/src/export.h index 2998325..6a12542 100644 --- a/src/export.h +++ b/src/export.h @@ -24,11 +24,32 @@ #include #include "tbo-window.h" +typedef enum +{ + TBO_EXPORT_SCOPE_ALL_PAGES, + TBO_EXPORT_SCOPE_CURRENT_PAGE, + TBO_EXPORT_SCOPE_SELECTION, +} TboExportScope; + gboolean tbo_export (TboWindow *tbo); gboolean tbo_export_file (TboWindow *tbo, const gchar *filename, const gchar *format_hint, gint width, gint height); +gboolean tbo_export_file_with_scope (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height, + TboExportScope scope); +gboolean tbo_export_file_with_scope_range (TboWindow *tbo, + const gchar *filename, + const gchar *format_hint, + gint width, + gint height, + TboExportScope scope, + gint from_page, + gint to_page); #endif diff --git a/src/frame.c b/src/frame.c index 6989596..39f7474 100644 --- a/src/frame.c +++ b/src/frame.c @@ -24,6 +24,7 @@ #include #include "frame.h" #include "tbo-types.h" +#include "tbo-list-utils.h" #include "tbo-object-base.h" #include "tbo-utils.h" @@ -262,13 +263,13 @@ tbo_frame_object_count (Frame *frame) gint tbo_frame_object_nth (Frame *frame, TboObjectBase *obj) { - return g_list_index (frame->objects, obj); + return tbo_current_list_index (frame->objects, obj); } gboolean tbo_frame_has_obj (Frame *frame, TboObjectBase *obj) { - return g_list_find (frame->objects, obj) != NULL; + return tbo_list_utils_contains (frame->objects, obj); } void @@ -425,10 +426,7 @@ tbo_frame_add_obj (Frame *frame, TboObjectBase *obj) void tbo_frame_insert_obj (Frame *frame, TboObjectBase *obj, int nth) { - if (nth < 0) - frame->objects = g_list_append (frame->objects, obj); - else - frame->objects = g_list_insert (frame->objects, obj, nth); + tbo_list_utils_insert (&frame->objects, obj, nth); } float @@ -440,8 +438,8 @@ tbo_frame_get_scale_factor (void) void tbo_frame_del_obj (Frame *frame, TboObjectBase *obj) { - frame->objects = g_list_remove (frame->objects, obj); - g_object_unref (obj); + if (tbo_list_utils_remove (&frame->objects, obj)) + g_object_unref (obj); } void @@ -450,7 +448,7 @@ tbo_frame_reorder_obj (Frame *frame, TboObjectBase *obj, int nth) if (!tbo_frame_has_obj (frame, obj)) return; - frame->objects = g_list_remove (frame->objects, obj); + tbo_list_utils_remove (&frame->objects, obj); tbo_frame_insert_obj (frame, obj, nth); } diff --git a/src/page.c b/src/page.c index 74c2e41..bf9b332 100644 --- a/src/page.c +++ b/src/page.c @@ -24,6 +24,7 @@ #include "comic.h" #include "page.h" #include "frame.h" +#include "tbo-list-utils.h" struct _Page { @@ -40,23 +41,6 @@ struct _PageClass G_DEFINE_TYPE (Page, tbo_page, G_TYPE_OBJECT); -static GList * -page_frame_link (Page *page, Frame *frame) -{ - return g_list_find (page->frames, frame); -} - -static void -page_set_current_frame_fallback (Page *page, GList *hint) -{ - if (hint != NULL) - page->current_frame = hint->data; - else if (page->frames != NULL) - page->current_frame = page->frames->data; - else - page->current_frame = NULL; -} - static void tbo_page_dispose (GObject *object) { @@ -102,6 +86,33 @@ tbo_page_free (Page *page) g_object_unref (page); } +Page * +tbo_page_clone (Page *page) +{ + Page *new_page; + GList *frames; + Frame *selected_clone = NULL; + + if (page == NULL) + return NULL; + + new_page = tbo_page_new (NULL); + for (frames = page->frames; frames != NULL; frames = frames->next) + { + Frame *frame = TBO_FRAME (frames->data); + Frame *cloned_frame = tbo_frame_clone (frame); + + tbo_page_add_frame (new_page, cloned_frame); + if (page->current_frame == frame) + selected_clone = cloned_frame; + } + + if (selected_clone != NULL) + tbo_page_set_current_frame (new_page, selected_clone); + + return new_page; +} + Frame * tbo_page_new_frame (Page *page, int x, int y, int w, int h) { @@ -122,13 +133,7 @@ tbo_page_add_frame (Page *page, Frame *frame) void tbo_page_insert_frame (Page *page, Frame *frame, int nth) { - if (nth < 0) - page->frames = g_list_append (page->frames, frame); - else - page->frames = g_list_insert (page->frames, frame, nth); - - if (page->current_frame == NULL) - page->current_frame = frame; + tbo_current_list_insert (&page->frames, (gpointer *) &page->current_frame, frame, nth); } void @@ -150,16 +155,17 @@ tbo_page_del_frame (Page *page, Frame *frame) if (frame == NULL) return; - link = page_frame_link (page, frame); + link = tbo_list_utils_link (page->frames, frame); if (link == NULL) return; next_link = link->next; prev_link = link->prev; - page->frames = g_list_remove (page->frames, frame); + if (!tbo_current_list_remove (&page->frames, (gpointer *) &page->current_frame, frame)) + return; - if (page->current_frame == frame) - page_set_current_frame_fallback (page, next_link != NULL ? next_link : prev_link); + if (page->current_frame == NULL && (next_link != NULL || prev_link != NULL)) + page->current_frame = (next_link != NULL ? next_link : prev_link)->data; tbo_frame_free (frame); } @@ -173,22 +179,27 @@ tbo_page_len (Page *page) int tbo_page_frame_index (Page *page) { - if (page->current_frame == NULL) - return 0; + return tbo_current_list_index (page->frames, page->current_frame); +} + +int +tbo_page_frame_position (Page *page) +{ + gint index = tbo_page_frame_index (page); - return g_list_index (page->frames, page->current_frame) + 1; + return index >= 0 ? index + 1 : 0; } int tbo_page_frame_nth (Page *page, Frame *frame) { - return g_list_index (page->frames, frame); + return tbo_current_list_index (page->frames, frame); } gboolean tbo_page_frame_first (Page *page) { - if (tbo_page_frame_index (page) == 1) + if (tbo_page_frame_index (page) == 0) return TRUE; return FALSE; } @@ -196,7 +207,7 @@ tbo_page_frame_first (Page *page) gboolean tbo_page_frame_last (Page *page) { - if (tbo_page_frame_index (page) == tbo_page_len (page)) + if (tbo_page_frame_index (page) == tbo_page_len (page) - 1) return TRUE; return FALSE; } @@ -204,35 +215,19 @@ tbo_page_frame_last (Page *page) Frame * tbo_page_next_frame (Page *page) { - GList *current_link; - if (page->current_frame == NULL) return NULL; - current_link = page_frame_link (page, page->current_frame); - if (current_link != NULL && current_link->next != NULL) - { - page->current_frame = current_link->next->data; - return tbo_page_get_current_frame (page); - } - return NULL; + return tbo_current_list_next (page->frames, (gpointer *) &page->current_frame); } Frame * tbo_page_prev_frame (Page *page) { - GList *current_link; - if (page->current_frame == NULL) return NULL; - current_link = page_frame_link (page, page->current_frame); - if (current_link != NULL && current_link->prev != NULL) - { - page->current_frame = current_link->prev->data; - return tbo_page_get_current_frame (page); - } - return NULL; + return tbo_current_list_prev (page->frames, (gpointer *) &page->current_frame); } Frame * @@ -250,20 +245,13 @@ tbo_page_set_current_frame (Page *page, Frame *frame) return; } - page->current_frame = page_frame_link (page, frame) != NULL ? frame : NULL; + tbo_current_list_set (page->frames, (gpointer *) &page->current_frame, frame); } Frame * tbo_page_first_frame (Page *page) { - if (page->frames != NULL) - { - page->current_frame = page->frames->data; - return page->current_frame; - } - - page->current_frame = NULL; - return NULL; + return tbo_current_list_first (page->frames, (gpointer *) &page->current_frame); } GList * diff --git a/src/page.h b/src/page.h index 8a71e07..12029ee 100644 --- a/src/page.h +++ b/src/page.h @@ -38,6 +38,7 @@ GType tbo_page_get_type (void); Page *tbo_page_new (Comic *comic); void tbo_page_free (Page *page); +Page *tbo_page_clone (Page *page); Frame *tbo_page_new_frame (Page *page, int x, int y, int w, int h); void tbo_page_add_frame (Page *page, Frame *frame); void tbo_page_insert_frame (Page *page, Frame *frame, int nth); @@ -45,6 +46,7 @@ void tbo_page_del_frame_by_index (Page *page, int nth); void tbo_page_del_frame (Page *page, Frame *frame); int tbo_page_len (Page *page); int tbo_page_frame_index (Page *page); +int tbo_page_frame_position (Page *page); int tbo_page_frame_nth (Page *page, Frame *frame); gboolean tbo_page_frame_first (Page *page); gboolean tbo_page_frame_last (Page *page); diff --git a/src/tbo-drawing.c b/src/tbo-drawing.c index 45e294e..fa30138 100644 --- a/src/tbo-drawing.c +++ b/src/tbo-drawing.c @@ -400,22 +400,35 @@ tbo_drawing_draw (TboDrawing *self, cairo_t *cr) /* TODO this method should be in TboPage */ void -tbo_drawing_draw_page (TboDrawing *self, cairo_t *cr, Page *page, gint w, gint h) +tbo_drawing_draw_page (TboDrawing *self, cairo_t *cr, Page *page, gdouble w, gdouble h) { Frame *frame; GList *frame_list; + gdouble scale_x; + gdouble scale_y; // white background cairo_set_source_rgb(cr, 1, 1, 1); cairo_rectangle(cr, 0, 0, w, h); cairo_fill(cr); + if (self->comic == NULL || tbo_comic_get_width (self->comic) <= 0 || tbo_comic_get_height (self->comic) <= 0) + return; + + scale_x = w / tbo_comic_get_width (self->comic); + scale_y = h / tbo_comic_get_height (self->comic); + + cairo_save (cr); + cairo_scale (cr, scale_x, scale_y); + for (frame_list = tbo_page_get_frames (page); frame_list; frame_list = frame_list->next) { // draw each frame frame = (Frame *)frame_list->data; tbo_frame_draw (frame, cr); } + + cairo_restore (cr); } void diff --git a/src/tbo-drawing.h b/src/tbo-drawing.h index ff0cf11..6fbfa49 100644 --- a/src/tbo-drawing.h +++ b/src/tbo-drawing.h @@ -79,7 +79,7 @@ Comic * tbo_drawing_get_comic (TboDrawing *self); void tbo_drawing_set_current_frame (TboDrawing *self, Frame *frame); Frame * tbo_drawing_get_current_frame (TboDrawing *self); void tbo_drawing_draw (TboDrawing *self, cairo_t *cr); -void tbo_drawing_draw_page (TboDrawing *self, cairo_t *cr, Page *page, gint w, gint h); +void tbo_drawing_draw_page (TboDrawing *self, cairo_t *cr, Page *page, gdouble w, gdouble h); void tbo_drawing_zoom_in (TboDrawing *self); void tbo_drawing_zoom_out (TboDrawing *self); void tbo_drawing_zoom_100 (TboDrawing *self); diff --git a/src/tbo-file-dialog.c b/src/tbo-file-dialog.c index 76ddd2c..0344f65 100644 --- a/src/tbo-file-dialog.c +++ b/src/tbo-file-dialog.c @@ -139,7 +139,7 @@ tbo_file_dialog_open_project (TboWindow *window) gchar * tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) { - GtkFileDialog *dialog = create_dialog (_("Save as"), window, _("_Save")); + GtkFileDialog *dialog = create_dialog (_("Save As"), window, _("_Save")); GListStore *filters = create_project_filters (); gchar *path; @@ -157,7 +157,7 @@ tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) gchar * tbo_file_dialog_open_image (TboWindow *window) { - GtkFileDialog *dialog = create_dialog (_("Add an Image"), window, _("_Open")); + GtkFileDialog *dialog = create_dialog (_("Add Image"), window, _("_Open")); GListStore *filters = g_list_store_new (GTK_TYPE_FILE_FILTER); GtkFileFilter *filter = gtk_file_filter_new (); gchar *path; @@ -185,7 +185,7 @@ tbo_file_dialog_open_image (TboWindow *window) gchar * tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) { - GtkFileDialog *dialog = create_dialog (_("Export as"), window, _("_Save")); + GtkFileDialog *dialog = create_dialog (_("Export"), window, _("_Save")); gchar *path; set_initial_folder (dialog, tbo_window_get_export_dir (window)); diff --git a/src/tbo-list-utils.h b/src/tbo-list-utils.h new file mode 100644 index 0000000..1966bd9 --- /dev/null +++ b/src/tbo-list-utils.h @@ -0,0 +1,142 @@ +#ifndef __TBO_LIST_UTILS_H__ +#define __TBO_LIST_UTILS_H__ + +#include + +static inline GList * +tbo_list_utils_link (GList *list, gpointer item) +{ + return g_list_find (list, item); +} + +static inline gboolean +tbo_list_utils_contains (GList *list, gpointer item) +{ + return tbo_list_utils_link (list, item) != NULL; +} + +static inline void +tbo_list_utils_insert (GList **list, gpointer item, gint nth) +{ + if (nth < 0) + *list = g_list_append (*list, item); + else + *list = g_list_insert (*list, item, nth); +} + +static inline gboolean +tbo_list_utils_remove (GList **list, gpointer item) +{ + GList *link = tbo_list_utils_link (*list, item); + + if (link == NULL) + return FALSE; + + *list = g_list_delete_link (*list, link); + return TRUE; +} + +static inline void +tbo_current_list_insert (GList **list, gpointer *current, gpointer item, gint nth) +{ + tbo_list_utils_insert (list, item, nth); + if (current != NULL && *current == NULL) + *current = item; +} + +static inline gboolean +tbo_current_list_remove (GList **list, gpointer *current, gpointer item) +{ + GList *link = tbo_list_utils_link (*list, item); + GList *fallback; + + if (link == NULL) + return FALSE; + + fallback = link->next != NULL ? link->next : link->prev; + *list = g_list_delete_link (*list, link); + + if (current != NULL && *current == item) + *current = fallback != NULL ? fallback->data : NULL; + + return TRUE; +} + +static inline gint +tbo_current_list_index (GList *list, gpointer current) +{ + return current != NULL ? g_list_index (list, current) : -1; +} + +static inline void +tbo_current_list_set (GList *list, gpointer *current, gpointer item) +{ + if (current == NULL) + return; + + if (item == NULL) + { + *current = NULL; + return; + } + + *current = tbo_list_utils_contains (list, item) ? item : NULL; +} + +static inline void +tbo_current_list_set_nth (GList *list, gpointer *current, gint nth) +{ + GList *link = g_list_nth (list, nth); + + if (current != NULL) + *current = link != NULL ? link->data : NULL; +} + +static inline gpointer +tbo_current_list_next (GList *list, gpointer *current) +{ + GList *link; + + if (current == NULL || *current == NULL) + return NULL; + + link = tbo_list_utils_link (list, *current); + if (link != NULL && link->next != NULL) + { + *current = link->next->data; + return *current; + } + + return NULL; +} + +static inline gpointer +tbo_current_list_prev (GList *list, gpointer *current) +{ + GList *link; + + if (current == NULL || *current == NULL) + return NULL; + + link = tbo_list_utils_link (list, *current); + if (link != NULL && link->prev != NULL) + { + *current = link->prev->data; + return *current; + } + + return NULL; +} + +static inline gpointer +tbo_current_list_first (GList *list, gpointer *current) +{ + gpointer item = list != NULL ? list->data : NULL; + + if (current != NULL) + *current = item; + + return item; +} + +#endif diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index 7686413..4a57f2b 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -1331,276 +1331,3 @@ tbo_tool_selector_reset_state (TboToolSelector *self) self->resizing = FALSE; self->rotating = FALSE; } - - -static void -frame_move_do (TboAction *act) -{ - TboActionFrameMove *action = (TboActionFrameMove*)act; - - if (action->frame == NULL) - return; - - tbo_frame_set_position (action->frame, action->x2, action->y2); -} - -static void -frame_transform_do (TboAction *act) -{ - TboActionFrameTransform *action = (TboActionFrameTransform *) act; - - if (action->frame == NULL) - return; - - tbo_frame_set_bounds (action->frame, - action->x2, - action->y2, - action->width2, - action->height2); -} - -static void -frame_transform_undo (TboAction *act) -{ - TboActionFrameTransform *action = (TboActionFrameTransform *) act; - - if (action->frame == NULL) - return; - - tbo_frame_set_bounds (action->frame, - action->x1, - action->y1, - action->width1, - action->height1); -} - -static void -frame_transform_free (TboAction *act) -{ - TboActionFrameTransform *action = (TboActionFrameTransform *) act; - - if (action->frame != NULL) - { - g_object_remove_weak_pointer (G_OBJECT (action->frame), - (gpointer *) &action->frame); - } -} - -static void -frame_move_undo (TboAction *act) -{ - TboActionFrameMove *action = (TboActionFrameMove*)act; - - if (action->frame == NULL) - return; - - tbo_frame_set_position (action->frame, action->x1, action->y1); -} - -static void -frame_move_free (TboAction *act) -{ - TboActionFrameMove *action = (TboActionFrameMove*)act; - - if (action->frame != NULL) - { - g_object_remove_weak_pointer (G_OBJECT (action->frame), - (gpointer *) &action->frame); - } -} - -TboAction * -tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2) -{ - TboActionFrameMove *action = (TboActionFrameMove*)tbo_action_new (TboActionFrameMove); - action->frame = frame; - action->x1 = x1; - action->x2 = x2; - action->y1 = y1; - action->y2 = y2; - action->action_do = frame_move_do; - action->action_undo = frame_move_undo; - action->action_free = frame_move_free; - - if (action->frame != NULL) - { - g_object_add_weak_pointer (G_OBJECT (action->frame), - (gpointer *) &action->frame); - } - - return (TboAction*)action; -} - -TboAction * -tbo_action_frame_transform_new (Frame *frame, - int x1, - int y1, - int width1, - int height1, - int x2, - int y2, - int width2, - int height2) -{ - TboActionFrameTransform *action = (TboActionFrameTransform *) tbo_action_new (TboActionFrameTransform); - - action->frame = frame; - action->x1 = x1; - action->y1 = y1; - action->width1 = width1; - action->height1 = height1; - action->x2 = x2; - action->y2 = y2; - action->width2 = width2; - action->height2 = height2; - action->action_do = frame_transform_do; - action->action_undo = frame_transform_undo; - action->action_free = frame_transform_free; - - if (action->frame != NULL) - { - g_object_add_weak_pointer (G_OBJECT (action->frame), - (gpointer *) &action->frame); - } - - return (TboAction *) action; -} - -static void -obj_move_do (TboAction *act) -{ - TboActionObjMove *action = (TboActionObjMove*)act; - - if (action->obj == NULL) - return; - - action->obj->x = action->x2; - action->obj->y = action->y2; -} - -static void -obj_transform_do (TboAction *act) -{ - TboActionObjTransform *action = (TboActionObjTransform *) act; - - if (action->obj == NULL) - return; - - action->obj->x = action->x2; - action->obj->y = action->y2; - action->obj->width = action->width2; - action->obj->height = action->height2; - action->obj->angle = action->angle2; -} - -static void -obj_transform_undo (TboAction *act) -{ - TboActionObjTransform *action = (TboActionObjTransform *) act; - - if (action->obj == NULL) - return; - - action->obj->x = action->x1; - action->obj->y = action->y1; - action->obj->width = action->width1; - action->obj->height = action->height1; - action->obj->angle = action->angle1; -} - -static void -obj_transform_free (TboAction *act) -{ - TboActionObjTransform *action = (TboActionObjTransform *) act; - - if (action->obj != NULL) - { - g_object_remove_weak_pointer (G_OBJECT (action->obj), - (gpointer *) &action->obj); - } -} - -static void -obj_move_undo (TboAction *act) -{ - TboActionObjMove *action = (TboActionObjMove*)act; - - if (action->obj == NULL) - return; - - action->obj->x = action->x1; - action->obj->y = action->y1; -} - -static void -obj_move_free (TboAction *act) -{ - TboActionObjMove *action = (TboActionObjMove*)act; - - if (action->obj != NULL) - { - g_object_remove_weak_pointer (G_OBJECT (action->obj), - (gpointer *) &action->obj); - } -} - -TboAction * -tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2) -{ - TboActionObjMove *action = (TboActionObjMove*)tbo_action_new (TboActionObjMove); - action->obj = object; - action->x1 = x1; - action->x2 = x2; - action->y1 = y1; - action->y2 = y2; - action->action_do = obj_move_do; - action->action_undo = obj_move_undo; - action->action_free = obj_move_free; - - if (action->obj != NULL) - { - g_object_add_weak_pointer (G_OBJECT (action->obj), - (gpointer *) &action->obj); - } - - return (TboAction*)action; -} - -TboAction * -tbo_action_object_transform_new (TboObjectBase *object, - int x1, - int y1, - int width1, - int height1, - gdouble angle1, - int x2, - int y2, - int width2, - int height2, - gdouble angle2) -{ - TboActionObjTransform *action = (TboActionObjTransform *) tbo_action_new (TboActionObjTransform); - - action->obj = object; - action->x1 = x1; - action->y1 = y1; - action->width1 = width1; - action->height1 = height1; - action->angle1 = angle1; - action->x2 = x2; - action->y2 = y2; - action->width2 = width2; - action->height2 = height2; - action->angle2 = angle2; - action->action_do = obj_transform_do; - action->action_undo = obj_transform_undo; - action->action_free = obj_transform_free; - - if (action->obj != NULL) - { - g_object_add_weak_pointer (G_OBJECT (action->obj), - (gpointer *) &action->obj); - } - - return (TboAction *) action; -} diff --git a/src/tbo-tool-selector.h b/src/tbo-tool-selector.h index 5011351..694e818 100644 --- a/src/tbo-tool-selector.h +++ b/src/tbo-tool-selector.h @@ -93,89 +93,5 @@ gboolean tbo_tool_selector_delete_selected (TboToolSelector *self); GObject * tbo_tool_selector_new (void); GObject * tbo_tool_selector_new_with_params (TboWindow *tbo); -/* - * TboActionFrameMove for undo and redo frame movements - */ -typedef struct _TboActionFrameMove TboActionFrameMove; -typedef struct _TboActionFrameTransform TboActionFrameTransform; -typedef struct _TboActionObjMove TboActionObjMove; -typedef struct _TboActionObjTransform TboActionObjTransform; - -struct _TboActionFrameMove { - void (*action_do) (TboAction *action); - void (*action_undo) (TboAction *action); - void (*action_free) (TboAction *action); - Frame *frame; - int x1; - int y1; - int x2; - int y2; -}; -TboAction * tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2); - -struct _TboActionFrameTransform { - void (*action_do) (TboAction *action); - void (*action_undo) (TboAction *action); - void (*action_free) (TboAction *action); - Frame *frame; - int x1; - int y1; - int width1; - int height1; - int x2; - int y2; - int width2; - int height2; -}; -TboAction * tbo_action_frame_transform_new (Frame *frame, - int x1, - int y1, - int width1, - int height1, - int x2, - int y2, - int width2, - int height2); - -struct _TboActionObjMove { - void (*action_do) (TboAction *action); - void (*action_undo) (TboAction *action); - void (*action_free) (TboAction *action); - TboObjectBase *obj; - int x1; - int y1; - int x2; - int y2; -}; -TboAction * tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2); - -struct _TboActionObjTransform { - void (*action_do) (TboAction *action); - void (*action_undo) (TboAction *action); - void (*action_free) (TboAction *action); - TboObjectBase *obj; - int x1; - int y1; - int width1; - int height1; - gdouble angle1; - int x2; - int y2; - int width2; - int height2; - gdouble angle2; -}; -TboAction * tbo_action_object_transform_new (TboObjectBase *object, - int x1, - int y1, - int width1, - int height1, - gdouble angle1, - int x2, - int y2, - int width2, - int height2, - gdouble angle2); - #endif /* __TBO_TOOL_SELECTOR_H__ */ diff --git a/src/tbo-toolbar.c b/src/tbo-toolbar.c index a0e3651..68b8fbf 100644 --- a/src/tbo-toolbar.c +++ b/src/tbo-toolbar.c @@ -45,31 +45,40 @@ G_DEFINE_TYPE (TboToolbar, tbo_toolbar, G_TYPE_OBJECT); static void on_tool_button_toggled (GtkToggleButton *button, TboToolbar *toolbar); +static GtkWidget * +create_icon_wrapper (GtkWidget *child) +{ + GtkWidget *wrapper = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + + gtk_widget_set_size_request (wrapper, 20, 20); + gtk_widget_set_halign (wrapper, GTK_ALIGN_CENTER); + gtk_widget_set_valign (wrapper, GTK_ALIGN_CENTER); + gtk_widget_add_css_class (wrapper, "tbo-toolbar-icon"); + tbo_widget_add_child (wrapper, child); + return wrapper; +} + static GtkWidget * create_icon_from_name (const gchar *icon_name) { GtkWidget *image = gtk_image_new_from_icon_name (icon_name); + gtk_image_set_pixel_size (GTK_IMAGE (image), 20); - return image; + return create_icon_wrapper (image); } static GtkWidget * create_icon_from_file (const gchar *path) { - GtkWidget *wrapper; GtkWidget *image; - wrapper = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); - gtk_widget_set_size_request (wrapper, 20, 20); - image = gtk_picture_new_for_filename (path); gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); gtk_picture_set_content_fit (GTK_PICTURE (image), GTK_CONTENT_FIT_CONTAIN); gtk_widget_set_halign (image, GTK_ALIGN_CENTER); gtk_widget_set_valign (image, GTK_ALIGN_CENTER); - tbo_widget_add_child (wrapper, image); - return wrapper; + return create_icon_wrapper (image); } static GtkWidget * @@ -129,7 +138,7 @@ add_new_page (GtkWidget *widget, TboWindow *tbo) Page *page = tbo_comic_new_page (tbo->comic); gint index = tbo_comic_page_nth (tbo->comic, page); - tbo_window_add_page_widget (tbo, create_darea (tbo)); + tbo_window_add_page_widget (tbo, create_darea (tbo), page); tbo_comic_set_current_page (tbo->comic, page); tbo_undo_stack_insert (tbo->undo_stack, tbo_action_page_add_new (tbo->comic, page, index)); tbo_window_set_current_tab_page (tbo, TRUE); @@ -139,6 +148,14 @@ add_new_page (GtkWidget *widget, TboWindow *tbo) return FALSE; } +static gboolean +duplicate_current_page (GtkWidget *widget, TboWindow *tbo) +{ + (void) widget; + tbo_window_duplicate_current_page (tbo); + return FALSE; +} + static gboolean del_current_page (GtkWidget *widget, TboWindow *tbo) { @@ -249,15 +266,18 @@ generate_toolbar (TboToolbar *self) toolbar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); gtk_widget_set_name (toolbar, "tbo-toolbar"); + gtk_widget_set_hexpand (toolbar, TRUE); + gtk_widget_set_halign (toolbar, GTK_ALIGN_FILL); gtk_widget_set_margin_start (toolbar, 12); gtk_widget_set_margin_end (toolbar, 12); gtk_widget_set_margin_top (toolbar, 8); gtk_widget_set_margin_bottom (toolbar, 8); section = create_section_box (); - self->button_new = create_button (create_project_icon ("icons/new.svg"), _("New comic")); - self->button_open = create_button (create_icon_from_name ("document-open-symbolic"), _("Open comic")); - self->button_save = create_button (create_icon_from_name ("document-save-symbolic"), _("Save comic")); + self->button_new = create_button (create_project_icon ("icons/new.svg"), _("New Comic (Ctrl+N)")); + self->button_open = create_button (create_icon_from_name ("document-open-symbolic"), _("Open Comic (Ctrl+O)")); + self->button_save = create_button (create_icon_from_name ("document-save-symbolic"), _("Save Comic (Ctrl+S)")); + gtk_widget_add_css_class (self->button_save, "suggested-action"); g_signal_connect (self->button_new, "clicked", G_CALLBACK (tbo_comic_new_dialog), self->tbo); g_signal_connect (self->button_open, "clicked", G_CALLBACK (tbo_comic_open_dialog), self->tbo); g_signal_connect (self->button_save, "clicked", G_CALLBACK (tbo_comic_save_dialog), self->tbo); @@ -267,8 +287,8 @@ generate_toolbar (TboToolbar *self) tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); section = create_section_box (); - self->button_undo = create_button (create_project_icon ("icons/undo.svg"), _("Undo")); - self->button_redo = create_button (create_project_icon ("icons/redo.svg"), _("Redo")); + self->button_undo = create_button (create_project_icon ("icons/undo.svg"), _("Undo (Ctrl+Z)")); + self->button_redo = create_button (create_project_icon ("icons/redo.svg"), _("Redo (Ctrl+Y)")); g_signal_connect (self->button_undo, "clicked", G_CALLBACK (tbo_window_undo_cb), self->tbo); g_signal_connect (self->button_redo, "clicked", G_CALLBACK (tbo_window_redo_cb), self->tbo); tbo_box_pack_start (section, self->button_undo, FALSE, FALSE, 0); @@ -276,15 +296,18 @@ generate_toolbar (TboToolbar *self) tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); section = create_section_box (); - self->button_new_page = create_button (create_icon_from_name ("list-add-symbolic"), _("New page")); - self->button_delete_page = create_button (create_icon_from_name ("edit-delete-symbolic"), _("Delete page")); - self->button_prev_page = create_button (create_icon_from_name ("go-previous-symbolic"), _("Previous page")); - self->button_next_page = create_button (create_icon_from_name ("go-next-symbolic"), _("Next page")); + self->button_new_page = create_button (create_icon_from_name ("list-add-symbolic"), _("New Page")); + self->button_duplicate_page = create_button (create_icon_from_name ("edit-copy-symbolic"), _("Duplicate Page")); + self->button_delete_page = create_button (create_icon_from_name ("edit-delete-symbolic"), _("Delete Page")); + self->button_prev_page = create_button (create_icon_from_name ("go-previous-symbolic"), _("Previous Page")); + self->button_next_page = create_button (create_icon_from_name ("go-next-symbolic"), _("Next Page")); g_signal_connect (self->button_new_page, "clicked", G_CALLBACK (add_new_page), self->tbo); + g_signal_connect (self->button_duplicate_page, "clicked", G_CALLBACK (duplicate_current_page), self->tbo); g_signal_connect (self->button_delete_page, "clicked", G_CALLBACK (del_current_page), self->tbo); g_signal_connect (self->button_prev_page, "clicked", G_CALLBACK (prev_page), self->tbo); g_signal_connect (self->button_next_page, "clicked", G_CALLBACK (next_page), self->tbo); tbo_box_pack_start (section, self->button_new_page, FALSE, FALSE, 0); + tbo_box_pack_start (section, self->button_duplicate_page, FALSE, FALSE, 0); tbo_box_pack_start (section, self->button_delete_page, FALSE, FALSE, 0); tbo_box_pack_start (section, self->button_prev_page, FALSE, FALSE, 0); tbo_box_pack_start (section, self->button_next_page, FALSE, FALSE, 0); @@ -292,15 +315,15 @@ generate_toolbar (TboToolbar *self) section = create_section_box (); register_tool_button (self, TBO_TOOLBAR_SELECTOR, - create_toggle_button (create_project_icon ("icons/selector.svg"), _("Selector"))); + create_toggle_button (create_project_icon ("icons/selector.svg"), _("Selector (S)"))); register_tool_button (self, TBO_TOOLBAR_FRAME, - create_toggle_button (create_project_icon ("icons/frame.svg"), _("New frame"))); + create_toggle_button (create_project_icon ("icons/frame.svg"), _("New Frame (F)"))); register_tool_button (self, TBO_TOOLBAR_DOODLE, - create_toggle_button (create_project_icon ("icons/doodle.svg"), _("Doodle"))); + create_toggle_button (create_project_icon ("icons/doodle.svg"), _("Doodle (D)"))); register_tool_button (self, TBO_TOOLBAR_BUBBLE, - create_toggle_button (create_project_icon ("icons/bubble.svg"), _("Bubble"))); + create_toggle_button (create_project_icon ("icons/bubble.svg"), _("Bubble (B)"))); register_tool_button (self, TBO_TOOLBAR_TEXT, - create_toggle_button (create_project_icon ("icons/text.svg"), _("Text"))); + create_toggle_button (create_project_icon ("icons/text.svg"), _("Text (T)"))); tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_SELECTOR]), FALSE, FALSE, 0); tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_FRAME]), FALSE, FALSE, 0); tbo_box_pack_start (section, GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_DOODLE]), FALSE, FALSE, 0); @@ -309,16 +332,16 @@ generate_toolbar (TboToolbar *self) tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); section = create_section_box (); - self->button_pix = create_button (create_project_icon ("icons/pix.svg"), _("Image")); + self->button_pix = create_button (create_project_icon ("icons/pix.svg"), _("Insert Image")); g_signal_connect (self->button_pix, "clicked", G_CALLBACK (add_pix), self->tbo); tbo_box_pack_start (section, self->button_pix, FALSE, FALSE, 0); tbo_box_pack_start (toolbar, section, FALSE, FALSE, 0); section = create_section_box (); - self->button_zoom_100 = create_button (create_icon_from_name ("zoom-original-symbolic"), _("Zoom 1:1")); - self->button_zoom_out = create_button (create_icon_from_name ("zoom-out-symbolic"), _("Zoom out")); - self->button_zoom_in = create_button (create_icon_from_name ("zoom-in-symbolic"), _("Zoom in")); - self->button_zoom_fit = create_button (create_project_icon ("icons/zoom-fit.svg"), _("Zoom fit")); + self->button_zoom_100 = create_button (create_icon_from_name ("zoom-original-symbolic"), _("Zoom 1:1 (1)")); + self->button_zoom_out = create_button (create_icon_from_name ("zoom-out-symbolic"), _("Zoom Out (-)")); + self->button_zoom_in = create_button (create_icon_from_name ("zoom-in-symbolic"), _("Zoom In (+)")); + self->button_zoom_fit = create_button (create_project_icon ("icons/zoom-fit.svg"), _("Zoom Fit (2)")); g_signal_connect (self->button_zoom_100, "clicked", G_CALLBACK (zoom_100), self->tbo); g_signal_connect (self->button_zoom_out, "clicked", G_CALLBACK (zoom_out), self->tbo); g_signal_connect (self->button_zoom_in, "clicked", G_CALLBACK (zoom_in), self->tbo); @@ -348,6 +371,7 @@ tbo_toolbar_init (TboToolbar *self) self->button_undo = NULL; self->button_redo = NULL; self->button_new_page = NULL; + self->button_duplicate_page = NULL; self->button_delete_page = NULL; self->button_prev_page = NULL; self->button_next_page = NULL; @@ -499,6 +523,7 @@ tbo_toolbar_update (TboToolbar *self) gtk_widget_set_sensitive (self->button_prev_page, !tbo_comic_page_first (tbo->comic)); gtk_widget_set_sensitive (self->button_next_page, !tbo_comic_page_last (tbo->comic)); + gtk_widget_set_sensitive (self->button_duplicate_page, tbo_comic_len (tbo->comic) > 0); gtk_widget_set_sensitive (self->button_delete_page, tbo_comic_len (tbo->comic) > 1); gtk_widget_set_sensitive (GTK_WIDGET (self->tool_buttons[TBO_TOOLBAR_DOODLE]), in_frame_view); diff --git a/src/tbo-toolbar.h b/src/tbo-toolbar.h index c98a148..14214a0 100644 --- a/src/tbo-toolbar.h +++ b/src/tbo-toolbar.h @@ -61,6 +61,7 @@ struct _TboToolbar GtkWidget *button_undo; GtkWidget *button_redo; GtkWidget *button_new_page; + GtkWidget *button_duplicate_page; GtkWidget *button_delete_page; GtkWidget *button_prev_page; GtkWidget *button_next_page; diff --git a/src/tbo-undo.c b/src/tbo-undo.c index 8ef1e72..e3c0312 100644 --- a/src/tbo-undo.c +++ b/src/tbo-undo.c @@ -43,6 +43,15 @@ typedef struct gint index; } TboActionPageChange; +typedef struct +{ + TboAction base; + Comic *comic; + Page *page; + gint index1; + gint index2; +} TboActionPageOrder; + typedef struct { TboAction base; @@ -79,6 +88,30 @@ typedef struct gdouble r2, g2, b2; } TboActionFrameState; +typedef struct +{ + TboAction base; + Frame *frame; + gint x1; + gint y1; + gint x2; + gint y2; +} TboActionFrameMove; + +typedef struct +{ + TboAction base; + Frame *frame; + gint x1; + gint y1; + gint width1; + gint height1; + gint x2; + gint y2; + gint width2; + gint height2; +} TboActionFrameTransform; + typedef struct { TboAction base; @@ -98,6 +131,32 @@ typedef struct gint index2; } TboActionObjectOrder; +typedef struct +{ + TboAction base; + TboObjectBase *obj; + gint x1; + gint y1; + gint x2; + gint y2; +} TboActionObjMove; + +typedef struct +{ + TboAction base; + TboObjectBase *obj; + gint x1; + gint y1; + gint width1; + gint height1; + gdouble angle1; + gint x2; + gint y2; + gint width2; + gint height2; + gdouble angle2; +} TboActionObjTransform; + typedef struct { TboAction base; @@ -250,6 +309,41 @@ page_change_free (TboAction *action) g_object_unref (page_action->page); } +static void +page_order_do (TboAction *action) +{ + TboActionPageOrder *page_action = (TboActionPageOrder *) action; + + if (page_action->comic == NULL || page_action->page == NULL || !comic_has_page (page_action->comic, page_action->page)) + return; + + tbo_comic_reorder_page (page_action->comic, page_action->page, page_action->index2); + tbo_comic_set_current_page (page_action->comic, page_action->page); +} + +static void +page_order_undo (TboAction *action) +{ + TboActionPageOrder *page_action = (TboActionPageOrder *) action; + + if (page_action->comic == NULL || page_action->page == NULL || !comic_has_page (page_action->comic, page_action->page)) + return; + + tbo_comic_reorder_page (page_action->comic, page_action->page, page_action->index1); + tbo_comic_set_current_page (page_action->comic, page_action->page); +} + +static void +page_order_free (TboAction *action) +{ + TboActionPageOrder *page_action = (TboActionPageOrder *) action; + + if (page_action->comic != NULL) + g_object_remove_weak_pointer (G_OBJECT (page_action->comic), (gpointer *) &page_action->comic); + if (page_action->page != NULL) + g_object_unref (page_action->page); +} + static void frame_remove_do (TboAction *action) { @@ -337,6 +431,52 @@ apply_frame_state (TboActionFrameState *action, tbo_frame_set_color_rgb (action->frame, r, g, b); } +static void +apply_frame_position (Frame *frame, gint x, gint y) +{ + if (frame != NULL) + tbo_frame_set_position (frame, x, y); +} + +static void +apply_frame_transform (TboActionFrameTransform *action, + gint x, + gint y, + gint width, + gint height) +{ + if (action->frame != NULL) + tbo_frame_set_bounds (action->frame, x, y, width, height); +} + +static void +apply_object_position (TboObjectBase *obj, gint x, gint y) +{ + if (obj != NULL) + { + obj->x = x; + obj->y = y; + } +} + +static void +apply_object_transform (TboActionObjTransform *action, + gint x, + gint y, + gint width, + gint height, + gdouble angle) +{ + if (action->obj == NULL) + return; + + action->obj->x = x; + action->obj->y = y; + action->obj->width = width; + action->obj->height = height; + action->obj->angle = angle; +} + static void frame_state_do (TboAction *action) { @@ -349,6 +489,43 @@ frame_state_do (TboAction *action) frame_action->r2, frame_action->g2, frame_action->b2); } +static void +frame_move_do (TboAction *action) +{ + TboActionFrameMove *frame_action = (TboActionFrameMove *) action; + + apply_frame_position (frame_action->frame, frame_action->x2, frame_action->y2); +} + +static void +frame_move_undo (TboAction *action) +{ + TboActionFrameMove *frame_action = (TboActionFrameMove *) action; + + apply_frame_position (frame_action->frame, frame_action->x1, frame_action->y1); +} + +static void +frame_move_free (TboAction *action) +{ + TboActionFrameMove *frame_action = (TboActionFrameMove *) action; + + if (frame_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (frame_action->frame), (gpointer *) &frame_action->frame); +} + +static void +frame_transform_do (TboAction *action) +{ + TboActionFrameTransform *frame_action = (TboActionFrameTransform *) action; + + apply_frame_transform (frame_action, + frame_action->x2, + frame_action->y2, + frame_action->width2, + frame_action->height2); +} + static void frame_state_undo (TboAction *action) { @@ -361,6 +538,18 @@ frame_state_undo (TboAction *action) frame_action->r1, frame_action->g1, frame_action->b1); } +static void +frame_transform_undo (TboAction *action) +{ + TboActionFrameTransform *frame_action = (TboActionFrameTransform *) action; + + apply_frame_transform (frame_action, + frame_action->x1, + frame_action->y1, + frame_action->width1, + frame_action->height1); +} + static void frame_state_free (TboAction *action) { @@ -370,6 +559,15 @@ frame_state_free (TboAction *action) g_object_remove_weak_pointer (G_OBJECT (frame_action->frame), (gpointer *) &frame_action->frame); } +static void +frame_transform_free (TboAction *action) +{ + TboActionFrameTransform *frame_action = (TboActionFrameTransform *) action; + + if (frame_action->frame != NULL) + g_object_remove_weak_pointer (G_OBJECT (frame_action->frame), (gpointer *) &frame_action->frame); +} + static void object_flags_do (TboAction *action) { @@ -382,6 +580,44 @@ object_flags_do (TboAction *action) obj_action->obj->fliph = obj_action->fliph2; } +static void +object_move_do (TboAction *action) +{ + TboActionObjMove *obj_action = (TboActionObjMove *) action; + + apply_object_position (obj_action->obj, obj_action->x2, obj_action->y2); +} + +static void +object_move_undo (TboAction *action) +{ + TboActionObjMove *obj_action = (TboActionObjMove *) action; + + apply_object_position (obj_action->obj, obj_action->x1, obj_action->y1); +} + +static void +object_move_free (TboAction *action) +{ + TboActionObjMove *obj_action = (TboActionObjMove *) action; + + if (obj_action->obj != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->obj), (gpointer *) &obj_action->obj); +} + +static void +object_transform_do (TboAction *action) +{ + TboActionObjTransform *obj_action = (TboActionObjTransform *) action; + + apply_object_transform (obj_action, + obj_action->x2, + obj_action->y2, + obj_action->width2, + obj_action->height2, + obj_action->angle2); +} + static void object_flags_undo (TboAction *action) { @@ -394,6 +630,19 @@ object_flags_undo (TboAction *action) obj_action->obj->fliph = obj_action->fliph1; } +static void +object_transform_undo (TboAction *action) +{ + TboActionObjTransform *obj_action = (TboActionObjTransform *) action; + + apply_object_transform (obj_action, + obj_action->x1, + obj_action->y1, + obj_action->width1, + obj_action->height1, + obj_action->angle1); +} + static void object_flags_free (TboAction *action) { @@ -403,6 +652,15 @@ object_flags_free (TboAction *action) g_object_remove_weak_pointer (G_OBJECT (obj_action->obj), (gpointer *) &obj_action->obj); } +static void +object_transform_free (TboAction *action) +{ + TboActionObjTransform *obj_action = (TboActionObjTransform *) action; + + if (obj_action->obj != NULL) + g_object_remove_weak_pointer (G_OBJECT (obj_action->obj), (gpointer *) &obj_action->obj); +} + static void object_order_do (TboAction *action) { @@ -690,6 +948,25 @@ tbo_action_page_remove_new (Comic *comic, Page *page, int index) return (TboAction *) action; } +TboAction * +tbo_action_page_reorder_new (Comic *comic, Page *page, int index1, int index2) +{ + TboActionPageOrder *action = (TboActionPageOrder *) tbo_action_new (TboActionPageOrder); + + action->comic = comic; + action->page = g_object_ref (page); + action->index1 = index1; + action->index2 = index2; + action->base.action_do = page_order_do; + action->base.action_undo = page_order_undo; + action->base.action_free = page_order_free; + + if (action->comic != NULL) + g_object_add_weak_pointer (G_OBJECT (action->comic), (gpointer *) &action->comic); + + return (TboAction *) action; +} + TboAction * tbo_action_frame_remove_new (Page *page, Frame *frame, int index) { @@ -750,6 +1027,58 @@ tbo_action_frame_state_new (Frame *frame, return (TboAction *) action; } +TboAction * +tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2) +{ + TboActionFrameMove *action = (TboActionFrameMove *) tbo_action_new (TboActionFrameMove); + + action->frame = frame; + action->x1 = x1; + action->y1 = y1; + action->x2 = x2; + action->y2 = y2; + action->base.action_do = frame_move_do; + action->base.action_undo = frame_move_undo; + action->base.action_free = frame_move_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + + return (TboAction *) action; +} + +TboAction * +tbo_action_frame_transform_new (Frame *frame, + int x1, + int y1, + int width1, + int height1, + int x2, + int y2, + int width2, + int height2) +{ + TboActionFrameTransform *action = (TboActionFrameTransform *) tbo_action_new (TboActionFrameTransform); + + action->frame = frame; + action->x1 = x1; + action->y1 = y1; + action->width1 = width1; + action->height1 = height1; + action->x2 = x2; + action->y2 = y2; + action->width2 = width2; + action->height2 = height2; + action->base.action_do = frame_transform_do; + action->base.action_undo = frame_transform_undo; + action->base.action_free = frame_transform_free; + + if (action->frame != NULL) + g_object_add_weak_pointer (G_OBJECT (action->frame), (gpointer *) &action->frame); + + return (TboAction *) action; +} + TboAction * tbo_action_object_flags_new (TboObjectBase *object, gboolean flipv1, @@ -798,6 +1127,62 @@ tbo_action_object_order_new (Frame *frame, return (TboAction *) action; } +TboAction * +tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2) +{ + TboActionObjMove *action = (TboActionObjMove *) tbo_action_new (TboActionObjMove); + + action->obj = object; + action->x1 = x1; + action->y1 = y1; + action->x2 = x2; + action->y2 = y2; + action->base.action_do = object_move_do; + action->base.action_undo = object_move_undo; + action->base.action_free = object_move_free; + + if (action->obj != NULL) + g_object_add_weak_pointer (G_OBJECT (action->obj), (gpointer *) &action->obj); + + return (TboAction *) action; +} + +TboAction * +tbo_action_object_transform_new (TboObjectBase *object, + int x1, + int y1, + int width1, + int height1, + gdouble angle1, + int x2, + int y2, + int width2, + int height2, + gdouble angle2) +{ + TboActionObjTransform *action = (TboActionObjTransform *) tbo_action_new (TboActionObjTransform); + + action->obj = object; + action->x1 = x1; + action->y1 = y1; + action->width1 = width1; + action->height1 = height1; + action->angle1 = angle1; + action->x2 = x2; + action->y2 = y2; + action->width2 = width2; + action->height2 = height2; + action->angle2 = angle2; + action->base.action_do = object_transform_do; + action->base.action_undo = object_transform_undo; + action->base.action_free = object_transform_free; + + if (action->obj != NULL) + g_object_add_weak_pointer (G_OBJECT (action->obj), (gpointer *) &action->obj); + + return (TboAction *) action; +} + TboAction * tbo_action_text_state_new (TboObjectText *object, const gchar *text1, diff --git a/src/tbo-undo.h b/src/tbo-undo.h index ee7b29f..d93843c 100644 --- a/src/tbo-undo.h +++ b/src/tbo-undo.h @@ -64,6 +64,7 @@ gboolean tbo_undo_active_redo (TboUndoStack *stack); TboAction * tbo_action_frame_add_new (Page *page, Frame *frame); TboAction * tbo_action_page_add_new (Comic *comic, Page *page, int index); TboAction * tbo_action_page_remove_new (Comic *comic, Page *page, int index); +TboAction * tbo_action_page_reorder_new (Comic *comic, Page *page, int index1, int index2); TboAction * tbo_action_frame_remove_new (Page *page, Frame *frame, int index); TboAction * tbo_action_object_add_new (Frame *frame, TboObjectBase *object); TboAction * tbo_action_object_remove_new (Frame *frame, TboObjectBase *object, int index); @@ -72,6 +73,16 @@ TboAction * tbo_action_frame_state_new (Frame *frame, gboolean border1, gdouble r1, gdouble g1, gdouble b1, int x2, int y2, int width2, int height2, gboolean border2, gdouble r2, gdouble g2, gdouble b2); +TboAction * tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2); +TboAction * tbo_action_frame_transform_new (Frame *frame, + int x1, + int y1, + int width1, + int height1, + int x2, + int y2, + int width2, + int height2); TboAction * tbo_action_object_flags_new (TboObjectBase *object, gboolean flipv1, gboolean fliph1, @@ -81,6 +92,18 @@ TboAction * tbo_action_object_order_new (Frame *frame, TboObjectBase *object, int index1, int index2); +TboAction * tbo_action_object_move_new (TboObjectBase *object, int x1, int y1, int x2, int y2); +TboAction * tbo_action_object_transform_new (TboObjectBase *object, + int x1, + int y1, + int width1, + int height1, + gdouble angle1, + int x2, + int y2, + int width2, + int height2, + gdouble angle2); TboAction * tbo_action_text_state_new (TboObjectText *object, const gchar *text1, const gchar *font1, diff --git a/src/tbo-utils.c b/src/tbo-utils.c index 9cc9f62..3225401 100644 --- a/src/tbo-utils.c +++ b/src/tbo-utils.c @@ -28,7 +28,7 @@ void -get_base_name (gchar *str, gchar *ret, int size) +get_base_name (const gchar *str, gchar *ret, int size) { gchar **paths; gchar **dirname; diff --git a/src/tbo-utils.h b/src/tbo-utils.h index 8dcd036..059adf0 100644 --- a/src/tbo-utils.h +++ b/src/tbo-utils.h @@ -24,7 +24,7 @@ typedef struct _TboObjectBase TboObjectBase; -void get_base_name (gchar *str, gchar *ret, int size); +void get_base_name (const gchar *str, gchar *ret, int size); gchar *tbo_get_data_path (const gchar *relative_path); gchar *tbo_get_locale_path (void); void tbo_init_i18n (void); diff --git a/src/tbo-widget.c b/src/tbo-widget.c index a5540b0..7f341ad 100644 --- a/src/tbo-widget.c +++ b/src/tbo-widget.c @@ -67,6 +67,8 @@ tbo_widget_add_child (GtkWidget *parent, GtkWidget *child) gtk_button_set_child (GTK_BUTTON (parent), child); else if (GTK_IS_EXPANDER (parent)) gtk_expander_set_child (GTK_EXPANDER (parent), child); + else if (GTK_IS_FRAME (parent)) + gtk_frame_set_child (GTK_FRAME (parent), child); } void @@ -80,6 +82,8 @@ tbo_widget_remove_child (GtkWidget *parent, GtkWidget *child) gtk_button_set_child (GTK_BUTTON (parent), NULL); else if (GTK_IS_EXPANDER (parent)) gtk_expander_set_child (GTK_EXPANDER (parent), NULL); + else if (GTK_IS_FRAME (parent)) + gtk_frame_set_child (GTK_FRAME (parent), NULL); else if (GTK_IS_WINDOW (parent)) gtk_window_set_child (GTK_WINDOW (parent), NULL); } diff --git a/src/tbo-window.c b/src/tbo-window.c index 770fba6..090c967 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -48,6 +48,580 @@ static gboolean on_key_cb (GtkEventControllerKey *controller, GdkModifierType state, TboWindow *tbo); +#define TBO_RECENT_PROJECT_LIMIT 5 + +static gchar *get_state_file_path (void); +static gchar *get_recovery_dir_path (void); +static gchar *get_recovery_meta_path (const gchar *autosave_file); +static GKeyFile *load_state_key_file (void); +static void save_state_key_file (GKeyFile *kf); +static gboolean confirm_close (TboWindow *tbo); +static void update_window_title (TboWindow *tbo); +static void apply_theme_preferences (void); +static TboThemeMode load_theme_mode_preference (void); +static void save_theme_mode_preference (TboThemeMode mode); +static void schedule_autosave (TboWindow *tbo); +static void delete_recovery_files_for_window (TboWindow *tbo); +static void set_window_path (gchar **slot, const gchar *path); + +#define TBO_PAGE_WIDGET_KEY "tbo-page" + +static gchar * +get_recovery_dir_path (void) +{ + gchar *dir = g_build_filename (g_get_user_config_dir (), "tbo", "recovery", NULL); + + g_mkdir_with_parents (dir, 0755); + return dir; +} + +static gchar * +get_recovery_meta_path (const gchar *autosave_file) +{ + return g_strdup_printf ("%s.ini", autosave_file); +} + +static GKeyFile * +load_state_key_file (void) +{ + GKeyFile *kf = g_key_file_new (); + gchar *state_file = get_state_file_path (); + + g_key_file_load_from_file (kf, state_file, G_KEY_FILE_NONE, NULL); + g_free (state_file); + return kf; +} + +static void +save_state_key_file (GKeyFile *kf) +{ + gchar *state_file = get_state_file_path (); + gchar *content; + gsize len; + + content = g_key_file_to_data (kf, &len, NULL); + g_file_set_contents (state_file, content, len, NULL); + g_free (content); + g_free (state_file); +} + +static const gchar * +theme_mode_to_string (TboThemeMode mode) +{ + switch (mode) + { + case TBO_THEME_MODE_DARK: + return "dark"; + case TBO_THEME_MODE_LIGHT: + return "light"; + case TBO_THEME_MODE_SYSTEM: + default: + return "system"; + } +} + +static TboThemeMode +theme_mode_from_string (const gchar *mode) +{ + if (g_strcmp0 (mode, "dark") == 0) + return TBO_THEME_MODE_DARK; + if (g_strcmp0 (mode, "light") == 0) + return TBO_THEME_MODE_LIGHT; + + return TBO_THEME_MODE_SYSTEM; +} + +static TboThemeMode +load_theme_mode_preference (void) +{ + GKeyFile *kf = load_state_key_file (); + TboThemeMode mode = TBO_THEME_MODE_SYSTEM; + + if (g_key_file_has_key (kf, "ui", "theme-mode", NULL)) + { + gchar *mode_name = g_key_file_get_string (kf, "ui", "theme-mode", NULL); + + mode = theme_mode_from_string (mode_name); + g_free (mode_name); + } + else if (g_key_file_has_key (kf, "ui", "light-theme", NULL)) + { + mode = g_key_file_get_boolean (kf, "ui", "light-theme", NULL) ? + TBO_THEME_MODE_LIGHT : + TBO_THEME_MODE_DARK; + } + + g_key_file_unref (kf); + return mode; +} + +static void +save_theme_mode_preference (TboThemeMode mode) +{ + GKeyFile *kf = load_state_key_file (); + + g_key_file_set_string (kf, "ui", "theme-mode", theme_mode_to_string (mode)); + g_key_file_remove_key (kf, "ui", "light-theme", NULL); + save_state_key_file (kf); + g_key_file_unref (kf); +} + +static void +update_window_title (TboWindow *tbo) +{ + const gchar *comic_title; + gchar *window_title; + + if (tbo == NULL || tbo->window == NULL || tbo->comic == NULL) + return; + + comic_title = tbo_comic_get_title (tbo->comic); + if (comic_title == NULL || *comic_title == '\0') + comic_title = _("Untitled"); + + if (tbo->dirty) + window_title = g_strdup_printf ("* %s", comic_title); + else + window_title = g_strdup (comic_title); + + gtk_window_set_title (GTK_WINDOW (tbo->window), window_title); + g_free (window_title); +} + +static void +apply_theme_preferences (void) +{ + static gboolean defaults_initialized = FALSE; + static gchar *system_theme_name = NULL; + static gboolean system_prefer_dark = FALSE; + static gboolean has_theme_name = FALSE; + static gboolean has_prefer_dark = FALSE; + GtkSettings *settings = gtk_settings_get_default (); + TboThemeMode mode; + + if (settings == NULL) + return; + + if (!defaults_initialized) + { + has_theme_name = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-theme-name") != NULL; + has_prefer_dark = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme") != NULL; + + if (has_theme_name) + g_object_get (settings, "gtk-theme-name", &system_theme_name, NULL); + if (has_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &system_prefer_dark, NULL); + + defaults_initialized = TRUE; + } + + mode = load_theme_mode_preference (); + + if (mode == TBO_THEME_MODE_SYSTEM) + { + if (has_theme_name && system_theme_name != NULL) + g_object_set (settings, "gtk-theme-name", system_theme_name, NULL); + if (has_prefer_dark) + g_object_set (settings, "gtk-application-prefer-dark-theme", system_prefer_dark, NULL); + return; + } + + if (has_theme_name) + g_object_set (settings, "gtk-theme-name", "Adwaita", NULL); + if (has_prefer_dark) + g_object_set (settings, + "gtk-application-prefer-dark-theme", + mode == TBO_THEME_MODE_DARK, + NULL); +} + +static void +remove_recent_project (const gchar *path) +{ + GKeyFile *kf; + gchar **recent_paths; + gsize recent_count = 0; + GPtrArray *filtered; + gsize i; + gchar *last_project; + + if (path == NULL || *path == '\0') + return; + + kf = load_state_key_file (); + recent_paths = g_key_file_get_string_list (kf, "recent", "files", &recent_count, NULL); + filtered = g_ptr_array_new_with_free_func (g_free); + + for (i = 0; i < recent_count; i++) + { + if (g_strcmp0 (recent_paths[i], path) != 0) + g_ptr_array_add (filtered, g_strdup (recent_paths[i])); + } + + if (filtered->len > 0) + { + gchar **values = (gchar **) filtered->pdata; + + g_key_file_set_string_list (kf, "recent", "files", (const gchar * const *) values, filtered->len); + } + else + { + g_key_file_remove_key (kf, "recent", "files", NULL); + } + + last_project = g_key_file_get_string (kf, "paths", "last_project", NULL); + if (g_strcmp0 (last_project, path) == 0) + g_key_file_remove_key (kf, "paths", "last_project", NULL); + + save_state_key_file (kf); + g_free (last_project); + g_strfreev (recent_paths); + g_ptr_array_free (filtered, TRUE); + g_key_file_unref (kf); +} + +void +tbo_window_add_recent_project (const gchar *path) +{ + GKeyFile *kf; + gchar **recent_paths; + gsize recent_count = 0; + GPtrArray *updated; + gsize i; + + if (path == NULL || *path == '\0') + return; + + kf = load_state_key_file (); + recent_paths = g_key_file_get_string_list (kf, "recent", "files", &recent_count, NULL); + updated = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (updated, g_strdup (path)); + + for (i = 0; i < recent_count && updated->len < TBO_RECENT_PROJECT_LIMIT; i++) + { + if (g_strcmp0 (recent_paths[i], path) != 0 && recent_paths[i][0] != '\0') + g_ptr_array_add (updated, g_strdup (recent_paths[i])); + } + + g_key_file_set_string (kf, "paths", "last_project", path); + g_key_file_set_string_list (kf, + "recent", + "files", + (const gchar * const *) updated->pdata, + updated->len); + save_state_key_file (kf); + + g_strfreev (recent_paths); + g_ptr_array_free (updated, TRUE); + g_key_file_unref (kf); +} + +gchar ** +tbo_window_get_recent_projects (gsize *n_projects) +{ + GKeyFile *kf = load_state_key_file (); + gchar **recent_paths; + gsize recent_count = 0; + + recent_paths = g_key_file_get_string_list (kf, "recent", "files", &recent_count, NULL); + g_key_file_unref (kf); + + if (n_projects != NULL) + *n_projects = recent_count; + + return recent_paths; +} + +gchar * +tbo_window_get_last_project (void) +{ + GKeyFile *kf = load_state_key_file (); + gchar *value = g_key_file_get_string (kf, "paths", "last_project", NULL); + + g_key_file_unref (kf); + return value; +} + +void +tbo_window_delete_recovery_file (const gchar *autosave_file) +{ + gchar *meta_file; + + if (autosave_file == NULL || *autosave_file == '\0') + return; + + g_remove (autosave_file); + meta_file = get_recovery_meta_path (autosave_file); + g_remove (meta_file); + g_free (meta_file); +} + +void +tbo_window_clear_persisted_state (void) +{ + gchar *state_file = get_state_file_path (); + gchar *recovery_dir = get_recovery_dir_path (); + GDir *dir = g_dir_open (recovery_dir, 0, NULL); + const gchar *name; + + g_remove (state_file); + if (dir != NULL) + { + while ((name = g_dir_read_name (dir)) != NULL) + { + gchar *path = g_build_filename (recovery_dir, name, NULL); + + g_remove (path); + g_free (path); + } + g_dir_close (dir); + } + g_rmdir (recovery_dir); + g_free (recovery_dir); + g_free (state_file); +} + +gchar ** +tbo_window_list_recovery_files (gsize *n_files) +{ + gchar *recovery_dir = get_recovery_dir_path (); + GDir *dir = g_dir_open (recovery_dir, 0, NULL); + GPtrArray *files = g_ptr_array_new_with_free_func (g_free); + const gchar *name; + + if (dir != NULL) + { + while ((name = g_dir_read_name (dir)) != NULL) + { + if (g_str_has_suffix (name, ".tbo")) + g_ptr_array_add (files, g_build_filename (recovery_dir, name, NULL)); + } + g_dir_close (dir); + } + + g_ptr_array_add (files, NULL); + g_free (recovery_dir); + + if (n_files != NULL) + *n_files = files->len > 0 ? files->len - 1 : 0; + + return (gchar **) g_ptr_array_free (files, FALSE); +} + +static gboolean +autosave_timeout_cb (gpointer user_data) +{ + TboWindow *tbo = user_data; + + tbo->autosave_timeout_id = 0; + tbo_window_run_autosave (tbo); + return G_SOURCE_REMOVE; +} + +static void +schedule_autosave (TboWindow *tbo) +{ + if (tbo == NULL || tbo->destroying || tbo->autosave_timeout_id != 0) + return; + + tbo->autosave_timeout_id = g_timeout_add_seconds (1, autosave_timeout_cb, tbo); +} + +static void +write_recovery_metadata (TboWindow *tbo) +{ + GKeyFile *kf; + gchar *meta_file; + + if (tbo == NULL || tbo->autosave_path == NULL) + return; + + kf = g_key_file_new (); + meta_file = get_recovery_meta_path (tbo->autosave_path); + g_key_file_set_string (kf, + "recovery", + "source_path", + tbo->path != NULL ? tbo->path : ""); + g_key_file_set_string (kf, + "recovery", + "title", + tbo_comic_get_title (tbo->comic)); + + { + gchar *content; + gsize len; + + content = g_key_file_to_data (kf, &len, NULL); + g_file_set_contents (meta_file, content, len, NULL); + g_free (content); + } + + g_free (meta_file); + g_key_file_unref (kf); +} + +static gchar * +load_recovery_source_path (const gchar *autosave_file) +{ + GKeyFile *kf = g_key_file_new (); + gchar *meta_file = get_recovery_meta_path (autosave_file); + gchar *source_path = NULL; + + if (g_key_file_load_from_file (kf, meta_file, G_KEY_FILE_NONE, NULL)) + { + source_path = g_key_file_get_string (kf, "recovery", "source_path", NULL); + if (source_path != NULL && source_path[0] == '\0') + g_clear_pointer (&source_path, g_free); + } + + g_free (meta_file); + g_key_file_unref (kf); + return source_path; +} + +static void +delete_recovery_files_for_window (TboWindow *tbo) +{ + if (tbo == NULL || tbo->autosave_path == NULL) + return; + + tbo_window_delete_recovery_file (tbo->autosave_path); +} + +static void +add_grid_template (Page *page, + gint comic_width, + gint comic_height, + gint columns, + gint rows, + gint margin_x, + gint margin_y, + gint gap_x, + gint gap_y) +{ + gint frame_width; + gint frame_height; + gint row; + gint column; + + frame_width = MAX (1, (comic_width - (2 * margin_x) - ((columns - 1) * gap_x)) / columns); + frame_height = MAX (1, (comic_height - (2 * margin_y) - ((rows - 1) * gap_y)) / rows); + + for (row = 0; row < rows; row++) + { + for (column = 0; column < columns; column++) + { + gint x = margin_x + (column * (frame_width + gap_x)); + gint y = margin_y + (row * (frame_height + gap_y)); + + tbo_page_new_frame (page, x, y, frame_width, frame_height); + } + } +} + +void +tbo_comic_template_get_default_size (TboComicTemplate template, gint *width, gint *height) +{ + gint default_width = 800; + gint default_height = 500; + + switch (template) + { + case TBO_COMIC_TEMPLATE_STRIP: + default_width = 1800; + default_height = 600; + break; + case TBO_COMIC_TEMPLATE_A4: + default_width = 1240; + default_height = 1754; + break; + case TBO_COMIC_TEMPLATE_STORYBOARD: + default_width = 1600; + default_height = 900; + break; + case TBO_COMIC_TEMPLATE_EMPTY: + case TBO_COMIC_TEMPLATE_N_TEMPLATES: + default: + break; + } + + if (width != NULL) + *width = default_width; + if (height != NULL) + *height = default_height; +} + +void +tbo_window_apply_comic_template (TboWindow *tbo, TboComicTemplate template) +{ + Comic *comic; + Page *page; + gint comic_width; + gint comic_height; + + if (tbo == NULL || tbo->comic == NULL) + return; + + comic = tbo->comic; + page = tbo_comic_get_current_page (comic); + if (page == NULL) + page = tbo_comic_new_page (comic); + + while (tbo_page_len (page) > 0) + tbo_page_del_frame_by_index (page, 0); + + comic_width = tbo_comic_get_width (comic); + comic_height = tbo_comic_get_height (comic); + + switch (template) + { + case TBO_COMIC_TEMPLATE_STRIP: + tbo_comic_set_paper (comic, TBO_COMIC_PAPER_NONE); + add_grid_template (page, + comic_width, + comic_height, + 3, + 1, + MAX (20, comic_width / 30), + MAX (20, comic_height / 12), + MAX (16, comic_width / 45), + 0); + break; + case TBO_COMIC_TEMPLATE_A4: + tbo_comic_set_paper (comic, TBO_COMIC_PAPER_A4); + add_grid_template (page, + comic_width, + comic_height, + 2, + 3, + MAX (24, comic_width / 16), + MAX (24, comic_height / 24), + MAX (16, comic_width / 45), + MAX (16, comic_height / 45)); + break; + case TBO_COMIC_TEMPLATE_STORYBOARD: + tbo_comic_set_paper (comic, TBO_COMIC_PAPER_NONE); + add_grid_template (page, + comic_width, + comic_height, + 2, + 2, + MAX (24, comic_width / 20), + MAX (24, comic_height / 12), + MAX (16, comic_width / 35), + MAX (16, comic_height / 20)); + break; + case TBO_COMIC_TEMPLATE_EMPTY: + case TBO_COMIC_TEMPLATE_N_TEMPLATES: + default: + tbo_comic_set_paper (comic, TBO_COMIC_PAPER_NONE); + break; + } + + gtk_widget_queue_draw (tbo->drawing); + tbo_window_refresh_status (tbo); +} + static void setup_darea_controllers (GtkWidget *darea, TboWindow *tbo) { @@ -78,25 +652,6 @@ detach_document_state (TboWindow *tbo) tbo_tooltip_reset (tbo); } -static void -apply_theme_preferences (void) -{ - GtkSettings *settings = gtk_settings_get_default (); - - if (settings == NULL) - return; - - if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-theme-name") != NULL) - { - g_object_set (settings, "gtk-theme-name", "Adwaita-dark", NULL); - } - - if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme") != NULL) - { - g_object_set (settings, "gtk-application-prefer-dark-theme", TRUE, NULL); - } -} - static void apply_window_icon (GtkWidget *window) { @@ -110,6 +665,31 @@ get_page_widget (TboWindow *tbo, gint nth) return gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), nth); } +static Page * +get_page_widget_page (GtkWidget *page_widget) +{ + return page_widget != NULL ? g_object_get_data (G_OBJECT (page_widget), TBO_PAGE_WIDGET_KEY) : NULL; +} + +static gint +find_page_widget_index (TboWindow *tbo, Page *page) +{ + gint i; + gint count; + + if (tbo == NULL || page == NULL) + return -1; + + count = gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); + for (i = 0; i < count; i++) + { + if (get_page_widget_page (get_page_widget (tbo, i)) == page) + return i; + } + + return -1; +} + static GtkWidget * create_page_tab_label (gint nth) { @@ -140,6 +720,7 @@ sync_page_widgets_with_comic (TboWindow *tbo) { gint widget_count; gint comic_count; + gint i; if (tbo == NULL) return; @@ -147,17 +728,43 @@ sync_page_widgets_with_comic (TboWindow *tbo) widget_count = tbo_window_get_page_count (tbo); comic_count = tbo_comic_len (tbo->comic); - while (widget_count < comic_count) - { - tbo_window_add_page_widget (tbo, create_darea (tbo)); - widget_count++; - } - while (widget_count > comic_count) { tbo_window_remove_page_widget (tbo, widget_count - 1); widget_count--; } + + for (i = 0; i < comic_count; i++) + { + Page *page = g_list_nth_data (tbo_comic_get_pages (tbo->comic), i); + GtkWidget *widget = i < widget_count ? get_page_widget (tbo, i) : NULL; + + if (widget == NULL) + { + tbo_window_insert_page_widget (tbo, create_darea (tbo), page, i); + widget_count++; + continue; + } + + if (get_page_widget_page (widget) != page) + { + gint current_index = find_page_widget_index (tbo, page); + + if (current_index >= 0) + { + tbo->syncing_page_reorder = TRUE; + gtk_notebook_reorder_child (GTK_NOTEBOOK (tbo->notebook), get_page_widget (tbo, current_index), i); + tbo->syncing_page_reorder = FALSE; + } + else + { + tbo_window_insert_page_widget (tbo, create_darea (tbo), page, i); + widget_count++; + } + } + } + + refresh_page_tab_labels (tbo); } static gboolean @@ -177,6 +784,36 @@ notebook_switch_page_cb (GtkNotebook *notebook, return FALSE; } +static void +notebook_page_reordered_cb (GtkNotebook *notebook, + GtkWidget *child, + guint page_num, + TboWindow *tbo) +{ + Page *page; + gint old_index; + + (void) notebook; + + if (tbo == NULL || tbo->destroying || tbo->syncing_page_reorder) + return; + + page = get_page_widget_page (child); + if (page == NULL) + return; + + old_index = tbo_comic_page_nth (tbo->comic, page); + if (old_index < 0 || old_index == (gint) page_num) + return; + + tbo_comic_reorder_page (tbo->comic, page, page_num); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_page_reorder_new (tbo->comic, page, old_index, page_num)); + tbo_window_mark_dirty (tbo); + refresh_page_tab_labels (tbo); + tbo_window_set_current_tab_page (tbo, FALSE); + tbo_window_refresh_status (tbo); +} + static void destroy_cb (GtkWidget *widget, TboWindow *tbo) { @@ -244,6 +881,12 @@ get_dirname_or_home (const gchar *path) return g_strdup (g_get_home_dir ()); } +gboolean +tbo_window_prepare_for_document_replace (TboWindow *tbo) +{ + return confirm_close (tbo); +} + static gboolean confirm_close (TboWindow *tbo) { @@ -268,7 +911,13 @@ confirm_close (TboWindow *tbo) if (response == 2) return tbo_comic_save_dialog (NULL, tbo); - return response == 1; + if (response == 1) + { + tbo_window_mark_clean (tbo); + return TRUE; + } + + return FALSE; } static void @@ -342,8 +991,10 @@ update_statusbar (TboWindow *tbo) status = g_string_new (NULL); + append_status_segment (status, current_frame != NULL ? _("Mode: Frame") : _("Mode: Page")); + segment = g_strdup_printf (_("Page %d of %d"), - tbo_comic_page_index (tbo->comic) + 1, + tbo_comic_page_position (tbo->comic), tbo_comic_len (tbo->comic)); append_status_segment (status, segment); g_free (segment); @@ -402,24 +1053,94 @@ load_app_css (void) return; css = + "headerbar {" + " background: shade(@theme_bg_color, 1.015);" + " border-bottom: 1px solid alpha(@theme_fg_color, 0.06);" + "}" "#tbo-toolbar {" - " background: shade(@theme_bg_color, 1.02);" + " background: shade(@theme_bg_color, 1.03);" " border-bottom: 1px solid alpha(@theme_fg_color, 0.08);" + " padding: 4px 0;" + "}" + ".tbo-toolbar-section {" + " margin-right: 8px;" + " padding: 2px;" + " border-radius: 12px;" + " background: alpha(@theme_fg_color, 0.035);" + " border: 1px solid alpha(@theme_fg_color, 0.05);" "}" ".tbo-toolbar-section button {" - " min-width: 36px;" - " min-height: 36px;" + " min-width: 38px;" + " min-height: 38px;" + " padding: 0;" + "}" + ".tbo-toolbar-section button:focus-visible {" + " box-shadow: inset 0 0 0 2px @accent_bg_color;" + "}" + ".tbo-toolbar-icon {" + " margin: 0 1px;" + "}" + "#tbo-pages > header {" + " background: shade(@theme_bg_color, 1.01);" + " border-bottom: 1px solid alpha(@theme_fg_color, 0.06);" + "}" + "#tbo-pages > header tabs tab {" + " margin: 4px 2px 0 2px;" + " padding: 7px 12px;" + " border-top-left-radius: 10px;" + " border-top-right-radius: 10px;" "}" "#tbo-sidebar {" - " background: shade(@theme_base_color, 0.98);" + " background: shade(@theme_base_color, 0.985);" + " border-left: 1px solid alpha(@theme_fg_color, 0.08);" "}" "#tbo-status {" - " padding: 8px 12px;" + " padding: 9px 12px;" " border-top: 1px solid alpha(@theme_fg_color, 0.08);" - " background: shade(@theme_bg_color, 1.01);" + " background: shade(@theme_bg_color, 1.015);" "}" "#tbo-toolarea {" - " padding: 12px;" + " padding: 14px;" + "}" + ".tbo-sidebar-search {" + " min-height: 38px;" + " margin-bottom: 6px;" + "}" + ".tbo-sidebar-group, .tbo-sidebar-subgroup {" + " border-radius: 10px;" + " background: alpha(@theme_fg_color, 0.03);" + " border: 1px solid alpha(@theme_fg_color, 0.045);" + " padding: 4px 8px;" + "}" + ".tbo-sidebar-subgroup {" + " background: transparent;" + " border-color: transparent;" + " padding-left: 0;" + " padding-right: 0;" + "}" + ".tbo-asset-grid {" + " margin-top: 6px;" + " margin-bottom: 6px;" + "}" + ".tbo-asset-button {" + " border-radius: 12px;" + " padding: 7px;" + " background: alpha(@theme_fg_color, 0.01);" + "}" + ".tbo-asset-button:hover {" + " background: alpha(@theme_fg_color, 0.06);" + "}" + ".tbo-asset-button:focus-visible {" + " box-shadow: inset 0 0 0 2px @accent_bg_color;" + " background: alpha(@accent_bg_color, 0.08);" + "}" + ".tbo-dialog-content {" + " padding-top: 6px;" + "}" + ".tbo-dialog-card {" + " border-radius: 12px;" + " background: alpha(@theme_fg_color, 0.03);" + " border: 1px solid alpha(@theme_fg_color, 0.05);" "}"; provider = gtk_css_provider_new (); @@ -561,6 +1282,7 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, tbo->drawing = tbo_scrolled_window_get_child (dw_scroll); tbo->status = status; tbo->vbox = vbox; + tbo->menu_button = NULL; tbo->comic = comic; tbo->toolarea = toolarea; tbo->notebook = notebook; @@ -569,9 +1291,25 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, tbo->path = NULL; tbo->browse_path = NULL; tbo->export_path = NULL; + { + gchar *recovery_dir = get_recovery_dir_path (); + gchar *uuid = g_uuid_string_random (); + + tbo->autosave_path = g_build_filename (recovery_dir, uuid, NULL); + { + gchar *with_suffix = g_strconcat (tbo->autosave_path, ".tbo", NULL); + g_free (tbo->autosave_path); + tbo->autosave_path = with_suffix; + } + g_free (uuid); + g_free (recovery_dir); + } + tbo->autosave_timeout_id = 0; + tbo->syncing_page_reorder = FALSE; tbo->key_binder = TRUE; tbo->dirty = FALSE; tbo->destroying = FALSE; + update_window_title (tbo); return tbo; } @@ -579,6 +1317,8 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, void tbo_window_free (TboWindow *tbo) { + if (tbo->autosave_timeout_id != 0) + g_source_remove (tbo->autosave_timeout_id); detach_document_state (tbo); if (tbo->toolbar) { @@ -589,6 +1329,7 @@ tbo_window_free (TboWindow *tbo) g_free (tbo->path); g_free (tbo->browse_path); g_free (tbo->export_path); + g_free (tbo->autosave_path); tbo_undo_stack_del (tbo->undo_stack); free (tbo); } @@ -644,12 +1385,21 @@ void tbo_window_mark_dirty (TboWindow *tbo) { tbo->dirty = TRUE; + update_window_title (tbo); + schedule_autosave (tbo); } void tbo_window_mark_clean (TboWindow *tbo) { tbo->dirty = FALSE; + update_window_title (tbo); + if (tbo->autosave_timeout_id != 0) + { + g_source_remove (tbo->autosave_timeout_id); + tbo->autosave_timeout_id = 0; + } + delete_recovery_files_for_window (tbo); } gboolean @@ -658,11 +1408,151 @@ tbo_window_has_unsaved_changes (TboWindow *tbo) return tbo->dirty; } +gboolean +tbo_window_run_autosave (TboWindow *tbo) +{ + if (tbo == NULL || tbo->comic == NULL || !tbo->dirty || tbo->autosave_path == NULL) + return FALSE; + + if (!tbo_comic_save_snapshot (tbo, tbo->autosave_path)) + return FALSE; + + write_recovery_metadata (tbo); + return TRUE; +} + +gboolean +tbo_window_recover_file (TboWindow *tbo, const gchar *autosave_file) +{ + gchar *source_path; + + if (tbo == NULL || autosave_file == NULL || *autosave_file == '\0') + return FALSE; + if (!g_file_test (autosave_file, G_FILE_TEST_EXISTS)) + return FALSE; + + source_path = load_recovery_source_path (autosave_file); + tbo_comic_open (tbo, (char *) autosave_file); + + set_window_path (&tbo->path, source_path); + if (source_path != NULL) + tbo_window_set_browse_path (tbo, source_path); + else + set_window_path (&tbo->browse_path, NULL); + tbo_window_mark_dirty (tbo); + tbo_window_delete_recovery_file (autosave_file); + g_free (source_path); + return TRUE; +} + +gboolean +tbo_window_open_recent_project (TboWindow *tbo, const gchar *path) +{ + if (tbo == NULL || path == NULL || *path == '\0') + return FALSE; + if (!g_file_test (path, G_FILE_TEST_EXISTS)) + { + remove_recent_project (path); + tbo_alert_show (GTK_WINDOW (tbo->window), _("Couldn't open recent project"), _("The file no longer exists.")); + return FALSE; + } + if (!tbo_window_prepare_for_document_replace (tbo)) + return FALSE; + + tbo_comic_open (tbo, (char *) path); + tbo_window_add_recent_project (path); + tbo_menu_refresh (tbo); + return TRUE; +} + +gboolean +tbo_window_reopen_last_project (TboWindow *tbo) +{ + gchar *last_project = tbo_window_get_last_project (); + gboolean opened = FALSE; + + if (last_project != NULL) + opened = tbo_window_open_recent_project (tbo, last_project); + + g_free (last_project); + return opened; +} + +TboThemeMode +tbo_window_get_theme_mode (void) +{ + return load_theme_mode_preference (); +} + void -tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page) +tbo_window_set_theme_mode (TboWindow *tbo, TboThemeMode mode) { - gint count = gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); - gtk_notebook_append_page (GTK_NOTEBOOK (tbo->notebook), page, create_page_tab_label (count)); + GtkApplication *app; + GList *windows; + + save_theme_mode_preference (mode); + apply_theme_preferences (); + + if (tbo == NULL) + return; + + app = gtk_window_get_application (GTK_WINDOW (tbo->window)); + if (app == NULL) + return; + + for (windows = gtk_application_get_windows (app); windows != NULL; windows = windows->next) + { + GAction *action = g_action_map_lookup_action (G_ACTION_MAP (windows->data), "theme-mode"); + + if (G_IS_SIMPLE_ACTION (action)) + g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string (theme_mode_to_string (mode))); + } +} + +void +tbo_window_insert_page_widget (TboWindow *tbo, GtkWidget *page, Page *comic_page, gint nth) +{ + gint index = nth < 0 ? gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)) : nth; + + g_object_set_data (G_OBJECT (page), TBO_PAGE_WIDGET_KEY, comic_page); + gtk_notebook_insert_page (GTK_NOTEBOOK (tbo->notebook), page, create_page_tab_label (index), index); + gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (tbo->notebook), page, TRUE); + refresh_page_tab_labels (tbo); +} + +void +tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page, Page *comic_page) +{ + tbo_window_insert_page_widget (tbo, page, comic_page, -1); +} + +gboolean +tbo_window_duplicate_current_page (TboWindow *tbo) +{ + Page *page; + Page *cloned_page; + gint index; + + if (tbo == NULL || tbo->comic == NULL) + return FALSE; + + page = tbo_comic_get_current_page (tbo->comic); + if (page == NULL) + return FALSE; + + cloned_page = tbo_page_clone (page); + if (cloned_page == NULL) + return FALSE; + + index = tbo_comic_page_nth (tbo->comic, page) + 1; + tbo_comic_insert_page (tbo->comic, cloned_page, index); + tbo_window_insert_page_widget (tbo, create_darea (tbo), cloned_page, index); + tbo_comic_set_current_page (tbo->comic, cloned_page); + tbo_undo_stack_insert (tbo->undo_stack, tbo_action_page_add_new (tbo->comic, cloned_page, index)); + tbo_window_set_current_tab_page (tbo, TRUE); + tbo_window_mark_dirty (tbo); + tbo_window_refresh_status (tbo); + return TRUE; } void @@ -753,23 +1643,19 @@ tbo_new_tbo (GtkApplication *app, int width, int height) headerbar = gtk_header_bar_new (); gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); gtk_window_set_titlebar (GTK_WINDOW (window), headerbar); - gtk_widget_add_css_class (window, "dark"); - container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); - gtk_widget_add_css_class (container, "dark"); tbo_widget_add_child (window, container); comic = tbo_comic_new (_("Untitled"), width, height); - gtk_window_set_title (GTK_WINDOW (window), tbo_comic_get_title (comic)); scrolled = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); darea = tbo_drawing_new_with_params (comic); tbo_scrolled_window_set_child (scrolled, darea); notebook = gtk_notebook_new (); + gtk_widget_set_name (notebook, "tbo-pages"); gtk_notebook_set_scrollable (GTK_NOTEBOOK (notebook), TRUE); gtk_widget_set_hexpand (notebook, TRUE); gtk_widget_set_vexpand (notebook, TRUE); - gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, create_page_tab_label (0)); hpaned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); gtk_widget_set_hexpand (hpaned, TRUE); @@ -802,6 +1688,7 @@ tbo_new_tbo (GtkApplication *app, int width, int height) // key press event g_signal_connect (tbo->notebook, "switch-page", G_CALLBACK (notebook_switch_page_cb), tbo); + g_signal_connect (tbo->notebook, "page-reordered", G_CALLBACK (notebook_page_reordered_cb), tbo); global_key = gtk_event_controller_key_new (); gtk_event_controller_set_propagation_phase (global_key, GTK_PHASE_CAPTURE); g_signal_connect (global_key, "key-pressed", G_CALLBACK (global_key_cb), tbo); @@ -819,12 +1706,22 @@ tbo_new_tbo (GtkApplication *app, int width, int height) tbo_widget_show_all (window); apply_window_icon (window); + tbo_window_add_page_widget (tbo, scrolled, tbo_comic_get_current_page (comic)); tbo_toolbar_set_selected_tool (toolbar, TBO_TOOLBAR_SELECTOR); tbo_window_refresh_status (tbo); return tbo; } +TboWindow * +tbo_new_tbo_with_template (GtkApplication *app, int width, int height, TboComicTemplate template) +{ + TboWindow *tbo = tbo_new_tbo (app, width, height); + + tbo_window_apply_comic_template (tbo, template); + return tbo; +} + void tbo_window_refresh_status (TboWindow *tbo) { @@ -948,7 +1845,9 @@ tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo) { tbo_window_mark_dirty (tbo); sync_page_widgets_with_comic (tbo); - if (old_page_count != tbo_window_get_page_count (tbo)) + if (old_page_count != tbo_window_get_page_count (tbo) || + gtk_notebook_get_current_page (GTK_NOTEBOOK (tbo->notebook)) != tbo_comic_page_index (tbo->comic) || + get_page_widget (tbo, tbo_comic_page_index (tbo->comic)) != tbo->dw_scroll) tbo_window_set_current_tab_page (tbo, TRUE); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); tbo_window_refresh_status (tbo); @@ -964,7 +1863,9 @@ tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo) { tbo_window_mark_dirty (tbo); sync_page_widgets_with_comic (tbo); - if (old_page_count != tbo_window_get_page_count (tbo)) + if (old_page_count != tbo_window_get_page_count (tbo) || + gtk_notebook_get_current_page (GTK_NOTEBOOK (tbo->notebook)) != tbo_comic_page_index (tbo->comic) || + get_page_widget (tbo, tbo_comic_page_index (tbo->comic)) != tbo->dw_scroll) tbo_window_set_current_tab_page (tbo, TRUE); tbo_drawing_update (TBO_DRAWING (tbo->drawing)); tbo_window_refresh_status (tbo); diff --git a/src/tbo-window.h b/src/tbo-window.h index f5a4879..2db7616 100644 --- a/src/tbo-window.h +++ b/src/tbo-window.h @@ -25,6 +25,22 @@ #include "tbo-types.h" #include "tbo-undo.h" +typedef enum +{ + TBO_COMIC_TEMPLATE_EMPTY, + TBO_COMIC_TEMPLATE_STRIP, + TBO_COMIC_TEMPLATE_A4, + TBO_COMIC_TEMPLATE_STORYBOARD, + TBO_COMIC_TEMPLATE_N_TEMPLATES +} TboComicTemplate; + +typedef enum +{ + TBO_THEME_MODE_SYSTEM, + TBO_THEME_MODE_DARK, + TBO_THEME_MODE_LIGHT, +} TboThemeMode; + struct _TboWindow { GtkWidget *window; @@ -35,12 +51,16 @@ struct _TboWindow GtkWidget *drawing; GtkWidget *status; GtkWidget *vbox; + GtkWidget *menu_button; TboToolbar *toolbar; TboUndoStack *undo_stack; Comic *comic; gchar *path; gchar *browse_path; gchar *export_path; + gchar *autosave_path; + guint autosave_timeout_id; + gboolean syncing_page_reorder; gboolean key_binder; gboolean dirty; gboolean destroying; @@ -51,6 +71,9 @@ void tbo_window_free (TboWindow *tbo); gboolean tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, TboWindow *tbo); gboolean tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo); TboWindow * tbo_new_tbo (GtkApplication *app, int width, int height); +TboWindow * tbo_new_tbo_with_template (GtkApplication *app, int width, int height, TboComicTemplate template); +void tbo_comic_template_get_default_size (TboComicTemplate template, gint *width, gint *height); +void tbo_window_apply_comic_template (TboWindow *tbo, TboComicTemplate template); void tbo_window_refresh_status (TboWindow *tbo); void tbo_empty_tool_area (TboWindow *tbo); void tbo_window_set_path (TboWindow *tbo, const gchar *path); @@ -58,14 +81,29 @@ void tbo_window_set_browse_path (TboWindow *tbo, const gchar *path); void tbo_window_set_export_path (TboWindow *tbo, const gchar *path); gchar *tbo_window_get_open_dir (TboWindow *tbo); gchar *tbo_window_get_export_dir (TboWindow *tbo); +gboolean tbo_window_prepare_for_document_replace (TboWindow *tbo); void tbo_window_mark_dirty (TboWindow *tbo); void tbo_window_mark_clean (TboWindow *tbo); gboolean tbo_window_has_unsaved_changes (TboWindow *tbo); -void tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page); +gboolean tbo_window_run_autosave (TboWindow *tbo); +gboolean tbo_window_recover_file (TboWindow *tbo, const gchar *autosave_file); +gchar **tbo_window_list_recovery_files (gsize *n_files); +void tbo_window_delete_recovery_file (const gchar *autosave_file); +void tbo_window_clear_persisted_state (void); +void tbo_window_add_recent_project (const gchar *path); +gchar **tbo_window_get_recent_projects (gsize *n_projects); +gchar *tbo_window_get_last_project (void); +gboolean tbo_window_open_recent_project (TboWindow *tbo, const gchar *path); +gboolean tbo_window_reopen_last_project (TboWindow *tbo); +TboThemeMode tbo_window_get_theme_mode (void); +void tbo_window_set_theme_mode (TboWindow *tbo, TboThemeMode mode); +void tbo_window_add_page_widget (TboWindow *tbo, GtkWidget *page, Page *comic_page); +void tbo_window_insert_page_widget (TboWindow *tbo, GtkWidget *page, Page *comic_page, gint nth); void tbo_window_remove_page_widget (TboWindow *tbo, gint nth); gint tbo_window_get_page_count (TboWindow *tbo); void tbo_window_set_current_tab_page (TboWindow *tbo, gboolean setit); GtkWidget *create_darea (TboWindow *tbo); +gboolean tbo_window_duplicate_current_page (TboWindow *tbo); void tbo_window_set_key_binder (TboWindow *tbo, gboolean keyb); void tbo_window_enter_frame (TboWindow *tbo, Frame *frame); void tbo_window_leave_frame (TboWindow *tbo); diff --git a/src/tbo.c b/src/tbo.c index f2c349e..5386dcb 100644 --- a/src/tbo.c +++ b/src/tbo.c @@ -23,7 +23,9 @@ #include "tbo-window.h" #include "comic.h" +#include "ui-menu.h" #include "tbo-utils.h" +#include "tbo-widget.h" static void present_window (TboWindow *tbo) @@ -45,8 +47,46 @@ present_window (TboWindow *tbo) static void activate_cb (GtkApplication *app, gpointer user_data) { - TboWindow *tbo = tbo_new_tbo (app, 800, 450); - present_window (tbo); + gsize n_recovery_files = 0; + gchar **recovery_files = tbo_window_list_recovery_files (&n_recovery_files); + + if (n_recovery_files > 0) + { + static const gchar *buttons[] = { + "_Discard", + "_Recover", + NULL, + }; + gint response = tbo_alert_choose (NULL, + _("Recover autosaved work?"), + _("TBO found autosaved documents from a previous session."), + buttons, + 0, + 1); + gsize i; + + if (response == 1) + { + for (i = 0; i < n_recovery_files; i++) + { + TboWindow *tbo = tbo_new_tbo (app, 800, 450); + + tbo_window_recover_file (tbo, recovery_files[i]); + present_window (tbo); + } + g_strfreev (recovery_files); + return; + } + + for (i = 0; i < n_recovery_files; i++) + tbo_window_delete_recovery_file (recovery_files[i]); + } + + g_strfreev (recovery_files); + { + TboWindow *tbo = tbo_new_tbo (app, 800, 450); + present_window (tbo); + } } static void @@ -64,6 +104,8 @@ open_cb (GtkApplication *app, GFile **files, gint n_files, const gchar *hint, gp { tbo_comic_open (tbo, path); tbo_window_set_path (tbo, path); + tbo_window_add_recent_project (path); + tbo_menu_refresh (tbo); g_free (path); } present_window (tbo); diff --git a/src/ui-menu.c b/src/ui-menu.c index 6a4d5fa..36f12ca 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -36,6 +36,7 @@ #include "tbo-object-group.h" #include "tbo-undo.h" #include "tbo-utils.h" +#include "tbo-widget.h" struct menu_accel { @@ -43,6 +44,12 @@ struct menu_accel const gchar * const *accels; }; +typedef struct +{ + const gchar *title; + const gchar *accelerator; +} ShortcutEntry; + static const gchar *ACCEL_NEW[] = {"n", NULL}; static const gchar *ACCEL_OPEN[] = {"o", NULL}; static const gchar *ACCEL_SAVE[] = {"s", NULL}; @@ -55,6 +62,7 @@ static const gchar *ACCEL_FLIP_V[] = {"v", NULL}; static const gchar *ACCEL_ORDER_UP[] = {"Page_Up", NULL}; static const gchar *ACCEL_ORDER_DOWN[] = {"Page_Down", NULL}; static const gchar *ACCEL_QUIT[] = {"q", NULL}; +static const gchar *ACCEL_SHORTCUTS[] = {"F1", NULL}; static const struct menu_accel MENU_ACCELS[] = { {"win.new", ACCEL_NEW}, @@ -69,6 +77,46 @@ static const struct menu_accel MENU_ACCELS[] = { {"win.order-up", ACCEL_ORDER_UP}, {"win.order-down", ACCEL_ORDER_DOWN}, {"win.quit", ACCEL_QUIT}, + {"win.shortcuts", ACCEL_SHORTCUTS}, +}; + +static const ShortcutEntry FILE_SHORTCUTS[] = { + {N_("New Comic"), "Ctrl+N"}, + {N_("Open Comic"), "Ctrl+O"}, + {N_("Save Comic"), "Ctrl+S"}, + {N_("Undo"), "Ctrl+Z"}, + {N_("Redo"), "Ctrl+Y"}, + {N_("Quit"), "Ctrl+Q"}, + {NULL, NULL}, +}; + +static const ShortcutEntry TOOL_SHORTCUTS[] = { + {N_("Selector"), "S"}, + {N_("New Frame"), "F"}, + {N_("Doodle"), "D"}, + {N_("Bubble"), "B"}, + {N_("Text"), "T"}, + {N_("Clone Selection"), "Ctrl+D"}, + {N_("Delete Selection"), "Delete"}, + {NULL, NULL}, +}; + +static const ShortcutEntry VIEW_SHORTCUTS[] = { + {N_("Zoom In"), "+"}, + {N_("Zoom Out"), "-"}, + {N_("Zoom 1:1"), "1"}, + {N_("Zoom Fit"), "2"}, + {N_("Enter Selected Frame"), "Enter"}, + {N_("Back to Page"), "Esc"}, + {NULL, NULL}, +}; + +static const ShortcutEntry ARRANGE_SHORTCUTS[] = { + {N_("Horizontal Flip"), "H"}, + {N_("Vertical Flip"), "V"}, + {N_("Move to Front"), "Page Up"}, + {N_("Move to Back"), "Page Down"}, + {NULL, NULL}, }; static GtkApplication * @@ -78,8 +126,129 @@ get_app (TboWindow *tbo) } static void -toggle_menu_cb (GtkButton *button, GtkPopover *popover) +shortcuts_window_destroy_cb (GtkWidget *widget, gpointer user_data) +{ + TboWindow *tbo = user_data; + + g_object_set_data (G_OBJECT (tbo->window), "tbo-shortcuts-window", NULL); + (void) widget; +} + +static gboolean +shortcuts_key_pressed_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer user_data) +{ + (void) controller; + (void) keycode; + (void) state; + + if (keyval == GDK_KEY_Escape) + { + gtk_window_close (GTK_WINDOW (user_data)); + return TRUE; + } + + return FALSE; +} + +static GtkWidget * +create_shortcuts_section (const gchar *title, const ShortcutEntry *entries) +{ + GtkWidget *frame = gtk_frame_new (title); + GtkWidget *grid = gtk_grid_new (); + gint row; + + gtk_widget_add_css_class (frame, "tbo-dialog-card"); + gtk_grid_set_row_spacing (GTK_GRID (grid), 6); + gtk_grid_set_column_spacing (GTK_GRID (grid), 24); + gtk_widget_set_margin_start (grid, 12); + gtk_widget_set_margin_end (grid, 12); + gtk_widget_set_margin_top (grid, 12); + gtk_widget_set_margin_bottom (grid, 12); + gtk_frame_set_child (GTK_FRAME (frame), grid); + + for (row = 0; entries[row].title != NULL; row++) + { + GtkWidget *label = gtk_label_new (_(entries[row].title)); + GtkWidget *accel = gtk_label_new (NULL); + gchar *markup = g_markup_printf_escaped ("%s", entries[row].accelerator); + + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_xalign (GTK_LABEL (accel), 1.0); + gtk_label_set_markup (GTK_LABEL (accel), markup); + gtk_grid_attach (GTK_GRID (grid), label, 0, row, 1, 1); + gtk_grid_attach (GTK_GRID (grid), accel, 1, row, 1, 1); + g_free (markup); + } + + return frame; +} + +static void +show_shortcuts (TboWindow *tbo) +{ + GtkWidget *window; + GtkWidget *headerbar; + GtkWidget *scrolled; + GtkWidget *content; + GtkEventController *key_controller; + + window = g_object_get_data (G_OBJECT (tbo->window), "tbo-shortcuts-window"); + if (window != NULL) + { + gtk_window_present (GTK_WINDOW (window)); + return; + } + + window = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (window), _("Keyboard Shortcuts")); + gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (tbo->window)); + gtk_window_set_modal (GTK_WINDOW (window), TRUE); + gtk_window_set_default_size (GTK_WINDOW (window), 520, 420); + + headerbar = gtk_header_bar_new (); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); + gtk_window_set_titlebar (GTK_WINDOW (window), headerbar); + + key_controller = gtk_event_controller_key_new (); + g_signal_connect (key_controller, "key-pressed", G_CALLBACK (shortcuts_key_pressed_cb), window); + gtk_widget_add_controller (window, key_controller); + + scrolled = gtk_scrolled_window_new (); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_window_set_child (GTK_WINDOW (window), scrolled); + + content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class (content, "tbo-dialog-content"); + gtk_widget_set_margin_start (content, 12); + gtk_widget_set_margin_end (content, 12); + gtk_widget_set_margin_top (content, 12); + gtk_widget_set_margin_bottom (content, 12); + tbo_scrolled_window_set_child (scrolled, content); + + tbo_widget_add_child (content, create_shortcuts_section (_("File"), FILE_SHORTCUTS)); + tbo_widget_add_child (content, create_shortcuts_section (_("Tools"), TOOL_SHORTCUTS)); + tbo_widget_add_child (content, create_shortcuts_section (_("View and Navigation"), VIEW_SHORTCUTS)); + tbo_widget_add_child (content, create_shortcuts_section (_("Arrange"), ARRANGE_SHORTCUTS)); + + g_object_set_data (G_OBJECT (tbo->window), "tbo-shortcuts-window", window); + g_signal_connect (window, "destroy", G_CALLBACK (shortcuts_window_destroy_cb), tbo); + tbo_widget_show_all (window); +} + +static void +toggle_menu_cb (GtkButton *button, gpointer user_data) { + GtkPopover *popover = GTK_POPOVER (g_object_get_data (G_OBJECT (button), "tbo-popover")); + + (void) user_data; + + if (popover == NULL) + return; + if (gtk_widget_get_visible (GTK_WIDGET (popover))) gtk_popover_popdown (popover); else @@ -87,9 +256,13 @@ toggle_menu_cb (GtkButton *button, GtkPopover *popover) } static void -menu_button_destroy_cb (GtkWidget *button, GtkWidget *popover) +menu_button_destroy_cb (GtkWidget *button, gpointer user_data) { - if (gtk_widget_get_parent (popover) == button) + GtkWidget *popover = g_object_get_data (G_OBJECT (button), "tbo-popover"); + + (void) user_data; + + if (popover != NULL && gtk_widget_get_parent (popover) == button) gtk_widget_unparent (popover); } @@ -232,12 +405,9 @@ flip_selection_h (TboWindow *tbo) TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); TboObjectBase *obj = selector->selected_object; Frame *frame = selector->selected_frame; - gint index1; - gint index2; if (obj != NULL && frame != NULL) { - index1 = tbo_frame_object_nth (frame, obj); tbo_object_base_fliph (obj); tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_flags_new (obj, @@ -369,7 +539,7 @@ show_about (TboWindow *tbo) NULL}; gtk_show_about_dialog (GTK_WINDOW (tbo->window), - "name", _("TBO comic editor"), + "name", _("TBO Comic Editor"), "version", VERSION, "logo-icon-name", "tbo", "authors", authors, @@ -381,6 +551,9 @@ show_about (TboWindow *tbo) static void action_new (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_new_dialog (NULL, user_data); } static void action_open (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_open_dialog (NULL, user_data); } +static void action_open_recent (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_window_open_recent_project (user_data, g_variant_get_string (parameter, NULL)); } +static void action_reopen_last (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_window_reopen_last_project (user_data); } +static void action_duplicate_page (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_window_duplicate_current_page (user_data); } static void action_save (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_save_dialog (NULL, user_data); } static void action_save_as (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_comic_saveas_dialog (NULL, user_data); } static void action_export (GSimpleAction *action, GVariant *parameter, gpointer user_data) { tbo_export (user_data); } @@ -393,6 +566,20 @@ static void action_flip_v (GSimpleAction *action, GVariant *parameter, gpointer static void action_order_up (GSimpleAction *action, GVariant *parameter, gpointer user_data) { order_selection_up (user_data); } static void action_order_down (GSimpleAction *action, GVariant *parameter, gpointer user_data) { order_selection_down (user_data); } static void action_tutorial (GSimpleAction *action, GVariant *parameter, gpointer user_data) { open_tutorial (user_data); } +static void action_shortcuts (GSimpleAction *action, GVariant *parameter, gpointer user_data) { show_shortcuts (user_data); } +static void action_theme_mode_change_state (GSimpleAction *action, GVariant *value, gpointer user_data) +{ + TboThemeMode mode = TBO_THEME_MODE_SYSTEM; + const gchar *mode_name = g_variant_get_string (value, NULL); + + if (g_strcmp0 (mode_name, "dark") == 0) + mode = TBO_THEME_MODE_DARK; + else if (g_strcmp0 (mode_name, "light") == 0) + mode = TBO_THEME_MODE_LIGHT; + + tbo_window_set_theme_mode (user_data, mode); + g_simple_action_set_state (action, value); +} static void action_about (GSimpleAction *action, GVariant *parameter, gpointer user_data) { show_about (user_data); } static void action_quit (GSimpleAction *action, GVariant *parameter, gpointer user_data) { gtk_window_close (GTK_WINDOW (((TboWindow *) user_data)->window)); } @@ -402,6 +589,9 @@ install_actions (TboWindow *tbo) static const GActionEntry entries[] = { {"new", action_new, NULL, NULL, NULL}, {"open", action_open, NULL, NULL, NULL}, + {"open-recent", action_open_recent, "s", NULL, NULL}, + {"reopen-last", action_reopen_last, NULL, NULL, NULL}, + {"duplicate-page", action_duplicate_page, NULL, NULL, NULL}, {"save", action_save, NULL, NULL, NULL}, {"save-as", action_save_as, NULL, NULL, NULL}, {"export", action_export, NULL, NULL, NULL}, @@ -414,6 +604,7 @@ install_actions (TboWindow *tbo) {"order-up", action_order_up, NULL, NULL, NULL}, {"order-down", action_order_down, NULL, NULL, NULL}, {"tutorial", action_tutorial, NULL, NULL, NULL}, + {"shortcuts", action_shortcuts, NULL, NULL, NULL}, {"about", action_about, NULL, NULL, NULL}, {"quit", action_quit, NULL, NULL, NULL}, }; @@ -422,36 +613,105 @@ install_actions (TboWindow *tbo) entries, G_N_ELEMENTS (entries), tbo); + + { + GSimpleAction *theme_mode = g_simple_action_new_stateful ("theme-mode", + G_VARIANT_TYPE_STRING, + g_variant_new_string ( + tbo_window_get_theme_mode () == TBO_THEME_MODE_DARK ? "dark" : + tbo_window_get_theme_mode () == TBO_THEME_MODE_LIGHT ? "light" : + "system")); + g_signal_connect (theme_mode, "change-state", G_CALLBACK (action_theme_mode_change_state), tbo); + g_action_map_add_action (G_ACTION_MAP (tbo->window), G_ACTION (theme_mode)); + } } static GMenuModel * -build_menu_model (void) +build_menu_model (TboWindow *window) { GMenu *root = g_menu_new (); GMenu *section; + gsize recent_count = 0; + gchar **recent_paths; + gchar *last_project; + gsize i; section = g_menu_new (); g_menu_append (section, _("New"), "win.new"); g_menu_append (section, _("Open"), "win.open"); + last_project = tbo_window_get_last_project (); + if (last_project != NULL) + g_menu_append (section, _("Reopen Last Project"), "win.reopen-last"); + g_menu_append (section, _("Duplicate Page"), "win.duplicate-page"); g_menu_append (section, _("Save"), "win.save"); g_menu_append (section, _("Save As"), "win.save-as"); g_menu_append (section, _("Export"), "win.export"); + + recent_paths = tbo_window_get_recent_projects (&recent_count); + if (recent_count > 0) + { + GMenu *recent = g_menu_new (); + + for (i = 0; i < recent_count; i++) + { + GMenuItem *item; + gchar *basename; + + basename = g_path_get_basename (recent_paths[i]); + item = g_menu_item_new (basename, NULL); + g_menu_item_set_action_and_target_value (item, + "win.open-recent", + g_variant_new_string (recent_paths[i])); + g_menu_append_item (recent, item); + g_object_unref (item); + g_free (basename); + } + + g_menu_append_submenu (section, _("Open Recent"), G_MENU_MODEL (recent)); + g_object_unref (recent); + } + g_menu_append_section (root, NULL, G_MENU_MODEL (section)); g_object_unref (section); + g_strfreev (recent_paths); + g_free (last_project); section = g_menu_new (); g_menu_append (section, _("Undo"), "win.undo"); g_menu_append (section, _("Redo"), "win.redo"); g_menu_append (section, _("Clone"), "win.clone"); g_menu_append (section, _("Delete"), "win.delete"); - g_menu_append (section, _("Horizontal flip"), "win.flip-h"); - g_menu_append (section, _("Vertical flip"), "win.flip-v"); - g_menu_append (section, _("Move to front"), "win.order-up"); - g_menu_append (section, _("Move to back"), "win.order-down"); + g_menu_append (section, _("Horizontal Flip"), "win.flip-h"); + g_menu_append (section, _("Vertical Flip"), "win.flip-v"); + g_menu_append (section, _("Move to Front"), "win.order-up"); + g_menu_append (section, _("Move to Back"), "win.order-down"); g_menu_append_section (root, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); + { + GMenu *theme_menu = g_menu_new (); + GMenuItem *item; + + item = g_menu_item_new (_("Follow System Theme"), NULL); + g_menu_item_set_action_and_target_value (item, "win.theme-mode", g_variant_new_string ("system")); + g_menu_append_item (theme_menu, item); + g_object_unref (item); + + item = g_menu_item_new (_("Dark Theme"), NULL); + g_menu_item_set_action_and_target_value (item, "win.theme-mode", g_variant_new_string ("dark")); + g_menu_append_item (theme_menu, item); + g_object_unref (item); + + item = g_menu_item_new (_("Light Theme"), NULL); + g_menu_item_set_action_and_target_value (item, "win.theme-mode", g_variant_new_string ("light")); + g_menu_append_item (theme_menu, item); + g_object_unref (item); + + g_menu_append_submenu (section, _("Theme"), G_MENU_MODEL (theme_menu)); + g_object_unref (theme_menu); + } + g_menu_append (section, _("Keyboard Shortcuts"), "win.shortcuts"); g_menu_append (section, _("Tutorial"), "win.tutorial"); g_menu_append (section, _("About"), "win.about"); g_menu_append (section, _("Quit"), "win.quit"); @@ -504,30 +764,47 @@ generate_menu (TboWindow *window) { GtkWidget *button; GtkWidget *icon; - GtkWidget *popover; - GMenuModel *model; install_actions (window); set_accels_enabled (window, TRUE); - model = build_menu_model (); button = gtk_button_new (); icon = gtk_image_new_from_icon_name ("open-menu-symbolic"); - gtk_image_set_pixel_size (GTK_IMAGE (icon), 12); + gtk_image_set_pixel_size (GTK_IMAGE (icon), 16); gtk_button_set_child (GTK_BUTTON (button), icon); - gtk_widget_set_focusable (button, FALSE); + gtk_widget_set_focusable (button, TRUE); gtk_widget_set_tooltip_text (button, _("Menu")); - popover = gtk_popover_menu_new_from_model (model); - gtk_widget_set_parent (popover, button); - gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM); - g_signal_connect (button, "clicked", G_CALLBACK (toggle_menu_cb), popover); - g_signal_connect (button, "destroy", G_CALLBACK (menu_button_destroy_cb), popover); - g_object_unref (model); + g_signal_connect (button, "clicked", G_CALLBACK (toggle_menu_cb), NULL); + g_signal_connect (button, "destroy", G_CALLBACK (menu_button_destroy_cb), NULL); + window->menu_button = button; + tbo_menu_refresh (window); update_menubar (window); return button; } +void +tbo_menu_refresh (TboWindow *tbo) +{ + GMenuModel *model; + GtkWidget *popover; + GtkWidget *old_popover; + + if (tbo == NULL || tbo->menu_button == NULL) + return; + + old_popover = g_object_get_data (G_OBJECT (tbo->menu_button), "tbo-popover"); + if (old_popover != NULL && gtk_widget_get_parent (old_popover) == tbo->menu_button) + gtk_widget_unparent (old_popover); + + model = build_menu_model (tbo); + popover = gtk_popover_menu_new_from_model (model); + gtk_widget_set_parent (popover, tbo->menu_button); + gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM); + g_object_set_data (G_OBJECT (tbo->menu_button), "tbo-popover", popover); + g_object_unref (model); +} + void tbo_menu_enable_accel_keys (TboWindow *tbo) { diff --git a/src/ui-menu.h b/src/ui-menu.h index 3d3d51f..f24b8d4 100644 --- a/src/ui-menu.h +++ b/src/ui-menu.h @@ -24,6 +24,7 @@ #include "tbo-window.h" GtkWidget *generate_menu (TboWindow *window); +void tbo_menu_refresh (TboWindow *tbo); void update_menubar (TboWindow *tbo); void tbo_menu_disable_accel_keys (TboWindow *tbo); void tbo_menu_enable_accel_keys (TboWindow *tbo); diff --git a/tests/a4_pdf_export_check.c b/tests/a4_pdf_export_check.c new file mode 100644 index 0000000..e82e012 --- /dev/null +++ b/tests/a4_pdf_export_check.c @@ -0,0 +1,82 @@ +#include +#include +#include + +#include "comic-load.h" +#include "comic.h" +#include "export.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *base; + gchar *pdffile; + gchar *tbofile; + gchar *pdfinfo_output = NULL; + Comic *loaded; + gchar *pdfinfo_program; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.a4pdfexport", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo_with_template (app, 1240, 1754, TBO_COMIC_TEMPLATE_A4); + if (tbo_comic_get_paper (tbo->comic) != TBO_COMIC_PAPER_A4) + return 3; + + base = make_tmp_base ("tbo-a4-export-XXXXXX"); + if (!tbo_export_file (tbo, base, "pdf", 1240, 1754)) + return 4; + + pdffile = g_strdup_printf ("%s.pdf", base); + pdfinfo_program = g_find_program_in_path ("pdfinfo"); + if (pdfinfo_program != NULL) + { + gchar *command = g_strdup_printf ("%s %s", pdfinfo_program, pdffile); + + if (!g_spawn_command_line_sync (command, &pdfinfo_output, NULL, NULL, NULL)) + return 5; + if (g_strstr_len (pdfinfo_output, -1, "Page size: 595.276 x 841.89 pts (A4)") == NULL) + return 6; + + g_free (command); + g_free (pdfinfo_output); + g_free (pdfinfo_program); + } + + tbofile = g_strdup_printf ("%s.tbo", base); + if (!tbo_comic_save (tbo, tbofile)) + return 7; + loaded = tbo_comic_load (tbofile); + if (loaded == NULL || tbo_comic_get_paper (loaded) != TBO_COMIC_PAPER_A4) + return 8; + + tbo_comic_free (loaded); + g_remove (pdffile); + g_remove (tbofile); + g_free (pdffile); + g_free (tbofile); + g_free (base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/assets_browser_search_check.c b/tests/assets_browser_search_check.c new file mode 100644 index 0000000..5e19351 --- /dev/null +++ b/tests/assets_browser_search_check.c @@ -0,0 +1,111 @@ +#include +#include + +#include "doodle-treeview.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static GtkWidget * +find_search_entry (GtkWidget *widget) +{ + GtkWidget *child; + + if (GTK_IS_SEARCH_ENTRY (widget)) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_search_entry (child); + + if (found != NULL) + return found; + } + + return NULL; +} + +static gboolean +widget_tree_contains_label (GtkWidget *widget, const gchar *text) +{ + GtkWidget *child; + + if (GTK_IS_LABEL (widget) && strstr (gtk_label_get_text (GTK_LABEL (widget)), text) != NULL) + return TRUE; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + if (widget_tree_contains_label (child, text)) + return TRUE; + } + + return FALSE; +} + +static gint +count_asset_buttons (GtkWidget *widget) +{ + GtkWidget *child; + gint count = 0; + + if (g_object_get_data (G_OBJECT (widget), "tbo-asset-full-path") != NULL) + count++; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + count += count_asset_buttons (child); + + return count; +} + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + GtkWidget *browser; + GtkWidget *search; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.assetssearch", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + browser = doodle_setup_tree (tbo, FALSE); + tbo_widget_add_child (tbo->toolarea, browser); + tbo_widget_show_all (browser); + + search = find_search_entry (browser); + if (search == NULL) + return 3; + if (g_strcmp0 (gtk_editable_get_text (GTK_EDITABLE (search)), "") != 0) + return 4; + if (!widget_tree_contains_label (browser, "Accesories (") && + !widget_tree_contains_label (browser, "Arcadia (") && + !widget_tree_contains_label (browser, "Doodle1 (")) + return 5; + + gtk_editable_set_text (GTK_EDITABLE (search), "face smile big"); + drain_events (); + if (!widget_tree_contains_label (browser, "Emotes (") || count_asset_buttons (browser) == 0) + return 6; + + gtk_editable_set_text (GTK_EDITABLE (search), "zzznomatch"); + drain_events (); + if (g_strcmp0 (gtk_editable_get_text (GTK_EDITABLE (search)), "zzznomatch") != 0) + return 7; + if (count_asset_buttons (browser) != 0) + return 8; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/autosave_recovery_check.c b/tests/autosave_recovery_check.c new file mode 100644 index 0000000..100a6ad --- /dev/null +++ b/tests/autosave_recovery_check.c @@ -0,0 +1,82 @@ +#include +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +static gchar * +make_tmp_project_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return g_strconcat (path, ".tbo", NULL); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboWindow *recovered; + Page *page; + gchar *project_path; + + gtk_init (); + tbo_window_clear_persisted_state (); + + app = gtk_application_new ("net.danigm.tbo.autosaverecovery", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page, 10, 10, 120, 90); + + project_path = make_tmp_project_path ("tbo-autosave-recovery-XXXXXX"); + if (!tbo_comic_save (tbo, project_path)) + return 3; + tbo_window_set_path (tbo, project_path); + + tbo_page_new_frame (page, 160, 10, 120, 90); + tbo_window_mark_dirty (tbo); + if (!tbo_window_run_autosave (tbo)) + return 4; + if (!g_file_test (tbo->autosave_path, G_FILE_TEST_EXISTS)) + return 5; + + recovered = tbo_new_tbo (app, 800, 450); + if (!tbo_window_recover_file (recovered, tbo->autosave_path)) + return 6; + if (g_file_test (tbo->autosave_path, G_FILE_TEST_EXISTS)) + return 7; + if (!tbo_window_has_unsaved_changes (recovered)) + return 8; + { + gchar *expected_title = g_strdup_printf ("* %s", tbo_comic_get_title (recovered->comic)); + + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (recovered->window)), expected_title) != 0) + return 11; + g_free (expected_title); + } + if (g_strcmp0 (recovered->path, project_path) != 0) + return 9; + if (tbo_page_len (tbo_comic_get_current_page (recovered->comic)) != 2) + return 10; + + g_remove (project_path); + g_free (project_path); + tbo_window_mark_clean (tbo); + tbo_window_mark_clean (recovered); + gtk_window_close (GTK_WINDOW (tbo->window)); + gtk_window_close (GTK_WINDOW (recovered->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/comic_template_check.c b/tests/comic_template_check.c new file mode 100644 index 0000000..9e11225 --- /dev/null +++ b/tests/comic_template_check.c @@ -0,0 +1,112 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +static gboolean +page_frames_are_valid (Page *page, gint width, gint height) +{ + GList *frames; + + for (frames = tbo_page_get_frames (page); frames != NULL; frames = frames->next) + { + Frame *frame = frames->data; + gint x; + gint y; + gint frame_width; + gint frame_height; + + tbo_frame_get_bounds (frame, &x, &y, &frame_width, &frame_height); + if (frame_width <= 0 || frame_height <= 0) + return FALSE; + if (x < 0 || y < 0) + return FALSE; + if (x + frame_width > width || y + frame_height > height) + return FALSE; + } + + return TRUE; +} + +static gint +expected_frame_count (TboComicTemplate template) +{ + switch (template) + { + case TBO_COMIC_TEMPLATE_STRIP: + return 3; + case TBO_COMIC_TEMPLATE_A4: + return 6; + case TBO_COMIC_TEMPLATE_STORYBOARD: + return 4; + case TBO_COMIC_TEMPLATE_EMPTY: + case TBO_COMIC_TEMPLATE_N_TEMPLATES: + default: + return 0; + } +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + gint width; + gint height; + guint template; + + gtk_init (); + + tbo_comic_template_get_default_size (TBO_COMIC_TEMPLATE_EMPTY, &width, &height); + if (width != 800 || height != 500) + return 2; + tbo_comic_template_get_default_size (TBO_COMIC_TEMPLATE_STRIP, &width, &height); + if (width != 1800 || height != 600) + return 3; + tbo_comic_template_get_default_size (TBO_COMIC_TEMPLATE_A4, &width, &height); + if (width != 1240 || height != 1754) + return 4; + tbo_comic_template_get_default_size (TBO_COMIC_TEMPLATE_STORYBOARD, &width, &height); + if (width != 1600 || height != 900) + return 5; + + app = gtk_application_new ("net.danigm.tbo.comictemplate", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 6; + + for (template = TBO_COMIC_TEMPLATE_EMPTY; template < TBO_COMIC_TEMPLATE_N_TEMPLATES; template++) + { + tbo_comic_template_get_default_size (template, &width, &height); + tbo = tbo_new_tbo_with_template (app, width, height, template); + page = tbo_comic_get_current_page (tbo->comic); + + if (tbo_comic_get_width (tbo->comic) != width || tbo_comic_get_height (tbo->comic) != height) + return 10 + template; + if (page == NULL) + return 20 + template; + if (tbo_page_len (page) != expected_frame_count (template)) + return 30 + template; + if ((template == TBO_COMIC_TEMPLATE_A4 && tbo_comic_get_paper (tbo->comic) != TBO_COMIC_PAPER_A4) || + (template != TBO_COMIC_TEMPLATE_A4 && tbo_comic_get_paper (tbo->comic) != TBO_COMIC_PAPER_NONE)) + return 35 + template; + if (!page_frames_are_valid (page, width, height)) + return 40 + template; + + tbo_window_apply_comic_template (tbo, TBO_COMIC_TEMPLATE_STRIP); + if (tbo_page_len (page) != 3) + return 50 + template; + tbo_window_apply_comic_template (tbo, TBO_COMIC_TEMPLATE_EMPTY); + if (tbo_page_len (page) != 0) + return 60 + template; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + } + + g_object_unref (app); + return 0; +} diff --git a/tests/dirty_title_check.c b/tests/dirty_title_check.c new file mode 100644 index 0000000..613b55e --- /dev/null +++ b/tests/dirty_title_check.c @@ -0,0 +1,72 @@ +#include +#include +#include + +#include "comic.h" +#include "tbo-window.h" + +static gchar * +make_tmp_project_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return g_strconcat (path, ".tbo", NULL); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboWindow *opened; + gchar *path; + gchar *expected_title; + gchar *basename; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.dirtytitle", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), tbo_comic_get_title (tbo->comic)) != 0) + return 3; + + tbo_window_mark_dirty (tbo); + expected_title = g_strdup_printf ("* %s", tbo_comic_get_title (tbo->comic)); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), expected_title) != 0) + return 4; + g_free (expected_title); + + path = make_tmp_project_path ("tbo-dirty-title-XXXXXX"); + if (!tbo_comic_save (tbo, path)) + return 5; + basename = g_path_get_basename (path); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), basename) != 0) + return 6; + if (tbo_window_has_unsaved_changes (tbo)) + return 7; + + opened = tbo_new_tbo (app, 300, 200); + tbo_comic_open (opened, path); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (opened->window)), basename) != 0) + return 8; + if (tbo_window_has_unsaved_changes (opened)) + return 9; + + g_free (basename); + g_remove (path); + g_free (path); + tbo_window_mark_clean (tbo); + tbo_window_mark_clean (opened); + gtk_window_close (GTK_WINDOW (tbo->window)); + gtk_window_close (GTK_WINDOW (opened->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/dnd_feedback_check.c b/tests/dnd_feedback_check.c new file mode 100644 index 0000000..73fae62 --- /dev/null +++ b/tests/dnd_feedback_check.c @@ -0,0 +1,72 @@ +#include +#include + +#include "comic.h" +#include "dnd.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-tooltip.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboDrawing *drawing; + Page *page; + Frame *frame; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.dndfeedback", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + drawing = TBO_DRAWING (tbo->drawing); + + if (tbo_dnd_insert_asset (tbo, "tbo/logo/tbo.svg", 0, 0) != NULL) + return 3; + if (drawing->tooltip == NULL || strcmp (drawing->tooltip->str, "Enter a frame before inserting an image.") != 0) + return 4; + if (drawing->tooltip_timeout_id == 0) + return 5; + + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 90); + tbo_window_enter_frame (tbo, frame); + tbo_tooltip_reset (tbo); + + if (tbo_dnd_insert_asset (tbo, + "tbo/logo/tbo.svg", + tbo_frame_get_width (frame) + 10, + tbo_frame_get_height (frame) + 10) != NULL) + return 6; + if (drawing->tooltip == NULL || strcmp (drawing->tooltip->str, "Drop the image inside the current frame.") != 0) + return 7; + + tbo_tooltip_reset (tbo); + tbo_window_leave_frame (tbo); + if (tbo_dnd_insert_asset_at_view_coords (tbo, "tbo/logo/tbo.svg", 200, 120) != NULL) + return 8; + if (drawing->tooltip == NULL || strcmp (drawing->tooltip->str, "Enter a frame before inserting an image.") != 0) + return 9; + + tbo_window_enter_frame (tbo, frame); + tbo_tooltip_reset (tbo); + if (tbo_dnd_insert_asset (tbo, + "tbo/logo/tbo.svg", + tbo_frame_get_width (frame) / 2, + tbo_frame_get_height (frame) / 2) == NULL) + return 10; + if (drawing->tooltip != NULL) + return 11; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_guided_dialog_check.c b/tests/export_guided_dialog_check.c new file mode 100644 index 0000000..5be181c --- /dev/null +++ b/tests/export_guided_dialog_check.c @@ -0,0 +1,183 @@ +#include +#include + +#include "comic.h" +#include "export.h" +#include "page.h" +#include "tbo-window.h" + +typedef struct +{ + GtkApplication *app; + TboWindow *tbo; + gint status; +} DialogCheckState; + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +static GtkWindow * +find_export_dialog (DialogCheckState *state) +{ + GListModel *toplevels = gtk_window_get_toplevels (); + guint i; + + for (i = 0; i < g_list_model_get_n_items (toplevels); i++) + { + GtkWindow *window = GTK_WINDOW (g_list_model_get_item (toplevels, i)); + + if (window != GTK_WINDOW (state->tbo->window) && + gtk_window_get_transient_for (window) == GTK_WINDOW (state->tbo->window) && + g_strcmp0 (gtk_window_get_title (window), "Export") == 0) + return window; + + g_object_unref (window); + } + + return NULL; +} + +static GtkWidget * +find_widget_by_name (GtkWidget *widget, const gchar *name) +{ + GtkWidget *child; + + if (g_strcmp0 (gtk_widget_get_name (widget), name) == 0) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_widget_by_name (child, name); + + if (found != NULL) + return found; + } + + return NULL; +} + +static GtkWidget * +find_label_with_prefix (GtkWidget *widget, const gchar *prefix) +{ + GtkWidget *child; + + if (GTK_IS_LABEL (widget) && g_str_has_prefix (gtk_label_get_text (GTK_LABEL (widget)), prefix)) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_label_with_prefix (child, prefix); + + if (found != NULL) + return found; + } + + return NULL; +} + +static gboolean +inspect_export_dialog_cb (gpointer data) +{ + DialogCheckState *state = data; + GtkWindow *dialog; + GtkWidget *scope_dropdown; + GtkWidget *range_from_spin; + GtkWidget *range_to_spin; + GtkWidget *preview_label; + GtkWidget *preview_box; + const gchar *label_text; + + dialog = find_export_dialog (state); + if (dialog == NULL) + return G_SOURCE_CONTINUE; + + scope_dropdown = find_widget_by_name (GTK_WIDGET (dialog), "export-scope"); + range_from_spin = find_widget_by_name (GTK_WIDGET (dialog), "export-range-from"); + range_to_spin = find_widget_by_name (GTK_WIDGET (dialog), "export-range-to"); + preview_label = find_label_with_prefix (GTK_WIDGET (dialog), "Preview:"); + preview_box = preview_label != NULL ? gtk_widget_get_next_sibling (preview_label) : NULL; + if (scope_dropdown == NULL) { state->status = 30; gtk_window_close (dialog); return G_SOURCE_REMOVE; } + if (range_from_spin == NULL) { state->status = 31; gtk_window_close (dialog); return G_SOURCE_REMOVE; } + if (range_to_spin == NULL) { state->status = 32; gtk_window_close (dialog); return G_SOURCE_REMOVE; } + if (preview_label == NULL) { state->status = 33; gtk_window_close (dialog); return G_SOURCE_REMOVE; } + if (preview_box == NULL) { state->status = 34; gtk_window_close (dialog); return G_SOURCE_REMOVE; } + + if (!gtk_widget_is_sensitive (range_from_spin) || + gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (range_from_spin)) != 1 || + gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (range_to_spin)) != 3) + { + state->status = 4; + gtk_window_close (dialog); + return G_SOURCE_REMOVE; + } + + label_text = gtk_label_get_text (GTK_LABEL (preview_label)); + if (strstr (label_text, "Page 1") == NULL || gtk_widget_get_first_child (preview_box) == NULL) + { + state->status = 5; + gtk_window_close (dialog); + return G_SOURCE_REMOVE; + } + + gtk_spin_button_set_value (GTK_SPIN_BUTTON (range_from_spin), 2); + drain_events (); + label_text = gtk_label_get_text (GTK_LABEL (preview_label)); + if (strstr (label_text, "Page 2") == NULL) + { + state->status = 6; + gtk_window_close (dialog); + return G_SOURCE_REMOVE; + } + + gtk_drop_down_set_selected (GTK_DROP_DOWN (scope_dropdown), 1); + drain_events (); + label_text = gtk_label_get_text (GTK_LABEL (preview_label)); + if (strstr (label_text, "Current Page 3") == NULL || gtk_widget_is_sensitive (range_from_spin)) + { + state->status = 7; + gtk_window_close (dialog); + return G_SOURCE_REMOVE; + } + + state->status = 0; + gtk_window_close (dialog); + return G_SOURCE_REMOVE; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + DialogCheckState state; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportguideddialog", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_new_page (tbo->comic); + tbo_comic_new_page (tbo->comic); + tbo_comic_set_current_page_nth (tbo->comic, 2); + + state.app = app; + state.tbo = tbo; + state.status = 99; + g_idle_add (inspect_export_dialog_cb, &state); + + if (tbo_export (tbo)) + return 8; + if (state.status != 0) + return state.status; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/export_page_range_check.c b/tests/export_page_range_check.c new file mode 100644 index 0000000..dd956f1 --- /dev/null +++ b/tests/export_page_range_check.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include + +#include "comic.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return path; +} + +static GdkPixbuf * +load_pixbuf (const gchar *filename) +{ + return gdk_pixbuf_new_from_file (filename, NULL); +} + +static gboolean +pixel_is_red (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[0] > 140 && pixel[0] > pixel[1] + 60 && pixel[0] > pixel[2] + 60; +} + +static gboolean +pixel_is_green (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[1] > 140 && pixel[1] > pixel[0] + 60 && pixel[1] > pixel[2] + 60; +} + +static gboolean +pixel_is_blue (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[2] > 140 && pixel[2] > pixel[0] + 60 && pixel[2] > pixel[1] + 60; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page1; + Page *page2; + Page *page3; + gchar *base; + gchar *png0; + gchar *png1; + gchar *png2; + GdkPixbuf *pixbuf; + GdkRGBA red = { 0.8, 0.2, 0.2, 1.0 }; + GdkRGBA green = { 0.2, 0.8, 0.2, 1.0 }; + GdkRGBA blue = { 0.2, 0.2, 0.8, 1.0 }; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportpagerange", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page1 = tbo_comic_get_current_page (tbo->comic); + tbo_frame_set_color_rgb (tbo_page_new_frame (page1, 10, 10, 120, 80), red.red, red.green, red.blue); + + page2 = tbo_comic_new_page (tbo->comic); + tbo_frame_set_color_rgb (tbo_page_new_frame (page2, 10, 10, 120, 80), green.red, green.green, green.blue); + + page3 = tbo_comic_new_page (tbo->comic); + tbo_frame_set_color_rgb (tbo_page_new_frame (page3, 10, 10, 120, 80), blue.red, blue.green, blue.blue); + + base = make_tmp_base ("tbo-export-range-XXXXXX"); + if (!tbo_export_file_with_scope_range (tbo, base, "png", 800, 450, TBO_EXPORT_SCOPE_ALL_PAGES, 2, 3)) + return 3; + + png0 = g_strdup_printf ("%s0.png", base); + png1 = g_strdup_printf ("%s1.png", base); + png2 = g_strdup_printf ("%s2.png", base); + if (!g_file_test (png0, G_FILE_TEST_EXISTS) || !g_file_test (png1, G_FILE_TEST_EXISTS) || g_file_test (png2, G_FILE_TEST_EXISTS)) + return 4; + + pixbuf = load_pixbuf (png0); + if (pixbuf == NULL || !pixel_is_green (pixbuf, 20, 20) || pixel_is_red (pixbuf, 20, 20)) + return 5; + g_object_unref (pixbuf); + + pixbuf = load_pixbuf (png1); + if (pixbuf == NULL || !pixel_is_blue (pixbuf, 20, 20) || pixel_is_green (pixbuf, 20, 20)) + return 6; + g_object_unref (pixbuf); + + g_remove (png0); + g_remove (png1); + g_free (png0); + g_free (png1); + g_free (png2); + g_free (base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_scope_check.c b/tests/export_scope_check.c new file mode 100644 index 0000000..50168c3 --- /dev/null +++ b/tests/export_scope_check.c @@ -0,0 +1,154 @@ +#include +#include +#include +#include + +#include "comic.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return path; +} + +static GdkPixbuf * +load_pixbuf (const gchar *filename) +{ + return gdk_pixbuf_new_from_file (filename, NULL); +} + +static gboolean +pixel_is_green (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel; + + pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[1] > 140 && pixel[1] > pixel[0] + 60 && pixel[1] > pixel[2] + 60; +} + +static gboolean +pixel_is_blue (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel; + + pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[2] > 140 && pixel[2] > pixel[0] + 60 && pixel[2] > pixel[1] + 60; +} + +static gboolean +pixel_is_white (GdkPixbuf *pixbuf, gint x, gint y) +{ + guchar *pixel; + + pixel = gdk_pixbuf_get_pixels (pixbuf) + + (y * gdk_pixbuf_get_rowstride (pixbuf)) + + (x * gdk_pixbuf_get_n_channels (pixbuf)); + return pixel[0] > 220 && pixel[1] > 220 && pixel[2] > 220; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page1; + Page *page2; + Frame *selection_frame; + Frame *page2_frame; + GdkRGBA red = { 0.8, 0.2, 0.2, 1.0 }; + GdkRGBA blue = { 0.2, 0.2, 0.8, 1.0 }; + GdkRGBA green = { 0.2, 0.8, 0.2, 1.0 }; + gchar *page_base; + gchar *selection_base; + gchar *page_png; + gchar *selection_png; + gchar *numbered_page_png; + GdkPixbuf *pixbuf; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportscope", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page1 = tbo_comic_get_current_page (tbo->comic); + + tbo_frame_set_color_rgb (tbo_page_new_frame (page1, 10, 10, 140, 100), red.red, red.green, red.blue); + selection_frame = tbo_page_new_frame (page1, 300, 50, 120, 80); + tbo_frame_set_color_rgb (selection_frame, blue.red, blue.green, blue.blue); + + page2 = tbo_comic_new_page (tbo->comic); + page2_frame = tbo_page_new_frame (page2, 20, 20, 160, 110); + tbo_frame_set_color_rgb (page2_frame, green.red, green.green, green.blue); + + tbo_comic_set_current_page (tbo->comic, page2); + page_base = make_tmp_base ("tbo-export-scope-page-XXXXXX"); + if (!tbo_export_file_with_scope (tbo, page_base, "png", 800, 450, TBO_EXPORT_SCOPE_CURRENT_PAGE)) + return 3; + page_png = g_strdup_printf ("%s.png", page_base); + numbered_page_png = g_strdup_printf ("%s0.png", page_base); + if (!g_file_test (page_png, G_FILE_TEST_EXISTS)) + return 4; + if (g_file_test (numbered_page_png, G_FILE_TEST_EXISTS)) + return 5; + + pixbuf = load_pixbuf (page_png); + if (pixbuf == NULL) + return 6; + if (!pixel_is_green (pixbuf, 40, 40) || !pixel_is_white (pixbuf, 340, 70)) + return 7; + g_object_unref (pixbuf); + + tbo_comic_set_current_page (tbo->comic, page1); + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, selection_frame); + tbo_tool_selector_set_selected_obj (selector, NULL); + + selection_base = make_tmp_base ("tbo-export-scope-selection-XXXXXX"); + if (!tbo_export_file_with_scope (tbo, selection_base, "png", 240, 160, TBO_EXPORT_SCOPE_SELECTION)) + return 8; + selection_png = g_strdup_printf ("%s.png", selection_base); + if (!g_file_test (selection_png, G_FILE_TEST_EXISTS)) + return 9; + + pixbuf = load_pixbuf (selection_png); + if (pixbuf == NULL) + return 10; + if (!pixel_is_blue (pixbuf, + gdk_pixbuf_get_width (pixbuf) / 2, + gdk_pixbuf_get_height (pixbuf) / 2)) + return 11; + g_object_unref (pixbuf); + + g_remove (page_png); + g_remove (selection_png); + g_free (page_png); + g_free (selection_png); + g_free (numbered_page_png); + g_free (page_base); + g_free (selection_base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/keyboard_accessibility_check.c b/tests/keyboard_accessibility_check.c new file mode 100644 index 0000000..e10fb09 --- /dev/null +++ b/tests/keyboard_accessibility_check.c @@ -0,0 +1,154 @@ +#include +#include + +#include "comic.h" +#include "doodle-treeview.h" +#include "frame.h" +#include "page.h" +#include "tbo-drawing.h" +#include "tbo-tooltip.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +static GtkWidget * +find_search_entry (GtkWidget *widget) +{ + GtkWidget *child; + + if (GTK_IS_SEARCH_ENTRY (widget)) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_search_entry (child); + + if (found != NULL) + return found; + } + + return NULL; +} + +static void +expand_all_expanders (GtkWidget *widget) +{ + GtkWidget *child; + + if (GTK_IS_EXPANDER (widget)) + gtk_expander_set_expanded (GTK_EXPANDER (widget), TRUE); + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + expand_all_expanders (child); +} + +static GtkWidget * +find_first_asset_button (GtkWidget *widget) +{ + GtkWidget *child; + + if (g_object_get_data (G_OBJECT (widget), "tbo-asset-full-path") != NULL) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_first_asset_button (child); + + if (found != NULL) + return found; + } + + return NULL; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboDrawing *drawing; + GtkWidget *browser; + GtkWidget *search; + GtkWidget *asset_button; + GtkWidget *popover; + Page *page; + Frame *frame; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.keyboardaccessibility", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + drawing = TBO_DRAWING (tbo->drawing); + + if (!gtk_widget_get_focusable (tbo->menu_button)) + return 3; + gtk_widget_grab_focus (tbo->menu_button); + drain_events (); + if (gtk_window_get_focus (GTK_WINDOW (tbo->window)) != tbo->menu_button) + return 4; + popover = g_object_get_data (G_OBJECT (tbo->menu_button), "tbo-popover"); + if (!gtk_widget_activate (tbo->menu_button)) + return 5; + drain_events (); + if (popover == NULL) + return 6; + + browser = doodle_setup_tree (tbo, TRUE); + tbo_widget_add_child (tbo->toolarea, browser); + tbo_widget_show_all (browser); + search = find_search_entry (browser); + if (search == NULL) + return 7; + expand_all_expanders (browser); + drain_events (); + + asset_button = find_first_asset_button (browser); + if (asset_button == NULL) + return 8; + if (!gtk_widget_get_focusable (asset_button)) + return 9; + if (gtk_widget_get_tooltip_text (asset_button) == NULL || *gtk_widget_get_tooltip_text (asset_button) == '\0') + return 10; + + gtk_widget_grab_focus (asset_button); + drain_events (); + if (gtk_window_get_focus (GTK_WINDOW (tbo->window)) != asset_button) + return 11; + + g_signal_emit_by_name (asset_button, "clicked"); + drain_events (); + if (drawing->tooltip == NULL || strcmp (drawing->tooltip->str, "Enter a frame before inserting an image.") != 0) + return 13; + + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 90); + tbo_window_enter_frame (tbo, frame); + tbo_tooltip_reset (tbo); + if (tbo_frame_object_count (frame) != 0) + return 14; + + gtk_widget_grab_focus (asset_button); + drain_events (); + g_signal_emit_by_name (asset_button, "clicked"); + drain_events (); + if (tbo_frame_object_count (frame) != 1) + return 16; + if (drawing->tooltip != NULL) + return 17; + if (gtk_window_get_focus (GTK_WINDOW (tbo->window)) != tbo->drawing) + return 18; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + return 0; +} diff --git a/tests/mode_status_check.c b/tests/mode_status_check.c new file mode 100644 index 0000000..4270944 --- /dev/null +++ b/tests/mode_status_check.c @@ -0,0 +1,48 @@ +#include +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + const gchar *status; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.modestatus", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 90); + + tbo_window_refresh_status (tbo); + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Mode: Page") == NULL) + return 3; + + tbo_window_enter_frame (tbo, frame); + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Mode: Frame") == NULL) + return 4; + + tbo_window_leave_frame (tbo); + status = gtk_label_get_text (GTK_LABEL (tbo->status)); + if (strstr (status, "Mode: Page") == NULL) + return 5; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/model_current_state_check.c b/tests/model_current_state_check.c index 126c1fe..9cc92da 100644 --- a/tests/model_current_state_check.c +++ b/tests/model_current_state_check.c @@ -70,7 +70,7 @@ main (void) return 15; tbo_page_set_current_frame (page2, frame3); - if (tbo_page_frame_index (page2) != 1) + if (tbo_page_frame_index (page2) != 0) return 16; if (tbo_comic_page_nth (comic, page4) != 1) diff --git a/tests/page_duplicate_check.c b/tests/page_duplicate_check.c new file mode 100644 index 0000000..c7fbf25 --- /dev/null +++ b/tests/page_duplicate_check.c @@ -0,0 +1,71 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *original_page; + Page *duplicate_page; + Frame *original_frame; + Frame *cloned_frame; + GdkRGBA color = { 0.1, 0.2, 0.8, 1.0 }; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.pageduplicate", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + original_page = tbo_comic_get_current_page (tbo->comic); + original_frame = tbo_page_new_frame (original_page, 20, 30, 140, 100); + tbo_frame_set_color_rgb (original_frame, color.red, color.green, color.blue); + tbo_frame_add_obj (original_frame, + TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 40, 20, "hello", "Sans 12", &color))); + + g_signal_emit_by_name (tbo->toolbar->button_duplicate_page, "clicked"); + if (tbo_comic_len (tbo->comic) != 2 || tbo_window_get_page_count (tbo) != 2) + return 3; + + duplicate_page = tbo_comic_get_current_page (tbo->comic); + if (duplicate_page == original_page || tbo_comic_page_nth (tbo->comic, duplicate_page) != 1) + return 4; + if (tbo_page_len (duplicate_page) != 1) + return 5; + + cloned_frame = tbo_page_get_frames (duplicate_page)->data; + if (cloned_frame == original_frame) + return 6; + if (tbo_frame_get_x (cloned_frame) != tbo_frame_get_x (original_frame) || + tbo_frame_get_y (cloned_frame) != tbo_frame_get_y (original_frame) || + tbo_frame_get_width (cloned_frame) != tbo_frame_get_width (original_frame) || + tbo_frame_get_height (cloned_frame) != tbo_frame_get_height (original_frame) || + tbo_frame_object_count (cloned_frame) != tbo_frame_object_count (original_frame)) + return 7; + + tbo_frame_set_bounds (original_frame, 1, 2, 3, 4); + if (tbo_frame_get_x (cloned_frame) == 1 || tbo_frame_get_width (cloned_frame) == 3) + return 8; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 1 || tbo_window_get_page_count (tbo) != 1) + return 9; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 2 || tbo_window_get_page_count (tbo) != 2) + return 10; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/page_reorder_check.c b/tests/page_reorder_check.c new file mode 100644 index 0000000..3462346 --- /dev/null +++ b/tests/page_reorder_check.c @@ -0,0 +1,65 @@ +#include + +#include "comic.h" +#include "page.h" +#include "tbo-undo.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page1; + Page *page2; + Page *page3; + GtkWidget *page_widget; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.pagereorder", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page1 = tbo_comic_get_current_page (tbo->comic); + page2 = tbo_comic_new_page (tbo->comic); + tbo_window_add_page_widget (tbo, create_darea (tbo), page2); + page3 = tbo_comic_new_page (tbo->comic); + tbo_window_add_page_widget (tbo, create_darea (tbo), page3); + tbo_comic_set_current_page (tbo->comic, page1); + tbo_window_set_current_tab_page (tbo, TRUE); + tbo_undo_stack_clear (tbo->undo_stack); + + page_widget = gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), 0); + gtk_notebook_reorder_child (GTK_NOTEBOOK (tbo->notebook), page_widget, 2); + + if (tbo_comic_page_nth (tbo->comic, page1) != 2 || + tbo_comic_page_nth (tbo->comic, page2) != 0 || + tbo_comic_page_nth (tbo->comic, page3) != 1) + return 3; + if (g_object_get_data (G_OBJECT (gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), 2)), "tbo-page") != page1) + return 4; + if (tbo_comic_get_current_page (tbo->comic) != page1 || + gtk_notebook_get_current_page (GTK_NOTEBOOK (tbo->notebook)) != 2) + return 5; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_page_nth (tbo->comic, page1) != 0 || + tbo_comic_page_nth (tbo->comic, page2) != 1 || + tbo_comic_page_nth (tbo->comic, page3) != 2) + return 6; + if (g_object_get_data (G_OBJECT (gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), 0)), "tbo-page") != page1) + return 7; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_comic_page_nth (tbo->comic, page1) != 2 || + g_object_get_data (G_OBJECT (gtk_notebook_get_nth_page (GTK_NOTEBOOK (tbo->notebook), 2)), "tbo-page") != page1) + return 8; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/page_status_check.c b/tests/page_status_check.c index d62e9b4..ae960ad 100644 --- a/tests/page_status_check.c +++ b/tests/page_status_check.c @@ -21,13 +21,13 @@ main (void) tbo = tbo_new_tbo (app, 800, 450); page2 = tbo_comic_new_page (tbo->comic); - tbo_window_add_page_widget (tbo, create_darea (tbo)); + tbo_window_add_page_widget (tbo, create_darea (tbo), page2); tbo_comic_set_current_page (tbo->comic, page2); tbo_window_set_current_tab_page (tbo, TRUE); tbo_window_refresh_status (tbo); status = gtk_label_get_text (GTK_LABEL (tbo->status)); - if (g_str_has_prefix (status, "Page 2 of 2 | Frames: 0 | Enter: frame") == FALSE) + if (g_str_has_prefix (status, "Mode: Page | Page 2 of 2 | Frames: 0 | Enter: frame") == FALSE) return 3; tbo_window_mark_clean (tbo); diff --git a/tests/recent_projects_check.c b/tests/recent_projects_check.c new file mode 100644 index 0000000..24b74a4 --- /dev/null +++ b/tests/recent_projects_check.c @@ -0,0 +1,87 @@ +#include +#include +#include + +#include "comic.h" +#include "tbo-window.h" + +static gchar * +make_tmp_project_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + g_remove (path); + return g_strconcat (path, ".tbo", NULL); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *first; + TboWindow *second; + TboWindow *reopened; + gchar *path1; + gchar *path2; + gchar **recent_paths; + gchar *last_project; + gsize recent_count = 0; + + gtk_init (); + tbo_window_clear_persisted_state (); + + app = gtk_application_new ("net.danigm.tbo.recentprojects", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + first = tbo_new_tbo (app, 810, 610); + path1 = make_tmp_project_path ("tbo-recent-one-XXXXXX"); + if (!tbo_comic_save (first, path1)) + return 3; + tbo_window_set_path (first, path1); + tbo_window_add_recent_project (path1); + + second = tbo_new_tbo (app, 920, 730); + path2 = make_tmp_project_path ("tbo-recent-two-XXXXXX"); + if (!tbo_comic_save (second, path2)) + return 4; + tbo_window_set_path (second, path2); + tbo_window_add_recent_project (path2); + + recent_paths = tbo_window_get_recent_projects (&recent_count); + if (recent_count != 2) + return 5; + if (g_strcmp0 (recent_paths[0], path2) != 0 || g_strcmp0 (recent_paths[1], path1) != 0) + return 6; + g_strfreev (recent_paths); + + last_project = tbo_window_get_last_project (); + if (g_strcmp0 (last_project, path2) != 0) + return 7; + g_free (last_project); + + reopened = tbo_new_tbo (app, 400, 300); + if (!tbo_window_reopen_last_project (reopened)) + return 8; + if (g_strcmp0 (reopened->path, path2) != 0) + return 9; + if (tbo_comic_get_width (reopened->comic) != 920 || tbo_comic_get_height (reopened->comic) != 730) + return 10; + + g_remove (path1); + g_remove (path2); + g_free (path1); + g_free (path2); + tbo_window_mark_clean (first); + tbo_window_mark_clean (second); + tbo_window_mark_clean (reopened); + gtk_window_close (GTK_WINDOW (first->window)); + gtk_window_close (GTK_WINDOW (second->window)); + gtk_window_close (GTK_WINDOW (reopened->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/shortcuts_guide_check.c b/tests/shortcuts_guide_check.c new file mode 100644 index 0000000..a4c0daf --- /dev/null +++ b/tests/shortcuts_guide_check.c @@ -0,0 +1,129 @@ +#include +#include + +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static GtkWindow * +find_shortcuts_window (TboWindow *tbo) +{ + GListModel *toplevels; + guint i; + + toplevels = gtk_window_get_toplevels (); + for (i = 0; i < g_list_model_get_n_items (toplevels); i++) + { + GtkWindow *window = GTK_WINDOW (g_list_model_get_item (toplevels, i)); + + if (window != GTK_WINDOW (tbo->window) && + gtk_window_get_transient_for (window) == GTK_WINDOW (tbo->window) && + g_strcmp0 (gtk_window_get_title (window), "Keyboard Shortcuts") == 0) + return window; + + g_object_unref (window); + } + + return NULL; +} + +static gboolean +widget_tree_contains_label (GtkWidget *widget, const gchar *text) +{ + GtkWidget *child; + + if (GTK_IS_LABEL (widget) && strstr (gtk_label_get_text (GTK_LABEL (widget)), text) != NULL) + return TRUE; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + if (widget_tree_contains_label (child, text)) + return TRUE; + } + + return FALSE; +} + +static GtkEventControllerKey * +find_key_controller (GtkWidget *widget) +{ + GListModel *controllers; + guint i; + + controllers = gtk_widget_observe_controllers (widget); + for (i = 0; i < g_list_model_get_n_items (controllers); i++) + { + GtkEventController *controller = g_list_model_get_item (controllers, i); + + if (GTK_IS_EVENT_CONTROLLER_KEY (controller)) + { + g_object_unref (controllers); + return GTK_EVENT_CONTROLLER_KEY (controller); + } + + g_object_unref (controller); + } + + g_object_unref (controllers); + return NULL; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + GtkWindow *shortcuts; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.shortcutsguide", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + + if (strstr (gtk_widget_get_tooltip_text (tbo->toolbar->button_save), "Ctrl+S") == NULL) + return 3; + if (strstr (gtk_widget_get_tooltip_text (GTK_WIDGET (tbo->toolbar->tool_buttons[TBO_TOOLBAR_SELECTOR])), "(S)") == NULL) + return 4; + if (strstr (gtk_widget_get_tooltip_text (tbo->toolbar->button_zoom_in), "(+)") == NULL) + return 5; + + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "shortcuts", NULL); + while (g_main_context_iteration (NULL, FALSE)); + + shortcuts = find_shortcuts_window (tbo); + if (shortcuts == NULL) + return 6; + if (!GTK_IS_HEADER_BAR (gtk_window_get_titlebar (shortcuts))) + return 9; + if (!widget_tree_contains_label (GTK_WIDGET (shortcuts), "Save Comic") || + !widget_tree_contains_label (GTK_WIDGET (shortcuts), "Ctrl+S") || + !widget_tree_contains_label (GTK_WIDGET (shortcuts), "Selector") || + !widget_tree_contains_label (GTK_WIDGET (shortcuts), "Esc")) + return 7; + + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "shortcuts", NULL); + while (g_main_context_iteration (NULL, FALSE)); + if (find_shortcuts_window (tbo) != shortcuts) + return 8; + + { + GtkEventControllerKey *controller = find_key_controller (GTK_WIDGET (shortcuts)); + gboolean handled = FALSE; + + if (controller == NULL) + return 10; + g_signal_emit_by_name (controller, "key-pressed", GDK_KEY_Escape, 0u, 0u, &handled); + g_object_unref (controller); + } + while (g_main_context_iteration (NULL, FALSE)); + if (find_shortcuts_window (tbo) != NULL) + return 11; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/status_hierarchy_check.c b/tests/status_hierarchy_check.c index efb901d..a3f23af 100644 --- a/tests/status_hierarchy_check.c +++ b/tests/status_hierarchy_check.c @@ -39,7 +39,8 @@ main (void) tbo_window_refresh_status (tbo); status = gtk_label_get_text (GTK_LABEL (tbo->status)); - if (strstr (status, "Page 1 of 1") == NULL || + if (strstr (status, "Mode: Page") == NULL || + strstr (status, "Page 1 of 1") == NULL || strstr (status, "Frames: 1") == NULL || strstr (status, "Frame 1 selected") == NULL) return 3; @@ -49,7 +50,8 @@ main (void) tbo_window_refresh_status (tbo); status = gtk_label_get_text (GTK_LABEL (tbo->status)); - if (strstr (status, "Page 1 of 1") == NULL || + if (strstr (status, "Mode: Frame") == NULL || + strstr (status, "Page 1 of 1") == NULL || strstr (status, "Editing frame 1") == NULL || strstr (status, "Object: Text") == NULL) return 4; diff --git a/tests/theme_respect_check.c b/tests/theme_respect_check.c new file mode 100644 index 0000000..fa521b5 --- /dev/null +++ b/tests/theme_respect_check.c @@ -0,0 +1,106 @@ +#include + +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboWindow *tbo2; + GtkSettings *settings; + gboolean had_theme_name; + gboolean had_prefer_dark; + gchar *theme_name_after = NULL; + gboolean prefer_dark_after = FALSE; + + gtk_init (); + tbo_window_clear_persisted_state (); + + settings = gtk_settings_get_default (); + if (settings == NULL) + return 2; + + had_theme_name = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-theme-name") != NULL; + had_prefer_dark = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme") != NULL; + + if (had_theme_name) + g_object_set (settings, "gtk-theme-name", "BlackMATE", NULL); + if (had_prefer_dark) + g_object_set (settings, "gtk-application-prefer-dark-theme", FALSE, NULL); + + app = gtk_application_new ("net.danigm.tbo.themerespect", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 3; + + tbo = tbo_new_tbo (app, 800, 450); + + if (had_theme_name) + g_object_get (settings, "gtk-theme-name", &theme_name_after, NULL); + if (had_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_after, NULL); + + if (tbo_window_get_theme_mode () != TBO_THEME_MODE_SYSTEM) + return 4; + if (had_theme_name && g_strcmp0 (theme_name_after, "BlackMATE") != 0) + return 5; + if (had_prefer_dark && prefer_dark_after) + return 6; + if (gtk_widget_has_css_class (tbo->window, "dark")) + return 7; + if (gtk_widget_has_css_class (tbo->vbox, "dark")) + return 8; + + g_action_group_change_action_state (G_ACTION_GROUP (tbo->window), + "theme-mode", + g_variant_new_string ("dark")); + if (had_theme_name) + g_object_get (settings, "gtk-theme-name", &theme_name_after, NULL); + if (had_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_after, NULL); + if (tbo_window_get_theme_mode () != TBO_THEME_MODE_DARK) + return 9; + if (had_theme_name && g_strcmp0 (theme_name_after, "Adwaita") != 0) + return 10; + if (had_prefer_dark && !prefer_dark_after) + return 11; + + tbo2 = tbo_new_tbo (app, 500, 400); + if (had_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_after, NULL); + if (had_prefer_dark && !prefer_dark_after) + return 12; + + g_action_group_change_action_state (G_ACTION_GROUP (tbo->window), + "theme-mode", + g_variant_new_string ("light")); + if (had_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_after, NULL); + if (tbo_window_get_theme_mode () != TBO_THEME_MODE_LIGHT) + return 13; + if (had_prefer_dark && prefer_dark_after) + return 14; + + g_action_group_change_action_state (G_ACTION_GROUP (tbo->window), + "theme-mode", + g_variant_new_string ("system")); + if (had_theme_name) + g_object_get (settings, "gtk-theme-name", &theme_name_after, NULL); + if (had_prefer_dark) + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_after, NULL); + if (tbo_window_get_theme_mode () != TBO_THEME_MODE_SYSTEM) + return 15; + if (had_theme_name && g_strcmp0 (theme_name_after, "BlackMATE") != 0) + return 16; + if (had_prefer_dark && prefer_dark_after) + return 17; + + g_free (theme_name_after); + tbo_window_mark_clean (tbo); + tbo_window_mark_clean (tbo2); + gtk_window_close (GTK_WINDOW (tbo->window)); + gtk_window_close (GTK_WINDOW (tbo2->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} From 67f31febeaa70762b0bd30570fa1ac6c126f74e2 Mon Sep 17 00:00:00 2001 From: jaime Date: Tue, 21 Apr 2026 21:51:53 +0200 Subject: [PATCH 17/22] Replace deprecated export preview pixbuf path --- src/export.c | 60 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/export.c b/src/export.c index 721288c..b22c167 100644 --- a/src/export.c +++ b/src/export.c @@ -204,42 +204,66 @@ get_preview_page_for_dialog (ExportDialogState *state, TboExportScope scope, gin return NULL; } -static GdkPixbuf * -create_page_preview_pixbuf (TboWindow *tbo, Page *page, gint width, gint height) +static GdkTexture * +create_texture_from_surface (cairo_surface_t *surface, gint width, gint height) +{ + GBytes *bytes; + guchar *copy; + gsize stride; + gsize size; + GdkTexture *texture; + + cairo_surface_flush (surface); + stride = cairo_image_surface_get_stride (surface); + size = stride * height; + copy = g_memdup2 (cairo_image_surface_get_data (surface), size); + bytes = g_bytes_new_take (copy, size); + texture = gdk_memory_texture_new (width, + height, + GDK_MEMORY_DEFAULT, + bytes, + stride); + g_bytes_unref (bytes); + + return texture; +} + +static GdkTexture * +create_page_preview_texture (TboWindow *tbo, Page *page, gint width, gint height) { cairo_surface_t *surface; cairo_t *cr; - GdkPixbuf *pixbuf; + GdkTexture *texture; surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); cr = cairo_create (surface); tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, page, width, height); - pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, width, height); + texture = create_texture_from_surface (surface, width, height); cairo_destroy (cr); cairo_surface_destroy (surface); - return pixbuf; + return texture; } -static GdkPixbuf * -create_frame_preview_pixbuf (Frame *frame, gint width, gint height) +static GdkTexture * +create_frame_preview_texture (Frame *frame, gint width, gint height) { cairo_surface_t *surface; cairo_t *cr; - GdkPixbuf *pixbuf; + GdkTexture *texture; surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); cr = cairo_create (surface); draw_frame_export (cr, frame, width, height); - pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, width, height); + texture = create_texture_from_surface (surface, width, height); cairo_destroy (cr); cairo_surface_destroy (surface); - return pixbuf; + return texture; } static void -set_preview_pixbuf (ExportDialogState *state, GdkPixbuf *pixbuf) +set_preview_texture (ExportDialogState *state, GdkTexture *texture) { - GtkWidget *picture = tbo_picture_new_for_pixbuf (pixbuf); + GtkWidget *picture = gtk_picture_new_for_paintable (GDK_PAINTABLE (texture)); gtk_picture_set_can_shrink (GTK_PICTURE (picture), TRUE); gtk_widget_set_size_request (picture, 220, 160); @@ -258,7 +282,7 @@ update_preview_and_range (ExportDialogState *state) gint height; gint preview_width; gint preview_height; - GdkPixbuf *pixbuf = NULL; + GdkTexture *texture = NULL; gchar *label = NULL; gboolean range_sensitive; @@ -287,7 +311,7 @@ update_preview_and_range (ExportDialogState *state) Frame *frame = get_export_selection_frame (state->tbo); if (frame != NULL) - pixbuf = create_frame_preview_pixbuf (frame, preview_width, preview_height); + texture = create_frame_preview_texture (frame, preview_width, preview_height); label = g_strdup (_("Preview: Selection")); } else @@ -295,7 +319,7 @@ update_preview_and_range (ExportDialogState *state) Page *page = get_preview_page_for_dialog (state, scope, from_page); if (page != NULL) - pixbuf = create_page_preview_pixbuf (state->tbo, page, preview_width, preview_height); + texture = create_page_preview_texture (state->tbo, page, preview_width, preview_height); if (scope == TBO_EXPORT_SCOPE_CURRENT_PAGE) label = g_strdup_printf (_("Preview: Current Page %d"), tbo_comic_page_position (state->tbo->comic)); @@ -306,10 +330,10 @@ update_preview_and_range (ExportDialogState *state) } gtk_label_set_text (GTK_LABEL (state->preview_label), label); - set_preview_pixbuf (state, pixbuf); + set_preview_texture (state, texture); g_free (label); - if (pixbuf != NULL) - g_object_unref (pixbuf); + if (texture != NULL) + g_object_unref (texture); } static void From 438606317e4ac7478075942d5a2f060bc8fb6715 Mon Sep 17 00:00:00 2001 From: jaime Date: Wed, 22 Apr 2026 10:14:14 +0200 Subject: [PATCH 18/22] Fix document lifecycle, undo state, persistence, and loader/export edge cases. Strengthen file-flow safety --- meson.build | 154 +++++++++++++++++++++++++ src/comic-load.c | 76 ++++++++++-- src/comic-open-dialog.c | 12 +- src/comic-saveas-dialog.c | 21 +++- src/comic-saveas-dialog.h | 2 +- src/comic.c | 85 +++++++++----- src/comic.h | 2 +- src/export.c | 42 ++++++- src/tbo-object-group.h | 1 - src/tbo-tool-selector.c | 8 +- src/tbo-undo.c | 29 +++-- src/tbo-undo.h | 5 +- src/tbo-widget.c | 53 +++------ src/tbo-widget.h | 3 +- src/tbo-window.c | 92 +++++++++------ src/tbo-window.h | 2 +- src/tbo.c | 10 +- src/ui-menu.c | 7 +- tests/assets_browser_search_check.c | 2 + tests/dnd_feedback_check.c | 2 + tests/document_replace_failure_check.c | 70 +++++++++++ tests/export_empty_comic_check.c | 60 ++++++++++ tests/frame_count_status_check.c | 2 + tests/frame_delete_undo_check.c | 49 ++++++++ tests/invalid_tbo_variants_check.c | 4 + tests/keyboard_accessibility_check.c | 2 + tests/mode_status_check.c | 2 + tests/object_delete_undo_check.c | 55 +++++++++ tests/page_status_check.c | 2 + tests/recent_missing_file_check.c | 57 +++++++++ tests/recover_file_failure_check.c | 66 +++++++++++ tests/save_failure_check.c | 47 ++++++++ tests/saveas_filename_check.c | 34 ++++++ tests/shortcuts_guide_check.c | 2 + tests/status_hierarchy_check.c | 2 + tests/tutorial_open_check.c | 57 +++++++++ tests/undo_branch_reset_check.c | 52 +++++++++ tests/undo_saved_state_check.c | 74 ++++++++++++ tests/xml_roundtrip_check.c | 5 +- 39 files changed, 1093 insertions(+), 157 deletions(-) create mode 100644 tests/document_replace_failure_check.c create mode 100644 tests/export_empty_comic_check.c create mode 100644 tests/frame_delete_undo_check.c create mode 100644 tests/object_delete_undo_check.c create mode 100644 tests/recent_missing_file_check.c create mode 100644 tests/recover_file_failure_check.c create mode 100644 tests/save_failure_check.c create mode 100644 tests/saveas_filename_check.c create mode 100644 tests/tutorial_open_check.c create mode 100644 tests/undo_branch_reset_check.c create mode 100644 tests/undo_saved_state_check.c diff --git a/meson.build b/meson.build index c9a958b..ff807c2 100644 --- a/meson.build +++ b/meson.build @@ -511,6 +511,94 @@ text_undo_check = executable( install: false ) +saveas_filename_check = executable( + 'saveas-filename-check', + common_sources + files('tests/saveas_filename_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +object_delete_undo_check = executable( + 'object-delete-undo-check', + common_sources + files('tests/object_delete_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +frame_delete_undo_check = executable( + 'frame-delete-undo-check', + common_sources + files('tests/frame_delete_undo_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +undo_branch_reset_check = executable( + 'undo-branch-reset-check', + common_sources + files('tests/undo_branch_reset_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +document_replace_failure_check = executable( + 'document-replace-failure-check', + common_sources + files('tests/document_replace_failure_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +tutorial_open_check = executable( + 'tutorial-open-check', + common_sources + files('tests/tutorial_open_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +export_empty_comic_check = executable( + 'export-empty-comic-check', + common_sources + files('tests/export_empty_comic_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +save_failure_check = executable( + 'save-failure-check', + common_sources + files('tests/save_failure_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +undo_saved_state_check = executable( + 'undo-saved-state-check', + common_sources + files('tests/undo_saved_state_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +recent_missing_file_check = executable( + 'recent-missing-file-check', + common_sources + files('tests/recent_missing_file_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + +recover_file_failure_check = executable( + 'recover-file-failure-check', + common_sources + files('tests/recover_file_failure_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + test( 'load-render-check', load_render_check, @@ -844,6 +932,72 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'saveas-filename-check', + saveas_filename_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'object-delete-undo-check', + object_delete_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'frame-delete-undo-check', + frame_delete_undo_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'undo-branch-reset-check', + undo_branch_reset_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'document-replace-failure-check', + document_replace_failure_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'tutorial-open-check', + tutorial_open_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'export-empty-comic-check', + export_empty_comic_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'save-failure-check', + save_failure_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'undo-saved-state-check', + undo_saved_state_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'recent-missing-file-check', + recent_missing_file_check, + env: ['G_DEBUG=fatal-criticals'] +) + +test( + 'recover-file-failure-check', + recover_file_failure_check, + env: ['G_DEBUG=fatal-criticals'] +) + install_data( 'data/tutorial.pdf', 'data/tut.tbo', diff --git a/src/comic-load.c b/src/comic-load.c index accf0c6..be19975 100644 --- a/src/comic-load.c +++ b/src/comic-load.c @@ -59,6 +59,30 @@ typedef struct GString *current_text_buffer; } TboLoadContext; +static gchar * +unwrap_saved_text (const gchar *text) +{ + gsize start = 0; + gsize end; + gsize trailing; + + if (text == NULL) + return g_strdup (""); + + end = strlen (text); + if (end > 0 && text[0] == '\n') + start = 1; + + trailing = end; + while (trailing > start && (text[trailing - 1] == ' ' || text[trailing - 1] == '\t')) + trailing--; + + if (trailing > start && text[trailing - 1] == '\n') + end = trailing - 1; + + return g_strndup (text + start, end - start); +} + static const gchar * find_attr_value (const gchar **attribute_names, const gchar **attribute_values, @@ -341,6 +365,15 @@ create_tbo_piximage (TboLoadContext *context, if (!parse_attrs ("piximage", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) return FALSE; + if (width < 0 || height < 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "piximage size cannot be negative"); + return FALSE; + } + path = dup_required_attr_string (attribute_names, attribute_values, "piximage", "path", error); if (path == NULL) return FALSE; @@ -393,6 +426,15 @@ create_tbo_svgimage (TboLoadContext *context, if (!parse_attrs ("svgimage", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) return FALSE; + if (width < 0 || height < 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "svgimage size cannot be negative"); + return FALSE; + } + path = dup_required_attr_string (attribute_names, attribute_values, "svgimage", "path", error); if (path == NULL) return FALSE; @@ -452,6 +494,15 @@ create_tbo_text (TboLoadContext *context, if (!parse_attrs ("text", attrs, G_N_ELEMENTS (attrs), attribute_names, attribute_values, error)) return FALSE; + if (width < 0 || height < 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "text size cannot be negative"); + return FALSE; + } + font = dup_required_attr_string (attribute_names, attribute_values, "text", "font", error); if (font == NULL) return FALSE; @@ -544,16 +595,8 @@ end_element (GMarkupParseContext *markup_context, if (context->current_text != NULL && context->current_text_buffer != NULL) { - normalized = g_strdup (context->current_text_buffer->str); - g_strstrip (normalized); - if (*normalized != '\0') - { - tbo_object_text_set_text (context->current_text, normalized); - } - else if (context->current_frame != NULL) - { - tbo_frame_del_obj (context->current_frame, TBO_OBJECT_BASE (context->current_text)); - } + normalized = unwrap_saved_text (context->current_text_buffer->str); + tbo_object_text_set_text (context->current_text, normalized); } g_free (normalized); @@ -629,6 +672,19 @@ tbo_comic_load_with_alerts (const gchar *filename, gboolean show_alerts) return NULL; } + if (tbo_comic_len (context.comic) == 0) + { + if (show_alerts) + tbo_alert_show (NULL, _("Couldn't parse file"), _("No pages found in file")); + tbo_comic_free (context.comic); + if (context.current_text_buffer != NULL) + g_string_free (context.current_text_buffer, TRUE); + g_markup_parse_context_free (markup_context); + g_free (file_text); + g_free (context.title); + return NULL; + } + comic = context.comic; context.comic = NULL; diff --git a/src/comic-open-dialog.c b/src/comic-open-dialog.c index 935bf31..f57798d 100644 --- a/src/comic-open-dialog.c +++ b/src/comic-open-dialog.c @@ -38,11 +38,13 @@ tbo_comic_open_dialog (GtkWidget *widget, TboWindow *window) tbo_window_set_browse_path (window, filename); if (tbo_window_prepare_for_document_replace (window)) { - tbo_comic_open (window, filename); - tbo_window_add_recent_project (filename); - tbo_menu_refresh (window); - tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_refresh_status (window); + if (tbo_comic_open (window, filename)) + { + tbo_window_add_recent_project (filename); + tbo_menu_refresh (window); + tbo_drawing_update (TBO_DRAWING (window->drawing)); + tbo_window_refresh_status (window); + } } g_free (filename); } diff --git a/src/comic-saveas-dialog.c b/src/comic-saveas-dialog.c index c36ed2e..02e37ba 100644 --- a/src/comic-saveas-dialog.c +++ b/src/comic-saveas-dialog.c @@ -36,16 +36,27 @@ tbo_comic_save_dialog (GtkWidget *widget, TboWindow *window) return tbo_comic_saveas_dialog (widget, window); } +gchar * +tbo_comic_build_save_filename (const gchar *title) +{ + if (title == NULL) + return g_strdup ("untitled.tbo"); + + if (g_str_has_suffix (title, ".tbo")) + return g_strdup (title); + + return g_strconcat (title, ".tbo", NULL); +} + gboolean tbo_comic_saveas_dialog (GtkWidget *widget, TboWindow *window) { gchar *filename; - char buffer[260]; + gchar *suggested_name; - g_strlcpy (buffer, tbo_comic_get_title (window->comic), sizeof (buffer)); - if (!g_str_has_suffix (tbo_comic_get_title (window->comic), ".tbo")) - strcat (buffer, ".tbo"); - filename = tbo_file_dialog_save_project (window, buffer); + suggested_name = tbo_comic_build_save_filename (tbo_comic_get_title (window->comic)); + filename = tbo_file_dialog_save_project (window, suggested_name); + g_free (suggested_name); if (filename != NULL) { diff --git a/src/comic-saveas-dialog.h b/src/comic-saveas-dialog.h index 3e9d09a..4900608 100644 --- a/src/comic-saveas-dialog.h +++ b/src/comic-saveas-dialog.h @@ -25,6 +25,6 @@ gboolean tbo_comic_save_dialog (GtkWidget *widget, TboWindow *window); gboolean tbo_comic_saveas_dialog (GtkWidget *widget, TboWindow *window); +gchar *tbo_comic_build_save_filename (const gchar *title); #endif - diff --git a/src/comic.c b/src/comic.c index bf8cfea..f495d93 100644 --- a/src/comic.c +++ b/src/comic.c @@ -341,8 +341,11 @@ save_comic_to_file (TboWindow *tbo, { GList *p; char buffer[255]; + char title[255] = {0}; FILE *file = fopen (filename, "w"); Comic *comic = tbo->comic; + gboolean success; + gint saved_errno = 0; if (!file) { @@ -357,10 +360,7 @@ save_comic_to_file (TboWindow *tbo, } if (update_window_state) - { - get_base_name (filename, comic->title, 255); - gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); - } + get_base_name (filename, title, sizeof (title)); if (comic->paper == TBO_COMIC_PAPER_A4) snprintf (buffer, 255, "\n", @@ -379,7 +379,31 @@ save_comic_to_file (TboWindow *tbo, snprintf (buffer, 255, "\n"); fwrite (buffer, sizeof (char), strlen (buffer), file); - fclose (file); + + success = ferror (file) == 0; + if (fclose (file) != 0) + success = FALSE; + + if (!success) + { + saved_errno = errno != 0 ? errno : EIO; + + if (show_errors) + { + perror (_("failed saving")); + tbo_alert_show (GTK_WINDOW (tbo->window), + _("Failed saving"), + strerror (saved_errno)); + } + + return FALSE; + } + + if (update_window_state) + { + g_strlcpy (comic->title, title, sizeof (comic->title)); + gtk_window_set_title (GTK_WINDOW (tbo->window), comic->title); + } if (mark_clean) tbo_window_mark_clean (tbo); @@ -399,7 +423,7 @@ tbo_comic_save_snapshot (TboWindow *tbo, const gchar *filename) return save_comic_to_file (tbo, filename, FALSE, FALSE, FALSE); } -void +gboolean tbo_comic_open (TboWindow *window, const gchar *filename) { Comic *newcomic = tbo_comic_load (filename); @@ -407,33 +431,34 @@ tbo_comic_open (TboWindow *window, const gchar *filename) int nth; int n_pages; - if (newcomic) - { - tbo_window_reset_document_state (window); - oldcomic = window->comic; + if (newcomic == NULL) + return FALSE; - n_pages = tbo_window_get_page_count (window); - for (nth = n_pages - 1; nth >= 0; nth--) - { - tbo_window_remove_page_widget (window, nth); - } + tbo_window_reset_document_state (window); + oldcomic = window->comic; - window->comic = newcomic; - gtk_window_set_title (GTK_WINDOW (window->window), tbo_comic_get_title (window->comic)); - tbo_comic_free (oldcomic); + n_pages = tbo_window_get_page_count (window); + for (nth = n_pages - 1; nth >= 0; nth--) + { + tbo_window_remove_page_widget (window, nth); + } - for (nth = 0; nth < tbo_comic_len (window->comic); nth++) - { - tbo_window_add_page_widget (window, - create_darea (window), - g_list_nth_data (tbo_comic_get_pages (window->comic), nth)); - } + window->comic = newcomic; + gtk_window_set_title (GTK_WINDOW (window->window), tbo_comic_get_title (window->comic)); + tbo_comic_free (oldcomic); - tbo_window_set_path (window, filename); - tbo_window_set_current_tab_page (window, TRUE); - tbo_drawing_adjust_scroll (TBO_DRAWING (window->drawing)); - tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_refresh_status (window); - tbo_window_mark_clean (window); + for (nth = 0; nth < tbo_comic_len (window->comic); nth++) + { + tbo_window_add_page_widget (window, + create_darea (window), + g_list_nth_data (tbo_comic_get_pages (window->comic), nth)); } + + tbo_window_set_path (window, filename); + tbo_window_set_current_tab_page (window, TRUE); + tbo_drawing_adjust_scroll (TBO_DRAWING (window->drawing)); + tbo_drawing_update (TBO_DRAWING (window->drawing)); + tbo_window_refresh_status (window); + tbo_window_mark_clean (window); + return TRUE; } diff --git a/src/comic.h b/src/comic.h index 9e97139..a83c414 100644 --- a/src/comic.h +++ b/src/comic.h @@ -69,6 +69,6 @@ void tbo_comic_set_current_page_nth (Comic *comic, int nth); void tbo_comic_reorder_page (Comic *comic, Page *page, int nth); gboolean tbo_comic_save (TboWindow *tbo, const gchar *filename); gboolean tbo_comic_save_snapshot (TboWindow *tbo, const gchar *filename); -void tbo_comic_open (TboWindow *window, const gchar *filename); +gboolean tbo_comic_open (TboWindow *window, const gchar *filename); #endif diff --git a/src/export.c b/src/export.c index b22c167..06c2538 100644 --- a/src/export.c +++ b/src/export.c @@ -163,7 +163,12 @@ build_export_page_range (Comic *comic, gint from_page, gint to_page, gint *n_pag normalize_export_page_range (comic, &from_page, &to_page); for (i = from_page - 1; i <= to_page - 1; i++) - pages = g_list_append (pages, g_list_nth_data (tbo_comic_get_pages (comic), i)); + { + Page *page = g_list_nth_data (tbo_comic_get_pages (comic), i); + + if (page != NULL) + pages = g_list_append (pages, page); + } if (n_pages != NULL) *n_pages = g_list_length (pages); @@ -604,15 +609,30 @@ export_page_list (TboWindow *tbo, gint count = n_pages; gint index = 0; + if (pages == NULL || n_pages <= 0) + { + show_export_error (tbo, _("There are no pages to export.")); + return FALSE; + } + for (; count; count /= 10, digits++); format_pages = g_strdup_printf ("%%s%%0%dd.%%s", MAX (1, digits)); for (; pages != NULL; pages = pages->next, index++) { + Page *page = TBO_PAGE (pages->data); gchar *path = g_strdup_printf (format_pages, base_filename, index, export_to); gdouble draw_width; gdouble draw_height; + if (page == NULL) + { + show_export_error (tbo, _("There are no pages to export.")); + g_free (path); + success = FALSE; + break; + } + if (n_pages == 1 || g_strcmp0 (export_to, "pdf") == 0) { g_free (path); @@ -672,7 +692,7 @@ export_page_list (TboWindow *tbo, } } - tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, TBO_PAGE (pages->data), draw_width, draw_height); + tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, page, draw_width, draw_height); success = finish_export_surface (tbo, export_to, path, surface, cr); g_free (path); @@ -791,10 +811,22 @@ tbo_export_file_with_scope_range (TboWindow *tbo, switch (scope) { case TBO_EXPORT_SCOPE_CURRENT_PAGE: - pages = g_list_append (NULL, tbo_comic_get_current_page (tbo->comic)); - success = export_page_list (tbo, base_filename, export_to, width, height, pages, 1, TRUE); - g_list_free (pages); + { + Page *current_page = tbo_comic_get_current_page (tbo->comic); + + if (current_page == NULL) + { + show_export_error (tbo, _("There are no pages to export.")); + success = FALSE; + } + else + { + pages = g_list_append (NULL, current_page); + success = export_page_list (tbo, base_filename, export_to, width, height, pages, 1, TRUE); + g_list_free (pages); + } break; + } case TBO_EXPORT_SCOPE_SELECTION: { Frame *frame = get_export_selection_frame (tbo); diff --git a/src/tbo-object-group.h b/src/tbo-object-group.h index ee3f27c..f96410f 100644 --- a/src/tbo-object-group.h +++ b/src/tbo-object-group.h @@ -22,7 +22,6 @@ #include #include "tbo-object-base.h" -#include "tbo-object-group.h" #define TBO_TYPE_OBJECT_GROUP (tbo_object_group_get_type ()) #define TBO_OBJECT_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TBO_TYPE_OBJECT_GROUP, TboObjectGroup)) diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index 4a57f2b..12b2b13 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -595,9 +595,11 @@ delete_selected (TboToolSelector *self) if (obj != NULL && tbo_drawing_get_current_frame (drawing) != NULL) { gint index = tbo_frame_object_nth (frame, obj); + TboAction *action = tbo_action_object_remove_new (frame, obj, index); + tbo_tool_selector_set_selected_object_pointer (self, NULL); tbo_frame_del_obj (frame, obj); - tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_remove_new (frame, obj, index)); + tbo_undo_stack_insert (tbo->undo_stack, action); tbo_window_mark_dirty (tbo); tbo_toolbar_update (tbo->toolbar); update_menubar (tbo); @@ -607,8 +609,10 @@ delete_selected (TboToolSelector *self) if (frame != NULL && tbo_drawing_get_current_frame (drawing) == NULL) { gint index = tbo_page_frame_nth (page, frame); + TboAction *action = tbo_action_frame_remove_new (page, frame, index); + tbo_page_del_frame (page, frame); - tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_remove_new (page, frame, index)); + tbo_undo_stack_insert (tbo->undo_stack, action); tbo_tool_selector_set_selected (self, NULL); tbo_window_mark_dirty (tbo); tbo_window_refresh_status (tbo); diff --git a/src/tbo-undo.c b/src/tbo-undo.c index e3c0312..e93170d 100644 --- a/src/tbo-undo.c +++ b/src/tbo-undo.c @@ -737,15 +737,6 @@ text_state_free (TboAction *action) g_free (text_action->font2); } -void -tbo_action_set (TboAction *action, - gpointer action_do, - gpointer action_undo) -{ - action->action_do = action_do; - action->action_undo = action_undo; -} - void tbo_action_del (TboAction *action) { @@ -755,12 +746,6 @@ tbo_action_del (TboAction *action) free (action); } -void -tbo_action_del_data (TboAction *action, gpointer user_data) -{ - tbo_action_del (action); -} - TboUndoStack * tbo_undo_stack_new (void) { @@ -768,16 +753,24 @@ tbo_undo_stack_new (void) stack->first = NULL; stack->list = NULL; stack->last_flag = TRUE; + stack->current_state_id = 0; + stack->next_state_id = 1; return stack; } void tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action) { + action->state_before = stack->current_state_id; + action->state_after = stack->next_state_id++; + // Removing each element before the actual one if (stack->first) { - while (stack->first != stack->list) + if (stack->last_flag) + tbo_undo_stack_clear (stack); + + while (stack->first != NULL && stack->first != stack->list) { GList *link = stack->first; @@ -792,6 +785,7 @@ tbo_undo_stack_insert (TboUndoStack *stack, TboAction *action) stack->last_flag = FALSE; stack->list = g_list_prepend (stack->list, (gpointer)action); stack->first = stack->list; + stack->current_state_id = action->state_after; } void @@ -814,6 +808,7 @@ tbo_undo_stack_clear (TboUndoStack *stack) stack->first = NULL; stack->list = NULL; stack->last_flag = TRUE; + stack->next_state_id = stack->current_state_id + 1; } void @@ -829,6 +824,7 @@ tbo_undo_stack_undo (TboUndoStack *stack) TboAction *action = NULL; action = (stack->list)->data; tbo_action_undo (action); + stack->current_state_id = action->state_before; if (stack->list->next) stack->list = (stack->list)->next; @@ -854,6 +850,7 @@ tbo_undo_stack_redo (TboUndoStack *stack) TboAction *action = NULL; action = (stack->list)->data; tbo_action_do (action); + stack->current_state_id = action->state_after; } void diff --git a/src/tbo-undo.h b/src/tbo-undo.h index d93843c..f9a9a38 100644 --- a/src/tbo-undo.h +++ b/src/tbo-undo.h @@ -40,15 +40,18 @@ struct _TboAction { void (*action_do) (TboAction *action); void (*action_undo) (TboAction *action); void (*action_free) (TboAction *action); + guint64 state_before; + guint64 state_after; }; -void tbo_action_set (TboAction *action, gpointer action_do, gpointer action_undo); void tbo_action_del (TboAction *action); struct _TboUndoStack { GList *first; GList *list; gboolean last_flag; + guint64 current_state_id; + guint64 next_state_id; }; TboUndoStack * tbo_undo_stack_new (void); diff --git a/src/tbo-widget.c b/src/tbo-widget.c index 7f341ad..40119b4 100644 --- a/src/tbo-widget.c +++ b/src/tbo-widget.c @@ -11,12 +11,15 @@ #include "tbo-widget.h" #define TBO_DIALOG_RUN_DATA_KEY "tbo-dialog-run-data" +#define TBO_ALERT_TEST_RESPONSE_NONE G_MININT struct alert_run_data { GMainLoop *loop; gint response; }; +static gint alert_test_response = TBO_ALERT_TEST_RESPONSE_NONE; + static void alert_response_cb (GObject *source, GAsyncResult *result, gpointer user_data) { @@ -210,6 +213,9 @@ tbo_alert_choose (GtkWindow *parent, GtkAlertDialog *dialog; struct alert_run_data data; + if (alert_test_response != TBO_ALERT_TEST_RESPONSE_NONE) + return alert_test_response; + dialog = gtk_alert_dialog_new ("%s", message); gtk_alert_dialog_set_detail (dialog, detail); gtk_alert_dialog_set_buttons (dialog, buttons); @@ -234,6 +240,18 @@ tbo_alert_show (GtkWindow *parent, const gchar *message, const gchar *detail) tbo_alert_choose (parent, message, detail, buttons, 0, 0); } +void +tbo_alert_set_test_response (gint response) +{ + alert_test_response = response; +} + +void +tbo_alert_clear_test_response (void) +{ + alert_test_response = TBO_ALERT_TEST_RESPONSE_NONE; +} + void tbo_widget_show_all (GtkWidget *widget) { @@ -292,38 +310,3 @@ tbo_picture_new_for_pixbuf (GdkPixbuf *pixbuf) g_object_unref (texture); return picture; } - -GtkWidget * -tbo_image_new_for_pixbuf (GdkPixbuf *pixbuf) -{ - gchar *buffer = NULL; - gsize size = 0; - GBytes *bytes; - GdkTexture *texture; - GtkWidget *image; - GError *error = NULL; - - if (pixbuf == NULL) - return gtk_image_new (); - - if (!gdk_pixbuf_save_to_buffer (pixbuf, &buffer, &size, "png", &error, NULL)) - { - if (error != NULL) - g_error_free (error); - return gtk_image_new (); - } - - bytes = g_bytes_new_take (buffer, size); - texture = gdk_texture_new_from_bytes (bytes, &error); - g_bytes_unref (bytes); - if (texture == NULL) - { - if (error != NULL) - g_error_free (error); - return gtk_image_new (); - } - - image = gtk_image_new_from_paintable (GDK_PAINTABLE (texture)); - g_object_unref (texture); - return image; -} diff --git a/src/tbo-widget.h b/src/tbo-widget.h index 11145b2..270ebf9 100644 --- a/src/tbo-widget.h +++ b/src/tbo-widget.h @@ -43,8 +43,9 @@ gint tbo_alert_choose (GtkWindow *parent, gint cancel_button, gint default_button); void tbo_alert_show (GtkWindow *parent, const gchar *message, const gchar *detail); +void tbo_alert_set_test_response (gint response); +void tbo_alert_clear_test_response (void); void tbo_widget_show_all (GtkWidget *widget); GtkWidget *tbo_picture_new_for_pixbuf (GdkPixbuf *pixbuf); -GtkWidget *tbo_image_new_for_pixbuf (GdkPixbuf *pixbuf); #endif diff --git a/src/tbo-window.c b/src/tbo-window.c index 090c967..5d814d1 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -55,7 +55,13 @@ static gchar *get_recovery_dir_path (void); static gchar *get_recovery_meta_path (const gchar *autosave_file); static GKeyFile *load_state_key_file (void); static void save_state_key_file (GKeyFile *kf); -static gboolean confirm_close (TboWindow *tbo); +typedef enum +{ + TBO_CONFIRM_CLOSE_CANCEL, + TBO_CONFIRM_CLOSE_CONTINUE, + TBO_CONFIRM_CLOSE_DISCARD, +} TboConfirmCloseResult; +static TboConfirmCloseResult confirm_close (TboWindow *tbo); static void update_window_title (TboWindow *tbo); static void apply_theme_preferences (void); static TboThemeMode load_theme_mode_preference (void); @@ -649,6 +655,8 @@ detach_document_state (TboWindow *tbo) } tbo_undo_stack_clear (tbo->undo_stack); + tbo->undo_stack->current_state_id = 0; + tbo->undo_stack->next_state_id = 1; tbo_tooltip_reset (tbo); } @@ -842,34 +850,24 @@ get_state_file_path (void) static gchar * load_persisted_path (const gchar *key) { - GKeyFile *kf = g_key_file_new (); - gchar *state_file = get_state_file_path (); + GKeyFile *kf = load_state_key_file (); gchar *value = NULL; - if (g_key_file_load_from_file (kf, state_file, G_KEY_FILE_NONE, NULL)) + if (g_key_file_has_group (kf, "paths")) value = g_key_file_get_string (kf, "paths", key, NULL); g_key_file_unref (kf); - g_free (state_file); return value; } static void store_persisted_path (const gchar *key, const gchar *value) { - GKeyFile *kf = g_key_file_new (); - gchar *state_file = get_state_file_path (); - gchar *content; - gsize len; + GKeyFile *kf = load_state_key_file (); - g_key_file_load_from_file (kf, state_file, G_KEY_FILE_NONE, NULL); g_key_file_set_string (kf, "paths", key, value); - content = g_key_file_to_data (kf, &len, NULL); - g_file_set_contents (state_file, content, len, NULL); - - g_free (content); + save_state_key_file (kf); g_key_file_unref (kf); - g_free (state_file); } static gchar * @@ -884,10 +882,10 @@ get_dirname_or_home (const gchar *path) gboolean tbo_window_prepare_for_document_replace (TboWindow *tbo) { - return confirm_close (tbo); + return confirm_close (tbo) != TBO_CONFIRM_CLOSE_CANCEL; } -static gboolean +static TboConfirmCloseResult confirm_close (TboWindow *tbo) { gint response; @@ -899,7 +897,7 @@ confirm_close (TboWindow *tbo) }; if (!tbo_window_has_unsaved_changes (tbo)) - return TRUE; + return TBO_CONFIRM_CLOSE_CONTINUE; response = tbo_alert_choose (GTK_WINDOW (tbo->window), _("Do you want to save your work before closing?"), @@ -909,15 +907,12 @@ confirm_close (TboWindow *tbo) 2); if (response == 2) - return tbo_comic_save_dialog (NULL, tbo); + return tbo_comic_save_dialog (NULL, tbo) ? TBO_CONFIRM_CLOSE_CONTINUE : TBO_CONFIRM_CLOSE_CANCEL; if (response == 1) - { - tbo_window_mark_clean (tbo); - return TRUE; - } + return TBO_CONFIRM_CLOSE_DISCARD; - return FALSE; + return TBO_CONFIRM_CLOSE_CANCEL; } static void @@ -1309,6 +1304,7 @@ tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, tbo->key_binder = TRUE; tbo->dirty = FALSE; tbo->destroying = FALSE; + tbo->clean_state_id = 0; update_window_title (tbo); return tbo; @@ -1389,10 +1385,24 @@ tbo_window_mark_dirty (TboWindow *tbo) schedule_autosave (tbo); } +static void +update_dirty_state_from_history (TboWindow *tbo) +{ + if (tbo == NULL || tbo->undo_stack == NULL) + return; + + if (tbo->undo_stack->current_state_id == tbo->clean_state_id) + tbo_window_mark_clean (tbo); + else + tbo_window_mark_dirty (tbo); +} + void tbo_window_mark_clean (TboWindow *tbo) { tbo->dirty = FALSE; + if (tbo->undo_stack != NULL) + tbo->clean_state_id = tbo->undo_stack->current_state_id; update_window_title (tbo); if (tbo->autosave_timeout_id != 0) { @@ -1432,7 +1442,11 @@ tbo_window_recover_file (TboWindow *tbo, const gchar *autosave_file) return FALSE; source_path = load_recovery_source_path (autosave_file); - tbo_comic_open (tbo, (char *) autosave_file); + if (!tbo_comic_open (tbo, (char *) autosave_file)) + { + g_free (source_path); + return FALSE; + } set_window_path (&tbo->path, source_path); if (source_path != NULL) @@ -1459,7 +1473,9 @@ tbo_window_open_recent_project (TboWindow *tbo, const gchar *path) if (!tbo_window_prepare_for_document_replace (tbo)) return FALSE; - tbo_comic_open (tbo, (char *) path); + if (!tbo_comic_open (tbo, (char *) path)) + return FALSE; + tbo_window_add_recent_project (path); tbo_menu_refresh (tbo); return TRUE; @@ -1573,18 +1589,18 @@ tbo_window_get_page_count (TboWindow *tbo) return gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); } -gboolean -tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, - TboWindow *tbo) -{ - return !confirm_close (tbo); -} - gboolean tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo) { - if (confirm_close (tbo)) + TboConfirmCloseResult result = confirm_close (tbo); + + (void) window; + + if (result != TBO_CONFIRM_CLOSE_CANCEL) { + if (result == TBO_CONFIRM_CLOSE_DISCARD) + tbo_window_mark_clean (tbo); + tbo_window_reset_document_state (tbo); tbo->destroying = TRUE; return FALSE; @@ -1841,9 +1857,11 @@ gboolean tbo_window_undo_cb (GtkWidget *widget, TboWindow *tbo) { gint old_page_count = tbo_window_get_page_count (tbo); + (void) widget; + tbo_undo_stack_undo (tbo->undo_stack); - tbo_window_mark_dirty (tbo); + update_dirty_state_from_history (tbo); sync_page_widgets_with_comic (tbo); if (old_page_count != tbo_window_get_page_count (tbo) || gtk_notebook_get_current_page (GTK_NOTEBOOK (tbo->notebook)) != tbo_comic_page_index (tbo->comic) || @@ -1859,9 +1877,11 @@ gboolean tbo_window_redo_cb (GtkWidget *widget, TboWindow *tbo) { gint old_page_count = tbo_window_get_page_count (tbo); + (void) widget; + tbo_undo_stack_redo (tbo->undo_stack); - tbo_window_mark_dirty (tbo); + update_dirty_state_from_history (tbo); sync_page_widgets_with_comic (tbo); if (old_page_count != tbo_window_get_page_count (tbo) || gtk_notebook_get_current_page (GTK_NOTEBOOK (tbo->notebook)) != tbo_comic_page_index (tbo->comic) || diff --git a/src/tbo-window.h b/src/tbo-window.h index 2db7616..4b5c14c 100644 --- a/src/tbo-window.h +++ b/src/tbo-window.h @@ -64,11 +64,11 @@ struct _TboWindow gboolean key_binder; gboolean dirty; gboolean destroying; + guint64 clean_state_id; }; TboWindow *tbo_window_new (GtkWidget *window, GtkWidget *dw_scroll, GtkWidget *scroll2, GtkWidget *notebook, GtkWidget *toolarea, GtkWidget *status, GtkWidget *vbox, Comic *comic); void tbo_window_free (TboWindow *tbo); -gboolean tbo_window_free_cb (GtkWidget *widget, GdkEvent *event, TboWindow *tbo); gboolean tbo_window_close_request_cb (GtkWindow *window, TboWindow *tbo); TboWindow * tbo_new_tbo (GtkApplication *app, int width, int height); TboWindow * tbo_new_tbo_with_template (GtkApplication *app, int width, int height, TboComicTemplate template); diff --git a/src/tbo.c b/src/tbo.c index 5386dcb..2a80fac 100644 --- a/src/tbo.c +++ b/src/tbo.c @@ -102,10 +102,12 @@ open_cb (GtkApplication *app, GFile **files, gint n_files, const gchar *hint, gp tbo = tbo_new_tbo (app, 800, 450); if (path != NULL) { - tbo_comic_open (tbo, path); - tbo_window_set_path (tbo, path); - tbo_window_add_recent_project (path); - tbo_menu_refresh (tbo); + if (tbo_comic_open (tbo, path)) + { + tbo_window_set_path (tbo, path); + tbo_window_add_recent_project (path); + tbo_menu_refresh (tbo); + } g_free (path); } present_window (tbo); diff --git a/src/ui-menu.c b/src/ui-menu.c index 36f12ca..99a53e1 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -508,7 +508,12 @@ open_tutorial (TboWindow *tbo) { gchar *filename = tbo_get_data_path ("tut.tbo"); - tbo_comic_open (tbo, filename); + if (tbo_window_prepare_for_document_replace (tbo) && tbo_comic_open (tbo, filename)) + { + g_free (tbo->path); + tbo->path = NULL; + } + g_free (filename); } diff --git a/tests/assets_browser_search_check.c b/tests/assets_browser_search_check.c index 5e19351..d4046b1 100644 --- a/tests/assets_browser_search_check.c +++ b/tests/assets_browser_search_check.c @@ -70,6 +70,8 @@ main (void) GtkWidget *browser; GtkWidget *search; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.assetssearch", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/dnd_feedback_check.c b/tests/dnd_feedback_check.c index 73fae62..4409e17 100644 --- a/tests/dnd_feedback_check.c +++ b/tests/dnd_feedback_check.c @@ -18,6 +18,8 @@ main (void) Page *page; Frame *frame; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.dndfeedback", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/document_replace_failure_check.c b/tests/document_replace_failure_check.c new file mode 100644 index 0000000..4408a72 --- /dev/null +++ b/tests/document_replace_failure_check.c @@ -0,0 +1,70 @@ +#include +#include +#include + +#include "comic.h" +#include "page.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static gchar * +make_tmp_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + gchar *invalid_path; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.documentreplacefailure", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page, 10, 10, 120, 90); + tbo_window_mark_dirty (tbo); + if (!tbo_window_run_autosave (tbo)) + return 3; + if (!g_file_test (tbo->autosave_path, G_FILE_TEST_EXISTS)) + return 4; + + invalid_path = make_tmp_path ("tbo-invalid-replace-XXXXXX.tbo"); + if (!g_file_set_contents (invalid_path, "autosave_path, G_FILE_TEST_EXISTS)) + return 8; + if (tbo_page_len (tbo_comic_get_current_page (tbo->comic)) != 1) + return 9; + if (tbo->path != NULL) + return 10; + + g_remove (invalid_path); + g_free (invalid_path); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/export_empty_comic_check.c b/tests/export_empty_comic_check.c new file mode 100644 index 0000000..21777b2 --- /dev/null +++ b/tests/export_empty_comic_check.c @@ -0,0 +1,60 @@ +#include +#include +#include + +#include "comic.h" +#include "export.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static gchar * +make_tmp_base (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + + g_remove (path); + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *base; + gchar *png_path; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.exportemptycomic", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_comic_del_page (tbo->comic, 0); + if (tbo_comic_len (tbo->comic) != 0) + return 3; + + base = make_tmp_base ("tbo-export-empty-XXXXXX"); + png_path = g_strdup_printf ("%s.png", base); + tbo_alert_set_test_response (0); + if (tbo_export_file_with_scope_range (tbo, base, "png", 800, 450, TBO_EXPORT_SCOPE_ALL_PAGES, 1, 1)) + return 4; + if (tbo_export_file_with_scope_range (tbo, base, "png", 800, 450, TBO_EXPORT_SCOPE_CURRENT_PAGE, 1, 1)) + return 5; + tbo_alert_clear_test_response (); + if (g_file_test (png_path, G_FILE_TEST_EXISTS)) + return 6; + + g_free (png_path); + g_free (base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/frame_count_status_check.c b/tests/frame_count_status_check.c index 28f85c0..cce11fe 100644 --- a/tests/frame_count_status_check.c +++ b/tests/frame_count_status_check.c @@ -15,6 +15,8 @@ main (void) TboPointerEvent release_event = { .x = 110, .y = 90 }; const gchar *status; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.framecountstatus", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/frame_delete_undo_check.c b/tests/frame_delete_undo_check.c new file mode 100644 index 0000000..dd44c81 --- /dev/null +++ b/tests/frame_delete_undo_check.c @@ -0,0 +1,49 @@ +#include + +#include "comic.h" +#include "page.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.framedeleteundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected (selector, frame); + if (!tbo_tool_selector_delete_selected (selector)) + return 3; + if (tbo_page_len (page) != 0) + return 4; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_page_len (page) != 1) + return 5; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_page_len (page) != 0) + return 6; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/invalid_tbo_variants_check.c b/tests/invalid_tbo_variants_check.c index 2cfe74a..01a2a75 100644 --- a/tests/invalid_tbo_variants_check.c +++ b/tests/invalid_tbo_variants_check.c @@ -45,6 +45,10 @@ main (int argc, char **argv) "", "broken", "", + "", + "", + "", + "text", }; guint i; Comic *comic; diff --git a/tests/keyboard_accessibility_check.c b/tests/keyboard_accessibility_check.c index e10fb09..4451d0b 100644 --- a/tests/keyboard_accessibility_check.c +++ b/tests/keyboard_accessibility_check.c @@ -79,6 +79,8 @@ main (void) Page *page; Frame *frame; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.keyboardaccessibility", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/mode_status_check.c b/tests/mode_status_check.c index 4270944..6ce59bc 100644 --- a/tests/mode_status_check.c +++ b/tests/mode_status_check.c @@ -15,6 +15,8 @@ main (void) Frame *frame; const gchar *status; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.modestatus", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/object_delete_undo_check.c b/tests/object_delete_undo_check.c new file mode 100644 index 0000000..56afe71 --- /dev/null +++ b/tests/object_delete_undo_check.c @@ -0,0 +1,55 @@ +#include + +#include "comic.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-text.h" +#include "tbo-tool-selector.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + TboToolSelector *selector; + Page *page; + Frame *frame; + TboObjectBase *text; + GdkRGBA color = { 0, 0, 0, 1 }; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.objectdeleteundo", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 120, 80); + text = TBO_OBJECT_BASE (tbo_object_text_new_with_params (10, 10, 60, 20, "delete me", "Sans 12", &color)); + tbo_frame_add_obj (frame, text); + + tbo_window_enter_frame (tbo, frame); + tbo_tool_selector_set_selected_obj (selector, text); + if (!tbo_tool_selector_delete_selected (selector)) + return 3; + if (tbo_frame_object_count (frame) != 0) + return 4; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_frame_object_count (frame) != 1) + return 5; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_frame_object_count (frame) != 0) + return 6; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/page_status_check.c b/tests/page_status_check.c index ae960ad..12fdca1 100644 --- a/tests/page_status_check.c +++ b/tests/page_status_check.c @@ -13,6 +13,8 @@ main (void) Page *page2; const gchar *status; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.pagestatus", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/recent_missing_file_check.c b/tests/recent_missing_file_check.c new file mode 100644 index 0000000..cb19129 --- /dev/null +++ b/tests/recent_missing_file_check.c @@ -0,0 +1,57 @@ +#include +#include +#include + +#include "tbo-widget.h" +#include "tbo-window.h" + +static gchar * +make_tmp_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *path; + gchar **recent_projects; + gsize n_projects = 0; + + gtk_init (); + tbo_window_clear_persisted_state (); + + app = gtk_application_new ("net.danigm.tbo.recentmissingfile", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + path = make_tmp_path ("tbo-missing-recent-XXXXXX.tbo"); + tbo_window_add_recent_project (path); + g_remove (path); + + tbo_alert_set_test_response (0); + if (tbo_window_open_recent_project (tbo, path)) + return 3; + tbo_alert_clear_test_response (); + + recent_projects = tbo_window_get_recent_projects (&n_projects); + if (n_projects != 0) + return 4; + + g_strfreev (recent_projects); + g_free (path); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/recover_file_failure_check.c b/tests/recover_file_failure_check.c new file mode 100644 index 0000000..a42c992 --- /dev/null +++ b/tests/recover_file_failure_check.c @@ -0,0 +1,66 @@ +#include +#include +#include + +#include "comic.h" +#include "page.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static gchar * +make_tmp_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + + return path; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *autosave_path; + gchar *original_title; + Page *page; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.recoverfilefailure", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page, 10, 10, 120, 90); + original_title = g_strdup (tbo_comic_get_title (tbo->comic)); + + autosave_path = make_tmp_path ("tbo-invalid-recovery-XXXXXX.tbo"); + if (!g_file_set_contents (autosave_path, "comic), original_title) != 0) + return 6; + if (tbo_page_len (tbo_comic_get_current_page (tbo->comic)) != 1) + return 7; + + g_remove (autosave_path); + g_free (autosave_path); + g_free (original_title); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/save_failure_check.c b/tests/save_failure_check.c new file mode 100644 index 0000000..efd03d3 --- /dev/null +++ b/tests/save_failure_check.c @@ -0,0 +1,47 @@ +#include + +#include "comic.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *window_title; + gchar *comic_title; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.savefailure", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + tbo_window_mark_dirty (tbo); + window_title = g_strdup (gtk_window_get_title (GTK_WINDOW (tbo->window))); + comic_title = g_strdup (tbo_comic_get_title (tbo->comic)); + + tbo_alert_set_test_response (0); + if (tbo_comic_save (tbo, "/dev/full")) + return 3; + tbo_alert_clear_test_response (); + + if (!tbo_window_has_unsaved_changes (tbo)) + return 4; + if (tbo->path != NULL) + return 5; + if (g_strcmp0 (tbo_comic_get_title (tbo->comic), comic_title) != 0) + return 6; + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), window_title) != 0) + return 7; + + g_free (window_title); + g_free (comic_title); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/saveas_filename_check.c b/tests/saveas_filename_check.c new file mode 100644 index 0000000..6a0d904 --- /dev/null +++ b/tests/saveas_filename_check.c @@ -0,0 +1,34 @@ +#include +#include + +#include "comic-saveas-dialog.h" + +int +main (void) +{ + gchar long_title[1024]; + gchar *filename; + + memset (long_title, 'a', sizeof (long_title) - 1); + long_title[sizeof (long_title) - 1] = '\0'; + + filename = tbo_comic_build_save_filename (long_title); + if (filename == NULL) + return 2; + if (!g_str_has_suffix (filename, ".tbo")) + return 3; + if (strlen (filename) != strlen (long_title) + strlen (".tbo")) + return 4; + if (strncmp (filename, long_title, strlen (long_title)) != 0) + return 5; + g_free (filename); + + filename = tbo_comic_build_save_filename ("already.tbo"); + if (filename == NULL) + return 6; + if (strcmp (filename, "already.tbo") != 0) + return 7; + g_free (filename); + + return 0; +} diff --git a/tests/shortcuts_guide_check.c b/tests/shortcuts_guide_check.c index a4c0daf..e1520a3 100644 --- a/tests/shortcuts_guide_check.c +++ b/tests/shortcuts_guide_check.c @@ -74,6 +74,8 @@ main (void) TboWindow *tbo; GtkWindow *shortcuts; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.shortcutsguide", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/status_hierarchy_check.c b/tests/status_hierarchy_check.c index a3f23af..39987cc 100644 --- a/tests/status_hierarchy_check.c +++ b/tests/status_hierarchy_check.c @@ -21,6 +21,8 @@ main (void) GdkRGBA color = { 0, 0, 0, 1 }; const gchar *status; + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); gtk_init (); app = gtk_application_new ("net.danigm.tbo.statushierarchy", G_APPLICATION_DEFAULT_FLAGS); diff --git a/tests/tutorial_open_check.c b/tests/tutorial_open_check.c new file mode 100644 index 0000000..fc04890 --- /dev/null +++ b/tests/tutorial_open_check.c @@ -0,0 +1,57 @@ +#include +#include + +#include "comic.h" +#include "page.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + gchar *original_title; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.tutorialopen", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + tbo_page_new_frame (page, 10, 10, 120, 90); + tbo_window_mark_dirty (tbo); + original_title = g_strdup (tbo_comic_get_title (tbo->comic)); + + tbo_alert_set_test_response (0); + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "tutorial", NULL); + if (!tbo_window_has_unsaved_changes (tbo)) + return 3; + if (strcmp (tbo_comic_get_title (tbo->comic), original_title) != 0) + return 4; + if (tbo_page_len (tbo_comic_get_current_page (tbo->comic)) != 1) + return 5; + + tbo_alert_set_test_response (1); + g_action_group_activate_action (G_ACTION_GROUP (tbo->window), "tutorial", NULL); + tbo_alert_clear_test_response (); + + if (tbo_window_has_unsaved_changes (tbo)) + return 6; + if (g_strcmp0 (tbo_comic_get_title (tbo->comic), "tut.tbo") != 0) + return 7; + if (tbo->path != NULL) + return 8; + if (tbo_comic_len (tbo->comic) == 0) + return 9; + + g_free (original_title); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/undo_branch_reset_check.c b/tests/undo_branch_reset_check.c new file mode 100644 index 0000000..427e214 --- /dev/null +++ b/tests/undo_branch_reset_check.c @@ -0,0 +1,52 @@ +#include + +#include "comic.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.undobranchreset", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + + g_signal_emit_by_name (tbo->toolbar->button_new_page, "clicked"); + if (tbo_comic_len (tbo->comic) != 2) + return 3; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 1) + return 4; + + g_signal_emit_by_name (tbo->toolbar->button_new_page, "clicked"); + if (tbo_comic_len (tbo->comic) != 2) + return 7; + if (g_list_length (tbo->undo_stack->first) != 1) + return 6; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 1) + return 8; + if (!tbo->undo_stack->last_flag || tbo->undo_stack->first != tbo->undo_stack->list) + return 9; + + tbo_window_redo_cb (NULL, tbo); + if (tbo_comic_len (tbo->comic) != 2) + return 10; + if (g_list_length (tbo->undo_stack->first) != 1) + return 11; + + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/undo_saved_state_check.c b/tests/undo_saved_state_check.c new file mode 100644 index 0000000..1dbdccd --- /dev/null +++ b/tests/undo_saved_state_check.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "comic.h" +#include "tbo-toolbar.h" +#include "tbo-window.h" + +static gchar * +make_tmp_project_path (const gchar *pattern) +{ + gchar *path = g_build_filename (g_get_tmp_dir (), pattern, NULL); + gint fd = g_mkstemp (path); + + if (fd >= 0) + close (fd); + + g_remove (path); + return g_strconcat (path, ".tbo", NULL); +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + gchar *path; + gchar *basename; + gchar *dirty_title; + + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.undosavedstate", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 2; + + tbo = tbo_new_tbo (app, 800, 450); + path = make_tmp_project_path ("tbo-undo-saved-state-XXXXXX"); + if (!tbo_comic_save (tbo, path)) + return 3; + + basename = g_path_get_basename (path); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), basename) != 0) + return 4; + + g_signal_emit_by_name (tbo->toolbar->button_new_page, "clicked"); + if (!tbo_window_has_unsaved_changes (tbo)) + return 5; + dirty_title = g_strdup_printf ("* %s", basename); + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), dirty_title) != 0) + return 6; + + tbo_window_undo_cb (NULL, tbo); + if (tbo_window_has_unsaved_changes (tbo)) + return 7; + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), basename) != 0) + return 8; + + tbo_window_redo_cb (NULL, tbo); + if (!tbo_window_has_unsaved_changes (tbo)) + return 9; + if (g_strcmp0 (gtk_window_get_title (GTK_WINDOW (tbo->window)), dirty_title) != 0) + return 10; + + g_free (dirty_title); + g_free (basename); + g_remove (path); + g_free (path); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + return 0; +} diff --git a/tests/xml_roundtrip_check.c b/tests/xml_roundtrip_check.c index 2a81eca..3b2b971 100644 --- a/tests/xml_roundtrip_check.c +++ b/tests/xml_roundtrip_check.c @@ -14,12 +14,14 @@ static gchar * build_long_text (void) { - GString *text = g_string_new ("Header <&> \"quoted\"\n"); + GString *text = g_string_new ("\n Header <&> \"quoted\"\n"); gint i; for (i = 0; i < 300; i++) g_string_append_printf (text, "Line %d &\"value\"\n", i); + g_string_append (text, "Tail with spaces \n"); + return g_string_free (text, FALSE); } @@ -53,7 +55,6 @@ main (void) frame = tbo_page_new_frame (page, 0, 0, 300, 200); long_text = build_long_text (); expected_text = g_strdup (long_text); - g_strstrip (expected_text); text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (10, 10, 200, 100, long_text, "Sans 12", &color)); svg = TBO_OBJECT_SVG (tbo_object_svg_new_with_params (5, 5, 20, 20, "assets/& weird \"quote\" .svg")); From 8ec0fc39db7119b978802cf40858c29d273b4750 Mon Sep 17 00:00:00 2001 From: jaime Date: Wed, 22 Apr 2026 15:20:05 +0200 Subject: [PATCH 19/22] Improve doodle asset handling and text editing workflow. Add raster asset support and sizing improvements --- meson.build | 28 ++ src/dnd.c | 109 ++++++- src/doodle-treeview.c | 34 ++- src/tbo-files.c | 45 ++- src/tbo-files.h | 3 +- src/tbo-object-pixmap.c | 16 +- src/tbo-tool-text.c | 25 ++ src/tbo-tool-text.h | 1 + tests/doodle_raster_catalog_check.c | 322 ++++++++++++++++++++ tests/frame_view_coordinate_mapping_check.c | 26 +- tests/keyboard_accessibility_check.c | 13 - tests/raster_render_check.c | 138 +++++++++ 12 files changed, 717 insertions(+), 43 deletions(-) create mode 100644 tests/doodle_raster_catalog_check.c create mode 100644 tests/raster_render_check.c diff --git a/meson.build b/meson.build index ff807c2..f75cba2 100644 --- a/meson.build +++ b/meson.build @@ -399,6 +399,14 @@ assets_browser_search_check = executable( install: false ) +doodle_raster_catalog_check = executable( + 'doodle-raster-catalog-check', + common_sources + files('tests/doodle_raster_catalog_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + export_page_range_check = executable( 'export-page-range-check', common_sources + files('tests/export_page_range_check.c'), @@ -455,6 +463,14 @@ export_formats_check = executable( install: false ) +raster_render_check = executable( + 'raster-render-check', + common_sources + files('tests/raster_render_check.c'), + dependencies: deps, + include_directories: inc, + install: false +) + invalid_tbo_variants_check = executable( 'invalid-tbo-variants-check', common_sources + files('tests/invalid_tbo_variants_check.c'), @@ -847,6 +863,12 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'doodle-raster-catalog-check', + doodle_raster_catalog_check, + env: ['G_DEBUG=fatal-criticals'] +) + test( 'export-page-range-check', export_page_range_check, @@ -889,6 +911,12 @@ test( env: ['G_DEBUG=fatal-criticals'] ) +test( + 'raster-render-check', + raster_render_check, + env: ['G_DEBUG=fatal-criticals'] +) + test( 'invalid-tbo-variants-check', invalid_tbo_variants_check, diff --git a/src/dnd.c b/src/dnd.c index a56b820..61f3643 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -17,6 +17,7 @@ */ +#include #include #include #include @@ -32,6 +33,9 @@ #include "tbo-undo.h" #include "tbo-widget.h" +#define TBO_DND_MAX_FRAME_WIDTH_FRACTION 1.0 +#define TBO_DND_MAX_FRAME_HEIGHT_FRACTION 0.5 + typedef enum { TBO_DND_INSERT_OK, @@ -45,12 +49,113 @@ typedef enum static TboObjectBase * create_asset (const gchar *asset_path, gint x, gint y) { - if (tbo_files_is_svg_file ((gchar *) asset_path)) + if (tbo_files_is_svg_file (asset_path)) return TBO_OBJECT_BASE (tbo_object_svg_new_with_params (x, y, 0, 0, (gchar *) asset_path)); + if (!tbo_files_is_supported_asset_file (asset_path)) + return NULL; + return TBO_OBJECT_BASE (tbo_object_pixmap_new_with_params (x, y, 0, 0, (gchar *) asset_path)); } +static gboolean +get_svg_asset_size (const gchar *asset_path, gint *width, gint *height) +{ + GError *error = NULL; + RsvgHandle *handle; + gdouble width_px = 0.0; + gdouble height_px = 0.0; + gchar *path; + gboolean ok = FALSE; + + path = tbo_files_expand_path (asset_path); + handle = rsvg_handle_new_from_file (path, &error); + if (handle != NULL) + { + ok = rsvg_handle_get_intrinsic_size_in_pixels (handle, &width_px, &height_px) && + width_px > 0.0 && height_px > 0.0; + g_object_unref (handle); + } + if (error != NULL) + g_error_free (error); + g_free (path); + + if (!ok) + return FALSE; + + *width = MAX (1, (gint) ceil (width_px)); + *height = MAX (1, (gint) ceil (height_px)); + return TRUE; +} + +static gboolean +get_pixbuf_asset_size (const gchar *asset_path, gint *width, gint *height) +{ + GdkPixbuf *pixbuf; + GError *error = NULL; + gchar *path; + gboolean ok = FALSE; + + path = tbo_files_expand_path (asset_path); + pixbuf = gdk_pixbuf_new_from_file (path, &error); + if (pixbuf != NULL) + { + *width = gdk_pixbuf_get_width (pixbuf); + *height = gdk_pixbuf_get_height (pixbuf); + ok = *width > 0 && *height > 0; + g_object_unref (pixbuf); + } + if (error != NULL) + g_error_free (error); + g_free (path); + + return ok; +} + +static gboolean +get_asset_size (const gchar *asset_path, gint *width, gint *height) +{ + if (asset_path == NULL || width == NULL || height == NULL) + return FALSE; + + if (tbo_files_is_svg_file (asset_path)) + return get_svg_asset_size (asset_path, width, height); + + return get_pixbuf_asset_size (asset_path, width, height); +} + +static void +apply_initial_asset_size_limit (Frame *frame, TboObjectBase *asset, const gchar *asset_path) +{ + gint asset_width; + gint asset_height; + gint max_width; + gint max_height; + gdouble scale; + + if (frame == NULL || asset == NULL) + return; + if (!get_asset_size (asset_path, &asset_width, &asset_height)) + return; + + max_width = MAX (1, (gint) floor (tbo_frame_get_width (frame) * TBO_DND_MAX_FRAME_WIDTH_FRACTION)); + max_height = MAX (1, (gint) floor (tbo_frame_get_height (frame) * TBO_DND_MAX_FRAME_HEIGHT_FRACTION)); + if (asset_width <= max_width && asset_height <= max_height) + return; + + scale = MIN (max_width / (gdouble) asset_width, + max_height / (gdouble) asset_height); + asset->width = MAX (1, (gint) round (asset_width * scale)); + asset->height = MAX (1, (gint) round (asset_height * scale)); + + asset->x = CLAMP (asset->x - (asset->width / 2), + 0, + MAX (0, tbo_frame_get_width (frame) - asset->width)); + asset->y = CLAMP (asset->y - (asset->height / 2), + 0, + MAX (0, tbo_frame_get_height (frame) - asset->height)); +} + static void select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset); @@ -103,6 +208,8 @@ insert_asset_into_frame (TboWindow *tbo, if (asset == NULL) return TBO_DND_INSERT_INVALID_ASSET; + apply_initial_asset_size_limit (frame, asset, asset_path); + tbo_frame_add_obj (frame, asset); tbo_undo_stack_insert (tbo->undo_stack, tbo_action_object_add_new (frame, asset)); select_inserted_asset (tbo, frame, asset); diff --git a/src/doodle-treeview.c b/src/doodle-treeview.c index c0c46eb..a3a4521 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -34,6 +34,7 @@ typedef struct TboWindow *tbo; GString *path; gboolean top_level; + gboolean bubble_mode; } DoodleExpanderData; typedef struct @@ -46,6 +47,9 @@ typedef struct static GHashTable *THUMB_CACHE = NULL; +#define TBO_BODY_THUMB_MIN_DIM 80 +#define TBO_BODY_THUMB_MAX_DIM 96 + static GdkPixbuf *get_thumbnail_pixbuf (const gchar *path, const gchar *relative_path); static gint compare_gstrings (gconstpointer a, gconstpointer b); static void sort_gstring_array (GArray *arr); @@ -97,10 +101,10 @@ get_thumbnail_pixbuf (const gchar *path, const gchar *relative_path) max_dim = MAX (width, height); is_body = g_strrstr (relative_path, "/body/") != NULL || g_str_has_prefix (relative_path, "body/"); - if (is_body && max_dim < 128) - scale = 128.0 / max_dim; - else if (is_body && max_dim > 160) - scale = 160.0 / max_dim; + if (is_body && max_dim < TBO_BODY_THUMB_MIN_DIM) + scale = (gdouble) TBO_BODY_THUMB_MIN_DIM / max_dim; + else if (is_body && max_dim > TBO_BODY_THUMB_MAX_DIM) + scale = (gdouble) TBO_BODY_THUMB_MAX_DIM / max_dim; else scale = 1.0; @@ -209,6 +213,12 @@ get_files (gchar *base_dir, gboolean isdir, gboolean bubble_mode) } else if (!isdir && !S_ISDIR (filestat.st_mode)) { + if (!tbo_files_is_supported_asset_file (complete_dir)) + { + g_free (complete_dir); + continue; + } + GString *filename_to_append = g_string_new (complete_dir); g_array_append_val (array, filename_to_append); } @@ -347,11 +357,14 @@ static void asset_button_clicked_cb (GtkButton *button, gpointer user_data) { TboWindow *tbo = user_data; - const gchar *asset_path = g_object_get_data (G_OBJECT (button), "tbo-asset-full-path"); + const gchar *asset_path = g_object_get_data (G_OBJECT (button), "tbo-asset-relative-path"); if (tbo == NULL) return; + if (asset_path == NULL) + asset_path = g_object_get_data (G_OBJECT (button), "tbo-asset-full-path"); + if (tbo_dnd_insert_asset_centered (tbo, asset_path) != NULL) gtk_widget_grab_focus (tbo->drawing); } @@ -408,7 +421,8 @@ build_image_grid_internal (TboWindow *tbo, gchar *dir, const gchar *query, gbool thumb_width = gdk_pixbuf_get_width (pixbuf); thumb_height = gdk_pixbuf_get_height (pixbuf); image = tbo_picture_new_for_pixbuf (pixbuf); - gtk_picture_set_can_shrink (GTK_PICTURE (image), FALSE); + gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); + gtk_picture_set_content_fit (GTK_PICTURE (image), GTK_CONTENT_FIT_CONTAIN); gtk_widget_set_size_request (image, thumb_width, thumb_height); button = gtk_button_new (); @@ -507,12 +521,19 @@ on_expand_cb (GtkExpander *expander, GParamSpec *pspec, DoodleExpanderData *data child_data->tbo = data->tbo; child_data->path = g_string_new (subdir->str); child_data->top_level = FALSE; + child_data->bubble_mode = data->bubble_mode; g_signal_connect_data (child_expander, "notify::expanded", G_CALLBACK (on_expand_cb), child_data, free_expander_data, 0); + + if (data->bubble_mode) + { + gtk_expander_set_expanded (GTK_EXPANDER (child_expander), TRUE); + on_expand_cb (GTK_EXPANDER (child_expander), NULL, child_data); + } } free_gstring_array (subdirs); @@ -624,6 +645,7 @@ rebuild_browser_content (DoodleBrowserState *state) expander_data->tbo = state->tbo; expander_data->path = g_string_new (dir->str); expander_data->top_level = TRUE; + expander_data->bubble_mode = state->bubble_mode; g_signal_connect_data (expander, "notify::expanded", G_CALLBACK (on_expand_cb), diff --git a/src/tbo-files.c b/src/tbo-files.c index a7e6a17..1b0f9ac 100644 --- a/src/tbo-files.c +++ b/src/tbo-files.c @@ -21,6 +21,7 @@ #include #include #include +#include #include "tbo-files.h" #include #include "tbo-utils.h" @@ -96,27 +97,39 @@ tbo_files_expand_path (const gchar *source) } gboolean -tbo_files_is_svg_file (char *source) +tbo_files_is_svg_file (const gchar *source) { - gchar **paths; - gchar **ext; - gchar *lower_ext; - gboolean is_svg = FALSE; + const gchar *ext; - paths = g_strsplit (source, ".", 0); + if (source == NULL || *source == '\0') + return FALSE; - ext = paths; - while (*ext) ext++; - ext--; + ext = strrchr (source, '.'); + if (ext == NULL) + return FALSE; - lower_ext = g_ascii_strdown (*ext, -1); + return g_ascii_strcasecmp (ext + 1, "svg") == 0; +} - if (strcmp (lower_ext, "svg") == 0) { - is_svg = TRUE; - } +gboolean +tbo_files_is_supported_asset_file (const gchar *source) +{ + GdkPixbufFormat *format; + gchar *path; + gboolean is_supported = FALSE; + + if (tbo_files_is_svg_file (source)) + return TRUE; + + if (source == NULL || *source == '\0') + return FALSE; + + path = tbo_files_expand_path (source); + format = gdk_pixbuf_get_file_info (path, NULL, NULL); + if (format != NULL) + is_supported = TRUE; - g_strfreev (paths); - g_free (lower_ext); + g_free (path); - return is_svg; + return is_supported; } diff --git a/src/tbo-files.h b/src/tbo-files.h index 1910ba5..cf73bc0 100644 --- a/src/tbo-files.h +++ b/src/tbo-files.h @@ -26,6 +26,7 @@ char **tbo_files_get_dirs (void); int tbo_files_prefix_len (char *str); void tbo_files_free (char **files); gchar *tbo_files_expand_path (const gchar *source); -gboolean tbo_files_is_svg_file (char *source); +gboolean tbo_files_is_svg_file (const gchar *source); +gboolean tbo_files_is_supported_asset_file (const gchar *source); #endif diff --git a/src/tbo-object-pixmap.c b/src/tbo-object-pixmap.c index a9b929a..ff81e21 100644 --- a/src/tbo-object-pixmap.c +++ b/src/tbo-object-pixmap.c @@ -70,9 +70,11 @@ update_surface_cache (TboObjectPixmap *pixmap) int width; int height; int src_stride; + int n_channels; guchar *src; int x; int y; + gboolean has_alpha; if (pixmap->scaled_pixbuf == NULL) return FALSE; @@ -86,8 +88,13 @@ update_surface_cache (TboObjectPixmap *pixmap) width = gdk_pixbuf_get_width (pixmap->scaled_pixbuf); height = gdk_pixbuf_get_height (pixmap->scaled_pixbuf); src_stride = gdk_pixbuf_get_rowstride (pixmap->scaled_pixbuf); + n_channels = gdk_pixbuf_get_n_channels (pixmap->scaled_pixbuf); + has_alpha = gdk_pixbuf_get_has_alpha (pixmap->scaled_pixbuf); src = gdk_pixbuf_get_pixels (pixmap->scaled_pixbuf); + if (n_channels < 3) + return FALSE; + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { @@ -105,10 +112,11 @@ update_surface_cache (TboObjectPixmap *pixmap) for (x = 0; x < width; x++) { - guchar r = row[x * 4 + 0]; - guchar g = row[x * 4 + 1]; - guchar b = row[x * 4 + 2]; - guchar a = row[x * 4 + 3]; + guchar *pixel = row + (x * n_channels); + guchar r = pixel[0]; + guchar g = pixel[1]; + guchar b = pixel[2]; + guchar a = has_alpha && n_channels >= 4 ? pixel[3] : 255; if (a != 255) { diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index 6cc3fa1..d4ddae5 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -45,6 +45,25 @@ static void on_text_end_user_action (GtkTextBuffer *buf, TboToolText *self); static void tbo_tool_text_capture_state (TboToolText *self); static void tbo_tool_text_clear_capture_state (TboToolText *self); static gboolean flush_pending_text_change (gpointer data); +static void tbo_tool_text_focus_editor (TboToolText *self, gboolean select_all); + +static void +tbo_tool_text_focus_editor (TboToolText *self, gboolean select_all) +{ + GtkTextIter start; + GtkTextIter end; + + if (self->text_view == NULL || self->text_buffer == NULL) + return; + + if (select_all) + { + gtk_text_buffer_get_bounds (self->text_buffer, &start, &end); + gtk_text_buffer_select_range (self->text_buffer, &start, &end); + } + + gtk_widget_grab_focus (self->text_view); +} static void tbo_tool_text_capture_state (TboToolText *self) @@ -360,6 +379,7 @@ setup_toolarea (TboToolText *self) scroll = gtk_scrolled_window_new (); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); view = gtk_text_view_new (); + self->text_view = view; g_signal_connect (view, "notify::has-focus", G_CALLBACK (on_tview_focus_changed), self); gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD); @@ -391,6 +411,8 @@ on_unselect (TboToolBase *tool) tbo_tool_text_set_selected (self, NULL); tbo_empty_tool_area (tool->tbo); + self->text_view = NULL; + self->text_buffer = NULL; tbo_window_set_key_binder (tool->tbo, TRUE); } @@ -452,6 +474,8 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) tbo_toolbar_update (tool->tbo->toolbar); } tbo_tool_text_set_selected (self, text); + if (!found) + tbo_tool_text_focus_editor (self, TRUE); tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); } @@ -488,6 +512,7 @@ tbo_tool_text_init (TboToolText *self) self->font = NULL; self->font_size = NULL; self->font_color = NULL; + self->text_view = NULL; self->text_selected = NULL; self->text_buffer = NULL; self->syncing_controls = FALSE; diff --git a/src/tbo-tool-text.h b/src/tbo-tool-text.h index 4dab37f..21a360e 100644 --- a/src/tbo-tool-text.h +++ b/src/tbo-tool-text.h @@ -45,6 +45,7 @@ struct _TboToolText GtkWidget *font; GtkWidget *font_size; GtkWidget *font_color; + GtkWidget *text_view; TboObjectText *text_selected; GtkTextBuffer *text_buffer; gboolean syncing_controls; diff --git a/tests/doodle_raster_catalog_check.c b/tests/doodle_raster_catalog_check.c new file mode 100644 index 0000000..dcf861d --- /dev/null +++ b/tests/doodle_raster_catalog_check.c @@ -0,0 +1,322 @@ +#include +#include +#include +#include + +#include "comic.h" +#include "doodle-treeview.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-pixmap.h" +#include "tbo-widget.h" +#include "tbo-window.h" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +static gboolean +write_test_png (const gchar *path, gboolean has_alpha, gint width, gint height) +{ + GdkPixbuf *pixbuf; + GError *error = NULL; + gboolean ok; + + pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, has_alpha, 8, width, height); + if (pixbuf == NULL) + return FALSE; + + gdk_pixbuf_fill (pixbuf, has_alpha ? 0x4477cc99 : 0x44aaeeff); + ok = gdk_pixbuf_save (pixbuf, path, "png", &error, NULL); + g_object_unref (pixbuf); + + if (!ok) + { + if (error != NULL) + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +static GtkWidget * +find_widget_by_tooltip (GtkWidget *widget, const gchar *tooltip) +{ + GtkWidget *child; + const gchar *current_tooltip = gtk_widget_get_tooltip_text (widget); + + if (current_tooltip != NULL && strcmp (current_tooltip, tooltip) == 0) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_widget_by_tooltip (child, tooltip); + + if (found != NULL) + return found; + } + + return NULL; +} + +static GtkWidget * +find_search_entry (GtkWidget *widget) +{ + GtkWidget *child; + + if (GTK_IS_SEARCH_ENTRY (widget)) + return widget; + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *found = find_search_entry (child); + + if (found != NULL) + return found; + } + + return NULL; +} + +static void +expand_all_expanders (GtkWidget *widget) +{ + GtkWidget *child; + + if (GTK_IS_EXPANDER (widget)) + gtk_expander_set_expanded (GTK_EXPANDER (widget), TRUE); + + for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) + expand_all_expanders (child); +} + +static GtkWidget * +find_asset_button_by_tooltip (GtkWidget *widget, const gchar *tooltip) +{ + GtkWidget *widget_with_tooltip = find_widget_by_tooltip (widget, tooltip); + + if (widget_with_tooltip != NULL && GTK_IS_BUTTON (widget_with_tooltip)) + return widget_with_tooltip; + + return NULL; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + GtkWidget *browser; + GtkWidget *search; + GtkWidget *asset_button; + Frame *body_frame; + Page *page; + Frame *frame; + TboObjectPixmap *pixmap; + gchar *home_dir; + gchar *xdg_data_home; + gchar *doodle_dir; + gchar *legacy_doodle_dir; + gchar *body_doodle_dir; + gchar *legacy_body_doodle_dir; + gchar *visible_png; + gchar *hidden_file; + gchar *legacy_visible_png; + gchar *legacy_hidden_file; + gchar *body_visible_png; + gchar *legacy_body_visible_png; + gchar *save_path; + gchar *contents = NULL; + GtkWidget *body_asset_button; + gint fd; + gint preview_width = 0; + gint preview_height = 0; + + g_setenv ("LC_ALL", "C.UTF-8", TRUE); + g_setenv ("LANGUAGE", "C", TRUE); + + home_dir = g_dir_make_tmp ("tbo-doodle-raster-XXXXXX", NULL); + if (home_dir == NULL) + return 2; + + xdg_data_home = g_build_filename (home_dir, ".local", "share", NULL); + doodle_dir = g_build_filename (xdg_data_home, "tbo", "doodle", "catalog", NULL); + legacy_doodle_dir = g_build_filename (home_dir, ".tbo", "doodle", "catalog", NULL); + body_doodle_dir = g_build_filename (xdg_data_home, "tbo", "doodle", "zorrupe", "body", NULL); + legacy_body_doodle_dir = g_build_filename (home_dir, ".tbo", "doodle", "zorrupe", "body", NULL); + visible_png = g_build_filename (doodle_dir, "zz-raster-visible.png", NULL); + hidden_file = g_build_filename (doodle_dir, "zz-raster-hidden.txt", NULL); + legacy_visible_png = g_build_filename (legacy_doodle_dir, "zz-raster-visible.png", NULL); + legacy_hidden_file = g_build_filename (legacy_doodle_dir, "zz-raster-hidden.txt", NULL); + body_visible_png = g_build_filename (body_doodle_dir, "zz-body-preview.png", NULL); + legacy_body_visible_png = g_build_filename (legacy_body_doodle_dir, "zz-body-preview.png", NULL); + if (g_mkdir_with_parents (doodle_dir, 0700) != 0) + return 3; + if (g_mkdir_with_parents (legacy_doodle_dir, 0700) != 0) + return 4; + if (g_mkdir_with_parents (body_doodle_dir, 0700) != 0) + return 5; + if (g_mkdir_with_parents (legacy_body_doodle_dir, 0700) != 0) + return 6; + if (!write_test_png (visible_png, TRUE, 8, 8)) + return 7; + if (!write_test_png (legacy_visible_png, TRUE, 8, 8)) + return 8; + if (!g_file_set_contents (hidden_file, "not an image", -1, NULL)) + return 9; + if (!g_file_set_contents (legacy_hidden_file, "not an image", -1, NULL)) + return 10; + if (!write_test_png (body_visible_png, TRUE, 1024, 1024)) + return 11; + if (!write_test_png (legacy_body_visible_png, TRUE, 1024, 1024)) + return 12; + + g_setenv ("HOME", home_dir, TRUE); + g_setenv ("XDG_DATA_HOME", xdg_data_home, TRUE); + gtk_init (); + + app = gtk_application_new ("net.danigm.tbo.doodlerastercatalog", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 13; + + tbo = tbo_new_tbo (app, 800, 450); + browser = doodle_setup_tree (tbo, FALSE); + tbo_widget_add_child (tbo->toolarea, browser); + tbo_widget_show_all (browser); + + search = find_search_entry (browser); + if (search == NULL) + return 14; + + gtk_editable_set_text (GTK_EDITABLE (search), "zz body preview"); + drain_events (); + body_asset_button = find_asset_button_by_tooltip (browser, "zorrupe/body/zz-body-preview.png"); + if (body_asset_button == NULL) + return 15; + gtk_widget_get_size_request (body_asset_button, &preview_width, &preview_height); + if (preview_width != 108 || preview_height != 108) + return 16; + + page = tbo_comic_get_current_page (tbo->comic); + body_frame = tbo_page_new_frame (page, 20, 20, 120, 90); + tbo_window_enter_frame (tbo, body_frame); + g_signal_emit_by_name (body_asset_button, "clicked"); + drain_events (); + if (tbo_frame_object_count (body_frame) != 1) + return 17; + pixmap = TBO_OBJECT_PIXMAP (tbo_frame_get_objects (body_frame)->data); + if (strcmp (pixmap->path->str, "zorrupe/body/zz-body-preview.png") != 0) + return 18; + if (TBO_OBJECT_BASE (pixmap)->width != 45 || TBO_OBJECT_BASE (pixmap)->height != 45) + return 19; + + tbo_empty_tool_area (tbo); + browser = doodle_setup_tree (tbo, FALSE); + tbo_widget_add_child (tbo->toolarea, browser); + tbo_widget_show_all (browser); + expand_all_expanders (browser); + drain_events (); + + asset_button = find_asset_button_by_tooltip (browser, "catalog/zz-raster-visible.png"); + if (asset_button == NULL) + return 20; + if (find_asset_button_by_tooltip (browser, "catalog/zz-raster-hidden.txt") != NULL) + return 21; + + frame = tbo_page_new_frame (page, 20, 20, 120, 90); + tbo_window_enter_frame (tbo, frame); + if (tbo_frame_object_count (frame) != 0) + return 22; + + g_signal_emit_by_name (asset_button, "clicked"); + drain_events (); + if (tbo_frame_object_count (frame) != 1) + return 23; + if (!TBO_IS_OBJECT_PIXMAP (tbo_frame_get_objects (frame)->data)) + return 24; + + pixmap = TBO_OBJECT_PIXMAP (tbo_frame_get_objects (frame)->data); + if (strcmp (pixmap->path->str, "catalog/zz-raster-visible.png") != 0) + return 25; + + save_path = g_build_filename (g_get_tmp_dir (), "tbo-doodle-raster-save-XXXXXX.tbo", NULL); + fd = g_mkstemp (save_path); + if (fd < 0) + return 26; + close (fd); + + if (!tbo_comic_save (tbo, save_path)) + return 27; + if (!g_file_get_contents (save_path, &contents, NULL, NULL)) + return 28; + if (strstr (contents, "path=\"catalog/zz-raster-visible.png\"") == NULL) + return 29; + if (strstr (contents, visible_png) != NULL) + return 30; + if (strstr (contents, legacy_visible_png) != NULL) + return 31; + + g_free (contents); + g_remove (save_path); + g_free (save_path); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + drain_events (); + g_object_unref (app); + + g_remove (visible_png); + g_remove (hidden_file); + g_remove (legacy_visible_png); + g_remove (legacy_hidden_file); + g_remove (body_visible_png); + g_remove (legacy_body_visible_png); + g_rmdir (doodle_dir); + g_rmdir (legacy_doodle_dir); + g_rmdir (body_doodle_dir); + g_rmdir (legacy_body_doodle_dir); + g_free (visible_png); + g_free (hidden_file); + g_free (legacy_visible_png); + g_free (legacy_hidden_file); + g_free (body_visible_png); + g_free (legacy_body_visible_png); + g_free (doodle_dir); + g_free (legacy_doodle_dir); + g_free (body_doodle_dir); + g_free (legacy_body_doodle_dir); + + doodle_dir = g_build_filename (xdg_data_home, "tbo", "doodle", "zorrupe", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + doodle_dir = g_build_filename (xdg_data_home, "tbo", "doodle", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + doodle_dir = g_build_filename (xdg_data_home, "tbo", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + + doodle_dir = g_build_filename (home_dir, ".local", "share", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + doodle_dir = g_build_filename (home_dir, ".local", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + + doodle_dir = g_build_filename (home_dir, ".tbo", "doodle", "zorrupe", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + doodle_dir = g_build_filename (home_dir, ".tbo", "doodle", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + doodle_dir = g_build_filename (home_dir, ".tbo", NULL); + g_rmdir (doodle_dir); + g_free (doodle_dir); + + g_free (xdg_data_home); + g_rmdir (home_dir); + g_free (home_dir); + return 0; +} diff --git a/tests/frame_view_coordinate_mapping_check.c b/tests/frame_view_coordinate_mapping_check.c index a50350d..8bdd6c6 100644 --- a/tests/frame_view_coordinate_mapping_check.c +++ b/tests/frame_view_coordinate_mapping_check.c @@ -12,6 +12,12 @@ #include "tbo-toolbar.h" #include "tbo-window.h" +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + int main (void) { @@ -39,16 +45,32 @@ main (void) text_state = TBO_TOOL_TEXT (text_tool); tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_TEXT); text_tool->on_click (text_tool, tbo->drawing, &click_event); + drain_events (); if (tbo_frame_object_count (frame) != 1 || text_state->text_selected == NULL) return 3; + if (text_state->text_view == NULL || gtk_window_get_focus (GTK_WINDOW (tbo->window)) != text_state->text_view) + return 4; + if (text_state->text_buffer != NULL) + { + GtkTextIter start; + GtkTextIter end; + GtkTextIter sel_start; + GtkTextIter sel_end; + + gtk_text_buffer_get_bounds (text_state->text_buffer, &start, &end); + if (!gtk_text_buffer_get_selection_bounds (text_state->text_buffer, &sel_start, &sel_end)) + return 5; + if (gtk_text_iter_compare (&start, &sel_start) != 0 || gtk_text_iter_compare (&end, &sel_end) != 0) + return 6; + } asset_path = g_build_filename (SOURCE_DATA_DIR, "bar", "body", "left-hand.svg", NULL); if (tbo_dnd_insert_asset_at_view_coords (tbo, asset_path, 410, 235) == NULL) - return 4; + return 7; g_free (asset_path); if (tbo_frame_object_count (frame) != 2) - return 5; + return 8; tbo_window_mark_clean (tbo); gtk_window_close (GTK_WINDOW (tbo->window)); diff --git a/tests/keyboard_accessibility_check.c b/tests/keyboard_accessibility_check.c index 4451d0b..1316d8b 100644 --- a/tests/keyboard_accessibility_check.c +++ b/tests/keyboard_accessibility_check.c @@ -35,18 +35,6 @@ find_search_entry (GtkWidget *widget) return NULL; } -static void -expand_all_expanders (GtkWidget *widget) -{ - GtkWidget *child; - - if (GTK_IS_EXPANDER (widget)) - gtk_expander_set_expanded (GTK_EXPANDER (widget), TRUE); - - for (child = gtk_widget_get_first_child (widget); child != NULL; child = gtk_widget_get_next_sibling (child)) - expand_all_expanders (child); -} - static GtkWidget * find_first_asset_button (GtkWidget *widget) { @@ -109,7 +97,6 @@ main (void) search = find_search_entry (browser); if (search == NULL) return 7; - expand_all_expanders (browser); drain_events (); asset_button = find_first_asset_button (browser); diff --git a/tests/raster_render_check.c b/tests/raster_render_check.c new file mode 100644 index 0000000..cfaf7f6 --- /dev/null +++ b/tests/raster_render_check.c @@ -0,0 +1,138 @@ +#include +#include +#include + +#include "comic.h" +#include "dnd.h" +#include "export.h" +#include "frame.h" +#include "page.h" +#include "tbo-object-base.h" +#include "tbo-object-pixmap.h" +#include "tbo-window.h" + +static gboolean +write_test_png (const gchar *path, gboolean has_alpha, gint width, gint height) +{ + GdkPixbuf *pixbuf; + GError *error = NULL; + gboolean ok; + + pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, has_alpha, 8, width, height); + if (pixbuf == NULL) + return FALSE; + + gdk_pixbuf_fill (pixbuf, has_alpha ? 0xaa553388 : 0x33aa55ff); + ok = gdk_pixbuf_save (pixbuf, path, "png", &error, NULL); + g_object_unref (pixbuf); + + if (!ok) + { + if (error != NULL) + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + Page *page; + Frame *frame; + GList *objects; + gint pixmap_count = 0; + gchar *tmpdir; + gchar *rgba_png; + gchar *rgb_png; + gchar *export_base; + gchar *export_png; + GdkPixbuf *exported; + TboObjectBase *rgba_asset; + TboObjectBase *rgb_asset; + gint fd; + + gtk_init (); + + tmpdir = g_dir_make_tmp ("tbo-raster-render-XXXXXX", NULL); + if (tmpdir == NULL) + return 2; + + rgba_png = g_build_filename (tmpdir, "rgba.png", NULL); + rgb_png = g_build_filename (tmpdir, "rgb.png", NULL); + if (!write_test_png (rgba_png, TRUE, 400, 400)) + return 3; + if (!write_test_png (rgb_png, FALSE, 400, 200)) + return 4; + + app = gtk_application_new ("net.danigm.tbo.rasterrender", G_APPLICATION_DEFAULT_FLAGS); + if (!g_application_register (G_APPLICATION (app), NULL, NULL)) + return 5; + + tbo = tbo_new_tbo (app, 800, 450); + page = tbo_comic_get_current_page (tbo->comic); + frame = tbo_page_new_frame (page, 20, 20, 200, 140); + tbo_window_enter_frame (tbo, frame); + + rgba_asset = tbo_dnd_insert_asset (tbo, rgba_png, 20, 20); + if (rgba_asset == NULL) + return 6; + rgb_asset = tbo_dnd_insert_asset (tbo, rgb_png, 80, 20); + if (rgb_asset == NULL) + return 7; + if (tbo_frame_object_count (frame) != 2) + return 8; + if (rgba_asset->width != 70 || rgba_asset->height != 70) + return 9; + if (rgb_asset->width != 140 || rgb_asset->height != 70) + return 10; + + for (objects = tbo_frame_get_objects (frame); objects != NULL; objects = objects->next) + { + if (!TBO_IS_OBJECT_PIXMAP (objects->data)) + return 11; + pixmap_count++; + } + if (pixmap_count != 2) + return 12; + + export_base = g_build_filename (g_get_tmp_dir (), "tbo-raster-render-export-XXXXXX", NULL); + fd = g_mkstemp (export_base); + if (fd < 0) + return 13; + close (fd); + g_remove (export_base); + + if (!tbo_export_file (tbo, export_base, "png", 800, 450)) + return 14; + + export_png = g_strdup_printf ("%s.png", export_base); + if (!g_file_test (export_png, G_FILE_TEST_EXISTS)) + return 15; + + exported = gdk_pixbuf_new_from_file (export_png, NULL); + if (exported == NULL) + return 16; + if (gdk_pixbuf_get_width (exported) != 800 || gdk_pixbuf_get_height (exported) != 450) + return 17; + + g_object_unref (exported); + g_remove (export_png); + g_free (export_png); + g_free (export_base); + tbo_window_mark_clean (tbo); + gtk_window_close (GTK_WINDOW (tbo->window)); + while (g_main_context_iteration (NULL, FALSE)); + g_object_unref (app); + + g_remove (rgba_png); + g_remove (rgb_png); + g_rmdir (tmpdir); + g_free (rgba_png); + g_free (rgb_png); + g_free (tmpdir); + return 0; +} From 0ab86d4f9c3fee761e2cb943160eed22915de445 Mon Sep 17 00:00:00 2001 From: jaime Date: Wed, 22 Apr 2026 16:09:45 +0200 Subject: [PATCH 20/22] Adjust PKGBUILD --- archlinux/PKGBUILD | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index 0a2d0e0..4fa2202 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Jaime pkgname=tbo-git -pkgver=r1.0 +pkgver=2.0+r272.8ec0fc3 pkgrel=1 pkgdesc="Comic editor built with GTK4" arch=('x86_64') @@ -14,7 +14,7 @@ sha256sums=('SKIP') pkgver() { cd TBO - printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" + printf "2.0+r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" } build() { From dee823b3ad35f7ee93bb51c17347e286c066bad5 Mon Sep 17 00:00:00 2001 From: jaime Date: Sun, 26 Apr 2026 19:21:19 +0200 Subject: [PATCH 21/22] Lowered gtk-4 requirements to make this app more compatible with more systems --- README | 40 +++++++-- archlinux/PKGBUILD | 12 +-- debian/control | 6 +- debian/rules | 4 +- meson.build | 31 +++++-- meson_options.txt | 34 ++++++++ src/doodle-treeview.c | 11 ++- src/tbo-file-dialog.c | 184 +++++++++++++++++++++++++++++++++++----- src/tbo-tool-selector.c | 19 ++--- src/tbo-tool-text.c | 47 +++++----- src/tbo-toolbar.c | 3 +- src/tbo-ui-utils.c | 94 ++++++++++++++++++++ src/tbo-ui-utils.h | 7 ++ src/tbo-widget.c | 78 +++++++++++++++++ 14 files changed, 477 insertions(+), 93 deletions(-) create mode 100644 meson_options.txt diff --git a/README b/README index b02aa7d..04127b8 100644 --- a/README +++ b/README @@ -6,21 +6,37 @@ Building TBO now uses Meson as its build system and GTK4 as its UI toolkit. -Required tools and libraries: +Base requirements: * meson * ninja * pkg-config - * gtk4 + * gtk4 (4.0 or newer) * cairo * librsvg-2.0 - * gettext -Build it from a fresh checkout with: +Optional build extras: + + * gettext (translations) + * xvfb and xauth (headless test runs) + +Build it from a fresh checkout with the default feature set: meson setup build meson compile -C build +For a lighter, more portable local build that skips translations and the test +suite: + + meson setup build -Dnls=false -Dtests=false + meson compile -C build + +For a minimal install that still keeps the bundled `tut.tbo` sample, but skips +AppStream metadata, the legacy `pixmaps` icon, and the tutorial PDF: + + meson setup build -Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false + meson compile -C build + Run the application with: ./build/tbo @@ -40,8 +56,13 @@ Run the smoke-test suite with: Translations ------------ -Translations are stored in `po/` and built automatically by Meson when gettext -tools are available. +Translations are stored in `po/` and can be disabled with `-Dnls=false`. + +Tests +----- + +The smoke-test suite can be disabled with `-Dtests=false` when packaging for a +minimal environment or when no headless GTK runner is available. Packaging --------- @@ -51,8 +72,11 @@ The repository includes packaging metadata for: * Arch Linux: `archlinux/PKGBUILD` * Debian-based systems: `debian/` -These files are intended to install the desktop file, icons and locale data in -their standard system locations. +These files are intended to install the desktop file and icons in their standard +system locations. + +To keep packaging lighter, the bundled Arch and Debian recipes now configure +Meson with `-Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false`. Using TBO --------- diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index 4fa2202..8fb1c48 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -8,7 +8,7 @@ arch=('x86_64') url="https://github.com/j4imefoo/TBO" license=('GPL3') depends=('gtk4' 'cairo' 'librsvg') -makedepends=('git' 'meson' 'ninja' 'pkgconf' 'gettext') +makedepends=('git' 'meson' 'ninja' 'pkgconf') source=("git+https://github.com/j4imefoo/TBO.git") sha256sums=('SKIP') @@ -18,18 +18,10 @@ pkgver() { } build() { - arch-meson TBO build + arch-meson TBO build -Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false meson compile -C build } -check() { - if command -v xvfb-run >/dev/null 2>&1; then - xvfb-run -a meson test -C build --no-rebuild --print-errorlogs --num-processes 1 - else - echo "==> Skipping tests: xvfb-run not available" - fi -} - package() { meson install -C build --destdir "$pkgdir" } diff --git a/debian/control b/debian/control index 93d21b0..2c05191 100644 --- a/debian/control +++ b/debian/control @@ -6,13 +6,9 @@ Build-Depends: debhelper-compat (= 13), meson, pkgconf, - gettext, libgtk-4-dev, libcairo2-dev, - librsvg2-dev, - desktop-file-utils, - xvfb, - xauth + librsvg2-dev Standards-Version: 4.7.0 Rules-Requires-Root: no Homepage: https://github.com/j4imefoo/TBO diff --git a/debian/rules b/debian/rules index 721f9d3..b31ca2f 100755 --- a/debian/rules +++ b/debian/rules @@ -3,5 +3,5 @@ %: dh $@ --buildsystem=meson -override_dh_auto_test: - xvfb-run -a meson test -C obj-$(DEB_HOST_GNU_TYPE) --no-rebuild --print-errorlogs --num-processes 1 +override_dh_auto_configure: + dh_auto_configure -- -Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false diff --git a/meson.build b/meson.build index f75cba2..65aec09 100644 --- a/meson.build +++ b/meson.build @@ -5,11 +5,13 @@ project( default_options: ['c_std=gnu11'] ) -subdir('po') +if get_option('nls') + subdir('po') +endif cc = meson.get_compiler('c') -gtk_dep = dependency('gtk4', version: '>= 4.22') +gtk_dep = dependency('gtk4', version: '>= 4.0') cairo_dep = dependency('cairo') rsvg_dep = dependency('librsvg-2.0') math_dep = cc.find_library('m', required: false) @@ -22,7 +24,7 @@ locale_dir = join_paths(get_option('prefix'), get_option('localedir')) conf = configuration_data() conf.set_quoted('GETTEXT_PACKAGE', 'tbo') conf.set_quoted('VERSION', meson.project_version()) -conf.set10('ENABLE_NLS', true) +conf.set10('ENABLE_NLS', get_option('nls')) configure_file(output: 'config.h', configuration: conf) configured_icon_png = configure_file(input: 'data/icon.png', output: 'tbo.png', copy: true) @@ -79,6 +81,7 @@ executable( install: true ) +if get_option('tests') load_render_check = executable( 'load-render-check', common_sources + files('tests/load_render_check.c'), @@ -1025,11 +1028,16 @@ test( recover_file_failure_check, env: ['G_DEBUG=fatal-criticals'] ) +endif + +installable_data_files = ['data/tut.tbo', 'data/icon.png'] + +if get_option('tutorial_pdf') + installable_data_files += 'data/tutorial.pdf' +endif install_data( - 'data/tutorial.pdf', - 'data/tut.tbo', - 'data/icon.png', + installable_data_files, install_dir: join_paths(get_option('datadir'), 'tbo') ) @@ -1038,7 +1046,14 @@ install_subdir('data/icons', install_dir: join_paths(get_option('datadir'), 'tbo install_subdir('data/doodle', install_dir: join_paths(get_option('datadir'), 'tbo')) install_data('data/applications/net.danigm.tbo.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) -install_data('data/metainfo/net.danigm.tbo.metainfo.xml', install_dir: join_paths(get_option('datadir'), 'metainfo')) -install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'pixmaps')) + +if get_option('appstream') + install_data('data/metainfo/net.danigm.tbo.metainfo.xml', install_dir: join_paths(get_option('datadir'), 'metainfo')) +endif + +if get_option('legacy_pixmap_icon') + install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'pixmaps')) +endif + install_data('data/tbo.svg', install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps')) install_data(configured_icon_png, install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '128x128', 'apps')) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..7a4fb98 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,34 @@ +option( + 'nls', + type: 'boolean', + value: true, + description: 'Build translation catalogs' +) + +option( + 'tests', + type: 'boolean', + value: true, + description: 'Build and register the smoke-test suite' +) + +option( + 'appstream', + type: 'boolean', + value: true, + description: 'Install AppStream metadata' +) + +option( + 'legacy_pixmap_icon', + type: 'boolean', + value: true, + description: 'Install the legacy pixmaps icon copy' +) + +option( + 'tutorial_pdf', + type: 'boolean', + value: true, + description: 'Install the bundled tutorial PDF' +) diff --git a/src/doodle-treeview.c b/src/doodle-treeview.c index a3a4521..5f9fa6c 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -27,6 +27,7 @@ #include "doodle-treeview.h" #include "dnd.h" #include "tbo-files.h" +#include "tbo-ui-utils.h" #include "tbo-widget.h" typedef struct @@ -422,7 +423,7 @@ build_image_grid_internal (TboWindow *tbo, gchar *dir, const gchar *query, gbool thumb_height = gdk_pixbuf_get_height (pixbuf); image = tbo_picture_new_for_pixbuf (pixbuf); gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); - gtk_picture_set_content_fit (GTK_PICTURE (image), GTK_CONTENT_FIT_CONTAIN); + tbo_picture_set_contain (GTK_PICTURE (image)); gtk_widget_set_size_request (image, thumb_width, thumb_height); button = gtk_button_new (); @@ -695,8 +696,16 @@ doodle_setup_tree (TboWindow *tbo, gboolean bubble_mode) gtk_widget_set_margin_end (search_entry, 4); gtk_widget_set_margin_top (search_entry, 4); gtk_widget_set_margin_bottom (search_entry, 4); +#if GTK_CHECK_VERSION(4, 10, 0) gtk_search_entry_set_placeholder_text (GTK_SEARCH_ENTRY (search_entry), bubble_mode ? _("Search Bubbles") : _("Search Assets")); +#else + if (g_object_class_find_property (G_OBJECT_GET_CLASS (search_entry), "placeholder-text") != NULL) + g_object_set (search_entry, + "placeholder-text", + bubble_mode ? _("Search Bubbles") : _("Search Assets"), + NULL); +#endif content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); tbo_widget_add_child (root, search_entry); diff --git a/src/tbo-file-dialog.c b/src/tbo-file-dialog.c index 0344f65..5a3018c 100644 --- a/src/tbo-file-dialog.c +++ b/src/tbo-file-dialog.c @@ -17,6 +17,10 @@ struct file_dialog_data { GError *error; }; +static gchar *finish_dialog (struct file_dialog_data *data); + +#if GTK_CHECK_VERSION(4, 10, 0) + static void file_open_cb (GObject *source, GAsyncResult *result, gpointer user_data) { @@ -49,8 +53,6 @@ create_dialog (const gchar *title, TboWindow *window, const gchar *accept_label) return dialog; } -static gchar *finish_dialog (struct file_dialog_data *data); - static gchar * run_open_dialog (GtkFileDialog *dialog, TboWindow *window) { @@ -73,23 +75,6 @@ run_save_dialog (GtkFileDialog *dialog, TboWindow *window) return finish_dialog (&data); } -static gchar * -finish_dialog (struct file_dialog_data *data) -{ - gchar *path = NULL; - - if (data->file != NULL) - { - path = g_file_get_path (data->file); - g_object_unref (data->file); - } - - if (data->error != NULL) - g_error_free (data->error); - g_main_loop_unref (data->loop); - return path; -} - static GListStore * create_project_filters (void) { @@ -120,9 +105,107 @@ set_initial_folder (GtkFileDialog *dialog, gchar *dirname) g_free (dirname); } +#else + +static void +file_response_cb (GtkNativeDialog *source, gint response, gpointer user_data) +{ + struct file_dialog_data *data = user_data; + + if (response == GTK_RESPONSE_ACCEPT) + data->file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (source)); + g_main_loop_quit (data->loop); +} + +static GtkFileChooserNative * +create_dialog (const gchar *title, TboWindow *window, const gchar *accept_label, GtkFileChooserAction action) +{ + return gtk_file_chooser_native_new (title, + GTK_WINDOW (window->window), + action, + accept_label, + _("_Cancel")); +} + +static gchar * +run_dialog (GtkFileChooserNative *dialog) +{ + struct file_dialog_data data = {0}; + + data.loop = g_main_loop_new (NULL, FALSE); + g_signal_connect (dialog, "response", G_CALLBACK (file_response_cb), &data); + gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog)); + g_main_loop_run (data.loop); + return finish_dialog (&data); +} + +static void +add_project_filters (GtkFileChooser *chooser) +{ + GtkFileFilter *filter = gtk_file_filter_new (); + + gtk_file_filter_set_name (filter, _("TBO files")); + gtk_file_filter_add_pattern (filter, "*.tbo"); + gtk_file_chooser_add_filter (chooser, filter); + g_object_unref (filter); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("All files")); + gtk_file_filter_add_pattern (filter, "*"); + gtk_file_chooser_add_filter (chooser, filter); + g_object_unref (filter); +} + +static void +add_image_filters (GtkFileChooser *chooser) +{ + GtkFileFilter *filter = gtk_file_filter_new (); + + gtk_file_filter_set_name (filter, _("Image files")); + gtk_file_filter_add_mime_type (filter, "image/*"); + gtk_file_chooser_add_filter (chooser, filter); + g_object_unref (filter); + + filter = gtk_file_filter_new (); + gtk_file_filter_set_name (filter, _("All files")); + gtk_file_filter_add_pattern (filter, "*"); + gtk_file_chooser_add_filter (chooser, filter); + g_object_unref (filter); +} + +static void +set_initial_folder (GtkFileChooserNative *dialog, gchar *dirname) +{ + GFile *folder = g_file_new_for_path (dirname); + + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), folder, NULL); + g_object_unref (folder); + g_free (dirname); +} + +#endif + +static gchar * +finish_dialog (struct file_dialog_data *data) +{ + gchar *path = NULL; + + if (data->file != NULL) + { + path = g_file_get_path (data->file); + g_object_unref (data->file); + } + + if (data->error != NULL) + g_error_free (data->error); + g_main_loop_unref (data->loop); + return path; +} + gchar * tbo_file_dialog_open_project (TboWindow *window) { +#if GTK_CHECK_VERSION(4, 10, 0) GtkFileDialog *dialog = create_dialog (_("Open"), window, _("_Open")); GListStore *filters = create_project_filters (); gchar *path; @@ -134,11 +217,23 @@ tbo_file_dialog_open_project (TboWindow *window) g_object_unref (filters); g_object_unref (dialog); return path; +#else + GtkFileChooserNative *dialog = create_dialog (_("Open"), window, _("_Open"), GTK_FILE_CHOOSER_ACTION_OPEN); + gchar *path; + + add_project_filters (GTK_FILE_CHOOSER (dialog)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + path = run_dialog (dialog); + + g_object_unref (dialog); + return path; +#endif } gchar * tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) { +#if GTK_CHECK_VERSION(4, 10, 0) GtkFileDialog *dialog = create_dialog (_("Save As"), window, _("_Save")); GListStore *filters = create_project_filters (); gchar *path; @@ -152,11 +247,25 @@ tbo_file_dialog_save_project (TboWindow *window, const gchar *suggested_name) g_object_unref (filters); g_object_unref (dialog); return path; +#else + GtkFileChooserNative *dialog = create_dialog (_("Save As"), window, _("_Save"), GTK_FILE_CHOOSER_ACTION_SAVE); + gchar *path; + + add_project_filters (GTK_FILE_CHOOSER (dialog)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + if (suggested_name != NULL && *suggested_name != '\0') + gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), suggested_name); + path = run_dialog (dialog); + + g_object_unref (dialog); + return path; +#endif } gchar * tbo_file_dialog_open_image (TboWindow *window) { +#if GTK_CHECK_VERSION(4, 10, 0) GtkFileDialog *dialog = create_dialog (_("Add Image"), window, _("_Open")); GListStore *filters = g_list_store_new (GTK_TYPE_FILE_FILTER); GtkFileFilter *filter = gtk_file_filter_new (); @@ -180,11 +289,23 @@ tbo_file_dialog_open_image (TboWindow *window) g_object_unref (filters); g_object_unref (dialog); return path; +#else + GtkFileChooserNative *dialog = create_dialog (_("Add Image"), window, _("_Open"), GTK_FILE_CHOOSER_ACTION_OPEN); + gchar *path; + + add_image_filters (GTK_FILE_CHOOSER (dialog)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + path = run_dialog (dialog); + + g_object_unref (dialog); + return path; +#endif } gchar * tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) { +#if GTK_CHECK_VERSION(4, 10, 0) GtkFileDialog *dialog = create_dialog (_("Export"), window, _("_Save")); gchar *path; @@ -208,4 +329,29 @@ tbo_file_dialog_save_export (TboWindow *window, const gchar *current_text) g_object_unref (dialog); return path; +#else + GtkFileChooserNative *dialog = create_dialog (_("Export"), window, _("_Save"), GTK_FILE_CHOOSER_ACTION_SAVE); + gchar *path; + + set_initial_folder (dialog, tbo_window_get_export_dir (window)); + + if (current_text != NULL && *current_text != '\0') + { + if (g_path_is_absolute (current_text)) + { + GFile *file = g_file_new_for_path (current_text); + gtk_file_chooser_set_file (GTK_FILE_CHOOSER (dialog), file, NULL); + g_object_unref (file); + } + else + { + gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), current_text); + } + } + + path = run_dialog (dialog); + + g_object_unref (dialog); + return path; +#endif } diff --git a/src/tbo-tool-selector.c b/src/tbo-tool-selector.c index 12b2b13..18a53cf 100644 --- a/src/tbo-tool-selector.c +++ b/src/tbo-tool-selector.c @@ -233,14 +233,14 @@ update_color_cb (GtkWidget *button, GParamSpec *pspec, TboToolSelector *tool) if (tool->resizing || tool->clicked || tool->selected_frame == NULL) return; - const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (button)); + GdkRGBA color = tbo_color_picker_get_rgba (button); tbo_frame_get_color (tool->selected_frame, ¤t_color); - if (gdk_rgba_equal (¤t_color, color)) + if (gdk_rgba_equal (¤t_color, &color)) return; border = tbo_frame_get_border (tool->selected_frame); - tbo_frame_set_color (tool->selected_frame, (GdkRGBA *) color); + tbo_frame_set_color (tool->selected_frame, &color); tbo_undo_stack_insert (tbo->undo_stack, tbo_action_frame_state_new (tool->selected_frame, tbo_frame_get_x (tool->selected_frame), @@ -256,9 +256,9 @@ update_color_cb (GtkWidget *button, GParamSpec *pspec, TboToolSelector *tool) tbo_frame_get_width (tool->selected_frame), tbo_frame_get_height (tool->selected_frame), border, - color->red, - color->green, - color->blue)); + color.red, + color.green, + color.blue)); tbo_window_mark_dirty (tbo); tbo_toolbar_update (tbo->toolbar); tbo_drawing_update (drawing); @@ -312,7 +312,6 @@ update_tool_area (TboToolSelector *self) GtkWidget *hpanel; GtkWidget *label; GdkRGBA gdk_color = { 0, 0, 0, 1 }; - GtkColorDialog *color_dialog; int frame_x, frame_y, frame_width, frame_height; tbo_frame_get_bounds (self->selected_frame, &frame_x, &frame_y, &frame_width, &frame_height); @@ -338,9 +337,7 @@ update_tool_area (TboToolSelector *self) label = gtk_label_new (_("Background color: ")); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_label_set_yalign (GTK_LABEL (label), 0.5); - color_dialog = gtk_color_dialog_new (); - self->color_button = gtk_color_dialog_button_new (color_dialog); - gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); + self->color_button = tbo_color_picker_new (&gdk_color); tbo_box_pack_start (hpanel, label, TRUE, TRUE, 5); tbo_box_pack_start (hpanel, self->color_button, TRUE, TRUE, 5); @@ -360,7 +357,7 @@ update_tool_area (TboToolSelector *self) gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_w), frame_width); gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_h), frame_height); gtk_check_button_set_active (GTK_CHECK_BUTTON (self->border_button), tbo_frame_get_border (self->selected_frame)); - gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->color_button), &gdk_color); + tbo_color_picker_set_rgba (self->color_button, &gdk_color); } static void diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index d4ddae5..7b47246 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -21,6 +21,7 @@ #include "tbo-window.h" #include "tbo-widget.h" #include "tbo-drawing.h" +#include "tbo-ui-utils.h" #include "tbo-object-base.h" #include "tbo-tool-selector.h" #include "tbo-tool-text.h" @@ -293,16 +294,16 @@ on_color_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) gchar *old_text = g_strdup (tbo_object_text_get_text (self->text_selected)); gchar *old_font = tbo_object_text_get_string (self->text_selected); GdkRGBA old_color = *self->text_selected->font_color; - const GdkRGBA *color = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color)); + GdkRGBA color = tbo_color_picker_get_rgba (self->font_color); - if (gdk_rgba_equal (&old_color, color)) + if (gdk_rgba_equal (&old_color, &color)) { g_free (old_text); g_free (old_font); return; } - tbo_object_text_change_color (self->text_selected, (GdkRGBA *) color); + tbo_object_text_change_color (self->text_selected, &color); tbo_undo_stack_insert (tbo->undo_stack, tbo_action_text_state_new (self->text_selected, old_text, @@ -310,7 +311,7 @@ on_color_change (GtkWidget *widget, GParamSpec *pspec, TboToolText *self) &old_color, old_text, old_font, - color)); + &color)); g_free (old_text); g_free (old_font); tbo_window_mark_dirty (tbo); @@ -330,8 +331,6 @@ setup_toolarea (TboToolText *self) GtkWidget *scroll; GtkWidget *view; GtkAdjustment *font_size_adjustment; - GtkFontDialog *font_dialog; - GtkColorDialog *color_dialog; GdkRGBA default_color = { 0, 0, 0, 1 }; gtk_label_set_xalign (GTK_LABEL (font_label), 0.0); @@ -341,9 +340,7 @@ setup_toolarea (TboToolText *self) gtk_label_set_xalign (GTK_LABEL (font_color_label), 0.0); gtk_label_set_yalign (GTK_LABEL (font_color_label), 0.5); - font_dialog = gtk_font_dialog_new (); - self->font = gtk_font_dialog_button_new (font_dialog); - gtk_font_dialog_button_set_use_size (GTK_FONT_DIALOG_BUTTON (self->font), FALSE); + self->font = tbo_font_picker_new (); g_signal_connect (self->font, "notify::font-desc", G_CALLBACK (on_font_change), self); font_size_adjustment = gtk_adjustment_new (27, 1, 300, 1, 5, 0); @@ -352,10 +349,8 @@ setup_toolarea (TboToolText *self) gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (self->font_size), TRUE); g_signal_connect (self->font_size, "value-changed", G_CALLBACK (on_font_size_change), self); - color_dialog = gtk_color_dialog_new (); - self->font_color = gtk_color_dialog_button_new (color_dialog); + self->font_color = tbo_color_picker_new (&default_color); g_signal_connect (self->font_color, "notify::rgba", G_CALLBACK (on_color_change), self); - gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), &default_color); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5); @@ -461,7 +456,7 @@ on_click (TboToolBase *tool, GtkWidget *widget, TboPointerEvent *event) return; gchar *font = tbo_tool_text_build_font (self); - color = *gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color)); + color = tbo_color_picker_get_rgba (self->font_color); text = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, 100, 0, _("Text"), font, @@ -580,8 +575,9 @@ tbo_tool_text_get_font_name (TboToolText *self) if (self->font) { - const PangoFontDescription *font = gtk_font_dialog_button_get_font_desc (GTK_FONT_DIALOG_BUTTON (self->font)); - pango_font = pango_font_description_copy (font); + pango_font = tbo_font_picker_dup_font_desc (self->font); + if (pango_font == NULL) + return NULL; gchar *family = g_strdup (pango_font_description_get_family (pango_font)); pango_font_description_free (pango_font); return family; @@ -602,24 +598,19 @@ tbo_tool_text_get_font_size (TboToolText *self) static gchar * tbo_tool_text_build_font (TboToolText *self) { - gchar *font_string; - PangoFontDescription *description; + PangoFontDescription *description = NULL; gchar *result; if (self->font) - { - const PangoFontDescription *font = gtk_font_dialog_button_get_font_desc (GTK_FONT_DIALOG_BUTTON (self->font)); - font_string = pango_font_description_to_string (font); - } - else - font_string = g_strdup (DEFAULT_PANGO_FONT); + description = tbo_font_picker_dup_font_desc (self->font); + + if (description == NULL) + description = pango_font_description_from_string (DEFAULT_PANGO_FONT); - description = pango_font_description_from_string (font_string); pango_font_description_set_size (description, tbo_tool_text_get_font_size (self) * PANGO_SCALE); result = pango_font_description_to_string (description); pango_font_description_free (description); - g_free (font_string); return result; } @@ -639,7 +630,7 @@ tbo_tool_text_sync_font_controls (TboToolText *self, const gchar *font_string) else size /= PANGO_SCALE; - gtk_font_dialog_button_set_font_desc (GTK_FONT_DIALOG_BUTTON (self->font), description); + tbo_font_picker_set_font_desc (self->font, description); gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->font_size), size); pango_font_description_free (description); @@ -672,7 +663,7 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) if (self->font_color) { GdkRGBA default_color = { 0, 0, 0, 1 }; - gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), &default_color); + tbo_color_picker_set_rgba (self->font_color, &default_color); } self->syncing_controls = FALSE; return; @@ -683,7 +674,7 @@ tbo_tool_text_set_selected (TboToolText *self, TboObjectText *text) gchar *font = tbo_object_text_get_string (text); tbo_tool_text_sync_font_controls (self, font); g_free (font); - gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (self->font_color), text->font_color); + tbo_color_picker_set_rgba (self->font_color, text->font_color); gtk_text_buffer_set_text (self->text_buffer, str, -1); self->text_selected = g_object_ref (text); self->syncing_controls = FALSE; diff --git a/src/tbo-toolbar.c b/src/tbo-toolbar.c index 68b8fbf..c4907f5 100644 --- a/src/tbo-toolbar.c +++ b/src/tbo-toolbar.c @@ -36,6 +36,7 @@ #include "tbo-tool-doodle.h" #include "tbo-tool-bubble.h" #include "tbo-tool-text.h" +#include "tbo-ui-utils.h" #include "ui-menu.h" #include "tbo-undo.h" #include "tbo-widget.h" @@ -74,7 +75,7 @@ create_icon_from_file (const gchar *path) image = gtk_picture_new_for_filename (path); gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); - gtk_picture_set_content_fit (GTK_PICTURE (image), GTK_CONTENT_FIT_CONTAIN); + tbo_picture_set_contain (GTK_PICTURE (image)); gtk_widget_set_halign (image, GTK_ALIGN_CENTER); gtk_widget_set_valign (image, GTK_ALIGN_CENTER); diff --git a/src/tbo-ui-utils.c b/src/tbo-ui-utils.c index 45d5d55..18a7402 100644 --- a/src/tbo-ui-utils.c +++ b/src/tbo-ui-utils.c @@ -41,3 +41,97 @@ add_spin_with_label (GtkWidget *container, const gchar *string, gint value) return spin; } + +GtkWidget * +tbo_font_picker_new (void) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + GtkFontDialog *dialog = gtk_font_dialog_new (); + GtkWidget *picker = gtk_font_dialog_button_new (dialog); + + gtk_font_dialog_button_set_use_size (GTK_FONT_DIALOG_BUTTON (picker), FALSE); +#else + GtkWidget *picker = gtk_font_button_new (); + + gtk_font_button_set_use_size (GTK_FONT_BUTTON (picker), FALSE); +#endif + + return picker; +} + +PangoFontDescription * +tbo_font_picker_dup_font_desc (GtkWidget *picker) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + const PangoFontDescription *font = gtk_font_dialog_button_get_font_desc (GTK_FONT_DIALOG_BUTTON (picker)); + + if (font == NULL) + return NULL; + + return pango_font_description_copy (font); +#else + return gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (picker)); +#endif +} + +void +tbo_font_picker_set_font_desc (GtkWidget *picker, const PangoFontDescription *description) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + gtk_font_dialog_button_set_font_desc (GTK_FONT_DIALOG_BUTTON (picker), description); +#else + gtk_font_chooser_set_font_desc (GTK_FONT_CHOOSER (picker), description); +#endif +} + +GtkWidget * +tbo_color_picker_new (const GdkRGBA *rgba) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + GtkColorDialog *dialog = gtk_color_dialog_new (); + GtkWidget *picker = gtk_color_dialog_button_new (dialog); + +#else + GtkWidget *picker = gtk_color_button_new (); +#endif + + tbo_color_picker_set_rgba (picker, rgba); + return picker; +} + +GdkRGBA +tbo_color_picker_get_rgba (GtkWidget *picker) +{ + GdkRGBA color = { 0, 0, 0, 1 }; + +#if GTK_CHECK_VERSION(4, 10, 0) + const GdkRGBA *selected = gtk_color_dialog_button_get_rgba (GTK_COLOR_DIALOG_BUTTON (picker)); + + if (selected != NULL) + color = *selected; +#else + gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (picker), &color); +#endif + + return color; +} + +void +tbo_color_picker_set_rgba (GtkWidget *picker, const GdkRGBA *rgba) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (picker), rgba); +#else + gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (picker), rgba); +#endif +} + +void +tbo_picture_set_contain (GtkPicture *picture) +{ +#if GTK_CHECK_VERSION(4, 8, 0) + gtk_picture_set_content_fit (picture, GTK_CONTENT_FIT_CONTAIN); +#else + gtk_picture_set_keep_aspect_ratio (picture, TRUE); +#endif +} diff --git a/src/tbo-ui-utils.h b/src/tbo-ui-utils.h index 26ba075..9ef47b1 100644 --- a/src/tbo-ui-utils.h +++ b/src/tbo-ui-utils.h @@ -22,5 +22,12 @@ #include GtkWidget * add_spin_with_label (GtkWidget *container, const gchar *string, gint value); +GtkWidget * tbo_font_picker_new (void); +PangoFontDescription * tbo_font_picker_dup_font_desc (GtkWidget *picker); +void tbo_font_picker_set_font_desc (GtkWidget *picker, const PangoFontDescription *description); +GtkWidget * tbo_color_picker_new (const GdkRGBA *rgba); +GdkRGBA tbo_color_picker_get_rgba (GtkWidget *picker); +void tbo_color_picker_set_rgba (GtkWidget *picker, const GdkRGBA *rgba); +void tbo_picture_set_contain (GtkPicture *picture); #endif diff --git a/src/tbo-widget.c b/src/tbo-widget.c index 40119b4..8b99b9e 100644 --- a/src/tbo-widget.c +++ b/src/tbo-widget.c @@ -21,6 +21,7 @@ struct alert_run_data { static gint alert_test_response = TBO_ALERT_TEST_RESPONSE_NONE; static void +#if GTK_CHECK_VERSION(4, 10, 0) alert_response_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GtkAlertDialog *dialog = GTK_ALERT_DIALOG (source); @@ -35,6 +36,12 @@ alert_response_cb (GObject *source, GAsyncResult *result, gpointer user_data) } g_main_loop_quit (data->loop); } +#else +alert_response_cb (GtkButton *button, GtkWindow *dialog) +{ + tbo_dialog_button_cb (button, dialog); +} +#endif GtkWidget * tbo_widget_get_first_child (GtkWidget *widget) @@ -210,6 +217,7 @@ tbo_alert_choose (GtkWindow *parent, gint cancel_button, gint default_button) { +#if GTK_CHECK_VERSION(4, 10, 0) GtkAlertDialog *dialog; struct alert_run_data data; @@ -230,6 +238,76 @@ tbo_alert_choose (GtkWindow *parent, g_object_unref (dialog); return data.response; +#else + GtkWidget *dialog; + GtkWidget *headerbar; + GtkWidget *content; + GtkWidget *label; + GtkWidget *actions; + TboDialogRunData data; + gint response; + + if (alert_test_response != TBO_ALERT_TEST_RESPONSE_NONE) + return alert_test_response; + + dialog = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (dialog), message); + if (parent != NULL) + gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); + + headerbar = gtk_header_bar_new (); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (headerbar), TRUE); + gtk_window_set_titlebar (GTK_WINDOW (dialog), headerbar); + + content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class (content, "tbo-dialog-content"); + gtk_widget_set_margin_start (content, 12); + gtk_widget_set_margin_end (content, 12); + gtk_widget_set_margin_top (content, 12); + gtk_widget_set_margin_bottom (content, 12); + tbo_widget_add_child (dialog, content); + + label = gtk_label_new (message); + gtk_label_set_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + tbo_widget_add_child (content, label); + + if (detail != NULL && *detail != '\0') + { + label = gtk_label_new (detail); + gtk_widget_add_css_class (label, "dim-label"); + gtk_label_set_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + tbo_widget_add_child (content, label); + } + + actions = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_halign (actions, GTK_ALIGN_END); + tbo_widget_add_child (content, actions); + + for (gint i = 0; buttons[i] != NULL; i++) + { + GtkWidget *button = gtk_button_new_with_label (buttons[i]); + + if (i == default_button) + gtk_widget_add_css_class (button, "suggested-action"); + g_object_set_data (G_OBJECT (button), "tbo-response", GINT_TO_POINTER (i)); + g_signal_connect (button, "clicked", G_CALLBACK (alert_response_cb), dialog); + tbo_widget_add_child (actions, button); + } + + tbo_dialog_run_data_init (&data, cancel_button); + g_signal_connect (dialog, "close-request", G_CALLBACK (tbo_dialog_close_request_cb), &data); + response = tbo_dialog_run (GTK_WINDOW (dialog), &data); + gtk_window_destroy (GTK_WINDOW (dialog)); + tbo_dialog_run_data_clear (&data); + + return response; +#endif } void From 43a669fe0cda58903a5d6fb910503d914b5a1ccf Mon Sep 17 00:00:00 2001 From: jaime Date: Sun, 26 Apr 2026 19:43:15 +0200 Subject: [PATCH 22/22] Fixes to allow compilation on Debian stable --- README | 2 +- meson.build | 2 +- src/tbo-window.c | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README b/README index 04127b8..e37bf0c 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ Base requirements: * meson * ninja * pkg-config - * gtk4 (4.0 or newer) + * gtk4 (4.8 or newer) * cairo * librsvg-2.0 diff --git a/meson.build b/meson.build index 65aec09..9ff8b6a 100644 --- a/meson.build +++ b/meson.build @@ -11,7 +11,7 @@ endif cc = meson.get_compiler('c') -gtk_dep = dependency('gtk4', version: '>= 4.0') +gtk_dep = dependency('gtk4', version: '>= 4.8') cairo_dep = dependency('cairo') rsvg_dep = dependency('librsvg-2.0') math_dep = cc.find_library('m', required: false) diff --git a/src/tbo-window.c b/src/tbo-window.c index 5d814d1..9e4b40a 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -1139,7 +1139,11 @@ load_app_css (void) "}"; provider = gtk_css_provider_new (); +#if GTK_CHECK_VERSION(4, 12, 0) gtk_css_provider_load_from_string (provider, css); +#else + gtk_css_provider_load_from_data (provider, css, -1); +#endif gtk_style_context_add_provider_for_display (gdk_display_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);