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/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/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..e37bf0c 100644 --- a/README +++ b/README @@ -1,6 +1,83 @@ 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. + +Base requirements: + + * meson + * ninja + * pkg-config + * gtk4 (4.8 or newer) + * cairo + * librsvg-2.0 + +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 + +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 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 +--------- + +The repository includes packaging metadata for: + + * Arch Linux: `archlinux/PKGBUILD` + * Debian-based systems: `debian/` + +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 --------- @@ -19,8 +96,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 +138,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..8fb1c48 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -1,36 +1,27 @@ -# Maintainer: Daniel Garcia +# Maintainer: Jaime pkgname=tbo-git -pkgver=20110623 +pkgver=2.0+r272.8ec0fc3 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') +source=("git+https://github.com/j4imefoo/TBO.git") +sha256sums=('SKIP') -_gitroot="git://git.danigm.net/tbo.git" -_gitname="tbo" +pkgver() { + cd TBO + printf "2.0+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..." - - cp -r ${srcdir}/${_gitname} ${srcdir}/${_gitname}-build - cd ${srcdir}/${_gitname}-build + arch-meson TBO build -Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false + meson compile -C build +} - ./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/applications/net.danigm.tbo.desktop b/data/applications/net.danigm.tbo.desktop new file mode 100644 index 0000000..de1a05f --- /dev/null +++ b/data/applications/net.danigm.tbo.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=TBO +GenericName=Comic Editor +Comment=Create comics and illustrated presentations +Exec=tbo %f +Icon=tbo +Terminal=false +StartupNotify=false +StartupWMClass=tbo +Categories=Graphics;2DGraphics;VectorGraphics;GTK; +Keywords=comic;comics;storyboard;presentation;editor;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/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/data/tbo.desktop b/data/tbo.desktop deleted file mode 100644 index 5877041..0000000 --- a/data/tbo.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Encoding=UTF-8 -Exec=tbo -Icon=tbo -Type=Application -Terminal=false -Name=TBO -GenericName=TBO -StartupNotify=true -Categories=Graphics;VectorGraphics;GTK; 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/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..2d7ffac 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,18 +1,5 @@ -tbo (1.0.0) unstable; urgency=low +tbo (2.0) 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..2c05191 100644 --- a/debian/control +++ b/debian/control @@ -1,23 +1,22 @@ 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, + libgtk-4-dev, + libcairo2-dev, + librsvg2-dev +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..b31ca2f 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_configure: + dh_auto_configure -- -Dnls=false -Dtests=false -Dappstream=false -Dlegacy_pixmap_icon=false -Dtutorial_pdf=false 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..a887aa7 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,3 @@ +Tests: smoke +Depends: @, xvfb, xauth +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..9ff8b6a --- /dev/null +++ b/meson.build @@ -0,0 +1,1059 @@ +project( + 'tbo', + 'c', + version: '2.0', + default_options: ['c_std=gnu11'] +) + +if get_option('nls') + subdir('po') +endif + +cc = meson.get_compiler('c') + +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) + +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', get_option('nls')) +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"', + '-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 +) + +if get_option('tests') +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 +) + +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 +) + +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 +) + +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 +) + +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'), + 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'), + 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 +) + +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'), + 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 +) + +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, + 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'] +) + +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( + '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( + '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( + 'doodle-raster-catalog-check', + doodle_raster_catalog_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, + 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( + 'raster-render-check', + raster_render_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'] +) + +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'] +) +endif + +installable_data_files = ['data/tut.tbo', 'data/icon.png'] + +if get_option('tutorial_pdf') + installable_data_files += 'data/tutorial.pdf' +endif + +install_data( + installable_data_files, + 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')) + +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/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/es.po b/po/es.po index bd11cb2..32bed8b 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,59 @@ 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:250 +msgid "Save comic" +msgstr "Guardar cómic" + +#: ../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" @@ -255,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" @@ -368,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/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..be19975 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" @@ -33,316 +34,671 @@ #include "tbo-object-pixmap.h" #include "tbo-utils.h" -char *TITLE; +typedef enum +{ + ATTR_INT, + ATTR_DOUBLE, +} TboLoadAttrType; -Comic *COMIC = NULL; -Page *CURRENT_PAGE; -Frame *CURRENT_FRAME; -TboObjectText *CURRENT_TEXT = NULL; +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 gchar * +unwrap_saved_text (const gchar *text) +{ + gsize start = 0; + gsize end; + gsize trailing; -struct attr { - char *name; - char *format; - void *pointer; -}; + if (text == NULL) + return g_strdup (""); -void -parse_attrs (struct attr attrs[], - int attrs_size, + 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, + const gchar *name) +{ + const gchar **name_cursor = attribute_names; + const gchar **value_cursor = attribute_values; + + while (*name_cursor != NULL) + { + if (strcmp (*name_cursor, name) == 0) + return *value_cursor; + name_cursor++; + value_cursor++; + } + + return NULL; +} + +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; - 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; + + if (width <= 0 || height <= 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Comic size must be positive"); + 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; +} + +static gboolean +create_tbo_page (TboLoadContext *context, GError **error) +{ + if (context->comic == NULL) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Element 'page' must be inside 'tbo'"); + return FALSE; + } - COMIC = tbo_comic_new (TITLE, width, height); - tbo_comic_del_page (COMIC, 0); + context->current_page = tbo_comic_new_page (context->comic); + context->current_frame = NULL; + return TRUE; } -void -create_tbo_frame (const gchar **attribute_names, const gchar **attribute_values) +static gboolean +create_tbo_frame (TboLoadContext *context, + const gchar **attribute_names, + const gchar **attribute_values, + 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}, + 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; + } - 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; + 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; + } + + 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; - char path[255]; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"flipv", "%d", &flipv}, - {"fliph", "%d", &fliph}, - {"angle", "%f", &angle}, - {"path", "%s", path}, + 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); + 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; + + 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; 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; - char path[255]; - - struct attr attrs[] = { - {"x", "%d", &x}, - {"y", "%d", &y}, - {"width", "%d", &width}, - {"height", "%d", &height}, - {"flipv", "%d", &flipv}, - {"fliph", "%d", &fliph}, - {"angle", "%f", &angle}, - {"path", "%s", path}, + 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); + 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; + + 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; 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; - GdkColor color; - int x=0, y=0; - int width=0, height=0; - float angle=0.0; - int flipv=0, fliph=0; - char font[255]; - 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}, - {"font", "%s", font}, - {"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); - color.red = (int)(r * COLORMAX); - color.green = (int)(g * COLORMAX); - color.blue = (int)(b * COLORMAX); - textobj = TBO_OBJECT_TEXT (tbo_object_text_new_with_params (x, y, width, height, "text", font, &color)); + 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; + + 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; + + color.red = r; + color.green = g; + color.blue = b; + + 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); -} -/* The handler functions. */ + 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; +} -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, 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 = unwrap_saved_text (context->current_text_buffer->str); + tbo_object_text_set_text (context->current_text, normalized); + } + + 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 (const gchar *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); - - 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)); + context.title = g_strdup (base_name); + + 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 (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; } - 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)); + 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; } - g_free(text); - g_markup_parse_context_free (context); + comic = context.comic; + context.comic = NULL; + + 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; + return comic; +} + +Comic * +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 7e004c0..4ddf82f 100644 --- a/src/comic-load.h +++ b/src/comic-load.h @@ -22,6 +22,7 @@ #include "tbo-types.h" -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 d577ee8..31da0bd 100644 --- a/src/comic-new-dialog.c +++ b/src/comic-new-dialog.c @@ -21,71 +21,146 @@ #include #include #include "comic-new-dialog.h" +#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; + GtkWidget *button; GtkWidget *label; GtkWidget *spin_w; GtkWidget *spin_h; + GtkWidget *dropdown; GtkAdjustment *adjustment; - gint response; + 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_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)); + 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); - hbox = gtk_hbox_new (FALSE, FALSE); - label = gtk_label_new (_("width: ")); + 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); + gtk_widget_set_margin_bottom (vbox, 12); + tbo_widget_add_child (dialog, vbox); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + 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_misc_set_alignment (GTK_MISC (label), 0, 0.5); - adjustment = gtk_adjustment_new (800, 0, 10000, 100, 100, 0); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + adjustment = gtk_adjustment_new (default_width, 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); - label = gtk_label_new (_("height: ")); + 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); - adjustment = gtk_adjustment_new (500, 0, 10000, 100, 100, 0); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + adjustment = gtk_adjustment_new (default_height, 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); + + 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); + + 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 (tbo_dialog_button_cb), dialog); + tbo_widget_add_child (actions, button); - gtk_widget_show_all (vbox); + 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 (tbo_dialog_button_cb), dialog); + tbo_widget_add_child (actions, button); - response = gtk_dialog_run (GTK_DIALOG (dialog)); + tbo_widget_add_child (vbox, actions); - if (response == GTK_RESPONSE_ACCEPT) + 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) { 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"); + 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_widget_destroy ((GtkWidget *) dialog); + gtk_window_destroy (GTK_WINDOW (dialog)); + tbo_dialog_run_data_clear (&data); return FALSE; } - diff --git a/src/comic-open-dialog.c b/src/comic-open-dialog.c index 66ac023..f57798d 100644 --- a/src/comic-open-dialog.c +++ b/src/comic-open-dialog.c @@ -21,70 +21,33 @@ #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); +#include "ui-menu.h" 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_comic_open (window, filename); - tbo_window_set_path (window, filename); - tbo_drawing_update (TBO_DRAWING (window->drawing)); - tbo_window_update_status (window, 0, 0); + tbo_window_set_browse_path (window, filename); + if (tbo_window_prepare_for_document_replace (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); } - 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..02e37ba 100644 --- a/src/comic-saveas-dialog.c +++ b/src/comic-saveas-dialog.c @@ -18,53 +18,59 @@ #include +#include #include #include #include "comic-saveas-dialog.h" +#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) { 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); +} + +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) { - gint response; - GtkWidget *filechooser; - char *filename; - char buffer[255]; + gchar *filename; + gchar *suggested_name; - 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); + 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); - snprintf (buffer, 250, "%s", window->comic->title); - 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)); - - 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); + tbo_window_add_recent_project (filename); + tbo_menu_refresh (window); + g_free (filename); + return TRUE; + } + g_free (filename); } - gtk_widget_destroy ((GtkWidget *) filechooser); - return FALSE; } 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 4ded367..f495d93 100644 --- a/src/comic.c +++ b/src/comic.c @@ -22,25 +22,80 @@ #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-list-utils.h" #include "tbo-utils.h" +#include "tbo-widget.h" + +struct _Comic +{ + GObject parent_instance; + + char title[255]; + int width; + int height; + TboComicPaper paper; + GList *pages; + Page *current_page; +}; + +struct _ComicClass +{ + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (Comic, tbo_comic, G_TYPE_OBJECT); + +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->paper = TBO_COMIC_PAPER_NONE; + 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; @@ -49,93 +104,198 @@ 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); +} + +const gchar * +tbo_comic_get_title (Comic *comic) +{ + return comic->title; +} + +gint +tbo_comic_get_width (Comic *comic) +{ + return comic->width; +} + +gint +tbo_comic_get_height (Comic *comic) +{ + return comic->height; +} - for (p=g_list_first (comic->pages); p; p = g_list_next(p)) +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) { - tbo_page_free ((Page *) p->data); + 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; } - g_list_free (g_list_first (comic->pages)); - free (comic); + if (width != NULL) + *width = page_width; + if (height != NULL) + *height = page_height; + + return TRUE; +} + +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) +{ + tbo_current_list_insert (&comic->pages, (gpointer *) &comic->current_page, page, nth); +} + 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 = tbo_list_utils_link (comic->pages, page); + next_link = link != NULL ? link->next : NULL; + prev_link = link != NULL ? link->prev : NULL; + + if (!tbo_current_list_remove (&comic->pages, (gpointer *) &comic->current_page, page)) + return; + + if (comic->current_page == NULL && (next_link != NULL || prev_link != NULL)) + comic->current_page = (next_link != NULL ? next_link : prev_link)->data; - 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); + return tbo_current_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 (g_list_first (comic->pages), page); + return tbo_current_list_index (comic->pages, page); } Page * tbo_comic_next_page (Comic *comic) { - if (comic->pages->next) - { - comic->pages = comic->pages->next; - return tbo_comic_get_current_page (comic); - } - return NULL; + if (comic->current_page == NULL) + return NULL; + + return tbo_current_list_next (comic->pages, (gpointer *) &comic->current_page); } Page * tbo_comic_prev_page (Comic *comic) { - if (comic->pages->prev) - { - comic->pages = comic->pages->prev; - return tbo_comic_get_current_page (comic); - } - return NULL; + if (comic->current_page == NULL) + return NULL; + + return tbo_current_list_prev (comic->pages, (gpointer *) &comic->current_page); } 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; + } + + tbo_current_list_set (comic->pages, (gpointer *) &comic->current_page, page); } void tbo_comic_set_current_page_nth (Comic *comic, int nth) { - comic->pages = g_list_nth (g_list_first (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; + + tbo_list_utils_remove (&comic->pages, page); + tbo_list_utils_insert (&comic->pages, page, nth); } gboolean @@ -172,69 +332,133 @@ tbo_comic_del_current_page (Comic *comic) return TRUE; } -void -tbo_comic_save (TboWindow *tbo, char *filename) +gboolean +save_comic_to_file (TboWindow *tbo, + const gchar *filename, + gboolean update_window_state, + gboolean mark_clean, + gboolean show_errors) { 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) { - 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; + 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, title, sizeof (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 = 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); } 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); + + return TRUE; } -void -tbo_comic_open (TboWindow *window, char *filename) +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); +} + +gboolean +tbo_comic_open (TboWindow *window, const gchar *filename) { Comic *newcomic = tbo_comic_load (filename); + Comic *oldcomic; int nth; - if (newcomic) + int n_pages; + + if (newcomic == NULL) + return FALSE; + + tbo_window_reset_document_state (window); + oldcomic = window->comic; + + n_pages = tbo_window_get_page_count (window); + for (nth = n_pages - 1; nth >= 0; nth--) { - tbo_comic_free (window->comic); - window->comic = newcomic; - gtk_window_set_title (GTK_WINDOW (window->window), window->comic->title); + tbo_window_remove_page_widget (window, nth); + } - for (nth=gtk_notebook_get_n_pages (GTK_NOTEBOOK (window->notebook)); nth>=0; nth--) - { - gtk_notebook_remove_page (GTK_NOTEBOOK (window->notebook), 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++) - { - gtk_notebook_insert_page (GTK_NOTEBOOK (window->notebook), - create_darea (window), - NULL, - 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)); } - tbo_toolbar_set_selected_tool (window->toolbar, TBO_TOOLBAR_NONE); - tbo_toolbar_set_selected_tool (window->toolbar, TBO_TOOLBAR_SELECTOR); + + 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 8b4a706..a83c414 100644 --- a/src/comic.h +++ b/src/comic.h @@ -20,17 +20,44 @@ #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; + +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); 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); 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); @@ -39,8 +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); -void 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); +gboolean tbo_comic_open (TboWindow *window, const gchar *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..61f3643 100644 --- a/src/dnd.c +++ b/src/dnd.c @@ -17,124 +17,372 @@ */ +#include #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" -static GtkWidget *DND_IMAGE = NULL; +#define TBO_DND_MAX_FRAME_WIDTH_FRACTION 1.0 +#define TBO_DND_MAX_FRAME_HEIGHT_FRACTION 0.5 -void -drag_data_received_handl (GtkWidget *widget, - GdkDragContext *context, - gint x, gint y, - GtkSelectionData *selection_data, - guint target_type, - guint time, - TboWindow *tbo) +typedef enum { - GtkAdjustment *adj; - float zoom = tbo_drawing_get_zoom (TBO_DRAWING (tbo->drawing)); - const gchar *_sdata; + TBO_DND_INSERT_OK, + TBO_DND_INSERT_NO_FRAME, + TBO_DND_INSERT_OUTSIDE_FRAME, + TBO_DND_INSERT_INVALID_ASSET, +} TboDndInsertResult; - gboolean dnd_success = FALSE; - gboolean delete_selection_data = FALSE; +#define TBO_DND_FEEDBACK_TIMEOUT_MS 2500 + +static TboObjectBase * +create_asset (const gchar *asset_path, gint x, gint y) +{ + 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)); +} - /* Deal with what we are given from source */ - if ((selection_data != NULL) && (gtk_selection_data_get_length (selection_data) >= 0)) +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) { - 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"); - } + 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; - if (dnd_success == FALSE) + path = tbo_files_expand_path (asset_path); + pixbuf = gdk_pixbuf_new_from_file (path, &error); + if (pixbuf != NULL) { - g_print ("DnD data transfer failed!\n"); + *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); - gtk_drag_finish (context, dnd_success, delete_selection_data, time); + return ok; } -void -drag_data_get_handl (GtkWidget *widget, - GdkDragContext *context, - GtkSelectionData *selection_data, - guint target_type, - guint time, - char *svg) -{ - g_assert (selection_data != NULL); - switch (target_type) +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); + +static void +show_insert_feedback (TboWindow *tbo, TboDndInsertResult result) +{ + const gchar *message = NULL; + + switch (result) { - case TARGET_STRING: - gtk_selection_data_set (selection_data, - gtk_selection_data_get_target (selection_data), - 8*sizeof(char), - (guchar*) svg, - strlen (svg)); + 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: - /* Default to some a safe target instead of fail. */ - g_assert_not_reached (); + break; } + + if (message != NULL) + tbo_tooltip_set_center_timeout (message, TBO_DND_FEEDBACK_TIMEOUT_MS, tbo); } -void -drag_begin_handl (GtkWidget *widget, - GdkDragContext *context, - char *svg) +static TboDndInsertResult +insert_asset_into_frame (TboWindow *tbo, + const gchar *asset_path, + gint x, + gint y, + TboObjectBase **inserted_asset) { - 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); + 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; + + 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); + 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; } -void -drag_end_handl (GtkWidget *widget, - GdkDragContext *context, - gpointer user_data) +static void +select_inserted_asset (TboWindow *tbo, Frame *frame, TboObjectBase *asset) { - if (DND_IMAGE) + 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]) { - gtk_widget_destroy (GTK_WIDGET (DND_IMAGE)); - DND_IMAGE = NULL; + return; } + + 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; + gdouble zoom = tbo_drawing_get_zoom (TBO_DRAWING (tbo->drawing)); + const gchar *asset_path = g_value_get_string (value); + + if (asset_path == NULL) + return FALSE; + + adj = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); + x = (x + gtk_adjustment_get_value (adj)) / zoom; + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (tbo->dw_scroll)); + y = (y + gtk_adjustment_get_value (adj)) / zoom; + + 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; + 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 asset; +} + +TboObjectBase * +tbo_dnd_insert_asset (TboWindow *tbo, const gchar *asset_path, gint x, gint y) +{ + TboObjectBase *asset; + TboDndInsertResult result; + + result = insert_asset_into_frame (tbo, asset_path, x, y, &asset); + if (result != TBO_DND_INSERT_OK) + { + show_insert_feedback (tbo, result); + return NULL; + } + + 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; + } + + return tbo_dnd_insert_asset (tbo, + asset_path, + tbo_frame_get_width (frame) / 2, + tbo_frame_get_height (frame) / 2); +} + +void +tbo_dnd_setup_asset_source (GtkWidget *widget, const gchar *full_path, const gchar *relative_path) +{ + 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 +tbo_dnd_setup_drawing_dest (TboDrawing *drawing, TboWindow *tbo) +{ + 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..fcdee0d 100644 --- a/src/dnd.h +++ b/src/dnd.h @@ -21,26 +21,14 @@ #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_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 524fceb..5f9fa6c 100644 --- a/src/doodle-treeview.c +++ b/src/doodle-treeview.c @@ -20,94 +20,192 @@ #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-ui-utils.h" +#include "tbo-widget.h" -void free_gstring_array (GArray *arr); +typedef struct +{ + TboWindow *tbo; + GString *path; + gboolean top_level; + gboolean bubble_mode; +} DoodleExpanderData; -static GArray *TO_FREE = NULL; -static TboWindow *TBO = NULL; +typedef struct +{ + TboWindow *tbo; + GtkWidget *search_entry; + GtkWidget *content_box; + gboolean bubble_mode; +} DoodleBrowserState; + +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); +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 () +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); } -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); -gboolean -on_doodle_click_cb (GtkWidget *widget, - GdkEventButton *event, - gpointer *data) -{ - 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)); + 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; - return FALSE; + 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 < 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; + + 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; } 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) { GError *error = NULL; - gchar complete_dir[255]; const gchar *filename; + 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) + { + 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))) + while ((filename = g_dir_read_name (dir)) != NULL) { - 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 (st) + { + g_free (complete_dir); + continue; + } - if (isdir && bubble_mode && strcmp (filename, "bubble")) + if (isdir && bubble_mode && strcmp (filename, "bubble") != 0) + { + g_free (complete_dir); continue; + } if (!strcmp (filename, "bubble") && !bubble_mode) + { + g_free (complete_dir); continue; + } if (isdir && S_ISDIR (filestat.st_mode)) { @@ -116,164 +214,511 @@ 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); } + + g_free (complete_dir); } 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 *table; - GtkWidget *image; - GtkWidget *ebox; - GdkPixbuf *pixbuf; - int r, c=2; - int left, top; - int w=50, h; + 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] = ' '; + } + + return normalized; +} + +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; - dirname = dir; + if (top_level) + label = g_strdup_printf ("%s (%d)", title, count); + else + label = g_strdup_printf ("%s (%d)", title, count); - GArray *arr = get_files (dirname, FALSE, FALSE); + 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; +} - r = (arr->len / c) + 1; - table = gtk_table_new (r, c, TRUE); +static gint +count_matching_assets_in_dir (const gchar *dir, const gchar *query) +{ + GArray *files; + GArray *subdirs; + gint count = 0; + gint i; - GString *mystr; - for (i=0; ilen; i++) + files = get_files ((gchar *) dir, FALSE, FALSE); + if (files != NULL) { - top = i / 2; - left = i % 2; - - 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); + 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); } - doodle_add_to_free (arr); + 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); + } - gtk_widget_show_all (GTK_WIDGET (table)); - return table; + 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-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); +} + +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) + { + if (allow_empty) + { + tbo_widget_show_all (grid); + return grid; + } + return NULL; + } + + for (i = 0; i < (int) arr->len; i++) + { + GString *mystr = g_array_index (arr, GString *, i); + const gchar *relative_path; + 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; + + 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), TRUE); + tbo_picture_set_contain (GTK_PICTURE (image)); + 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, 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); + } + + free_gstring_array (arr); + + 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 *table = doodle_add_images (dir); - gtk_container_add (GTK_CONTAINER (expander), table); + 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); - gtk_container_add (GTK_CONTAINER (box), expander); + tbo_widget_add_child (box, expander); } -gboolean -on_expand_cb (GtkExpander *expander, GString *str) +static void +free_expander_data (gpointer data, GClosure *closure) { - 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))); - if (numofchilds == 0) + 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) +{ + GtkWidget *vbox = gtk_expander_get_child (expander); + gint num_children; + GArray *subdirs; + GtkWidget *grid; + gint i; + + (void) pspec; + + if (vbox == NULL || !gtk_expander_get_expanded (expander) || data == NULL) + return; + + num_children = tbo_widget_get_child_count (vbox); + if (num_children > 0) + return; + + subdirs = get_files (data->path->str, TRUE, FALSE); + if (subdirs != NULL) { - GArray *subdir = get_files (str->str, TRUE, FALSE); - for (i=0; ilen; i++) + for (i = 0; i < (int) subdirs->len; i++) { - mystr2 = g_array_index (subdir, GString*, i); - doodle_add_dir_images (mystr2->str, vbox); + 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; + 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 (subdir); - g_string_free (str, TRUE); + + free_gstring_array (subdirs); } - gtk_widget_show_all (GTK_WIDGET (vbox)); - return FALSE; + + 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 *expander; - GtkWidget *vbox; - GtkWidget *vbox2; - gchar *dirname; + GtkWidget *label = gtk_label_new (_("No assets match this search.")); - TBO = tbo; + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_add_css_class (label, "dim-label"); + return label; +} - dirname = malloc (255*sizeof(char)); - char label_format[255]; - int i, k; +static gboolean +populate_filtered_dir (TboWindow *tbo, const gchar *dir, GtkWidget *box, const gchar *query, gboolean top_level) +{ + GArray *subdirs; + GtkWidget *expander; + GtkWidget *content; + GtkWidget *grid; + gint i; + gint count = count_matching_assets_in_dir (dir, query); - vbox = gtk_vbox_new (FALSE, 5); + if (count == 0) + return FALSE; - GArray *arr = NULL; - GString *mystr, *mystr2; + 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); - char **possible_dirs = tbo_files_get_dirs (); - for (k=0; possible_dirs[k]; k++) + subdirs = get_files ((gchar *) dir, TRUE, FALSE); + if (subdirs != NULL) { - arr = get_files (possible_dirs[k], TRUE, bubble_mode); - if (!arr) continue; - - for (i=0; ilen; i++) + for (i = 0; i < (int) subdirs->len; i++) { - mystr = g_array_index (arr, GString*, i); + GString *subdir = g_array_index (subdirs, GString *, i); + + populate_filtered_dir (tbo, subdir->str, content, query, FALSE); + } + free_gstring_array (subdirs); + } + + 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; + + 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 (); - vbox2 = gtk_vbox_new (FALSE, 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); + for (k = 0; possible_dirs[k] != NULL; k++) + { + GArray *arr = get_files (possible_dirs[k], TRUE, state->bubble_mode); + gint i; - mystr2 = g_string_new (mystr->str); - g_signal_connect (GTK_EXPANDER (expander), "activate", G_CALLBACK (on_expand_cb), mystr2); + if (arr == NULL) + continue; - if (bubble_mode) + for (i = 0; i < (int) arr->len; i++) + { + GString *dir = g_array_index (arr, GString *, i); + + 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), 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; + expander_data->bubble_mode = state->bubble_mode; + 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); +#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); + 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/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..06c2538 100644 --- a/src/export.c +++ b/src/export.c @@ -25,253 +25,1126 @@ #include #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; +typedef struct +{ + GtkWidget *spinw; + GtkWidget *spinh; + gint base_width; + gint base_height; + gboolean updating; +} ExportSizeState; + +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) +{ + 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) +{ + tbo_alert_show (GTK_WINDOW (tbo->window), message, NULL); +} + +static Frame * +get_export_selection_frame (TboWindow *tbo) +{ + TboDrawing *drawing; + TboToolSelector *selector; + Frame *current_frame; + + if (tbo == NULL || tbo->drawing == NULL || tbo->toolbar == NULL || tbo->toolbar->tools == NULL) + return NULL; + + drawing = TBO_DRAWING (tbo->drawing); + current_frame = tbo_drawing_get_current_frame (drawing); + if (current_frame != NULL) + return current_frame; -struct export_spin_args { - gint current_size; - gint current_size2; - GtkWidget *spin2; - gdouble *scale; -}; + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + return tbo_tool_selector_get_selected_frame (selector); +} static gboolean -export_size_cb (GtkWidget *widget, struct export_spin_args *args) +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) { - if (!LOCK) + 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) { - 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; + *width = tbo_frame_get_width (selection); + *height = tbo_frame_get_height (selection); } - return FALSE; } -gboolean -filedialog_cb (GtkWidget *widget, gpointer data) +static void +normalize_export_page_range (Comic *comic, gint *from_page, gint *to_page) { - 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)); + gint page_count; - if (response == GTK_RESPONSE_ACCEPT) + 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++) { - filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (filechooserdialog)); - gtk_entry_set_text (entry, filename); + Page *page = g_list_nth_data (tbo_comic_get_pages (comic), i); + + if (page != NULL) + pages = g_list_append (pages, page); } - gtk_widget_destroy (GTK_WIDGET (filechooserdialog)); - return FALSE; + if (n_pages != NULL) + *n_pages = g_list_length (pages); + + return pages; } -gboolean -tbo_export (TboWindow *tbo) +static void +get_dialog_range (ExportDialogState *state, gint *from_page, gint *to_page) { - cairo_surface_t *surface = NULL; + 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 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; - gint width = tbo->comic->width; - gint height = tbo->comic->height; - gchar rpath[255]; - gchar format_pages[255]; - gchar *filename; - GList *page_list; - gint i, n, n2; - gint response; - gdouble scale = 1.0; - gchar *export_to; - gint export_to_index; - struct export_spin_args spin_args; - struct export_spin_args spin_args2; + GdkTexture *texture; - GtkWidget *dialog; - GtkWidget *vbox; - GtkWidget *hbox; - GtkWidget *fileinput; - GtkWidget *filelabel; - GtkWidget *filebutton; - GtkWidget *spinw; - GtkWidget *spinh; - GtkWidget *combobox; + 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); + texture = create_texture_from_surface (surface, width, height); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return texture; +} - GtkWidget *button; +static GdkTexture * +create_frame_preview_texture (Frame *frame, gint width, gint height) +{ + cairo_surface_t *surface; + cairo_t *cr; + GdkTexture *texture; - 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); + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + draw_frame_export (cr, frame, width, height); + texture = create_texture_from_surface (surface, width, height); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return texture; +} - button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); - gtk_widget_grab_focus (GTK_WIDGET (button)); +static void +set_preview_texture (ExportDialogState *state, GdkTexture *texture) +{ + GtkWidget *picture = gtk_picture_new_for_paintable (GDK_PAINTABLE (texture)); - filebutton = gtk_button_new_from_stock (GTK_STOCK_OPEN); - vbox = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + 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); +} - hbox = gtk_hbox_new (FALSE, 5); - 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); +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; + GdkTexture *texture = NULL; + gchar *label = NULL; + gboolean range_sensitive; - spinw = add_spin_with_label (vbox, _("width: "), tbo->comic->width); - spinh = add_spin_with_label (vbox, _("height: "), tbo->comic->height); + if (state == NULL) + return; - spin_args.current_size = tbo->comic->width; - spin_args.current_size2 = tbo->comic->height; - spin_args.spin2 = spinh; - spin_args.scale = &scale; - g_signal_connect (spinw, "value-changed", G_CALLBACK (export_size_cb), &spin_args); + 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); - spin_args2.current_size = tbo->comic->height; - spin_args2.current_size2 = tbo->comic->width; - spin_args2.spin2 = spinw; - spin_args2.scale = &scale; - g_signal_connect (spinh, "value-changed", G_CALLBACK (export_size_cb), &spin_args2); + range_sensitive = scope == TBO_EXPORT_SCOPE_ALL_PAGES && tbo_comic_len (state->tbo->comic) > 1; + gtk_widget_set_sensitive (state->range_row, range_sensitive); - 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); + 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))); + } - gtk_widget_show_all (GTK_WIDGET (vbox)); + if (scope == TBO_EXPORT_SCOPE_SELECTION) + { + Frame *frame = get_export_selection_frame (state->tbo); - g_signal_connect (filebutton, "clicked", G_CALLBACK (filedialog_cb), fileinput); + if (frame != NULL) + texture = create_frame_preview_texture (frame, preview_width, preview_height); + label = g_strdup (_("Preview: Selection")); + } + else + { + Page *page = get_preview_page_for_dialog (state, scope, from_page); - response = gtk_dialog_run (GTK_DIALOG (dialog)); + if (page != NULL) + texture = create_page_preview_texture (state->tbo, page, preview_width, preview_height); - if (response == GTK_RESPONSE_ACCEPT) + 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_texture (state, texture); + g_free (label); + if (texture != NULL) + g_object_unref (texture); +} + +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) { - width = (gint) (width * scale); - height = (gint) (height * scale); + gtk_editable_set_text (GTK_EDITABLE (args->entry), filename); + tbo_window_set_export_path (args->tbo, filename); + g_free (filename); + } + return FALSE; +} - filename = (gchar *)gtk_entry_get_text (GTK_ENTRY (fileinput)); - /* 0 guess, 1 png, 2 pdf, 3 svg */ - export_to_index = gtk_combo_box_get_active (GTK_COMBO_BOX (combobox)); +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)) + { + *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); + } + + *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))); + if (*surface != NULL) + cairo_surface_destroy (*surface); + if (*cr != NULL) + cairo_destroy (*cr); + *surface = NULL; + *cr = NULL; + return FALSE; + } + + return TRUE; +} + +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) + { + cairo_status_t status = cairo_surface_write_to_png (surface, path); + + if (status != CAIRO_STATUS_SUCCESS) + { + show_export_error (tbo, cairo_status_to_string (status)); + return FALSE; + } + } + + 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; - switch (export_to_index) + 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); + path = g_strdup_printf ("%s.%s", base_filename, export_to); + } + + if (g_strcmp0 (export_to, "pdf") == 0) { - case 0: - //guess - if (strlen (filename) > 4) + if (surface == NULL) + { + if (!begin_export_surface (tbo, + export_to, + path, + width, + height, + use_pdf_page_size, + &surface, + &cr, + &draw_width, + &draw_height)) { - export_to = filename + strlen (filename) - 3; - filename = g_strndup (filename, strlen(filename) - 4); + g_free (path); + success = FALSE; + break; } - else + } + else + { + if (use_pdf_page_size && !tbo_comic_get_pdf_page_size (tbo->comic, &draw_width, &draw_height)) { - filename = g_strdup (filename); - export_to = "png"; + draw_width = width; + draw_height = height; } + else if (!use_pdf_page_size) + { + draw_width = width; + draw_height = height; + } + } + } + else + { + 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; - case 1: - export_to = "png"; - break; - case 2: - export_to = "pdf"; - break; - case 3: - export_to = "svg"; - break; - default: - export_to = "png"; - break; + } } - 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); - for (i=0, page_list = g_list_first (tbo->comic->pages); page_list; i++, page_list = page_list->next) + 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); + + if (!success) + break; + + exported = TRUE; + + if (g_strcmp0 (export_to, "pdf") != 0) { - snprintf (rpath, 255, format_pages, filename, i, export_to); - if (n2 == 1) - snprintf (rpath, 255, "%s.%s", filename, export_to); - // PDF - if (strcmp (export_to, "pdf") == 0) + cairo_surface_destroy (surface); + cairo_destroy (cr); + surface = NULL; + cr = NULL; + } + } + + if (surface != NULL) + { + cairo_surface_destroy (surface); + cairo_destroy (cr); + } + + g_free (format_pages); + return success && exported; +} + +static gboolean +export_single_frame (TboWindow *tbo, + const gchar *base_filename, + const gchar *export_to, + gint width, + gint height, + Frame *frame) +{ + 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)) + { + g_free (path); + 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 +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) +{ + gchar *base_filename = NULL; + gchar *export_to = NULL; + GList *pages = NULL; + gint n_pages = 0; + gboolean success = FALSE; + + if (filename == NULL || *filename == '\0' || width <= 0 || height <= 0) + return FALSE; + + if (format_hint != NULL && *format_hint != '\0') + { + export_to = g_ascii_strdown (format_hint, -1); + base_filename = strip_matching_extension (filename, export_to); + } + 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: + { + Page *current_page = tbo_comic_get_current_page (tbo->comic); + + if (current_page == NULL) { - if (!surface) - { - snprintf (rpath, 255, "%s.%s", filename, export_to); - surface = cairo_pdf_surface_create (rpath, width, height); - cr = cairo_create (surface); - } + show_export_error (tbo, _("There are no pages to export.")); + success = FALSE; } - // SVG - else if (strcmp (export_to, "svg") == 0) + else { - surface = cairo_svg_surface_create (rpath, width, height); - cr = cairo_create (surface); + 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); + + if (frame == NULL) + { + show_export_error (tbo, _("Please select a frame to export.")); + success = FALSE; } - // PNG or unknown format... default is png else { - surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); - cr = cairo_create (surface); + 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; + } - cairo_scale (cr, scale, scale); + g_free (base_filename); + g_free (export_to); + return success; +} - // drawing the stuff - tbo_drawing_draw_page (TBO_DRAWING (tbo->drawing), cr, (Page *)page_list->data, width/scale, height/scale); +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)); +} - if (strcmp (export_to, "pdf") == 0) - cairo_show_page (cr); - else if (strcmp (export_to, "png") == 0) - cairo_surface_write_to_png (surface, rpath); +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); +} - cairo_scale (cr, 1/scale, 1/scale); +gboolean +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; + gint export_to_index; + gint from_page; + gint to_page; + TboExportScope scope; + ExportFileArgs file_args; + ExportDialogState dialog_state; - // Not destroying for multipage - if (strcmp (export_to, "pdf") != 0) - { - cairo_surface_destroy (surface); - cairo_destroy (cr); - surface = NULL; - } + GtkWidget *dialog; + GtkWidget *headerbar; + GtkWidget *vbox; + GtkWidget *hbox; + GtkWidget *fileinput; + GtkWidget *filelabel; + GtkWidget *filebutton; + GtkWidget *spinw; + GtkWidget *spinh; + 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", + ".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")); + 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); + + 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); + 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 (_("File Name: ")); + fileinput = gtk_entry_new (); + 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_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); + + 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); + + 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); + + 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); + + 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); + + 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 (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 (tbo_dialog_button_cb), dialog); + tbo_widget_add_child (actions, button); + + tbo_widget_add_child (vbox, actions); + tbo_widget_show_all (GTK_WIDGET (vbox)); + + file_args.tbo = tbo; + file_args.entry = GTK_ENTRY (fileinput); + g_signal_connect (filebutton, "clicked", G_CALLBACK (filedialog_cb), &file_args); + + 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; + + if (response == GTK_RESPONSE_ACCEPT) + { + 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') + { + show_export_error (tbo, _("Please choose a filename to export.")); + g_free (filename); + gtk_window_destroy (GTK_WINDOW (dialog)); + return FALSE; } - if (surface) + tbo_window_set_export_path (tbo, filename); + 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) + format_hint = "pdf"; + else if (export_to_index == 3) + format_hint = "svg"; + + if (!tbo_export_file_with_scope_range (tbo, filename, format_hint, width, height, scope, from_page, to_page)) { - cairo_surface_destroy (surface); - cairo_destroy (cr); + gtk_window_destroy (GTK_WINDOW (dialog)); + tbo_dialog_run_data_clear (&data); + g_free (filename); + return FALSE; } } - if (!export_to_index) - g_free (filename); - gtk_widget_destroy (GTK_WIDGET (dialog)); - - return FALSE; + 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 0af7d93..6a12542 100644 --- a/src/export.h +++ b/src/export.h @@ -24,6 +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 8e6fe75..39f7474 100644 --- a/src/frame.c +++ b/src/frame.c @@ -20,17 +20,87 @@ #include #include #include -#include #include #include #include "frame.h" #include "tbo-types.h" +#include "tbo-list-utils.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 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) +{ + 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 = DEFAULT_FRAME_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) @@ -38,8 +108,8 @@ 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 +120,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 +130,192 @@ 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; +} + +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 tbo_current_list_index (frame->objects, obj); +} + +gboolean +tbo_frame_has_obj (Frame *frame, TboObjectBase *obj) +{ + return tbo_list_utils_contains (frame->objects, obj); } 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,32 +396,41 @@ 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) +{ + tbo_list_utils_insert (&frame->objects, obj, nth); } float -tbo_frame_get_scale_factor () +tbo_frame_get_scale_factor (void) { return SCALE_FACTOR; } @@ -274,44 +438,52 @@ tbo_frame_get_scale_factor () void tbo_frame_del_obj (Frame *frame, TboObjectBase *obj) { - frame->objects = g_list_remove (g_list_first (frame->objects), obj); - g_object_unref (obj); + if (tbo_list_utils_remove (&frame->objects, obj)) + g_object_unref (obj); } void -tbo_frame_set_color (Frame *frame, GdkColor *color) +tbo_frame_reorder_obj (Frame *frame, TboObjectBase *obj, int nth) { - frame->color->r = color->red / 65535.0; - frame->color->g = color->green / 65535.0; - frame->color->b = color->blue / 65535.0; - BASE_COLOR.r = frame->color->r; - BASE_COLOR.g = frame->color->g; - BASE_COLOR.b = frame->color->b; + if (!tbo_frame_has_obj (frame, obj)) + return; + + tbo_list_utils_remove (&frame->objects, obj); + tbo_frame_insert_obj (frame, obj, nth); +} + +void +tbo_frame_set_color (Frame *frame, GdkRGBA *color) +{ + 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 +491,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 b61b9af..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,14 +67,15 @@ 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 (); +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..bf9b332 100644 --- a/src/page.c +++ b/src/page.c @@ -20,45 +20,106 @@ #include #include #include -#include +#include #include "comic.h" #include "page.h" #include "frame.h" +#include "tbo-list-utils.h" + +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 void +tbo_page_dispose (GObject *object) +{ + Page *self = TBO_PAGE (object); + + self->current_frame = NULL; + + if (self->frames != NULL) + { + g_list_free_full (self->frames, (GDestroyNotify) tbo_frame_free); + self->frames = NULL; + } + + 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) { - Page *new_page; - new_page = malloc(sizeof(Page)); - new_page->comic = comic; - new_page->frames = NULL; + (void) comic; + return g_object_new (TBO_TYPE_PAGE, NULL); +} - return new_page; +void +tbo_page_free (Page *page) +{ + if (page != NULL) + g_object_unref (page); } -void tbo_page_free (Page *page) +Page * +tbo_page_clone (Page *page) { - GList *f; + Page *new_page; + GList *frames; + Frame *selected_clone = NULL; + + if (page == NULL) + return NULL; - if (tbo_page_len (page) > 0) + new_page = tbo_page_new (NULL); + for (frames = page->frames; frames != NULL; frames = frames->next) { - for (f=tbo_page_get_frames (page); f; f=g_list_next(f)) - { - tbo_frame_free ((Frame *) f->data); - } + 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; } - g_list_free (tbo_page_get_frames (page)); - free (page); + + 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) +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 +127,13 @@ 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) +{ + tbo_current_list_insert (&page->frames, (gpointer *) &page->current_frame, frame, nth); } void @@ -74,34 +141,65 @@ 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 = tbo_list_utils_link (page->frames, frame); + if (link == NULL) + return; + + next_link = link->next; + prev_link = link->prev; + if (!tbo_current_list_remove (&page->frames, (gpointer *) &page->current_frame, frame)) + return; + + 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); } 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; + 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 index >= 0 ? index + 1 : 0; +} + +int +tbo_page_frame_nth (Page *page, Frame *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; } @@ -109,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; } @@ -117,50 +215,49 @@ tbo_page_frame_last (Page *page) Frame * tbo_page_next_frame (Page *page) { - if (page->frames->next) - { - page->frames = page->frames->next; - return tbo_page_get_current_frame (page); - } - return NULL; + if (page->current_frame == NULL) + return NULL; + + return tbo_current_list_next (page->frames, (gpointer *) &page->current_frame); } Frame * tbo_page_prev_frame (Page *page) { - if (page->frames->prev) - { - page->frames = page->frames->prev; - return tbo_page_get_current_frame (page); - } - return NULL; + if (page->current_frame == NULL) + return NULL; + + return tbo_current_list_prev (page->frames, (gpointer *) &page->current_frame); } 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; + } + + tbo_current_list_set (page->frames, (gpointer *) &page->current_frame, frame); } Frame * tbo_page_first_frame (Page *page) { - page->frames = g_list_first (page->frames); - if (page->frames != NULL) - return page->frames->data; - return NULL; + return tbo_current_list_first (page->frames, (gpointer *) &page->current_frame); } GList * tbo_page_get_frames (Page *page) { - return g_list_first (page->frames); + return page->frames; } void @@ -168,10 +265,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..12029ee 100644 --- a/src/page.h +++ b/src/page.h @@ -20,18 +20,34 @@ #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); +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); 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); Frame *tbo_page_first_frame (Page *page); @@ -43,4 +59,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 f5d9ac5..fa30138 100644 --- a/src/tbo-drawing.c +++ b/src/tbo-drawing.c @@ -31,79 +31,210 @@ #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); + +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) +{ + 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); + } +} -/* 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; +} + +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)); + } +} - 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))); +/* 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); + tbo_tooltip_draw (cr, self); // 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); + gdouble zoom = clamp_zoom (self->zoom); + TboPointerEvent event = { + .x = x / zoom, + .y = y / 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); + gdouble zoom = clamp_zoom (self->zoom); + TboPointerEvent event = { + .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)), + }; + + 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); + gdouble zoom = clamp_zoom (self->zoom); + TboPointerEvent event = { + .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)), + }; 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 +242,51 @@ 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->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); -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) { + 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); } @@ -143,21 +294,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); @@ -168,27 +313,42 @@ GtkWidget * 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); + 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) { - 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 tbo_drawing_set_current_frame (TboDrawing *self, Frame *frame) { - self->current_frame = frame; + tbo_drawing_set_current_frame_pointer (self, frame); } Frame * @@ -206,8 +366,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); @@ -237,42 +400,55 @@ 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 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); } @@ -281,14 +457,12 @@ 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; - - z1 = fabs ((float)w / (float)self->comic->width); - z2 = fabs ((float)h / (float)self->comic->height); - self->zoom = z1 < z2 ? z1 : z2; + + get_view_size (self, &w, &h); + + z1 = fabs ((float)w / (float)tbo_comic_get_width (self->comic)); + z2 = fabs ((float)h / (float)tbo_comic_get_height (self->comic)); + self->zoom = clamp_zoom (z1 < z2 ? z1 : z2); tbo_drawing_adjust_scroll (self); } @@ -298,18 +472,119 @@ 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) { + 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); + + 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); 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_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 47a029d..6fbfa49 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 ()) @@ -40,18 +41,25 @@ typedef struct _TboDrawingClass TboDrawingClass; struct _TboDrawing { - GtkLayout parent_instance; + GtkDrawingArea parent_instance; /* instance members */ TboToolBase *tool; 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; }; struct _TboDrawingClass { - GtkLayoutClass parent_class; + GtkDrawingAreaClass parent_class; /* class members */ }; @@ -63,20 +71,25 @@ 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_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); -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); 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); #endif /* __TBO_DRAWING_H__ */ - diff --git a/src/tbo-file-dialog.c b/src/tbo-file-dialog.c new file mode 100644 index 0000000..5a3018c --- /dev/null +++ b/src/tbo-file-dialog.c @@ -0,0 +1,357 @@ +/* + * 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 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) +{ + 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 * +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 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); +} + +#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; + + gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters)); + set_initial_folder (dialog, tbo_window_get_open_dir (window)); + path = run_open_dialog (dialog, 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; + + 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); + path = run_save_dialog (dialog, window); + + 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 (); + gchar *path; + + 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)); + path = run_open_dialog (dialog, 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; + + 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); + } + } + + path = run_save_dialog (dialog, window); + + 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-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..1b0f9ac 100644 --- a/src/tbo-files.c +++ b/src/tbo-files.c @@ -21,21 +21,18 @@ #include #include #include +#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 +40,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,55 +61,75 @@ 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 -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; + + if (source == NULL || *source == '\0') + return FALSE; - paths = g_strsplit (source, ".", 0); + ext = strrchr (source, '.'); + if (ext == NULL) + return FALSE; - ext = paths; - while (*ext) ext++; - ext--; + return g_ascii_strcasecmp (ext + 1, "svg") == 0; +} + +gboolean +tbo_files_is_supported_asset_file (const gchar *source) +{ + GdkPixbufFormat *format; + gchar *path; + gboolean is_supported = FALSE; - lower_ext = g_ascii_strdown (*ext, -1); + if (tbo_files_is_svg_file (source)) + return TRUE; - if (strcmp (lower_ext, "svg") == 0) { - is_svg = 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 4fdb95f..cf73bc0 100644 --- a/src/tbo-files.h +++ b/src/tbo-files.h @@ -22,10 +22,11 @@ #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); -gboolean tbo_files_is_svg_file (char *source); +gchar *tbo_files_expand_path (const gchar *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-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-object-base.c b/src/tbo-object-base.c index db47519..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); @@ -117,7 +118,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); @@ -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-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..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)) @@ -57,7 +56,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 +64,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..ff81e21 100644 --- a/src/tbo-object-pixmap.c +++ b/src/tbo-object-pixmap.c @@ -19,10 +19,14 @@ #include #include +#include #include #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); @@ -31,70 +35,192 @@ 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; + int n_channels; + guchar *src; + int x; + int y; + gboolean has_alpha; + + if (pixmap->scaled_pixbuf == NULL) + return FALSE; + + if (pixmap->surface != NULL) + { + cairo_surface_destroy (pixmap->surface); + pixmap->surface = NULL; } - w = gdk_pixbuf_get_width (pixbuf); - h = gdk_pixbuf_get_height (pixbuf); + 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) + { + cairo_surface_destroy (surface); + return FALSE; + } - if (!self->width) self->width = w; - if (!self->height) self->height = h; + 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 *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) + { + 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; + } + } - float factorw = (float)self->width / (float)w; - float factorh = (float)self->height / (float)h; + 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; + } + + 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; + } + + pixmap->cache_width = self->width; + pixmap->cache_height = self->height; + return TRUE; +} + +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); - 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_translate (cr, -(frame_x + self->x), -(frame_y + self->y)); cairo_reset_clip (cr); - - cairo_surface_destroy (image); } 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 * @@ -122,6 +248,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 +262,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 +284,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 +314,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..a794052 100644 --- a/src/tbo-object-svg.c +++ b/src/tbo-object-svg.c @@ -24,9 +24,10 @@ #include #include #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); @@ -35,77 +36,137 @@ 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; + } + + 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) + { + cairo_surface_destroy (svg->surface); + svg->surface = NULL; } - else + + 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) { - 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); + 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); + 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; + + 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 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 * @@ -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..2a68928 100644 --- a/src/tbo-object-text.c +++ b/src/tbo-object-text.c @@ -21,7 +21,10 @@ #include #include #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); @@ -35,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; @@ -46,7 +53,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); @@ -70,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); @@ -82,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 = (float)text->font_color->red / (float)COLORMAX; - g = (float)text->font_color->green / (float)COLORMAX; - b = (float)text->font_color->blue / (float)COLORMAX; - - 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 * @@ -121,15 +125,18 @@ tclone (TboObjectBase *self) { TboObjectText *text; TboObjectBase *newtext; + gchar *font; text = TBO_OBJECT_TEXT (self); + font = tbo_object_text_get_string (text); newtext = TBO_OBJECT_BASE (tbo_object_text_new_with_params (self->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 +167,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 +182,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 +203,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 +216,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 +249,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..48b9994 100644 --- a/src/tbo-tool-frame.c +++ b/src/tbo-tool-frame.c @@ -23,22 +23,24 @@ #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); #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); +static void finalize (GObject *object); /* 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); @@ -57,17 +59,18 @@ on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *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)); } 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 +78,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; @@ -86,9 +89,14 @@ on_release (TboToolBase *tool, GtkWidget *widget, GdkEventButton *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; @@ -129,12 +137,22 @@ 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 */ GObject * -tbo_tool_frame_new () +tbo_tool_frame_new (void) { GObject *tbo_tool; tbo_tool = g_object_new (TBO_TYPE_TOOL_FRAME, NULL); @@ -152,3 +170,18 @@ tbo_tool_frame_new_with_params (TboWindow *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 0703424..15845f6 100644 --- a/src/tbo-tool-frame.h +++ b/src/tbo-tool-frame.h @@ -60,8 +60,8 @@ 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); +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 ba498c7..18a53cf 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,68 @@ 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); +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 +#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 */ @@ -56,112 +108,296 @@ 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 gboolean -update_color_cb (GtkColorButton *button, TboToolSelector *tool) +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 +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) { 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 FALSE; + return; + + GdkRGBA color = tbo_color_picker_get_rgba (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); - GdkColor color = { 0, 0, 0, 0 }; - gtk_color_button_get_color (button, &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), + 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); - 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); + 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 = !tool->selected_frame->border; + 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; } -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 }; + 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) { - 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); - 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); 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_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.5); + self->color_button = tbo_color_picker_new (&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); + 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); - 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); + self->border_button = gtk_check_button_new_with_label (_("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); - gtk_widget_show_all (toolarea); + 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_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)); + tbo_color_picker_set_rgba (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 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)); @@ -184,7 +420,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)); @@ -209,7 +447,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; @@ -229,13 +469,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) { @@ -245,7 +478,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 +491,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,41 +504,132 @@ 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)) { + 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 && + 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) + { + 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, action); + 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); + TboAction *action = tbo_action_frame_remove_new (page, frame, index); + + tbo_page_del_frame (page, frame); + 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); + tbo_toolbar_update (tbo->toolbar); + 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,10 +645,11 @@ 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); + TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); x = (int)event->x; y = (int)event->y; @@ -336,14 +661,16 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *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) { - 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) { @@ -368,6 +695,7 @@ frame_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *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); @@ -395,7 +723,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 +736,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); @@ -423,11 +752,11 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event { 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); - if (tbo_frame_point_inside_obj (obj, x, y)) + if (tbo_drawing_point_inside_object (drawing, obj, x, y)) { // Selecting last occurrence. obj2 = obj; @@ -453,6 +782,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); } } @@ -467,6 +800,7 @@ frame_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *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; } self->clicked = TRUE; } @@ -487,6 +821,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)) + { + tbo_tool_selector_set_selected_object_pointer (self, NULL); + current_obj = NULL; + } + + if (current_obj != NULL && frame != NULL && !tbo_frame_has_obj (frame, current_obj)) + { + tbo_tool_selector_set_selected_object_pointer (self, NULL); + current_obj = NULL; + } if (current_obj != NULL) { @@ -498,7 +845,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); @@ -574,23 +921,26 @@ 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) + 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: tbo_object_base_resize (current_obj, RESIZE_LESS); @@ -613,6 +963,28 @@ frame_view_on_key (TboToolBase *tool, GtkWidget *widget, GdkEventKey *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); } @@ -635,12 +1007,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 @@ -658,8 +1034,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)); @@ -677,7 +1053,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 +1064,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 +1091,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; @@ -732,17 +1104,17 @@ page_view_on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *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; } 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; @@ -762,16 +1134,18 @@ page_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *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); } @@ -799,9 +1173,9 @@ page_view_on_move (TboToolBase *tool, GtkWidget *widget, GdkEventMotion *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); - tbo_tooltip_set (_("double click here"), x1, y1, tbo); + 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; } } @@ -822,16 +1196,23 @@ 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; 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 +1223,30 @@ 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); + + clear_selected_group (self); + 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); + + 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 +1279,13 @@ tbo_tool_selector_get_selected_obj (TboToolSelector *self) void tbo_tool_selector_set_selected (TboToolSelector *self, Frame *frame) { - self->selected_frame = frame; - empty_tool_area (self); + if (self->selected_frame == frame) + { + update_menubar (TBO_TOOL_BASE (self)->tbo); + return; + } + + 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); @@ -890,73 +1294,41 @@ 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); - tbo_frame_del_obj (frame, self->selected_object); - } - self->selected_object = obj; - update_menubar (TBO_TOOL_BASE (self)->tbo); -} - - -static void -frame_move_do (TboAction *act) -{ - TboActionFrameMove *action = (TboActionFrameMove*)act; - action->frame->x = action->x2; - action->frame->y = action->y2; -} + if (obj != self->selected_object) + clear_selected_group (self); -static void -frame_move_undo (TboAction *act) -{ - TboActionFrameMove *action = (TboActionFrameMove*)act; - action->frame->x = action->x1; - action->frame->y = action->y1; + tbo_tool_selector_set_selected_object_pointer (self, obj); + update_menubar (TBO_TOOL_BASE (self)->tbo); } -TboAction * -tbo_action_frame_move_new (Frame *frame, int x1, int y1, int x2, int y2) +gboolean +tbo_tool_selector_delete_selected (TboToolSelector *self) { - 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; - return (TboAction*)action; + return delete_selected (self); } -static void -obj_move_do (TboAction *act) -{ - TboActionObjMove *action = (TboActionObjMove*)act; - action->obj->x = action->x2; - action->obj->y = action->y2; -} - -static void -obj_move_undo (TboAction *act) +void +tbo_tool_selector_reset_state (TboToolSelector *self) { - TboActionObjMove *action = (TboActionObjMove*)act; - action->obj->x = action->x1; - action->obj->y = action->y1; -} + if (self == NULL) + return; -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; - return (TboAction*)action; + 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; + 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; } diff --git a/src/tbo-tool-selector.h b/src/tbo-tool-selector.h index 49aaef2..694e818 100644 --- a/src/tbo-tool-selector.h +++ b/src/tbo-tool-selector.h @@ -55,15 +55,20 @@ 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; 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,37 +88,10 @@ 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 (); +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); -/* - * TboActionFrameMove for undo and redo frame movements - */ -typedef struct _TboActionFrameMove TboActionFrameMove; -typedef struct _TboActionObjMove TboActionObjMove; - -struct _TboActionFrameMove { - void (*action_do) (TboAction *action); - void (*action_undo) (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 _TboActionObjMove { - void (*action_do) (TboAction *action); - void (*action_undo) (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); - #endif /* __TBO_TOOL_SELECTOR_H__ */ - diff --git a/src/tbo-tool-text.c b/src/tbo-tool-text.c index fd5c24e..7b47246 100644 --- a/src/tbo-tool-text.c +++ b/src/tbo-tool-text.c @@ -19,9 +19,13 @@ #include #include "frame.h" #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" +#include "tbo-undo.h" G_DEFINE_TYPE (TboToolText, tbo_tool_text, TBO_TYPE_TOOL_BASE); @@ -30,26 +34,106 @@ 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); +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_focus_editor (TboToolText *self, gboolean select_all); -/* Definitions */ +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) +{ + 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); +} -/* aux */ static gboolean -on_tview_focus_in (GtkWidget *view, GdkEventFocus *event, TboToolText *self) +flush_pending_text_change (gpointer data) { + TboToolText *self = TBO_TOOL_TEXT (data); TboWindow *tbo = TBO_TOOL_BASE (self)->tbo; - tbo_window_set_key_binder (tbo, FALSE); - return FALSE; + 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; } -static gboolean -on_tview_focus_out (GtkWidget *view, GdkEventFocus *event, TboToolText *self) +/* Definitions */ + +/* aux */ +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, TRUE); - return FALSE; + tbo_window_set_key_binder (tbo, !gtk_widget_has_focus (view)); } @@ -61,38 +145,179 @@ 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) { - tbo_object_text_set_text (self->text_selected, gtk_text_buffer_get_text (buf, &start, &end, FALSE)); + 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_drawing_update (TBO_DRAWING (tbo->drawing)); } return FALSE; } -static gboolean -on_font_change (GtkFontButton *fbutton, TboToolText *self) +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) { - tbo_object_text_change_font (self->text_selected, tbo_tool_text_get_pango_font (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; + 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)); } - 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->syncing_controls) + return; + if (self->text_selected) { - GdkColor color; - gtk_color_button_get_color (GTK_COLOR_BUTTON (self->font_color), &color); + 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; + GdkRGBA color = tbo_color_picker_get_rgba (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, &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)); } - return FALSE; } GtkWidget * @@ -102,42 +327,64 @@ 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; + GtkAdjustment *font_size_adjustment; + 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); - gtk_misc_set_alignment (GTK_MISC (font_label), 0, 0); - gtk_misc_set_alignment (GTK_MISC (font_color_label), 0, 0); + self->font = tbo_font_picker_new (); + g_signal_connect (self->font, "notify::font-desc", G_CALLBACK (on_font_change), self); - 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); + 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); - vbox = gtk_vbox_new (FALSE, 5); + self->font_color = tbo_color_picker_new (&default_color); + g_signal_connect (self->font_color, "notify::rgba", G_CALLBACK (on_color_change), self); - 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); + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 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); + 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); - scroll = gtk_scrolled_window_new (NULL, NULL); + 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); + 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); 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); + 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); return vbox; } @@ -147,21 +394,25 @@ 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); + self->text_view = NULL; + self->text_buffer = NULL; 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,14 +420,28 @@ 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)); + TboDrawing *drawing = TBO_DRAWING (tool->tbo->drawing); + Frame *frame = tbo_drawing_get_current_frame (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) + 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; @@ -184,16 +449,28 @@ on_click (TboToolBase *tool, GtkWidget *widget, GdkEventButton *event) } if (!found) { - 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 (!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; + + gchar *font = tbo_tool_text_build_font (self); + color = tbo_color_picker_get_rgba (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_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); + if (!found) + tbo_tool_text_focus_editor (self, TRUE); tbo_drawing_update (TBO_DRAWING (tool->tbo->drawing)); } @@ -211,7 +488,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); @@ -228,9 +505,16 @@ static void 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; + 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; @@ -241,12 +525,27 @@ 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) +{ + 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); } /* 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 +566,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,32 +575,129 @@ 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); + 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; } 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) +{ + PangoFontDescription *description = NULL; + gchar *result; + + if (self->font) + description = tbo_font_picker_dup_font_desc (self->font); + + if (description == NULL) + description = pango_font_description_from_string (DEFAULT_PANGO_FONT); + + 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); + 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; + + 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); +} + void 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; } 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 }; + tbo_color_picker_set_rgba (self->font_color, &default_color); + } + self->syncing_controls = FALSE; 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); + 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; +} + +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 caef357..21a360e 100644 --- a/src/tbo-tool-text.h +++ b/src/tbo-tool-text.h @@ -43,9 +43,17 @@ struct _TboToolText /* instance members */ GtkWidget *font; + GtkWidget *font_size; GtkWidget *font_color; + GtkWidget *text_view; 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 @@ -61,11 +69,11 @@ 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); +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 8f63973..c4907f5 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,302 +27,365 @@ #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" #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" +#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_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 create_icon_wrapper (image); +} + +static GtkWidget * +create_icon_from_file (const gchar *path) +{ + GtkWidget *image; + + image = gtk_picture_new_for_filename (path); + gtk_picture_set_can_shrink (GTK_PICTURE (image), TRUE); + tbo_picture_set_contain (GTK_PICTURE (image)); + gtk_widget_set_halign (image, GTK_ALIGN_CENTER); + gtk_widget_set_valign (image, GTK_ALIGN_CENTER); + + return create_icon_wrapper (image); +} + +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"); -static gboolean select_tool (GtkAction *action, TboToolbar *toolbar); + 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_update_status (tbo, 0, 0); + gint index = tbo_comic_page_nth (tbo->comic, page); + + 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); + tbo_window_mark_dirty (tbo); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); return FALSE; } static gboolean -del_current_page (GtkAction *action, TboWindow *tbo) +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) { 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); - gtk_notebook_remove_page (GTK_NOTEBOOK (tbo->notebook), nth); - tbo_window_update_status (tbo, 0, 0); + tbo_window_mark_dirty (tbo); + tbo_window_refresh_status (tbo); tbo_toolbar_update (tbo->toolbar); + g_object_unref (page); 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); 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; } 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); 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; } 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_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 (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); + 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 (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); + 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_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); + 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 (S)"))); + register_tool_button (self, TBO_TOOLBAR_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 (D)"))); + register_tool_button (self, TBO_TOOLBAR_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 (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); + 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"), _("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 (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); + 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_duplicate_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 +401,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 +414,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 +428,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 +469,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 +510,37 @@ 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_duplicate_page, tbo_comic_len (tbo->comic) > 0); + 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))) - { - 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 + 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, 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..14214a0 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,24 @@ 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_duplicate_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 +89,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 +98,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..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 (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 +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,27 +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; - GtkAllocation alloc; - gtk_widget_get_allocation (tbo->drawing, &alloc); - x = alloc.width / 2; - y = alloc.height / 2; + + 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 4c9177a..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 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 9681e1a..0016837 100644 --- a/src/tbo-types.h +++ b/src/tbo-types.h @@ -31,36 +31,26 @@ typedef struct double b; } Color; -typedef struct -{ - char title[255]; - int width; - int height; - GList *pages; - -} Comic; +typedef struct _Comic Comic; +typedef struct _Page Page; +typedef struct _Frame Frame; typedef struct { - Comic *comic; - GList *frames; - -} Page; + double x; + double y; + guint button; + guint n_press; + GdkModifierType state; +} TboPointerEvent; typedef struct { - int x; - int y; - int width; - int height; - gboolean border; - Color *color; - GList *objects; - -} Frame; + 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..18a7402 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,109 @@ 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; } + +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-undo.c b/src/tbo-undo.c index 2346309..e93170d 100644 --- a/src/tbo-undo.c +++ b/src/tbo-undo.c @@ -19,53 +19,796 @@ */ #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" -void -tbo_action_set (TboAction *action, - gpointer action_do, - gpointer action_undo) +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; + Comic *comic; + Page *page; + gint index1; + gint index2; +} TboActionPageOrder; + +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; + 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; + 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; + 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 { - action->action_do = action_do; - action->action_undo = action_undo; + 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); } -void -tbo_action_del (TboAction *action) +static gboolean +page_has_frame (Page *page, Frame *frame) { - free (action); + 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 +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) +{ + 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 +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) +{ + 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_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) +{ + 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_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) +{ + 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 +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) +{ + 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_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) +{ + 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_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) +{ + 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_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) +{ + 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_del_data (TboAction *action, gpointer user_data) +tbo_action_del (TboAction *action) { + if (action->action_free != NULL) + action->action_free (action); + free (action); } TboUndoStack * -tbo_undo_stack_new () +tbo_undo_stack_new (void) { TboUndoStack *stack = malloc (sizeof (TboUndoStack)); 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) { - tbo_action_del ((TboAction*)((stack->first)->data)); - stack->first = g_list_remove_link (stack->first, stack->first); + if (stack->first) + { + if (stack->last_flag) + tbo_undo_stack_clear (stack); + + while (stack->first != NULL && 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); } } 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 +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; + stack->next_state_id = stack->current_state_id + 1; } void @@ -81,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; @@ -106,12 +850,13 @@ 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 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 +872,338 @@ 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_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) +{ + 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_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, + 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_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, + 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 e03c97d..f9a9a38 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,19 +39,24 @@ typedef struct _TboUndoStack TboUndoStack; 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 tbo_undo_stack_del (); +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 +64,55 @@ 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_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); +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_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, + gboolean flipv2, + gboolean fliph2); +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, + 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 1b187aa..3225401 100644 --- a/src/tbo-utils.c +++ b/src/tbo-utils.c @@ -17,12 +17,18 @@ */ +#include #include +#include +#include +#include +#include "config.h" +#include "tbo-object-base.h" #include "tbo-utils.h" void -get_base_name (gchar *str, gchar *ret, int size) +get_base_name (const gchar *str, gchar *ret, int size) { gchar **paths; gchar **dirname; @@ -33,3 +39,161 @@ 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); +} + +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 21d2741..059adf0 100644 --- a/src/tbo-utils.h +++ b/src/tbo-utils.h @@ -22,6 +22,19 @@ #include -void get_base_name (gchar *str, gchar *ret, int size); +typedef struct _TboObjectBase TboObjectBase; + +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); +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 new file mode 100644 index 0000000..8b99b9e --- /dev/null +++ b/src/tbo-widget.c @@ -0,0 +1,390 @@ +/* + * 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" + +#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 +#if GTK_CHECK_VERSION(4, 10, 0) +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); +} +#else +alert_response_cb (GtkButton *button, GtkWindow *dialog) +{ + tbo_dialog_button_cb (button, dialog); +} +#endif + +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); + else if (GTK_IS_FRAME (parent)) + gtk_frame_set_child (GTK_FRAME (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_FRAME (parent)) + gtk_frame_set_child (GTK_FRAME (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); +} + +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, + const gchar *detail, + const gchar * const *buttons, + gint cancel_button, + gint default_button) +{ +#if GTK_CHECK_VERSION(4, 10, 0) + 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); + 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; +#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 +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_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) +{ + 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; +} diff --git a/src/tbo-widget.h b/src/tbo-widget.h new file mode 100644 index 0000000..270ebf9 --- /dev/null +++ b/src/tbo-widget.h @@ -0,0 +1,51 @@ +/* + * 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 + +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); +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); +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, + const gchar * const *buttons, + 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); + +#endif diff --git a/src/tbo-window.c b/src/tbo-window.c index 8b93bd4..9e4b40a 100644 --- a/src/tbo-window.c +++ b/src/tbo-window.c @@ -18,94 +18,1251 @@ #include -#include +#include #include #include -#include +#include #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" +#include "comic-saveas-dialog.h" -static int NWINDOWS = 0; -static gboolean KEY_BINDER = TRUE; +static gboolean on_key_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + 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); +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); +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) +{ + 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->undo_stack->current_state_id = 0; + tbo->undo_stack->next_state_id = 1; + tbo_tooltip_reset (tbo); +} + +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 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) +{ + 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 void +sync_page_widgets_with_comic (TboWindow *tbo) +{ + gint widget_count; + gint comic_count; + gint i; + + 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_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 +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_refresh_status (tbo); + tbo_drawing_adjust_scroll (TBO_DRAWING (tbo->drawing)); + 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) +{ + 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_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 = load_state_key_file (); + gchar *value = NULL; + + if (g_key_file_has_group (kf, "paths")) + value = g_key_file_get_string (kf, "paths", key, NULL); + + g_key_file_unref (kf); + return value; +} + +static void +store_persisted_path (const gchar *key, const gchar *value) +{ + GKeyFile *kf = load_state_key_file (); + + g_key_file_set_string (kf, "paths", key, value); + save_state_key_file (kf); + g_key_file_unref (kf); +} + +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 ()); +} + +gboolean +tbo_window_prepare_for_document_replace (TboWindow *tbo) +{ + return confirm_close (tbo) != TBO_CONFIRM_CLOSE_CANCEL; +} + +static TboConfirmCloseResult +confirm_close (TboWindow *tbo) +{ + gint response; + static const gchar *buttons[] = { + "_Cancel", + "_Don't Save", + "_Save", + NULL, + }; + + if (!tbo_window_has_unsaved_changes (tbo)) + return TBO_CONFIRM_CLOSE_CONTINUE; + + 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) ? TBO_CONFIRM_CLOSE_CONTINUE : TBO_CONFIRM_CLOSE_CANCEL; + + if (response == 1) + return TBO_CONFIRM_CLOSE_DISCARD; + + return TBO_CONFIRM_CLOSE_CANCEL; +} + +static void +append_status_segment (GString *status, const gchar *segment) +{ + if (segment == NULL || *segment == '\0') + return; + + if (status->len > 0) + g_string_append (status, " | "); + + g_string_append (status, segment); +} + +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) + { + selected_frame = tbo_tool_selector_get_selected_frame (selector); + selected_object = tbo_tool_selector_get_selected_obj (selector); + } + + 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_position (tbo->comic), + 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 + { + 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), status->str); + g_string_free (status, TRUE); +} + +static void +load_app_css (void) +{ + static gboolean loaded = FALSE; + GtkCssProvider *provider; + const gchar *css; + + if (loaded) + 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.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: 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.985);" + " border-left: 1px solid alpha(@theme_fg_color, 0.08);" + "}" + "#tbo-status {" + " padding: 9px 12px;" + " border-top: 1px solid alpha(@theme_fg_color, 0.08);" + " background: shade(@theme_bg_color, 1.015);" + "}" + "#tbo-toolarea {" + " 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 (); +#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); + g_object_unref (provider); + loaded = TRUE; +} static gboolean -notebook_switch_page_cb (GtkNotebook *notebook, - gpointer *page, - guint page_num, - TboWindow *tbo) +tbo_window_apply_unmodified_key (TboWindow *tbo, guint keyval) { - 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_drawing_adjust_scroll (TBO_DRAWING (tbo->drawing)); - return FALSE; + 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 (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); + tbo_window_refresh_status (tbo); - if (KEY_BINDER) - { - switch (event->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; } 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); + GtkWidget *focus; + + 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; + } + + 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; + } + } + return FALSE; } @@ -116,22 +1273,43 @@ 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->menu_button = NULL; tbo->comic = comic; tbo->toolarea = toolarea; tbo->notebook = notebook; tbo->undo_stack = tbo_undo_stack_new (); 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; + tbo->clean_state_id = 0; + update_window_title (tbo); return tbo; } @@ -139,74 +1317,332 @@ 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) + { + g_object_unref (tbo->toolbar); + tbo->toolbar = NULL; + } tbo_comic_free (tbo->comic); - gtk_widget_destroy (tbo->window); - if (tbo->path) - free (tbo->path); + 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); } void -tbo_window_set_path (TboWindow *tbo, const char *path) +tbo_window_set_path (TboWindow *tbo, const gchar *path) { - if (tbo->path) - free (tbo->path); - tbo->path = malloc (255 * sizeof (char)); - snprintf (tbo->path, 255, "%s", path); + set_window_path (&tbo->path, path); + tbo_window_set_browse_path (tbo, path); } -gboolean -tbo_window_free_cb (GtkWidget *widget, GdkEventExpose *event, - TboWindow *tbo) +void +tbo_window_set_browse_path (TboWindow *tbo, const gchar *path) { - tbo_window_free (tbo); - NWINDOWS--; - if (!NWINDOWS) - gtk_main_quit (); - return FALSE; + 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); + + return get_dirname_or_home (tbo->path); +} + +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); + + return tbo_window_get_open_dir (tbo); +} + +void +tbo_window_mark_dirty (TboWindow *tbo) +{ + tbo->dirty = TRUE; + update_window_title (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) + { + g_source_remove (tbo->autosave_timeout_id); + tbo->autosave_timeout_id = 0; + } + delete_recovery_files_for_window (tbo); +} + +gboolean +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); + 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) + 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; + + if (!tbo_comic_open (tbo, (char *) path)) + return FALSE; + + 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_set_theme_mode (TboWindow *tbo, TboThemeMode mode) +{ + 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 +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) +{ + return gtk_notebook_get_n_pages (GTK_NOTEBOOK (tbo->notebook)); } -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); - } + TboConfirmCloseResult result = confirm_close (tbo); + + (void) window; + + if (result != TBO_CONFIRM_CLOSE_CANCEL) + { + if (result == TBO_CONFIRM_CLOSE_DISCARD) + tbo_window_mark_clean (tbo); - return pixbuf; + tbo_window_reset_document_state (tbo); + tbo->destroying = TRUE; + return FALSE; + } + + return TRUE; } + GtkWidget * create_darea (TboWindow *tbo) { 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_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); + tbo_scrolled_window_set_child (scrolled, darea); + setup_darea_controllers (darea, tbo); + 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 +1650,53 @@ 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); + container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + 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_widget_set_name (notebook, "tbo-pages"); gtk_notebook_set_scrollable (GTK_NOTEBOOK (notebook), TRUE); - gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, NULL); + gtk_widget_set_hexpand (notebook, TRUE); + gtk_widget_set_vexpand (notebook, TRUE); - hpaned = gtk_hpaned_new (); - tool_paned = gtk_vbox_new (FALSE, 0); - scrolled2 = gtk_scrolled_window_new (NULL, NULL); + 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); @@ -253,61 +1704,64 @@ tbo_new_tbo (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); - 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); + 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); + 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_window_add_page_widget (tbo, scrolled, tbo_comic_get_current_page (comic)); 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) -{ - 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); +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; } -gboolean -remove_cb (GtkWidget *widget, gpointer data) +void +tbo_window_refresh_status (TboWindow *tbo) { - gtk_widget_destroy (widget); - return FALSE; + if (tbo == NULL || tbo->destroying) + return; + + update_statusbar (tbo); + 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 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 @@ -319,31 +1773,126 @@ 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); - 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_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) +{ + 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_refresh_status (tbo); + tbo_drawing_adjust_scroll (drawing); +} + gboolean -tbo_window_undo_cb (GtkAction *action, TboWindow *tbo) { +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); + 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) || + 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); tbo_toolbar_update (tbo->toolbar); return FALSE; } gboolean -tbo_window_redo_cb (GtkAction *action, TboWindow *tbo) { +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); + 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) || + 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); tbo_toolbar_update (tbo->toolbar); return FALSE; } diff --git a/src/tbo-window.h b/src/tbo-window.h index e64fa49..4b5c14c 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,25 +51,66 @@ struct _TboWindow GtkWidget *drawing; GtkWidget *status; GtkWidget *vbox; + GtkWidget *menu_button; TboToolbar *toolbar; TboUndoStack *undo_stack; Comic *comic; - char *path; + 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; + 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, GdkEventExpose *event, TboWindow *tbo); -GdkPixbuf *create_pixbuf (const gchar * filename); -TboWindow * tbo_new_tbo (int width, int height); -void tbo_window_update_status (TboWindow *tbo, int x, int y); +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 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); +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); +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); -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); +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); #endif diff --git a/src/tbo.c b/src/tbo.c index 3e92a95..2a80fac 100644 --- a/src/tbo.c +++ b/src/tbo.c @@ -19,32 +19,114 @@ #include #include - #include "config.h" -#include "custom-stock.h" #include "tbo-window.h" #include "comic.h" +#include "ui-menu.h" +#include "tbo-utils.h" +#include "tbo-widget.h" -int main (int argc, char**argv){ +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); + } +} -#ifdef ENABLE_NLS - /* Initialize the i18n stuff */ - bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR); - bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); - textdomain (GETTEXT_PACKAGE); -#endif +static void +activate_cb (GtkApplication *app, gpointer user_data) +{ + gsize n_recovery_files = 0; + gchar **recovery_files = tbo_window_list_recovery_files (&n_recovery_files); - TboWindow *tbo; + 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; - gtk_init (&argc, &argv); - load_custom_stock (); - tbo = tbo_new_tbo (800, 450); - if (argc == 2) - tbo_comic_open (tbo, argv[1]); + if (response == 1) + { + for (i = 0; i < n_recovery_files; i++) + { + TboWindow *tbo = tbo_new_tbo (app, 800, 450); - gtk_main (); + tbo_window_recover_file (tbo, recovery_files[i]); + present_window (tbo); + } + g_strfreev (recovery_files); + return; + } - return 0; + 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 +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) + { + 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); + } +} + +int main (int argc, char**argv){ + GtkApplication *app; + int status; + + g_set_application_name ("TBO"); + tbo_init_i18n (); + + 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); + + status = g_application_run (G_APPLICATION (app), argc, argv); + g_object_unref (app); + + 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..99a53e1 100644 --- a/src/ui-menu.c +++ b/src/ui-menu.c @@ -17,7 +17,6 @@ */ -#include #include #include @@ -34,66 +33,307 @@ #include "frame.h" #include "page.h" #include "tbo-object-base.h" +#include "tbo-object-group.h" #include "tbo-undo.h" +#include "tbo-utils.h" +#include "tbo-widget.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) +typedef struct { - GtkAction *action; - int i; - char *actions[20] = {"FlipHObj", "FlipVObj", "OrderUpObj", "OrderDownObj", "DeleteObj", "CloneObj"}; - int elements = 6; - int obj_n_elements = 4; + 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}; +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 gchar *ACCEL_SHORTCUTS[] = {"F1", 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}, + {"win.shortcuts", ACCEL_SHORTCUTS}, +}; - if (!tbo->toolbar) - return; +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}, +}; - 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 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}, +}; - if (!MENU_ACTION_GROUP) - return; +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}, +}; - if (tbo_drawing_get_current_frame (drawing) && obj) +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 * +get_app (TboWindow *tbo) +{ + return gtk_window_get_application (GTK_WINDOW (tbo->window)); +} + +static void +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) { - for (i=0; i%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 + gtk_popover_popup (popover); +} + +static void +menu_button_destroy_cb (GtkWidget *button, gpointer user_data) +{ + 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); +} + +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; + + for (i = 0; i < G_N_ELEMENTS (MENU_ACCELS); i++) { - for (i=0; iobjs; 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; } - // undo and redo - action = gtk_action_group_get_action (MENU_ACTION_GROUP, "Undo"); - gtk_action_set_sensitive (action, tbo_undo_active_undo (tbo->undo_stack)); - action = gtk_action_group_get_action (MENU_ACTION_GROUP, "Redo"); - gtk_action_set_sensitive (action, tbo_undo_active_redo (tbo->undo_stack)); + if (last_cloned != NULL) + { + tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); + tbo_tool_selector_set_selected_obj (selector, last_cloned); + } } -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; @@ -101,124 +341,199 @@ clone_obj_cb (GtkWidget *widget, 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)) { - TboObjectBase *cloned_obj = obj->clone (obj); - cloned_obj->x += 10; - cloned_obj->y -= 10; - tbo_frame_add_obj (frame, cloned_obj); - tbo_toolbar_set_selected_tool (tbo->toolbar, TBO_TOOLBAR_SELECTOR); - tbo_tool_selector_set_selected_obj (selector, cloned_obj); + 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) + { + tbo_window_mark_dirty (tbo); + if (!tbo_drawing_get_current_frame (drawing)) + tbo_window_refresh_status (tbo); + } + tbo_drawing_update (drawing); +} + +static void +delete_selection (TboWindow *tbo) +{ + TboToolSelector *selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + + if (tbo_tool_selector_delete_selected (selector)) + { + tbo_window_mark_dirty (tbo); + tbo_drawing_update (TBO_DRAWING (tbo->drawing)); } - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -delete_obj_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; 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 (obj != NULL && frame != NULL) { - tbo_frame_del_obj (frame, obj); - tbo_tool_selector_set_selected_obj (selector, NULL); + 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)); } - else if (!tbo_drawing_get_current_frame (drawing) && frame) + + if (obj != NULL) { - tbo_page_del_frame (page, frame); - tbo_tool_selector_set_selected (selector, NULL); + tbo_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); } tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; } -gboolean -flip_v_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) + Frame *frame = selector->selected_frame; + + if (obj != NULL && frame != NULL) + { tbo_object_base_flipv (obj); - tbo_drawing_update (TBO_DRAWING (tbo->drawing)); - return FALSE; -} + tbo_undo_stack_insert (tbo->undo_stack, + tbo_action_object_flags_new (obj, + !obj->flipv, + obj->fliph, + obj->flipv, + obj->fliph)); + } -gboolean -flip_h_cb (GtkWidget *widget, 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_window_mark_dirty (tbo); + tbo_toolbar_update (tbo->toolbar); + } 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); + Frame *frame = selector->selected_frame; + gint index1; + gint index2; + + 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)); - 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); + Frame *frame = selector->selected_frame; + gint index1; + gint index2; + + 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)); - 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; + if (tbo_window_prepare_for_document_replace (tbo) && tbo_comic_open (tbo, filename)) + { + g_free (tbo->path); + tbo->path = NULL; + } + + g_free (filename); } -gboolean -about_cb (GtkWidget *widget, TboWindow *tbo){ - const gchar *authors[] = {"danigm ", NULL}; +static void +show_about (TboWindow *tbo) +{ + const gchar *authors[] = { + "danigm ", + "", + "Actualizado por Jaime, 2026", + NULL + }; const gchar *artists[] = {"danigm ", - "", + "", "Arcadia http://www.arcadiaproject.org :", "Samuel Navas Portillo", "Daniel Pavón Pérez", "Juan Jesús Pérez Luna", "", - "Zapatero & Rajoy:", + "Zapatero y Rajoy:", "Alfonso de Cala", "", "South park style:", @@ -229,140 +544,280 @@ 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); + "name", _("TBO Comic Editor"), + "version", VERSION, + "logo-icon-name", "tbo", + "authors", authors, + "artists", artists, + "website", "https://github.com/danigm/TBO/", + "translator-credits", _("translator-credits"), + NULL); +} - return FALSE; +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); } +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_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)); } -gboolean -tbo_menu_export (GtkWidget *widget, TboWindow *tbo) +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}, + {"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}, + {"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}, + {"shortcuts", action_shortcuts, 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); + + { + 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 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 (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); + } -GtkWidget *generate_menu (TboWindow *window){ - GtkWidget *menu; - GtkUIManager *manager; - GError *error = NULL; + g_menu_append_submenu (section, _("Open Recent"), G_MENU_MODEL (recent)); + g_object_unref (recent); + } - manager = gtk_ui_manager_new (); - gtk_ui_manager_add_ui_from_file (manager, DATA_DIR "/ui/tbo-menu-ui.xml", &error); - if (error != NULL) + 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 (root, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + section = g_menu_new (); { - g_warning (_("Could not merge tbo-menu-ui.xml: %s"), error->message); - g_error_free (error); + 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"); + g_menu_append_section (root, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + return G_MENU_MODEL (root); +} - 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); +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; - gtk_ui_manager_insert_action_group (manager, MENU_ACTION_GROUP, 0); + drawing = TBO_DRAWING (tbo->drawing); + selector = TBO_TOOL_SELECTOR (tbo->toolbar->tools[TBO_TOOLBAR_SELECTOR]); + obj = selector->selected_object; + frame = selector->selected_frame; - menu = gtk_ui_manager_get_widget (manager, "/menubar"); + if (tbo_drawing_get_current_frame (drawing) && obj) + { + active = TRUE; + object_selected = TRUE; + } + else if (!tbo_drawing_get_current_frame (drawing) && frame) + { + active = TRUE; + } - ACCEL = gtk_ui_manager_get_accel_group (manager); - gtk_window_add_accel_group (GTK_WINDOW (window->window), ACCEL); - ACCEL_SET = TRUE; + 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); +} - return menu; +GtkWidget * +generate_menu (TboWindow *window) +{ + GtkWidget *button; + GtkWidget *icon; + + install_actions (window); + set_accels_enabled (window, TRUE); + + button = gtk_button_new (); + icon = gtk_image_new_from_icon_name ("open-menu-symbolic"); + gtk_image_set_pixel_size (GTK_IMAGE (icon), 16); + gtk_button_set_child (GTK_BUTTON (button), icon); + gtk_widget_set_focusable (button, TRUE); + gtk_widget_set_tooltip_text (button, _("Menu")); + 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) { - 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/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/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/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/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 new file mode 100644 index 0000000..4ff4ee1 --- /dev/null +++ b/tests/asset_bounds_check.c @@ -0,0 +1,62 @@ +#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 = 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 = tbo_frame_object_count (frame); + if (after_inside != before + 1) + return 6; + + 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 = tbo_frame_object_count (frame); + if (after_outside != after_inside) + return 8; + + return 0; +} diff --git a/tests/assets_browser_search_check.c b/tests/assets_browser_search_check.c new file mode 100644 index 0000000..d4046b1 --- /dev/null +++ b/tests/assets_browser_search_check.c @@ -0,0 +1,113 @@ +#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; + + 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); + 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/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/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/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/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..4409e17 --- /dev/null +++ b/tests/dnd_feedback_check.c @@ -0,0 +1,74 @@ +#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; + + 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); + 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/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/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/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/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/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_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/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_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_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_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/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/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/frame_count_status_check.c b/tests/frame_count_status_check.c new file mode 100644 index 0000000..cce11fe --- /dev/null +++ b/tests/frame_count_status_check.c @@ -0,0 +1,46 @@ +#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; + + 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); + 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_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_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/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/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/frame_view_coordinate_mapping_check.c b/tests/frame_view_coordinate_mapping_check.c new file mode 100644 index 0000000..8bdd6c6 --- /dev/null +++ b/tests/frame_view_coordinate_mapping_check.c @@ -0,0 +1,80 @@ +#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" + +static void +drain_events (void) +{ + while (g_main_context_iteration (NULL, FALSE)); +} + +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); + 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 7; + g_free (asset_path); + + if (tbo_frame_object_count (frame) != 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/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/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..01a2a75 --- /dev/null +++ b/tests/invalid_tbo_variants_check.c @@ -0,0 +1,72 @@ +#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", + "", + "", + "", + "", + "text", + }; + 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/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/keyboard_accessibility_check.c b/tests/keyboard_accessibility_check.c new file mode 100644 index 0000000..1316d8b --- /dev/null +++ b/tests/keyboard_accessibility_check.c @@ -0,0 +1,143 @@ +#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 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; + + 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); + 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; + 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/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 new file mode 100644 index 0000000..a27a616 --- /dev/null +++ b/tests/load_render_check.c @@ -0,0 +1,55 @@ +#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, + tbo_comic_get_width (comic), + tbo_comic_get_height (comic)); + cr = cairo_create (surface); + 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) + { + 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/mode_status_check.c b/tests/mode_status_check.c new file mode 100644 index 0000000..6ce59bc --- /dev/null +++ b/tests/mode_status_check.c @@ -0,0 +1,50 @@ +#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; + + 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); + 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 new file mode 100644 index 0000000..9cc92da --- /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) != 0) + return 16; + + if (tbo_comic_page_nth (comic, page4) != 1) + return 17; + + tbo_comic_free (comic); + return 0; +} 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_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_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_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 new file mode 100644 index 0000000..12fdca1 --- /dev/null +++ b/tests/page_status_check.c @@ -0,0 +1,40 @@ +#include +#include + +#include "comic.h" +#include "page.h" +#include "tbo-window.h" + +int +main (void) +{ + GtkApplication *app; + TboWindow *tbo; + 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); + 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), 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, "Mode: Page | 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/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; +} 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/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/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/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/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/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; +} 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 new file mode 100644 index 0000000..e1520a3 --- /dev/null +++ b/tests/shortcuts_guide_check.c @@ -0,0 +1,131 @@ +#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; + + 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); + 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 new file mode 100644 index 0000000..39987cc --- /dev/null +++ b/tests/status_hierarchy_check.c @@ -0,0 +1,66 @@ +#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; + + 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); + 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, "Mode: Page") == NULL || + 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, "Mode: Frame") == NULL || + 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/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; +} 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/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 new file mode 100644 index 0000000..3b2b971 --- /dev/null +++ b/tests/xml_roundtrip_check.c @@ -0,0 +1,123 @@ +#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 ("\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); +} + +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); + + 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; +} 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; +}