summaryrefslogtreecommitdiff
path: root/tikzit/src/gtk
diff options
context:
space:
mode:
authorrandomguy3 <randomguy3@7c02a99a-9b00-45e3-bf44-6f3dd7fddb64>2012-01-18 10:05:06 +0000
committerrandomguy3 <randomguy3@7c02a99a-9b00-45e3-bf44-6f3dd7fddb64>2012-01-18 10:05:06 +0000
commitdcf64eac4fdb246cf6fb169c1279b67966478173 (patch)
tree563a9b0bfff1050775f693f5832683ad2590d1b2 /tikzit/src/gtk
parent0dc14c2123819e1996d989b766c87571ac4a2866 (diff)
Rename src/linux to src/gtk (which is more accurate)
git-svn-id: https://tikzit.svn.sourceforge.net/svnroot/tikzit/trunk@392 7c02a99a-9b00-45e3-bf44-6f3dd7fddb64
Diffstat (limited to 'tikzit/src/gtk')
-rw-r--r--tikzit/src/gtk/CairoRenderContext.h59
-rw-r--r--tikzit/src/gtk/CairoRenderContext.m346
-rw-r--r--tikzit/src/gtk/ColorRGB+Gtk.h30
-rw-r--r--tikzit/src/gtk/ColorRGB+Gtk.m46
-rw-r--r--tikzit/src/gtk/ColorRGB+IntegerListStorage.h32
-rw-r--r--tikzit/src/gtk/ColorRGB+IntegerListStorage.m57
-rw-r--r--tikzit/src/gtk/Configuration.h447
-rw-r--r--tikzit/src/gtk/Configuration.m446
-rw-r--r--tikzit/src/gtk/Edge+Render.h33
-rw-r--r--tikzit/src/gtk/Edge+Render.m251
-rw-r--r--tikzit/src/gtk/EdgeStyle+Storage.h29
-rw-r--r--tikzit/src/gtk/EdgeStyle+Storage.m49
-rw-r--r--tikzit/src/gtk/EdgeStyleEditor.h39
-rw-r--r--tikzit/src/gtk/EdgeStyleEditor.m322
-rw-r--r--tikzit/src/gtk/EdgeStyleSelector.h69
-rw-r--r--tikzit/src/gtk/EdgeStyleSelector.m454
-rw-r--r--tikzit/src/gtk/EdgeStylesPalette.h46
-rw-r--r--tikzit/src/gtk/EdgeStylesPalette.m283
-rw-r--r--tikzit/src/gtk/FileChooserDialog.h56
-rw-r--r--tikzit/src/gtk/FileChooserDialog.m152
-rw-r--r--tikzit/src/gtk/GraphInputHandler.h69
-rw-r--r--tikzit/src/gtk/GraphInputHandler.m410
-rw-r--r--tikzit/src/gtk/GraphRenderer.h105
-rw-r--r--tikzit/src/gtk/GraphRenderer.m607
-rw-r--r--tikzit/src/gtk/InputDelegate.h76
-rw-r--r--tikzit/src/gtk/MainWindow.h221
-rw-r--r--tikzit/src/gtk/MainWindow.m1035
-rw-r--r--tikzit/src/gtk/Menu.h99
-rw-r--r--tikzit/src/gtk/Menu.m796
-rw-r--r--tikzit/src/gtk/NSError+Glib.h28
-rw-r--r--tikzit/src/gtk/NSError+Glib.m50
-rw-r--r--tikzit/src/gtk/NSFileManager+Glib.h31
-rw-r--r--tikzit/src/gtk/NSFileManager+Glib.m55
-rw-r--r--tikzit/src/gtk/NSString+Glib.h50
-rw-r--r--tikzit/src/gtk/NSString+Glib.m96
-rw-r--r--tikzit/src/gtk/Node+Render.h44
-rw-r--r--tikzit/src/gtk/Node+Render.m188
-rw-r--r--tikzit/src/gtk/NodeStyle+Gtk.h31
-rw-r--r--tikzit/src/gtk/NodeStyle+Gtk.m41
-rw-r--r--tikzit/src/gtk/NodeStyle+Render.h30
-rw-r--r--tikzit/src/gtk/NodeStyle+Storage.h29
-rw-r--r--tikzit/src/gtk/NodeStyle+Storage.m62
-rw-r--r--tikzit/src/gtk/NodeStyleEditor.h43
-rw-r--r--tikzit/src/gtk/NodeStyleEditor.m472
-rw-r--r--tikzit/src/gtk/NodeStyleSelector.h69
-rw-r--r--tikzit/src/gtk/NodeStyleSelector.m426
-rw-r--r--tikzit/src/gtk/NodeStylesPalette.h46
-rw-r--r--tikzit/src/gtk/NodeStylesPalette.m279
-rw-r--r--tikzit/src/gtk/PreambleEditor.h50
-rw-r--r--tikzit/src/gtk/PreambleEditor.m556
-rw-r--r--tikzit/src/gtk/Preambles+Storage.h29
-rw-r--r--tikzit/src/gtk/Preambles+Storage.m84
-rw-r--r--tikzit/src/gtk/PreviewRenderer.h48
-rw-r--r--tikzit/src/gtk/PreviewRenderer.m230
-rw-r--r--tikzit/src/gtk/PreviewWindow.h50
-rw-r--r--tikzit/src/gtk/PreviewWindow.m191
-rw-r--r--tikzit/src/gtk/PropertyListEditor.h63
-rw-r--r--tikzit/src/gtk/PropertyListEditor.m422
-rw-r--r--tikzit/src/gtk/PropertyPane.h65
-rw-r--r--tikzit/src/gtk/PropertyPane.m598
-rw-r--r--tikzit/src/gtk/RecentManager.h30
-rw-r--r--tikzit/src/gtk/RecentManager.m74
-rw-r--r--tikzit/src/gtk/SettingsDialog.h46
-rw-r--r--tikzit/src/gtk/SettingsDialog.m273
-rw-r--r--tikzit/src/gtk/Shape+Render.h29
-rw-r--r--tikzit/src/gtk/Shape+Render.m57
-rw-r--r--tikzit/src/gtk/StyleManager+Storage.h26
-rw-r--r--tikzit/src/gtk/StyleManager+Storage.m81
-rw-r--r--tikzit/src/gtk/StylesPane.h49
-rw-r--r--tikzit/src/gtk/StylesPane.m143
-rw-r--r--tikzit/src/gtk/Surface.h90
-rw-r--r--tikzit/src/gtk/TZFoundation.h28
-rw-r--r--tikzit/src/gtk/TikzDocument.h163
-rw-r--r--tikzit/src/gtk/TikzDocument.m807
-rw-r--r--tikzit/src/gtk/WidgetSurface.h55
-rw-r--r--tikzit/src/gtk/WidgetSurface.m539
-rw-r--r--tikzit/src/gtk/cairo_helpers.h25
-rw-r--r--tikzit/src/gtk/cairo_helpers.m28
-rw-r--r--tikzit/src/gtk/clipboard.h41
-rw-r--r--tikzit/src/gtk/clipboard.m57
-rw-r--r--tikzit/src/gtk/gtkhelpers.h49
-rw-r--r--tikzit/src/gtk/gtkhelpers.m244
-rw-r--r--tikzit/src/gtk/logo.h32
-rw-r--r--tikzit/src/gtk/logo.m64
-rw-r--r--tikzit/src/gtk/main.m75
-rw-r--r--tikzit/src/gtk/mkdtemp.h32
-rw-r--r--tikzit/src/gtk/mkdtemp.m204
-rw-r--r--tikzit/src/gtk/test/gtk.m27
-rw-r--r--tikzit/src/gtk/test/main.m50
89 files changed, 14538 insertions, 0 deletions
diff --git a/tikzit/src/gtk/CairoRenderContext.h b/tikzit/src/gtk/CairoRenderContext.h
new file mode 100644
index 0000000..ac9c5ee
--- /dev/null
+++ b/tikzit/src/gtk/CairoRenderContext.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "RenderContext.h"
+#import "Transformer.h"
+#import <cairo/cairo.h>
+#import <pango/pango.h>
+#import <gtk/gtk.h>
+
+@interface PangoTextLayout: NSObject<TextLayout> {
+ PangoLayout *layout;
+ cairo_t *context;
+}
+
++ (PangoTextLayout*) layoutForContext:(cairo_t*)cr withFontSize:(CGFloat)fontSize;
+- (id) initWithContext:(cairo_t*)cr fontSize:(CGFloat)fontSize;
+- (void) setText:(NSString*)text;
+
+@end
+
+@interface CairoRenderContext: NSObject<RenderContext> {
+ cairo_t *context;
+}
+
++ (CairoRenderContext*) contextForSurface:(cairo_surface_t*)surface;
+- (id) initForSurface:(cairo_surface_t*)surface;
+
++ (CairoRenderContext*) contextForWidget:(GtkWidget*)widget;
+- (id) initForWidget:(GtkWidget*)widget;
+
++ (CairoRenderContext*) contextForDrawable:(GdkDrawable*)d;
+- (id) initForDrawable:(GdkDrawable*)d;
+
++ (CairoRenderContext*) contextForPixbuf:(GdkPixbuf*)buf;
+- (id) initForPixbuf:(GdkPixbuf*)buf;
+
+- (cairo_t*) cairoContext;
+- (void) applyTransform:(Transformer*)transformer;
+
+- (void) clearSurface;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/CairoRenderContext.m b/tikzit/src/gtk/CairoRenderContext.m
new file mode 100644
index 0000000..bed06a6
--- /dev/null
+++ b/tikzit/src/gtk/CairoRenderContext.m
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "CairoRenderContext.h"
+
+#import "cairo_helpers.h"
+#import "util.h"
+
+#import <pango/pangocairo.h>
+
+@implementation PangoTextLayout
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
++ (PangoTextLayout*) layoutForContext:(cairo_t*)cr withFontSize:(CGFloat)fontSize {
+ return [[[self alloc] initWithContext:cr fontSize:fontSize] autorelease];
+}
+
+- (id) initWithContext:(cairo_t*)cr fontSize:(CGFloat)fontSize {
+ self = [super init];
+
+ if (self) {
+ cairo_reference (cr);
+ context = cr;
+ layout = pango_cairo_create_layout (cr);
+
+ PangoFontDescription *font_desc = pango_font_description_new ();
+ pango_font_description_set_family_static (font_desc, "Sans");
+ pango_font_description_set_size (font_desc, pango_units_from_double (fontSize));
+ pango_layout_set_font_description (layout, font_desc);
+ pango_font_description_free (font_desc);
+ }
+
+ return self;
+}
+
+- (void) setText:(NSString*)text {
+ pango_layout_set_text (layout, [text UTF8String], -1);
+}
+
+- (NSSize) size {
+ int width, height;
+ pango_layout_get_size (layout, &width, &height);
+ return NSMakeSize (pango_units_to_double (width), pango_units_to_double (height));
+}
+
+- (NSString*) text {
+ return [NSString stringWithUTF8String:pango_layout_get_text (layout)];
+}
+
+- (void) showTextAt:(NSPoint)topLeft withColor:(RColor)color {
+ cairo_save (context);
+
+ cairo_move_to(context, topLeft.x, topLeft.y);
+ cairo_set_source_rcolor (context, color);
+ pango_cairo_show_layout (context, layout);
+
+ cairo_restore (context);
+}
+
+- (void) dealloc {
+ if (layout)
+ g_object_unref (G_OBJECT (layout));
+ if (context)
+ cairo_destroy (context);
+
+ [super dealloc];
+}
+
+@end
+
+@implementation CairoRenderContext
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
++ (CairoRenderContext*) contextForSurface:(cairo_surface_t*)surface {
+ return [[[self alloc] initForSurface:surface] autorelease];
+}
+
+- (id) initForSurface:(cairo_surface_t*)surface {
+ self = [super init];
+
+ if (self) {
+ context = cairo_create (surface);
+ }
+
+ return self;
+}
+
++ (CairoRenderContext*) contextForWidget:(GtkWidget*)widget {
+ return [[[self alloc] initForWidget:widget] autorelease];
+}
+
+- (id) initForWidget:(GtkWidget*)widget {
+ self = [super init];
+
+ if (self) {
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ context = gdk_cairo_create (window);
+ } else {
+ [self release];
+ self = nil;
+ }
+ }
+
+ return self;
+}
+
++ (CairoRenderContext*) contextForDrawable:(GdkDrawable*)d {
+ return [[[self alloc] initForDrawable:d] autorelease];
+}
+
+- (id) initForDrawable:(GdkDrawable*)d {
+ self = [super init];
+
+ if (self) {
+ context = gdk_cairo_create (d);
+ }
+
+ return self;
+}
+
++ (CairoRenderContext*) contextForPixbuf:(GdkPixbuf*)pixbuf {
+ return [[[self alloc] initForPixbuf:pixbuf] autorelease];
+}
+
+- (id) initForPixbuf:(GdkPixbuf*)pixbuf {
+ self = [super init];
+
+ if (self) {
+ cairo_format_t format = -1;
+
+ if (gdk_pixbuf_get_colorspace (pixbuf) != GDK_COLORSPACE_RGB) {
+ NSLog(@"Unsupported colorspace (must be RGB)");
+ [self release];
+ return nil;
+ }
+ if (gdk_pixbuf_get_bits_per_sample (pixbuf) != 8) {
+ NSLog(@"Unsupported bits per sample (must be 8)");
+ [self release];
+ return nil;
+ }
+ if (gdk_pixbuf_get_has_alpha (pixbuf)) {
+ if (gdk_pixbuf_get_n_channels (pixbuf) != 4) {
+ NSLog(@"Unsupported bits per sample (must be 4 for an image with alpha)");
+ [self release];
+ return nil;
+ }
+ format = CAIRO_FORMAT_ARGB32;
+ } else {
+ if (gdk_pixbuf_get_n_channels (pixbuf) != 3) {
+ NSLog(@"Unsupported bits per sample (must be 3 for an image without alpha)");
+ [self release];
+ return nil;
+ }
+ format = CAIRO_FORMAT_RGB24;
+ }
+
+ cairo_surface_t *surface = cairo_image_surface_create_for_data(
+ gdk_pixbuf_get_pixels(pixbuf),
+ format,
+ gdk_pixbuf_get_width(pixbuf),
+ gdk_pixbuf_get_height(pixbuf),
+ gdk_pixbuf_get_rowstride(pixbuf));
+ context = cairo_create (surface);
+ cairo_surface_destroy (surface);
+ }
+
+ return self;
+}
+
+- (cairo_t*) cairoContext {
+ return context;
+}
+
+- (void) applyTransform:(Transformer*)transformer {
+ NSPoint origin = [transformer toScreen:NSZeroPoint];
+ cairo_translate (context, origin.x, origin.y);
+ NSPoint scale = [transformer toScreen:NSMakePoint (1.0f, 1.0f)];
+ scale.x -= origin.x;
+ scale.y -= origin.y;
+ cairo_scale (context, scale.x, scale.y);
+}
+
+- (void) saveState {
+ cairo_save (context);
+}
+
+- (void) restoreState {
+ cairo_restore (context);
+}
+
+- (NSRect) clipBoundingBox {
+ double clipx1, clipx2, clipy1, clipy2;
+ cairo_clip_extents (context, &clipx1, &clipy1, &clipx2, &clipy2);
+ return NSMakeRect (clipx1, clipy1, clipx2-clipx1, clipy2-clipy1);
+}
+
+- (BOOL) strokeIncludesPoint:(NSPoint)p {
+ return cairo_in_stroke (context, p.x, p.y);
+}
+
+- (BOOL) fillIncludesPoint:(NSPoint)p {
+ return cairo_in_fill (context, p.x, p.y);
+}
+
+- (id<TextLayout>) layoutText:(NSString*)text withSize:(CGFloat)fontSize {
+ PangoTextLayout *layout = [PangoTextLayout layoutForContext:context withFontSize:fontSize];
+ [layout setText:text];
+ return layout;
+}
+
+- (void) setAntialiasMode:(AntialiasMode)mode {
+ if (mode == AntialiasDisabled) {
+ cairo_set_antialias (context, CAIRO_ANTIALIAS_NONE);
+ } else {
+ cairo_set_antialias (context, CAIRO_ANTIALIAS_DEFAULT);
+ }
+}
+
+- (void) setLineWidth:(CGFloat)width {
+ cairo_set_line_width (context, width);
+}
+
+- (void) setLineDash:(CGFloat)dashLength {
+ if (dashLength <= 0.0) {
+ cairo_set_dash (context, NULL, 0, 0);
+ } else {
+ double dashes[] = { dashLength };
+ cairo_set_dash (context, dashes, 1, 0);
+ }
+}
+
+// paths
+- (void) startPath {
+ cairo_new_path (context);
+}
+
+- (void) closeSubPath {
+ cairo_close_path (context);
+}
+
+- (void) moveTo:(NSPoint)p {
+ cairo_move_to(context, p.x, p.y);
+}
+
+- (void) curveTo:(NSPoint)end withCp1:(NSPoint)cp1 andCp2:(NSPoint)cp2 {
+ cairo_curve_to (context, cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
+}
+
+- (void) lineTo:(NSPoint)end {
+ cairo_line_to (context, end.x, end.y);
+}
+
+- (void) rect:(NSRect)rect {
+ cairo_rectangle (context, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+}
+
+- (void) circleAt:(NSPoint)c withRadius:(CGFloat)r {
+ cairo_new_sub_path (context);
+ cairo_arc (context, c.x, c.y, r, 0, 2 * M_PI);
+}
+
+- (void) strokePathWithColor:(RColor)color {
+ cairo_set_source_rcolor (context, color);
+ cairo_stroke (context);
+}
+
+- (void) fillPathWithColor:(RColor)color {
+ cairo_set_source_rcolor (context, color);
+ cairo_fill (context);
+}
+
+- (void) strokePathWithColor:(RColor)scolor
+ andFillWithColor:(RColor)fcolor {
+ cairo_set_source_rcolor (context, fcolor);
+ cairo_fill_preserve (context);
+ cairo_set_source_rcolor (context, scolor);
+ cairo_stroke (context);
+}
+
+- (void) strokePathWithColor:(RColor)scolor
+ andFillWithColor:(RColor)fcolor
+ usingAlpha:(CGFloat)alpha {
+ cairo_push_group (context);
+ cairo_set_source_rcolor (context, fcolor);
+ cairo_fill_preserve (context);
+ cairo_set_source_rcolor (context, scolor);
+ cairo_stroke (context);
+ cairo_pop_group_to_source (context);
+ cairo_paint_with_alpha (context, alpha);
+}
+
+- (void) clipToPath {
+ cairo_clip (context);
+}
+
+- (void) paintWithColor:(RColor)color {
+ cairo_set_source_rcolor (context, color);
+ cairo_paint (context);
+}
+
+- (void) clearSurface {
+ cairo_operator_t old_op = cairo_get_operator (context);
+
+ cairo_set_operator (context, CAIRO_OPERATOR_SOURCE);
+ cairo_set_source_rgba (context, 0.0, 0.0, 0.0, 0.0);
+ cairo_paint (context);
+
+ cairo_set_operator (context, old_op);
+}
+
+- (void) dealloc {
+ if (context) {
+ cairo_destroy (context);
+ }
+
+ [super dealloc];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/ColorRGB+Gtk.h b/tikzit/src/gtk/ColorRGB+Gtk.h
new file mode 100644
index 0000000..5cfb4d7
--- /dev/null
+++ b/tikzit/src/gtk/ColorRGB+Gtk.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "ColorRGB.h"
+#import <gdk/gdk.h>
+
+@interface ColorRGB (Gtk)
+
++ (ColorRGB*) colorWithGdkColor:(GdkColor)color;
+- (id) initWithGdkColor:(GdkColor)color;
+- (GdkColor) gdkColor;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/ColorRGB+Gtk.m b/tikzit/src/gtk/ColorRGB+Gtk.m
new file mode 100644
index 0000000..be5dd56
--- /dev/null
+++ b/tikzit/src/gtk/ColorRGB+Gtk.m
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "ColorRGB+Gtk.h"
+
+// 257 = 65535/255
+// GdkColor values run from 0 to 65535, not from 0 to 255
+#define GDK_FACTOR 257
+
+@implementation ColorRGB (Gtk)
+
++ (ColorRGB*) colorWithGdkColor:(GdkColor)color {
+ return [ColorRGB colorWithRed:color.red/GDK_FACTOR green:color.green/GDK_FACTOR blue:color.blue/GDK_FACTOR];
+}
+
+- (id) initWithGdkColor:(GdkColor)color {
+ return [self initWithRed:color.red/GDK_FACTOR green:color.green/GDK_FACTOR blue:color.blue/GDK_FACTOR];
+}
+
+- (GdkColor) gdkColor {
+ GdkColor color;
+ color.pixel = 0;
+ color.red = GDK_FACTOR * [self red];
+ color.green = GDK_FACTOR * [self green];
+ color.blue = GDK_FACTOR * [self blue];
+ return color;
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/ColorRGB+IntegerListStorage.h b/tikzit/src/gtk/ColorRGB+IntegerListStorage.h
new file mode 100644
index 0000000..118eaee
--- /dev/null
+++ b/tikzit/src/gtk/ColorRGB+IntegerListStorage.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "ColorRGB.h"
+
+/**
+ * Stores a ColorRGB as a list of short integers
+ */
+@interface ColorRGB (IntegerListStorage)
+
++ (ColorRGB*) colorFromValueList:(NSArray*)values;
+- (id) initFromValueList:(NSArray*)values;
+- (NSArray*) valueList;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/ColorRGB+IntegerListStorage.m b/tikzit/src/gtk/ColorRGB+IntegerListStorage.m
new file mode 100644
index 0000000..0103a3c
--- /dev/null
+++ b/tikzit/src/gtk/ColorRGB+IntegerListStorage.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "ColorRGB+IntegerListStorage.h"
+
+@implementation ColorRGB (IntegerListStorage)
+
++ (ColorRGB*) colorFromValueList:(NSArray*)values {
+ if ([values count] != 3) {
+ return nil;
+ }
+
+ unsigned short redValue = [[values objectAtIndex:0] intValue];
+ unsigned short greenValue = [[values objectAtIndex:1] intValue];
+ unsigned short blueValue = [[values objectAtIndex:2] intValue];
+ return [ColorRGB colorWithRed:redValue green:greenValue blue:blueValue];
+}
+
+- (id) initFromValueList:(NSArray*)values {
+ if ([values count] != 3) {
+ [self release];
+ return nil;
+ }
+
+ unsigned short redValue = [[values objectAtIndex:0] intValue];
+ unsigned short greenValue = [[values objectAtIndex:1] intValue];
+ unsigned short blueValue = [[values objectAtIndex:2] intValue];
+
+ return [self initWithRed:redValue green:greenValue blue:blueValue];
+}
+
+- (NSArray*) valueList {
+ NSMutableArray *array = [NSMutableArray arrayWithCapacity:3];
+ [array addObject:[NSNumber numberWithInt:[self red]]];
+ [array addObject:[NSNumber numberWithInt:[self green]]];
+ [array addObject:[NSNumber numberWithInt:[self blue]]];
+ return array;
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Configuration.h b/tikzit/src/gtk/Configuration.h
new file mode 100644
index 0000000..93a74fa
--- /dev/null
+++ b/tikzit/src/gtk/Configuration.h
@@ -0,0 +1,447 @@
+//
+// MainWindow.h
+// TikZiT
+//
+// Copyright 2010 Alex Merry
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+
+#import "TZFoundation.h"
+
+/**
+ * Manages configuration information in a grouped key-value format.
+ */
+@interface Configuration : NSObject {
+ NSString *name;
+ GKeyFile *file;
+}
+
+/**
+ * Check whether there is any existing configuration.
+ */
++ (BOOL) configurationExistsWithName:(NSString*)name;
+/**
+ * Create a blank configuration with the given name, without loading
+ * any existing configuration information.
+ *
+ * @param name the name of the configuration
+ */
++ (Configuration*) emptyConfigurationWithName:(NSString*)name;
+/**
+ * Load an existing configuration for the given name.
+ *
+ * If there was no existing configuration, or it could not be opened,
+ * an empty configuration will be returned.
+ *
+ * @param name the name of the configuration
+ */
++ (Configuration*) configurationWithName:(NSString*)name;
+/**
+ * Load an existing configuration for the given name.
+ *
+ * If there was no existing configuration, or it could not be opened,
+ * an empty configuration will be returned.
+ *
+ * @param name the name of the configuration
+ * @param error this will be set if the configuration exists, but could
+ * not be opened.
+ */
++ (Configuration*) configurationWithName:(NSString*)name loadError:(NSError**)error;
+
+/**
+ * Initialise the configuration to be empty
+ *
+ * Does not attempt to load any existing configuration data.
+ *
+ * @param name the name of the configuration
+ */
+- (id) initEmptyWithName:(NSString*)name;
+/**
+ * Initialise a configuration, loading it if it had previously been stored.
+ *
+ * If there was no existing configuration, or it could not be opened,
+ * an empty configuration will be returned.
+ *
+ * @param name the name of the configuration
+ */
+- (id) initWithName:(NSString*)name;
+/**
+ * Initialise a configuration, loading it if it had previously been stored.
+ *
+ * If there was no existing configuration, or it could not be opened,
+ * an empty configuration will be returned.
+ *
+ * @param name the name of the configuration
+ * @param error this will be set if the configuration exists, but could
+ * not be opened.
+ */
+- (id) initWithName:(NSString*)name loadError:(NSError**)error;
+
+/**
+ * The name of the configuration.
+ *
+ * Configurations with different names are stored independently.
+ */
+- (NSString*) name;
+/**
+ * Set the name of the configuration.
+ *
+ * This will affect the behaviour of [-writeToStore]
+ *
+ * Configurations with different names are stored independently.
+ */
+- (void) setName:(NSString*)name;
+
+/**
+ * Writes the configuration to the backing store.
+ *
+ * The location the configuration is written to is determined by the
+ * [-name] property.
+ *
+ * @result YES if the configuration was successfully written, NO otherwise
+ */
+- (BOOL) writeToStore;
+/**
+ * Writes the configuration to the backing store.
+ *
+ * The location the configuration is written to is determined by the
+ * [-name] property.
+ *
+ * @param error this will be set if the configuration could not be written
+ * to the backing store
+ * @result YES if the configuration was successfully written, NO otherwise
+ */
+- (BOOL) writeToStoreWithError:(NSError**)error;
+
+/**
+ * Check whether a particular key exists within a group
+ *
+ * @param key the key to check for
+ * @param group the name of the group to look in
+ * @result YES if the key exists, NO otherwise
+ */
+- (BOOL) hasKey:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Check whether a particular group exists
+ *
+ * @param group the name of the group to check for
+ * @result YES if the group exists, NO otherwise
+ */
+- (BOOL) hasGroup:(NSString*)group;
+/**
+ * List the groups in the configuration.
+ *
+ * @result a list of group names
+ */
+- (NSArray*) groups;
+
+/**
+ * Get the value associated with a key as a string
+ *
+ * This is only guaranteed to work if the value was stored as a string.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a string, or nil
+ * if no string value was associated with key
+ */
+- (NSString*) stringEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a string
+ *
+ * This is only guaranteed to work if the value was stored as a string.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no string value was associated with key
+ * @result the value associated with key as a string, or default
+ */
+- (NSString*) stringEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSString*)def;
+/**
+ * Get the value associated with a key as a boolean
+ *
+ * This is only guaranteed to work if the value was stored as a boolean.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a boolean, or NO
+ * if no boolean value was associated with key
+ */
+- (BOOL) booleanEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a boolean
+ *
+ * This is only guaranteed to work if the value was stored as a boolean.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no boolean value was associated with key
+ * @result the value associated with key as a boolean, or def
+ */
+- (BOOL) booleanEntry:(NSString*)key inGroup:(NSString*)group withDefault:(BOOL)def;
+/**
+ * Get the value associated with a key as a integer
+ *
+ * This is only guaranteed to work if the value was stored as a integer.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a integer, or 0
+ * if no integer value was associated with key
+ */
+- (int) integerEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a integer
+ *
+ * This is only guaranteed to work if the value was stored as a integer.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no integer value was associated with key
+ * @result the value associated with key as a integer, or def
+ */
+- (int) integerEntry:(NSString*)key inGroup:(NSString*)group withDefault:(int)def;
+/**
+ * Get the value associated with a key as a double
+ *
+ * This is only guaranteed to work if the value was stored as a double.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a double, or 0
+ * if no double value was associated with key
+ */
+- (double) doubleEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a double
+ *
+ * This is only guaranteed to work if the value was stored as a double.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no double value was associated with key
+ * @result the value associated with key as a double, or def
+ */
+- (double) doubleEntry:(NSString*)key inGroup:(NSString*)group withDefault:(double)def;
+
+/**
+ * Get the value associated with a key as a list of strings
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of strings.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a list of strings,
+ * or nil if no list of strings was associated with key
+ */
+- (NSArray*) stringListEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a list of strings
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of strings.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no string list value was associated with key
+ * @result the value associated with key as a list of strings, or def
+ */
+- (NSArray*) stringListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def;
+/**
+ * Get the value associated with a key as a list of booleans
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of booleans.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing boolean values, or nil
+ */
+- (NSArray*) booleanListEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a list of booleans
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of booleans.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no boolean list value was associated with key
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing boolean values, or def
+ */
+- (NSArray*) booleanListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def;
+/**
+ * Get the value associated with a key as a list of integers
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of integers.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing integer values, or nil
+ */
+- (NSArray*) integerListEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a list of integers
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of integers.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no integer list value was associated with key
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing integer values, or def
+ */
+- (NSArray*) integerListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def;
+/**
+ * Get the value associated with a key as a list of doubles
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of doubles.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing double values, or nil
+ */
+- (NSArray*) doubleListEntry:(NSString*)key inGroup:(NSString*)group;
+/**
+ * Get the value associated with a key as a list of doubles
+ *
+ * This is only guaranteed to work if the value was stored as a
+ * list of doubles.
+ *
+ * @param key the key to fetch the data for
+ * @param group the name of the group to look in
+ * @param def the value to return if no double list value was associated with key
+ * @result the value associated with key as a list of NSNumber
+ * objects, containing double values, or def
+ */
+- (NSArray*) doubleListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def;
+
+/**
+ * Associate a string value with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the value with
+ * @param group the group to store the association in
+ * @param value the value to store
+ */
+- (void) setStringEntry:(NSString*)key inGroup:(NSString*)group value:(NSString*)value;
+/**
+ * Associate a boolean value with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the value with
+ * @param group the group to store the association in
+ * @param value the value to store
+ */
+- (void) setBooleanEntry:(NSString*)key inGroup:(NSString*)group value:(BOOL)value;
+/**
+ * Associate a integer value with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the value with
+ * @param group the group to store the association in
+ * @param value the value to store
+ */
+- (void) setIntegerEntry:(NSString*)key inGroup:(NSString*)group value:(int)value;
+/**
+ * Associate a double value with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the value with
+ * @param group the group to store the association in
+ * @param value the value to store
+ */
+- (void) setDoubleEntry:(NSString*)key inGroup:(NSString*)group value:(double)value;
+
+/**
+ * Associate a list of string values with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the list with
+ * @param group the group to store the association in
+ * @param value the list to store, as an array of strings
+ */
+- (void) setStringListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+/**
+ * Associate a list of boolean values with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the list with
+ * @param group the group to store the association in
+ * @param value the list to store, as an array of NSNumber objects
+ */
+- (void) setBooleanListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+/**
+ * Associate a list of integer values with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the list with
+ * @param group the group to store the association in
+ * @param value the list to store, as an array of NSNumber objects
+ */
+- (void) setIntegerListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+/**
+ * Associate a list of double values with a key.
+ *
+ * Any previous value (of any type) with the same key and group will
+ * be overwritten.
+ *
+ * @param key the key to associate the list with
+ * @param group the group to store the association in
+ * @param value the list to store, as an array of NSNumber objects
+ */
+- (void) setDoubleListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+
+/**
+ * Remove a group from the configuration
+ *
+ * This will remove all the groups key-value associations.
+ */
+- (void) removeGroup:(NSString*)group;
+/**
+ * Remove a key from the configuration
+ *
+ * @param key the key to remove
+ * @param group the group to remove it from
+ */
+- (void) removeKey:(NSString*)key inGroup:(NSString*)group;
+
+@end
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/Configuration.m b/tikzit/src/gtk/Configuration.m
new file mode 100644
index 0000000..4904eed
--- /dev/null
+++ b/tikzit/src/gtk/Configuration.m
@@ -0,0 +1,446 @@
+//
+// MainWindow.h
+// TikZiT
+//
+// Copyright 2010 Alex Merry
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+
+#import "Configuration.h"
+#import "SupportDir.h"
+
+@implementation Configuration
+
++ (NSString*) _pathFromName:(NSString*)name {
+ return [NSString stringWithFormat:@"%@/%@.conf", [SupportDir userSupportDir], name];
+}
+
++ (BOOL) configurationExistsWithName:(NSString*)name {
+ BOOL isDir;
+ BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:[self _pathFromName:name] isDirectory:&isDir];
+ return exists && !isDir;
+}
+
++ (Configuration*) emptyConfigurationWithName:(NSString*)name
+ { return [[[self alloc] initEmptyWithName:name] autorelease]; }
++ (Configuration*) configurationWithName:(NSString*)name
+ { return [[[self alloc] initWithName:name] autorelease]; }
++ (Configuration*) configurationWithName:(NSString*)name loadError:(NSError**)error
+ { return [[[self alloc] initWithName:name loadError:error] autorelease]; }
+
+- (id) init
+{
+ [self release];
+ return nil;
+}
+
+- (id) initEmptyWithName:(NSString*)n
+{
+ self = [super init];
+ if (self) {
+ name = [n retain];
+ file = g_key_file_new ();
+ }
+
+ return self;
+}
+
+- (id) _initFromFile:(NSString*)path error:(NSError**)error
+{
+ self = [super init];
+ if (self) {
+ file = g_key_file_new ();
+
+ NSFileManager *manager = [NSFileManager defaultManager];
+ if ([manager fileExistsAtPath:path]) {
+ gchar *filename = [path glibFilename];
+
+ GError *gerror = NULL;
+ g_key_file_load_from_file (file,
+ filename,
+ G_KEY_FILE_NONE,
+ &gerror);
+ g_free (filename);
+
+ if (gerror) {
+ GErrorToNSError (gerror, error);
+ g_error_free (gerror);
+ }
+ }
+ }
+
+ return self;
+}
+
+- (id) initWithName:(NSString*)n {
+ return [self initWithName:n loadError:NULL];
+}
+
+- (id) initWithName:(NSString*)n loadError:(NSError**)error {
+ self = [self _initFromFile:[Configuration _pathFromName:n] error:error];
+
+ if (self) {
+ name = [n retain];
+ }
+
+ return self;
+}
+
+- (BOOL) _ensureParentExists:(NSString*)path error:(NSError**)error {
+ NSString *directory = [path stringByDeletingLastPathComponent];
+ return [[NSFileManager defaultManager] ensureDirectoryExists:directory error:error];
+}
+
+- (BOOL) _writeFileTo:(NSString*)path error:(NSError**)error
+{
+ if (![self _ensureParentExists:path error:error]) {
+ return NO;
+ }
+
+ BOOL success = NO;
+ gsize length;
+ gchar *data = g_key_file_to_data (file, &length, NULL); // never reports an error
+ if (data && length) {
+ GError *gerror = NULL;
+ gchar* nativePath = [path glibFilename];
+ success = g_file_set_contents (nativePath, data, length, &gerror) ? YES : NO;
+ g_free (data);
+ g_free (nativePath);
+ if (gerror) {
+ g_warning ("Failed to write file: %s\n", gerror->message);
+ GErrorToNSError (gerror, error);
+ g_error_free (gerror);
+ }
+ } else {
+ [[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
+ success = YES;
+ }
+
+ return success;
+}
+
+- (NSString*) name {
+ return name;
+}
+
+- (void) setName:(NSString*)n {
+ [n retain];
+ [name release];
+ name = n;
+}
+
+- (BOOL) writeToStore {
+ return [self writeToStoreWithError:NULL];
+}
+
+- (BOOL) writeToStoreWithError:(NSError**)error {
+ return [self _writeFileTo:[Configuration _pathFromName:name] error:error];
+}
+
+- (BOOL) hasKey:(NSString*)key inGroup:(NSString*)group
+{
+ gboolean result = g_key_file_has_key (file, [group UTF8String], [key UTF8String], NULL);
+ return result ? YES : NO;
+}
+
+- (BOOL) hasGroup:(NSString*)group
+{
+ gboolean result = g_key_file_has_group (file, [group UTF8String]);
+ return result ? YES : NO;
+}
+
+- (NSArray*) keys:(NSString*)group
+{
+ gsize length;
+ gchar **keys = g_key_file_get_keys (file, [group UTF8String], &length, NULL);
+ if (!keys)
+ length = 0;
+
+ NSMutableArray *array = [NSMutableArray arrayWithCapacity:length];
+ for (int i = 0; i < length; ++i) {
+ [array addObject:[NSString stringWithUTF8String:keys[i]]];
+ }
+ g_strfreev (keys);
+ return array;
+}
+
+- (NSArray*) groups
+{
+ gsize length;
+ gchar **groups = g_key_file_get_groups (file, &length);
+ NSMutableArray *array = [NSMutableArray arrayWithCapacity:length];
+ for (int i = 0; i < length; ++i) {
+ [array addObject:[NSString stringWithUTF8String:groups[i]]];
+ }
+ g_strfreev (groups);
+ return array;
+}
+
+- (NSString*) stringEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return [self stringEntry:key inGroup:group withDefault:nil];
+}
+
+- (NSString*) stringEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSString*)def
+{
+ NSString *result = def;
+ gchar *entry = g_key_file_get_string (file, [group UTF8String], [key UTF8String], NULL);
+ if (entry) {
+ result = [NSString stringWithUTF8String:entry];
+ g_free (entry);
+ }
+ return result;
+}
+
+- (BOOL) booleanEntry:(NSString*)key inGroup:(NSString*)group withDefault:(BOOL)def
+{
+ GError *error = NULL;
+ gboolean result = g_key_file_get_boolean (file, [group UTF8String], [key UTF8String], &error);
+ if (error != NULL) {
+ g_error_free (error);
+ return def;
+ } else {
+ return result ? YES : NO;
+ }
+}
+
+- (BOOL) booleanEntry:(NSString*)key inGroup:(NSString*)group
+{
+ gboolean result = g_key_file_get_boolean (file, [group UTF8String], [key UTF8String], NULL);
+ return result ? YES : NO;
+}
+
+- (int) integerEntry:(NSString*)key inGroup:(NSString*)group withDefault:(int)def
+{
+ GError *error = NULL;
+ int result = g_key_file_get_integer (file, [group UTF8String], [key UTF8String], &error);
+ if (error != NULL) {
+ g_error_free (error);
+ return def;
+ } else {
+ return result;
+ }
+}
+
+- (int) integerEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return g_key_file_get_integer (file, [group UTF8String], [key UTF8String], NULL);
+}
+
+- (double) doubleEntry:(NSString*)key inGroup:(NSString*)group withDefault:(double)def
+{
+ GError *error = NULL;
+ double result = g_key_file_get_double (file, [group UTF8String], [key UTF8String], &error);
+ if (error != NULL) {
+ g_error_free (error);
+ return def;
+ } else {
+ return result;
+ }
+}
+
+- (double) doubleEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return g_key_file_get_double (file, [group UTF8String], [key UTF8String], NULL);
+}
+
+- (NSArray*) stringListEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return [self stringListEntry:key inGroup:group withDefault:nil];
+}
+
+- (NSArray*) stringListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def
+{
+ gsize length;
+ gchar **list = g_key_file_get_string_list (file, [group UTF8String], [key UTF8String], &length, NULL);
+ if (list) {
+ NSMutableArray *result = [NSMutableArray new];
+ for (int i = 0; i < length; ++i) {
+ [result addObject:[NSString stringWithUTF8String:list[i]]];
+ }
+ return result;
+ } else {
+ return def;
+ }
+}
+
+- (NSArray*) booleanListEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return [self booleanListEntry:key inGroup:group withDefault:nil];
+}
+
+- (NSArray*) booleanListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def
+{
+ gsize length;
+ gboolean *list = g_key_file_get_boolean_list (file, [group UTF8String], [key UTF8String], &length, NULL);
+ if (list) {
+ NSMutableArray *result = [NSMutableArray new];
+ for (int i = 0; i < length; ++i) {
+ [result addObject:[NSNumber numberWithBool:list[i]]];
+ }
+ return result;
+ } else {
+ return def;
+ }
+}
+
+- (NSArray*) integerListEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return [self integerListEntry:key inGroup:group withDefault:nil];
+}
+
+- (NSArray*) integerListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def
+{
+ gsize length;
+ gint *list = g_key_file_get_integer_list (file, [group UTF8String], [key UTF8String], &length, NULL);
+ if (list) {
+ NSMutableArray *result = [NSMutableArray new];
+ for (int i = 0; i < length; ++i) {
+ [result addObject:[NSNumber numberWithInt:list[i]]];
+ }
+ return result;
+ } else {
+ return def;
+ }
+}
+
+- (NSArray*) doubleListEntry:(NSString*)key inGroup:(NSString*)group
+{
+ return [self doubleListEntry:key inGroup:group withDefault:nil];
+}
+
+- (NSArray*) doubleListEntry:(NSString*)key inGroup:(NSString*)group withDefault:(NSArray*)def
+{
+ gsize length;
+ double *list = g_key_file_get_double_list (file, [group UTF8String], [key UTF8String], &length, NULL);
+ if (list) {
+ NSMutableArray *result = [NSMutableArray new];
+ for (int i = 0; i < length; ++i) {
+ [result addObject:[NSNumber numberWithDouble:list[i]]];
+ }
+ return result;
+ } else {
+ return def;
+ }
+}
+
+- (void) setStringEntry:(NSString*)key inGroup:(NSString*)group value:(NSString*)value
+{
+ if (value == nil) {
+ [self removeKey:key inGroup:group];
+ return;
+ }
+ g_key_file_set_string (file, [group UTF8String], [key UTF8String], [value UTF8String]);
+}
+
+- (void) setBooleanEntry:(NSString*)key inGroup:(NSString*)group value:(BOOL)value;
+{
+ g_key_file_set_boolean (file, [group UTF8String], [key UTF8String], value);
+}
+
+- (void) setIntegerEntry:(NSString*)key inGroup:(NSString*)group value:(int)value;
+{
+ g_key_file_set_integer (file, [group UTF8String], [key UTF8String], value);
+}
+
+- (void) setDoubleEntry:(NSString*)key inGroup:(NSString*)group value:(double)value;
+{
+ g_key_file_set_double (file, [group UTF8String], [key UTF8String], value);
+}
+
+
+- (void) setStringListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value
+{
+ if (value == nil) {
+ [self removeKey:key inGroup:group];
+ return;
+ }
+ gsize length = [value count];
+ const gchar * *list = g_new (const gchar *, length);
+ for (int i = 0; i < length; ++i) {
+ list[i] = [[value objectAtIndex:i] UTF8String];
+ }
+ g_key_file_set_string_list (file, [group UTF8String], [key UTF8String], list, length);
+ g_free (list);
+}
+
+- (void) setBooleanListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+{
+ if (value == nil) {
+ [self removeKey:key inGroup:group];
+ return;
+ }
+ gsize length = [value count];
+ gboolean *list = g_new (gboolean, length);
+ for (int i = 0; i < length; ++i) {
+ list[i] = [[value objectAtIndex:i] boolValue];
+ }
+ g_key_file_set_boolean_list (file, [group UTF8String], [key UTF8String], list, length);
+ g_free (list);
+}
+
+- (void) setIntegerListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+{
+ if (value == nil) {
+ [self removeKey:key inGroup:group];
+ return;
+ }
+ gsize length = [value count];
+ gint *list = g_new (gint, length);
+ for (int i = 0; i < length; ++i) {
+ list[i] = [[value objectAtIndex:i] intValue];
+ }
+ g_key_file_set_integer_list (file, [group UTF8String], [key UTF8String], list, length);
+ g_free (list);
+}
+
+- (void) setDoubleListEntry:(NSString*)key inGroup:(NSString*)group value:(NSArray*)value;
+{
+ if (value == nil) {
+ [self removeKey:key inGroup:group];
+ return;
+ }
+ gsize length = [value count];
+ gdouble *list = g_new (gdouble, length);
+ for (int i = 0; i < length; ++i) {
+ list[i] = [[value objectAtIndex:i] doubleValue];
+ }
+ g_key_file_set_double_list (file, [group UTF8String], [key UTF8String], list, length);
+ g_free (list);
+}
+
+- (void) removeGroup:(NSString*)group
+{
+ g_key_file_remove_group (file, [group UTF8String], NULL);
+}
+
+- (void) removeKey:(NSString*)key inGroup:(NSString*)group;
+{
+ g_key_file_remove_key (file, [group UTF8String], [key UTF8String], NULL);
+}
+
+- (void) dealloc
+{
+ [name release];
+ g_key_file_free (file);
+ file = NULL;
+ [super dealloc];
+}
+
+@end
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/Edge+Render.h b/tikzit/src/gtk/Edge+Render.h
new file mode 100644
index 0000000..11f02b1
--- /dev/null
+++ b/tikzit/src/gtk/Edge+Render.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "Edge.h"
+#import "RenderContext.h"
+#import "Surface.h"
+
+@interface Edge (Render)
+
++ (float) controlPointRadius;
+- (void) renderControlsToSurface:(id<Surface>)surface withContext:(id<RenderContext>)context;
+- (void) renderToSurface:(id<Surface>)surface withContext:(id<RenderContext>)context selected:(BOOL)selected;
+- (NSRect) renderedBoundsWithTransformer:(Transformer*)t whenSelected:(BOOL)selected;
+- (BOOL) hitByPoint:(NSPoint)p onSurface:(id<Surface>)surface withFuzz:(float)fuzz;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Edge+Render.m b/tikzit/src/gtk/Edge+Render.m
new file mode 100644
index 0000000..d4a49c9
--- /dev/null
+++ b/tikzit/src/gtk/Edge+Render.m
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Edge+Render.h"
+#import "Node+Render.h"
+#import "../common/util.h"
+
+static const float edgeWidth = 2.0;
+static const float cpRadius = 3.0;
+static const float cpLineWidth = 1.0;
+
+@implementation Edge (Render)
+
++ (float) controlPointRadius {
+ return cpRadius;
+}
+
+- (float) controlDistanceWithTransformer:(Transformer*)transformer {
+ NSPoint c_source = [transformer toScreen:src];
+ NSPoint c_target = [transformer toScreen:targ];
+ const float dx = (c_target.x - c_source.x);
+ const float dy = (c_target.y - c_source.y);
+ if (dx == 0 && dy == 0) {
+ return [transformer scaleToScreen:weight];
+ } else {
+ return NSDistanceBetweenPoints(c_source, c_target) * weight;
+ }
+}
+
+- (void) renderControlsToSurface:(id <Surface>)surface withContext:(id<RenderContext>)context {
+ Transformer *transformer = [surface transformer];
+
+ [context saveState];
+
+ [self updateControls];
+
+ NSPoint c_source = [transformer toScreen:src];
+ NSPoint c_target = [transformer toScreen:targ];
+ NSPoint c_mid = [transformer toScreen:mid];
+
+ const float dx = (c_target.x - c_source.x);
+ const float dy = (c_target.y - c_source.y);
+
+ [context setLineWidth:cpLineWidth];
+ RColor fillColor = MakeRColor (1.0, 1.0, 1.0, 0.5);
+
+ // draw a circle at the mid point
+ [context startPath];
+ [context circleAt:c_mid withRadius:cpRadius];
+ [context strokePathWithColor:MakeSolidRColor(0, 0, 1) andFillWithColor:fillColor];
+
+ //[context setAntialiasMode:AntialiasDisabled];
+
+ // size of control circles
+ float c_dist = 0.0f;
+ if (dx == 0 && dy == 0) {
+ c_dist = [transformer scaleToScreen:weight];
+ } else {
+ c_dist = NSDistanceBetweenPoints(c_source, c_target) * weight;
+ }
+
+ // basic bend is blue, in-out is green
+ RColor controlTrackColor;
+ if ([self bendMode] == EdgeBendModeBasic) {
+ controlTrackColor = MakeRColor (0.0, 0.0, 1.0, 0.4);
+ } else {
+ controlTrackColor = MakeRColor (0.0, 0.7, 0.0, 0.4);
+ }
+
+ [context startPath];
+ [context circleAt:c_source withRadius:c_dist];
+ if (dx != 0 || dy != 0) {
+ [context circleAt:c_target withRadius:c_dist];
+ }
+ [context strokePathWithColor:controlTrackColor];
+
+ RColor handleColor = MakeRColor (1.0, 0.0, 1.0, 0.6);
+ if ([self bendMode] == EdgeBendModeBasic) {
+ if (bend % 45 != 0) {
+ handleColor = MakeRColor (0.0, 0.0, 0.1, 0.4);
+ }
+ } else if ([self bendMode] == EdgeBendModeInOut) {
+ if (outAngle % 45 != 0) {
+ handleColor = MakeRColor (0.0, 0.7, 0.0, 0.4);
+ }
+ }
+
+ NSPoint c_cp1 = [transformer toScreen:cp1];
+ [context moveTo:c_source];
+ [context lineTo:c_cp1];
+ [context circleAt:c_cp1 withRadius:cpRadius];
+ [context strokePathWithColor:handleColor];
+
+ if ([self bendMode] == EdgeBendModeInOut) {
+ // recalculate color based on inAngle
+ if (inAngle % 45 == 0) {
+ handleColor = MakeRColor (1.0, 0.0, 1.0, 0.6);
+ } else {
+ handleColor = MakeRColor (0.0, 0.7, 0.0, 0.4);
+ }
+ }
+
+ NSPoint c_cp2 = [transformer toScreen:cp2];
+ [context moveTo:c_target];
+ [context lineTo:c_cp2];
+ [context circleAt:c_cp2 withRadius:cpRadius];
+ [context strokePathWithColor:handleColor];
+
+ [context restoreState];
+}
+
+- (void) createArrowStrokePathInContext:(id<RenderContext>)context withTransformer:(Transformer*)transformer {
+ [context startPath];
+
+ if ([self style] != nil) {
+ switch ([[self style] headStyle]) {
+ case AH_None:
+ break;
+ case AH_Plain:
+ [context moveTo:[transformer toScreen:[self leftHeadNormal]]];
+ [context lineTo:[transformer toScreen:head]];
+ [context lineTo:[transformer toScreen:[self rightHeadNormal]]];
+ break;
+ case AH_Latex:
+ [context moveTo:[transformer toScreen:[self leftHeadNormal]]];
+ [context lineTo:[transformer toScreen:head]];
+ [context lineTo:[transformer toScreen:[self rightHeadNormal]]];
+ [context closeSubPath];
+ break;
+ }
+ switch ([[self style] tailStyle]) {
+ case AH_None:
+ break;
+ case AH_Plain:
+ [context moveTo:[transformer toScreen:[self leftTailNormal]]];
+ [context lineTo:[transformer toScreen:tail]];
+ [context lineTo:[transformer toScreen:[self rightTailNormal]]];
+ break;
+ case AH_Latex:
+ [context moveTo:[transformer toScreen:[self leftTailNormal]]];
+ [context lineTo:[transformer toScreen:tail]];
+ [context lineTo:[transformer toScreen:[self rightTailNormal]]];
+ [context closeSubPath];
+ break;
+ }
+ }
+}
+
+- (void) createStrokePathInContext:(id<RenderContext>)context withTransformer:(Transformer*)transformer {
+ NSPoint c_head = [transformer toScreen:head];
+ NSPoint c_cp1 = [transformer toScreen:cp1];
+ NSPoint c_cp2 = [transformer toScreen:cp2];
+ NSPoint c_tail = [transformer toScreen:tail];
+
+ [context startPath];
+ [context moveTo:c_head];
+ [context curveTo:c_tail withCp1:c_cp1 andCp2:c_cp2];
+
+ if ([self style] != nil) {
+ // draw edge decoration
+ switch ([[self style] decorationStyle]) {
+ case ED_None:
+ break;
+ case ED_Tick:
+ [context moveTo:[transformer toScreen:[self leftNormal]]];
+ [context lineTo:[transformer toScreen:[self rightNormal]]];
+ break;
+ case ED_Arrow:
+ [context moveTo:[transformer toScreen:[self leftNormal]]];
+ [context lineTo:[transformer toScreen:[self midTan]]];
+ [context lineTo:[transformer toScreen:[self rightNormal]]];
+ break;
+ }
+
+ }
+}
+
+- (void) renderToSurface:(id <Surface>)surface withContext:(id<RenderContext>)context selected:(BOOL)selected {
+ [self updateControls];
+
+ [context saveState];
+ const CGFloat lineWidth = style ? [style thickness] : edgeWidth;
+ [context setLineWidth:lineWidth];
+ RColor color = BlackRColor;
+ if (selected) {
+ color.alpha = 0.5;
+ }
+
+ [self createStrokePathInContext:context withTransformer:[surface transformer]];
+ [context strokePathWithColor:color];
+
+ [self createArrowStrokePathInContext:context withTransformer:[surface transformer]];
+ [context strokePathWithColor:color andFillWithColor:color];
+
+ [context restoreState];
+
+ if (selected) {
+ [self renderControlsToSurface:surface withContext:context];
+ }
+
+ if ([self hasEdgeNode]) {
+ NSPoint labelPt = [[surface transformer] toScreen:[self mid]];
+ [[self edgeNode] renderLabelAt:labelPt
+ withContext:context];
+ }
+}
+
+- (NSRect) renderedBoundsWithTransformer:(Transformer*)t whenSelected:(BOOL)selected {
+ NSRect bRect = [t rectToScreen:[self boundingRect]];
+ if (selected) {
+ float c_dist = [self controlDistanceWithTransformer:t];
+ return NSInsetRect (bRect, -c_dist, -c_dist);
+ } else {
+ return bRect;
+ }
+}
+
+- (BOOL) hitByPoint:(NSPoint)p onSurface:(id<Surface>)surface withFuzz:(float)fuzz {
+ [self updateControls];
+
+ NSRect boundingRect = [[surface transformer] rectToScreen:[self boundingRect]];
+ if (!NSPointInRect(p, NSInsetRect(boundingRect, -fuzz, -fuzz))) {
+ return NO;
+ }
+
+ id<RenderContext> cr = [surface createRenderContext];
+
+ [cr setLineWidth:edgeWidth + 2 * fuzz];
+ [self createStrokePathInContext:cr withTransformer:[surface transformer]];
+
+ return [cr strokeIncludesPoint:p];
+}
+
+@end
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/EdgeStyle+Storage.h b/tikzit/src/gtk/EdgeStyle+Storage.h
new file mode 100644
index 0000000..74881f3
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyle+Storage.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "EdgeStyle.h"
+#import "Configuration.h"
+
+@interface EdgeStyle (Storage)
+
+- (id) initFromConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile;
+- (void) storeToConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/EdgeStyle+Storage.m b/tikzit/src/gtk/EdgeStyle+Storage.m
new file mode 100644
index 0000000..0904e1c
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyle+Storage.m
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "EdgeStyle+Storage.h"
+#import "ColorRGB+IntegerListStorage.h"
+
+@implementation EdgeStyle (Storage)
+
+- (id) initFromConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile {
+ self = [self init];
+
+ if (self) {
+ [self setName:[configFile stringEntry:@"Name" inGroup:groupName withDefault:name]];
+ [self setCategory:[configFile stringEntry:@"Category" inGroup:groupName withDefault:category]];
+ headStyle = [configFile integerEntry:@"HeadStyle" inGroup:groupName withDefault:headStyle];
+ tailStyle = [configFile integerEntry:@"TailStyle" inGroup:groupName withDefault:tailStyle];
+ decorationStyle = [configFile integerEntry:@"DecorationStyle" inGroup:groupName withDefault:decorationStyle];
+ thickness = [configFile doubleEntry:@"Thickness" inGroup:groupName withDefault:thickness];
+ }
+
+ return self;
+}
+
+- (void) storeToConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile {
+ [configFile setStringEntry:@"Name" inGroup:groupName value:name];
+ [configFile setStringEntry:@"Category" inGroup:groupName value:category];
+ [configFile setIntegerEntry:@"HeadStyle" inGroup:groupName value:headStyle];
+ [configFile setIntegerEntry:@"TailStyle" inGroup:groupName value:tailStyle];
+ [configFile setIntegerEntry:@"DecorationStyle" inGroup:groupName value:decorationStyle];
+ [configFile setDoubleEntry:@"Thickness" inGroup:groupName value:thickness];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/EdgeStyleEditor.h b/tikzit/src/gtk/EdgeStyleEditor.h
new file mode 100644
index 0000000..70a8747
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyleEditor.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class EdgeStyle;
+
+@interface EdgeStyleEditor: NSObject {
+ EdgeStyle *style;
+ GtkTable *table;
+ GtkEntry *nameEdit;
+ GtkComboBox *decorationCombo;
+ GtkAdjustment *thicknessAdj;
+ BOOL blockSignals;
+}
+
+@property (retain) EdgeStyle *style;
+@property (readonly) GtkWidget *widget;
+
+- (id) init;
+
+@end
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/EdgeStyleEditor.m b/tikzit/src/gtk/EdgeStyleEditor.m
new file mode 100644
index 0000000..a0b3df2
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyleEditor.m
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "EdgeStyleEditor.h"
+
+#import "EdgeStyle.h"
+#import "Shape.h"
+
+#include <gdk-pixbuf/gdk-pixdata.h>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpointer-sign"
+#import "edgedecdata.m"
+#pragma GCC diagnostic pop
+
+enum {
+ ED_NAME_COL = 0,
+ ED_ICON_COL,
+ ED_VALUE_COL,
+ ED_N_COLS
+};
+
+static const guint row_count = 5;
+
+// {{{ Internal interfaces
+// {{{ GTK+ Callbacks
+static void style_name_edit_changed_cb (GtkEditable *widget, EdgeStyleEditor *editor);
+static void decoration_combo_changed_cb (GtkComboBox *widget, EdgeStyleEditor *editor);
+static void thickness_adjustment_changed_cb (GtkAdjustment *widget, EdgeStyleEditor *editor);
+// }}}
+// {{{ Notifications
+
+@interface EdgeStyleEditor (Notifications)
+- (void) nameChangedTo:(NSString*)value;
+- (void) edgeDecorationChangedTo:(EdgeDectorationStyle)value;
+- (void) thicknessChangedTo:(double)value;
+@end
+
+// }}}
+// {{{ Private
+
+@interface EdgeStyleEditor (Private)
+- (void) loadDecorationStylesInto:(GtkListStore*)list;
+- (void) clearEdgeDecorationStyle;
+- (void) setEdgeDecorationStyle:(EdgeDectorationStyle)value;
+@end
+
+// }}}
+// }}}
+// {{{ API
+
+@implementation EdgeStyleEditor
+
+- (void) _addWidget:(GtkWidget*)w withLabel:(gchar *)label atRow:(guint)row {
+ NSAssert(row < row_count, @"row_count is wrong!");
+
+ GtkWidget *l = gtk_label_new (label);
+ gtk_misc_set_alignment (GTK_MISC (l), 0, 0.5);
+ gtk_widget_show (l);
+ gtk_widget_show (w);
+
+ gtk_table_attach (table, l,
+ 0, 1, row, row+1, // l, r, t, b
+ GTK_FILL, // x opts
+ GTK_FILL | GTK_EXPAND, // y opts
+ 5, // x padding
+ 0); // y padding
+
+ gtk_table_attach (table, w,
+ 1, 2, row, row+1, // l, r, t, b
+ GTK_FILL | GTK_EXPAND, // x opts
+ GTK_FILL | GTK_EXPAND, // y opts
+ 0, // x padding
+ 0); // y padding
+}
+
+- (id) init {
+ self = [super init];
+
+ if (self != nil) {
+ style = nil;
+ table = GTK_TABLE (gtk_table_new (row_count, 2, FALSE));
+ gtk_table_set_col_spacings (table, 6);
+ gtk_table_set_row_spacings (table, 6);
+ gtk_widget_set_sensitive (GTK_WIDGET (table), FALSE);
+ blockSignals = NO;
+
+ /**
+ * Name
+ */
+ nameEdit = GTK_ENTRY (gtk_entry_new ());
+ g_object_ref_sink (nameEdit);
+ [self _addWidget:GTK_WIDGET (nameEdit)
+ withLabel:"Name"
+ atRow:0];
+ g_signal_connect (G_OBJECT (nameEdit),
+ "changed",
+ G_CALLBACK (style_name_edit_changed_cb),
+ self);
+
+
+ /**
+ * Edge decoration style
+ */
+ GtkListStore *store = gtk_list_store_new (ED_N_COLS, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_INT);
+ [self loadDecorationStylesInto:store];
+
+ decorationCombo = GTK_COMBO_BOX (gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)));
+ g_object_unref (store);
+ GtkCellRenderer *cellRend = gtk_cell_renderer_pixbuf_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (decorationCombo),
+ cellRend,
+ TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (decorationCombo), cellRend, "pixbuf", ED_ICON_COL);
+ g_object_ref_sink (decorationCombo);
+
+ [self _addWidget:GTK_WIDGET (decorationCombo)
+ withLabel:"Decoration"
+ atRow:1];
+ g_signal_connect (G_OBJECT (decorationCombo),
+ "changed",
+ G_CALLBACK (decoration_combo_changed_cb),
+ self);
+
+
+ /**
+ * Thickness
+ */
+ thicknessAdj = GTK_ADJUSTMENT (gtk_adjustment_new (
+ 1.0, // value
+ 0.0, // lower
+ 50.0, // upper
+ 0.20, // step
+ 1.0, // page
+ 0.0)); // (irrelevant)
+ g_object_ref_sink (thicknessAdj);
+ GtkWidget *scaleSpin = gtk_spin_button_new (thicknessAdj, 0.0, 2);
+ [self _addWidget:scaleSpin
+ withLabel:"Thickness"
+ atRow:4];
+ g_signal_connect (G_OBJECT (thicknessAdj),
+ "value-changed",
+ G_CALLBACK (thickness_adjustment_changed_cb),
+ self);
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ g_object_unref (nameEdit);
+ g_object_unref (decorationCombo);
+ g_object_unref (thicknessAdj);
+ g_object_unref (table);
+ [style release];
+
+ [super dealloc];
+}
+
+- (EdgeStyle*) style {
+ return style;
+}
+
+- (void) setStyle:(EdgeStyle*)s {
+ blockSignals = YES;
+ EdgeStyle *oldStyle = style;
+ style = [s retain];
+
+ if (style != nil) {
+ gtk_widget_set_sensitive (GTK_WIDGET (table), TRUE);
+
+ gtk_entry_set_text(nameEdit, [[style name] UTF8String]);
+
+ [self setEdgeDecorationStyle:[style decorationStyle]];
+
+ gtk_adjustment_set_value(thicknessAdj, [style thickness]);
+ } else {
+ gtk_entry_set_text(nameEdit, "");
+ [self clearEdgeDecorationStyle];
+ gtk_adjustment_set_value(thicknessAdj, 1.0);
+ gtk_widget_set_sensitive (GTK_WIDGET (table), FALSE);
+ }
+
+ [oldStyle release];
+ blockSignals = NO;
+}
+
+- (GtkWidget*) widget {
+ return GTK_WIDGET (table);
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation EdgeStyleEditor (Notifications)
+- (void) nameChangedTo:(NSString*)value {
+ [style setName:value];
+}
+
+- (void) edgeDecorationChangedTo:(EdgeDectorationStyle)value {
+ [style setDecorationStyle:value];
+}
+
+- (void) thicknessChangedTo:(double)value {
+ [style setThickness:(float)value];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation EdgeStyleEditor (Private)
+- (BOOL) signalsBlocked { return blockSignals; }
+
+- (void) loadDecorationStylesInto:(GtkListStore*)list {
+ GtkTreeIter iter;
+
+ GdkPixbuf *buf = gdk_pixbuf_from_pixdata (&ED_none_pixdata, FALSE, NULL);
+ gtk_list_store_append (list, &iter);
+ gtk_list_store_set (list, &iter,
+ ED_NAME_COL, "None",
+ ED_ICON_COL, buf,
+ ED_VALUE_COL, ED_None,
+ -1);
+ g_object_unref (buf);
+
+ buf = gdk_pixbuf_from_pixdata (&ED_arrow_pixdata, FALSE, NULL);
+ gtk_list_store_append (list, &iter);
+ gtk_list_store_set (list, &iter,
+ ED_NAME_COL, "Arrow",
+ ED_ICON_COL, buf,
+ ED_VALUE_COL, ED_Arrow,
+ -1);
+ g_object_unref (buf);
+
+ buf = gdk_pixbuf_from_pixdata (&ED_tick_pixdata, FALSE, NULL);
+ gtk_list_store_append (list, &iter);
+ gtk_list_store_set (list, &iter,
+ ED_NAME_COL, "Tick",
+ ED_ICON_COL, buf,
+ ED_VALUE_COL, ED_Tick,
+ -1);
+ g_object_unref (buf);
+}
+
+- (void) clearEdgeDecorationStyle {
+ gtk_combo_box_set_active (decorationCombo, -1);
+}
+
+- (void) setEdgeDecorationStyle:(EdgeDectorationStyle)value {
+ GtkTreeModel *model = gtk_combo_box_get_model (decorationCombo);
+ GtkTreeIter iter;
+ if (gtk_tree_model_get_iter_first (model, &iter)) {
+ do {
+ EdgeDectorationStyle ed_style;
+ gtk_tree_model_get (model, &iter, ED_VALUE_COL, &ed_style, -1);
+ if (ed_style == value) {
+ gtk_combo_box_set_active_iter (decorationCombo, &iter);
+ return;
+ }
+ } while (gtk_tree_model_iter_next (model, &iter));
+ }
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void style_name_edit_changed_cb (GtkEditable *widget, EdgeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ const gchar *contents = gtk_entry_get_text (GTK_ENTRY (widget));
+ [editor nameChangedTo:[NSString stringWithUTF8String:contents]];
+
+ [pool drain];
+}
+
+static void decoration_combo_changed_cb (GtkComboBox *widget, EdgeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ GtkTreeIter iter;
+ gtk_combo_box_get_active_iter (widget, &iter);
+ EdgeDectorationStyle dec = ED_None;
+ gtk_tree_model_get (gtk_combo_box_get_model (widget), &iter, ED_VALUE_COL, &dec, -1);
+ [editor edgeDecorationChangedTo:dec];
+
+ [pool drain];
+}
+
+static void thickness_adjustment_changed_cb (GtkAdjustment *adj, EdgeStyleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor thicknessChangedTo:gtk_adjustment_get_value (adj)];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/EdgeStyleSelector.h b/tikzit/src/gtk/EdgeStyleSelector.h
new file mode 100644
index 0000000..935c9b7
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyleSelector.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "StyleManager.h"
+
+@interface EdgeStyleSelector: NSObject {
+ GtkListStore *store;
+ GtkTreeView *view;
+ StyleManager *styleManager;
+ BOOL linkedToActiveStyle;
+ BOOL suppressSetActiveStyle;
+}
+
+/*!
+ @property widget
+ @brief The GTK widget
+ */
+@property (readonly) GtkWidget *widget;
+
+/*!
+ @property manager
+ @brief The StyleManager to use. Default is [StyleManager manager]
+ */
+@property (retain) StyleManager *styleManager;
+
+/*!
+ @property linkedToActiveStyles
+ @brief Whether the current selection should be the same as the
+ style manager's active style
+ */
+@property (getter=isLinkedToActiveStyle) BOOL linkedToActiveStyle;
+
+/*!
+ @property selectedStyle
+ @brief The selected style. If linkedToActiveStyle is YES, this
+ will be the same as [manager activeStyle].
+
+ When this changes, a SelectedStyleChanged notification will be posted
+ */
+@property (assign) EdgeStyle *selectedStyle;
+
+/*!
+ * Initialise with the default style manager
+ */
+- (id) init;
+/*!
+ * Initialise with the given style manager
+ */
+- (id) initWithStyleManager:(StyleManager*)m;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/EdgeStyleSelector.m b/tikzit/src/gtk/EdgeStyleSelector.m
new file mode 100644
index 0000000..243d176
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStyleSelector.m
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "EdgeStyleSelector.h"
+
+#import "CairoRenderContext.h"
+#import "Shape.h"
+#import "Shape+Render.h"
+#import "ShapeNames.h"
+#import "StyleManager.h"
+
+#import <gdk-pixbuf/gdk-pixbuf.h>
+
+// {{{ Internal interfaces
+// {{{ Signals
+static void selection_changed_cb (GtkTreeSelection *sel, EdgeStyleSelector *mgr);
+// }}}
+
+enum {
+ STYLES_NAME_COL = 0,
+ STYLES_ICON_COL,
+ STYLES_PTR_COL,
+ STYLES_N_COLS
+};
+
+@interface EdgeStyleSelector (Notifications)
+- (void) stylesReplaced:(NSNotification*)notification;
+- (void) styleAdded:(NSNotification*)notification;
+- (void) styleRemoved:(NSNotification*)notification;
+- (void) activeStyleChanged:(NSNotification*)notification;
+- (void) stylePropertyChanged:(NSNotification*)notification;
+- (void) shapeDictionaryReplaced:(NSNotification*)n;
+- (void) selectionChanged;
+@end
+
+@interface EdgeStyleSelector (Private)
+- (void) clearModel;
+- (cairo_surface_t*) createEdgeIconSurface;
+- (GdkPixbuf*) pixbufOfEdgeInStyle:(EdgeStyle*)style;
+- (GdkPixbuf*) pixbufFromSurface:(cairo_surface_t*)surface;
+- (GdkPixbuf*) pixbufOfEdgeInStyle:(EdgeStyle*)style usingSurface:(cairo_surface_t*)surface;
+- (void) addStyle:(EdgeStyle*)style;
+- (void) postSelectedStyleChanged;
+- (void) reloadStyles;
+@end
+
+// }}}
+// {{{ API
+
+@implementation EdgeStyleSelector
+
+- (id) init {
+ self = [self initWithStyleManager:[StyleManager manager]];
+ return self;
+}
+
+- (id) initWithStyleManager:(StyleManager*)m {
+ self = [super init];
+
+ if (self) {
+ styleManager = nil;
+ linkedToActiveStyle = YES;
+
+ store = gtk_list_store_new (STYLES_N_COLS,
+ G_TYPE_STRING,
+ GDK_TYPE_PIXBUF,
+ G_TYPE_POINTER);
+ g_object_ref (store);
+
+ view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)));
+ gtk_tree_view_set_headers_visible (view, FALSE);
+ g_object_ref (view);
+
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ renderer = gtk_cell_renderer_pixbuf_new ();
+ column = gtk_tree_view_column_new_with_attributes ("Preview",
+ renderer,
+ "pixbuf", STYLES_ICON_COL,
+ NULL);
+ gtk_tree_view_append_column (view, column);
+ gtk_tree_view_set_tooltip_column (view, STYLES_NAME_COL);
+
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (view);
+ gtk_tree_selection_set_mode (sel, GTK_SELECTION_SINGLE);
+
+ g_signal_connect (G_OBJECT (sel),
+ "changed",
+ G_CALLBACK (selection_changed_cb),
+ self);
+
+ [self setStyleManager:m];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(stylePropertyChanged:)
+ name:@"EdgeStylePropertyChanged"
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(shapeDictionaryReplaced:)
+ name:@"ShapeDictionaryReplaced"
+ object:[Shape class]];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ g_object_unref (view);
+ [self clearModel];
+ g_object_unref (store);
+ [styleManager release];
+
+ [super dealloc];
+}
+
+- (StyleManager*) styleManager {
+ return styleManager;
+}
+
+- (void) setStyleManager:(StyleManager*)m {
+ if (m == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Style manager cannot be nil"];
+ }
+ [m retain];
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:styleManager];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(stylesReplaced:)
+ name:@"EdgeStylesReplaced"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(styleAdded:)
+ name:@"EdgeStyleAdded"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(styleRemoved:)
+ name:@"EdgeStyleRemoved"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(activeStyleChanged:)
+ name:@"ActiveEdgeStyleChanged"
+ object:m];
+
+ [styleManager release];
+ styleManager = m;
+
+ [self reloadStyles];
+}
+
+- (GtkWidget*) widget {
+ return GTK_WIDGET (view);
+}
+
+- (BOOL) isLinkedToActiveStyle {
+ return linkedToActiveStyle;
+}
+
+- (void) setLinkedToActiveStyle:(BOOL)linked {
+ linkedToActiveStyle = linked;
+ if (linkedToActiveStyle) {
+ EdgeStyle *style = [self selectedStyle];
+ if ([styleManager activeEdgeStyle] != style) {
+ [self setSelectedStyle:[styleManager activeEdgeStyle]];
+ }
+ }
+}
+
+- (EdgeStyle*) selectedStyle {
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (view);
+ GtkTreeIter iter;
+
+ if (!gtk_tree_selection_get_selected (sel, NULL, &iter)) {
+ return nil;
+ }
+
+ EdgeStyle *style = nil;
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, STYLES_PTR_COL, &style, -1);
+
+ return style;
+}
+
+- (void) setSelectedStyle:(EdgeStyle*)style {
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (view);
+
+ if (style == nil) {
+ gtk_tree_selection_unselect_all (sel);
+ return;
+ }
+
+ GtkTreeModel *m = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (m, &row)) {
+ do {
+ EdgeStyle *rowStyle;
+ gtk_tree_model_get (m, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ gtk_tree_selection_unselect_all (sel);
+ GtkTreePath *path = gtk_tree_model_get_path (m, &row);
+ gtk_tree_selection_select_path (sel, path);
+ gtk_tree_path_free (path);
+ // styleManager.activeStyle will be updated by the GTK+ callback
+ return;
+ }
+ } while (gtk_tree_model_iter_next (m, &row));
+ }
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation EdgeStyleSelector (Notifications)
+
+- (void) stylesReplaced:(NSNotification*)notification {
+ [self reloadStyles];
+}
+
+- (void) styleAdded:(NSNotification*)notification {
+ [self addStyle:[[notification userInfo] objectForKey:@"style"]];
+}
+
+- (void) styleRemoved:(NSNotification*)notification {
+ EdgeStyle *style = [[notification userInfo] objectForKey:@"style"];
+
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ EdgeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ gtk_list_store_remove (store, &row);
+ [rowStyle release];
+ return;
+ }
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+}
+
+- (void) activeStyleChanged:(NSNotification*)notification {
+ if (linkedToActiveStyle) {
+ EdgeStyle *style = [self selectedStyle];
+ if ([styleManager activeEdgeStyle] != style) {
+ [self setSelectedStyle:[styleManager activeEdgeStyle]];
+ }
+ }
+}
+
+- (void) stylePropertyChanged:(NSNotification*)notification {
+ EdgeStyle *style = [notification object];
+
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ EdgeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ if ([@"name" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ gtk_list_store_set (store, &row, STYLES_NAME_COL, [[style name] UTF8String], -1);
+ } else if (![@"scale" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ GdkPixbuf *pixbuf = [self pixbufOfEdgeInStyle:style];
+ gtk_list_store_set (store, &row, STYLES_ICON_COL, pixbuf, -1);
+ gdk_pixbuf_unref (pixbuf);
+ }
+ }
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+}
+
+- (void) shapeDictionaryReplaced:(NSNotification*)n {
+ [self reloadStyles];
+}
+
+- (void) selectionChanged {
+ if (linkedToActiveStyle) {
+ EdgeStyle *style = [self selectedStyle];
+ if ([styleManager activeEdgeStyle] != style) {
+ [styleManager setActiveEdgeStyle:style];
+ }
+ }
+ [self postSelectedStyleChanged];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation EdgeStyleSelector (Private)
+- (void) clearModel {
+ [self setSelectedStyle:nil];
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ EdgeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ [rowStyle release];
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+ gtk_list_store_clear (store);
+}
+
+- (cairo_surface_t*) createEdgeIconSurface {
+ return cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 48, 18);
+}
+
+- (GdkPixbuf*) pixbufOfEdgeInStyle:(EdgeStyle*)style {
+ cairo_surface_t *surface = [self createEdgeIconSurface];
+ GdkPixbuf *pixbuf = [self pixbufOfEdgeInStyle:style usingSurface:surface];
+ cairo_surface_destroy (surface);
+ return pixbuf;
+}
+
+// Bring on GTK+3 and gdk_pixbuf_get_from_surface()
+- (GdkPixbuf*) pixbufFromSurface:(cairo_surface_t*)surface {
+ cairo_surface_flush (surface);
+
+ int width = cairo_image_surface_get_width (surface);
+ int height = cairo_image_surface_get_height (surface);
+ int stride = cairo_image_surface_get_stride (surface);
+ unsigned char *data = cairo_image_surface_get_data (surface);
+
+ GdkPixbuf *pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
+ TRUE,
+ 8,
+ width,
+ height);
+ unsigned char *pbdata = gdk_pixbuf_get_pixels (pixbuf);
+ int pbstride = gdk_pixbuf_get_rowstride (pixbuf);
+
+ for (int y = 0; y < height; ++y) {
+ uint32_t *line = (uint32_t*)(data + y*stride);
+ unsigned char *pbline = pbdata + (y*pbstride);
+ for (int x = 0; x < width; ++x) {
+ uint32_t pixel = *(line + x);
+ unsigned char *pbpixel = pbline + (x*4);
+ // NB: We should un-pre-mult the alpha here.
+ // However, in our world, alpha is always
+ // on or off, so it doesn't really matter
+ pbpixel[3] = ((pixel & 0xff000000) >> 24);
+ pbpixel[0] = ((pixel & 0x00ff0000) >> 16);
+ pbpixel[1] = ((pixel & 0x0000ff00) >> 8);
+ pbpixel[2] = (pixel & 0x000000ff);
+ }
+ }
+
+ return pixbuf;
+}
+
+- (GdkPixbuf*) pixbufOfEdgeInStyle:(EdgeStyle*)style usingSurface:(cairo_surface_t*)surface {
+ Transformer *transformer = [Transformer defaultTransformer];
+ [transformer setFlippedAboutXAxis:YES];
+
+ int width = cairo_image_surface_get_width (surface);
+ int height = cairo_image_surface_get_height (surface);
+ NSRect pixbufBounds = NSMakeRect(0.0, 0.0, width, height);
+ NSRect graphBounds = [transformer rectFromScreen:pixbufBounds];
+
+ NSPoint mid = NSMakePoint (NSMidX (graphBounds), NSMidY (graphBounds));
+ NSPoint start = NSMakePoint (NSMinX (graphBounds) + 0.1f, mid.y);
+ NSPoint end = NSMakePoint (NSMaxX (graphBounds) - 0.1f, mid.y);
+ NSPoint midTan = NSMakePoint (mid.x + 0.1f, mid.y);
+ NSPoint leftNormal = NSMakePoint (mid.x, mid.y - 0.1f);
+ NSPoint rightNormal = NSMakePoint (mid.x, mid.y + 0.1f);
+
+ CairoRenderContext *context = [[CairoRenderContext alloc] initForSurface:surface];
+ [context clearSurface];
+
+ [context startPath];
+ [context moveTo:[transformer toScreen:start]];
+ [context lineTo:[transformer toScreen:end]];
+
+ switch ([style decorationStyle]) {
+ case ED_None:
+ break;
+ case ED_Tick:
+ [context moveTo:[transformer toScreen:leftNormal]];
+ [context lineTo:[transformer toScreen:rightNormal]];
+ break;
+ case ED_Arrow:
+ [context moveTo:[transformer toScreen:leftNormal]];
+ [context lineTo:[transformer toScreen:midTan]];
+ [context lineTo:[transformer toScreen:rightNormal]];
+ break;
+ }
+
+ [context setLineWidth:[style thickness]];
+ [context strokePathWithColor:BlackRColor];
+
+ [context release];
+
+ return [self pixbufFromSurface:surface];
+}
+
+- (void) addStyle:(EdgeStyle*)style usingSurface:(cairo_surface_t*)surface {
+ GtkTreeIter iter;
+ gtk_list_store_append (store, &iter);
+
+ GdkPixbuf *pixbuf = [self pixbufOfEdgeInStyle:style usingSurface:surface];
+ gtk_list_store_set (store, &iter,
+ STYLES_NAME_COL, [[style name] UTF8String],
+ STYLES_ICON_COL, pixbuf,
+ STYLES_PTR_COL, (gpointer)[style retain],
+ -1);
+ gdk_pixbuf_unref (pixbuf);
+}
+
+- (void) addStyle:(EdgeStyle*)style {
+ cairo_surface_t *surface = [self createEdgeIconSurface];
+ [self addStyle:style usingSurface:surface];
+ cairo_surface_destroy (surface);
+}
+
+- (void) postSelectedStyleChanged {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SelectedStyleChanged" object:self];
+}
+
+- (void) reloadStyles {
+ [self clearModel];
+ for (EdgeStyle *style in [styleManager edgeStyles]) {
+ [self addStyle:style];
+ }
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void selection_changed_cb (GtkTreeSelection *sel, EdgeStyleSelector *mgr) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [mgr selectionChanged];
+ [pool drain];
+}
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
+
diff --git a/tikzit/src/gtk/EdgeStylesPalette.h b/tikzit/src/gtk/EdgeStylesPalette.h
new file mode 100644
index 0000000..a7d715b
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStylesPalette.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class StyleManager;
+@class EdgeStyleSelector;
+@class EdgeStyleEditor;
+@class TikzDocument;
+
+@interface EdgeStylesPalette: NSObject {
+ TikzDocument *document;
+ EdgeStyleSelector *selector;
+ EdgeStyleEditor *editor;
+
+ GtkWidget *palette;
+
+ GtkWidget *removeStyleButton;
+ GtkWidget *applyStyleButton;
+ GtkWidget *clearStyleButton;
+}
+
+@property (retain) StyleManager *styleManager;
+@property (retain) TikzDocument *document;
+@property (readonly) GtkWidget *widget;
+
+- (id) initWithManager:(StyleManager*)m;
+
+@end
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/EdgeStylesPalette.m b/tikzit/src/gtk/EdgeStylesPalette.m
new file mode 100644
index 0000000..bcb631e
--- /dev/null
+++ b/tikzit/src/gtk/EdgeStylesPalette.m
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "EdgeStylesPalette.h"
+
+#import "EdgeStyleSelector.h"
+#import "EdgeStyleEditor.h"
+#import "StyleManager.h"
+#import "TikzDocument.h"
+
+// {{{ Internal interfaces
+// {{{ GTK+ Callbacks
+static void add_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette);
+static void remove_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette);
+static void apply_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette);
+static void clear_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette);
+// }}}
+// {{{ Notifications
+
+@interface EdgeStylesPalette (Notifications)
+- (void) selectedStyleChanged:(NSNotification*)notification;
+- (void) edgeSelectionChanged:(NSNotification*)n;
+@end
+
+// }}}
+// {{{ Private
+
+@interface EdgeStylesPalette (Private)
+- (void) updateButtonState;
+- (void) removeSelectedStyle;
+- (void) applySelectedStyle;
+- (void) clearSelectedStyle;
+@end
+
+// }}}
+// }}}
+// {{{ API
+
+@implementation EdgeStylesPalette
+
+@synthesize widget=palette;
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithManager:(StyleManager*)m {
+ self = [super init];
+
+ if (self) {
+ document = nil;
+ selector = [[EdgeStyleSelector alloc] initWithStyleManager:m];
+ editor = [[EdgeStyleEditor alloc] init];
+
+ palette = gtk_vbox_new (FALSE, 0);
+ // FIXME: remove this line when we add edge styles
+ gtk_container_set_border_width (GTK_CONTAINER (palette), 6);
+ gtk_box_set_spacing (GTK_BOX (palette), 6);
+ g_object_ref_sink (palette);
+
+ gtk_box_pack_start (GTK_BOX (palette), [editor widget], FALSE, FALSE, 0);
+ gtk_widget_show ([editor widget]);
+ GtkWidget *selectorFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (selectorFrame), [selector widget]);
+ gtk_box_pack_start (GTK_BOX (palette), selectorFrame, TRUE, TRUE, 0);
+ gtk_widget_show (selectorFrame);
+ gtk_widget_show ([selector widget]);
+
+ GtkBox *buttonBox = GTK_BOX (gtk_hbox_new(FALSE, 5));
+ gtk_box_pack_start (GTK_BOX (palette), GTK_WIDGET (buttonBox), FALSE, FALSE, 0);
+
+ GtkBox *bbox1 = GTK_BOX (gtk_hbox_new(FALSE, 0));
+ gtk_box_pack_start (buttonBox, GTK_WIDGET (bbox1), FALSE, FALSE, 0);
+
+ GtkWidget *addStyleButton = gtk_button_new ();
+ gtk_widget_set_tooltip_text (addStyleButton, "Add a new style");
+ GtkWidget *addIcon = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (addStyleButton), addIcon);
+ gtk_box_pack_start (bbox1, addStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (addStyleButton),
+ "clicked",
+ G_CALLBACK (add_style_button_cb),
+ self);
+
+ removeStyleButton = gtk_button_new ();
+ g_object_ref_sink (removeStyleButton);
+ gtk_widget_set_tooltip_text (removeStyleButton, "Delete selected style");
+ GtkWidget *removeIcon = gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (removeStyleButton), removeIcon);
+ gtk_box_pack_start (bbox1, removeStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (removeStyleButton),
+ "clicked",
+ G_CALLBACK (remove_style_button_cb),
+ self);
+
+ GtkBox *bbox2 = GTK_BOX (gtk_hbox_new(FALSE, 0));
+ gtk_box_pack_start (buttonBox, GTK_WIDGET (bbox2), FALSE, FALSE, 0);
+
+ applyStyleButton = gtk_button_new_with_label ("Apply");
+ g_object_ref_sink (applyStyleButton);
+ gtk_widget_set_tooltip_text (applyStyleButton, "Apply style to selected edges");
+ gtk_box_pack_start (bbox2, applyStyleButton, FALSE, FALSE, 5);
+ g_signal_connect (G_OBJECT (applyStyleButton),
+ "clicked",
+ G_CALLBACK (apply_style_button_cb),
+ self);
+
+ clearStyleButton = gtk_button_new_with_label ("Clear");
+ g_object_ref_sink (clearStyleButton);
+ gtk_widget_set_tooltip_text (clearStyleButton, "Clear style from selected edges");
+ gtk_box_pack_start (bbox2, clearStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (clearStyleButton),
+ "clicked",
+ G_CALLBACK (clear_style_button_cb),
+ self);
+
+ gtk_widget_show_all (GTK_WIDGET (buttonBox));
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(selectedStyleChanged:)
+ name:@"SelectedStyleChanged"
+ object:selector];
+
+ [self updateButtonState];
+ }
+
+ return self;
+}
+
+- (StyleManager*) styleManager {
+ return [selector styleManager];
+}
+
+- (void) setStyleManager:(StyleManager*)m {
+ [selector setStyleManager:m];
+}
+
+- (TikzDocument*) document {
+ return document;
+}
+
+- (void) setDocument:(TikzDocument*)doc {
+ if (document != nil) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:nil
+ object:[document pickSupport]];
+ }
+
+ [doc retain];
+ [document release];
+ document = doc;
+
+ if (document != nil) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(edgeSelectionChanged:)
+ name:@"EdgeSelectionChanged"
+ object:[document pickSupport]];
+ }
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [editor release];
+ [selector release];
+ [document release];
+
+ g_object_unref (palette);
+ g_object_unref (removeStyleButton);
+ g_object_unref (applyStyleButton);
+ g_object_unref (clearStyleButton);
+
+ [super dealloc];
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation EdgeStylesPalette (Notifications)
+- (void) selectedStyleChanged:(NSNotification*)notification {
+ [editor setStyle:[selector selectedStyle]];
+ [self updateButtonState];
+}
+
+- (void) edgeSelectionChanged:(NSNotification*)n {
+ [self updateButtonState];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation EdgeStylesPalette (Private)
+- (void) updateButtonState {
+ gboolean hasEdgeSelection = [[[document pickSupport] selectedEdges] count] > 0;
+ gboolean hasStyleSelection = [selector selectedStyle] != nil;
+
+ gtk_widget_set_sensitive (applyStyleButton, hasEdgeSelection && hasStyleSelection);
+ gtk_widget_set_sensitive (clearStyleButton, hasEdgeSelection);
+ gtk_widget_set_sensitive (removeStyleButton, hasStyleSelection);
+}
+
+- (void) removeSelectedStyle {
+ EdgeStyle *style = [selector selectedStyle];
+ if (style)
+ [[selector styleManager] removeEdgeStyle:style];
+}
+
+- (void) applySelectedStyle {
+ [document startModifyEdges:[[document pickSupport] selectedEdges]];
+
+ EdgeStyle *style = [selector selectedStyle];
+ for (Edge *edge in [[document pickSupport] selectedEdges]) {
+ [edge setStyle:style];
+ }
+
+ [document endModifyEdges];
+}
+
+- (void) clearSelectedStyle {
+ [document startModifyEdges:[[document pickSupport] selectedEdges]];
+
+ for (Edge *edge in [[document pickSupport] selectedEdges]) {
+ [edge setStyle:nil];
+ }
+
+ [document endModifyEdges];
+}
+
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void add_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ EdgeStyle *newStyle = [EdgeStyle defaultEdgeStyleWithName:@"newstyle"];
+ [[palette styleManager] addEdgeStyle:newStyle];
+ [[palette styleManager] setActiveEdgeStyle:newStyle];
+
+ [pool drain];
+}
+
+static void remove_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette removeSelectedStyle];
+ [pool drain];
+}
+
+static void apply_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette applySelectedStyle];
+ [pool drain];
+}
+
+static void clear_style_button_cb (GtkButton *widget, EdgeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette clearSelectedStyle];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/FileChooserDialog.h b/tikzit/src/gtk/FileChooserDialog.h
new file mode 100644
index 0000000..80b03f5
--- /dev/null
+++ b/tikzit/src/gtk/FileChooserDialog.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@interface FileChooserDialog: NSObject {
+ GtkFileChooser *dialog;
+}
+
++ (FileChooserDialog*) saveDialog;
++ (FileChooserDialog*) saveDialogWithParent:(GtkWindow*)parent;
++ (FileChooserDialog*) saveDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent;
++ (FileChooserDialog*) openDialog;
++ (FileChooserDialog*) openDialogWithParent:(GtkWindow*)parent;
++ (FileChooserDialog*) openDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent;
+
+- (id) initSaveDialog;
+- (id) initSaveDialogWithParent:(GtkWindow*)parent;
+- (id) initSaveDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent;
+- (id) initOpenDialog;
+- (id) initOpenDialogWithParent:(GtkWindow*)parent;
+- (id) initOpenDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent;
+
+- (void) addStandardFilters;
+- (void) addFileFilter:(NSString*)filterName withPattern:(NSString*)filePattern;
+- (void) addFileFilter:(NSString*)filterName withPattern:(NSString*)filePattern setSelected:(BOOL)selected;
+
+- (void) setCurrentFolder:(NSString*)path;
+- (NSString*) currentFolder;
+
+- (void) setSuggestedName:(NSString*)fileName;
+
+- (NSString*) filePath;
+
+- (BOOL) showDialog;
+
+- (void) destroy;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/FileChooserDialog.m b/tikzit/src/gtk/FileChooserDialog.m
new file mode 100644
index 0000000..9498e4c
--- /dev/null
+++ b/tikzit/src/gtk/FileChooserDialog.m
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "FileChooserDialog.h"
+
+@implementation FileChooserDialog: NSObject
+
++ (FileChooserDialog*) saveDialog { return [[[self alloc] initSaveDialog] autorelease]; }
++ (FileChooserDialog*) saveDialogWithParent:(GtkWindow*)parent
+ { return [[[self alloc] initSaveDialogWithParent:parent] autorelease]; }
++ (FileChooserDialog*) saveDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent
+ { return [[[self alloc] initSaveDialogWithTitle:title parent:parent] autorelease]; }
++ (FileChooserDialog*) openDialog { return [[[self alloc] initOpenDialog] autorelease]; }
++ (FileChooserDialog*) openDialogWithParent:(GtkWindow*)parent
+ { return [[[self alloc] initOpenDialogWithParent:parent] autorelease]; }
++ (FileChooserDialog*) openDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent
+ { return [[[self alloc] initOpenDialogWithTitle:title parent:parent] autorelease]; }
+
+- (id) initSaveDialog { return [self initSaveDialogWithParent:NULL]; }
+- (id) initSaveDialogWithParent:(GtkWindow*)parent
+ { return [self initSaveDialogWithTitle:@"Save file" parent:parent]; }
+- (id) initSaveDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent {
+ self = [super init];
+
+ if (self) {
+ dialog = GTK_FILE_CHOOSER (gtk_file_chooser_dialog_new (
+ [title UTF8String],
+ parent,
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
+ NULL));
+ gtk_file_chooser_set_do_overwrite_confirmation (dialog, TRUE);
+ }
+
+ return self;
+}
+
+- (id) initOpenDialog { return [self initOpenDialogWithParent:NULL]; }
+- (id) initOpenDialogWithParent:(GtkWindow*)parent
+ { return [self initOpenDialogWithTitle:@"Open file" parent:parent]; }
+- (id) initOpenDialogWithTitle:(NSString*)title parent:(GtkWindow*)parent {
+ self = [super init];
+
+ if (self) {
+ dialog = GTK_FILE_CHOOSER (gtk_file_chooser_dialog_new (
+ [title UTF8String],
+ parent,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+ NULL));
+ }
+
+ return self;
+}
+
+- (void) addStandardFilters {
+ GtkFileFilter *tikzfilter = gtk_file_filter_new();
+ gtk_file_filter_set_name(tikzfilter, ".tikz files");
+ gtk_file_filter_add_pattern(tikzfilter, "*.tikz");
+ gtk_file_chooser_add_filter(dialog, tikzfilter);
+ GtkFileFilter *allfilter = gtk_file_filter_new();
+ gtk_file_filter_set_name(allfilter, "all files");
+ gtk_file_filter_add_pattern(allfilter, "*");
+ gtk_file_chooser_add_filter(dialog, allfilter);
+ gtk_file_chooser_set_filter(dialog, tikzfilter);
+}
+
+- (void) addFileFilter:(NSString*)filterName withPattern:(NSString*)filePattern {
+ [self addFileFilter:filterName withPattern:filePattern setSelected:NO];
+}
+
+- (void) addFileFilter:(NSString*)filterName withPattern:(NSString*)filePattern setSelected:(BOOL)selected {
+ GtkFileFilter *oldFilter = selected ? NULL : gtk_file_chooser_get_filter (dialog);
+ GtkFileFilter *filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, [filterName UTF8String]);
+ gtk_file_filter_add_pattern(filter, [filePattern UTF8String]);
+ gtk_file_chooser_add_filter(dialog, filter);
+ if (selected) {
+ gtk_file_chooser_set_filter (dialog, filter);
+ } else if (oldFilter) {
+ gtk_file_chooser_set_filter (dialog, oldFilter);
+ }
+}
+
+- (void) setCurrentFolder:(NSString*)path {
+ gchar *folder = [path glibFilename];
+ if (folder) {
+ gtk_file_chooser_set_current_folder(dialog, folder);
+ g_free (folder);
+ }
+}
+
+- (NSString*) currentFolder {
+ NSString *path = nil;
+ gchar *folder = gtk_file_chooser_get_current_folder(dialog);
+ if (folder) {
+ path = [NSString stringWithGlibFilename:folder];
+ g_free (folder);
+ }
+ return path;
+}
+
+- (void) setSuggestedName:(NSString*)fileName {
+ gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), [fileName UTF8String]);
+}
+
+- (NSString*) filePath {
+ NSString *path = nil;
+ gchar *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
+ if (filename) {
+ path = [NSString stringWithGlibFilename:filename];
+ g_free (filename);
+ }
+ return path;
+}
+
+- (BOOL) showDialog {
+ return (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) ? YES : NO;
+}
+
+- (void) destroy {
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+ dialog = NULL;
+}
+
+- (void) dealloc {
+ if (dialog) {
+ g_warning ("Failed to destroy file chooser dialog!\n");
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+ }
+ [super dealloc];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/GraphInputHandler.h b/tikzit/src/gtk/GraphInputHandler.h
new file mode 100644
index 0000000..61af36f
--- /dev/null
+++ b/tikzit/src/gtk/GraphInputHandler.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "GraphRenderer.h"
+#import "InputDelegate.h"
+#import "StyleManager.h"
+
+typedef enum {
+ SelectMode,
+ CreateNodeMode,
+ DrawEdgeMode,
+ BoundingBoxMode,
+ HandMode
+} InputMode;
+
+typedef enum {
+ QuietState,
+ SelectBoxState,
+ ToggleSelectState,
+ MoveSelectedNodesState,
+ DragEdgeControlPoint1,
+ DragEdgeControlPoint2,
+ EdgeDragState,
+ BoundingBoxState,
+ CanvasDragState
+} MouseState;
+
+@interface GraphInputHandler: NSObject {
+ GraphRenderer *renderer;
+ InputMode mode;
+ MouseState state;
+ float edgeFuzz;
+ NSPoint dragOrigin;
+ Node *leaderNode;
+ NSPoint oldLeaderPos;
+ Edge *modifyEdge;
+ NSMutableSet *selectionBoxContents;
+ ResizeHandle grabbedResizeHandle;
+ NSPoint oldOrigin;
+}
+
+- (id) initWithGraphRenderer:(GraphRenderer*)r;
+
+- (float) edgeFuzz;
+- (void) setEdgeFuzz:(float)fuzz;
+
+- (InputMode) mode;
+- (void) setMode:(InputMode)mode;
+
+- (void) resetState;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/GraphInputHandler.m b/tikzit/src/gtk/GraphInputHandler.m
new file mode 100644
index 0000000..4d77045
--- /dev/null
+++ b/tikzit/src/gtk/GraphInputHandler.m
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "GraphInputHandler.h"
+#import <gdk/gdkkeysyms.h>
+#import "Edge+Render.h"
+
+static const InputMask unionSelectMask = ShiftMask;
+
+@implementation GraphInputHandler
+- (id) initWithGraphRenderer:(GraphRenderer*)r {
+ self = [super init];
+
+ if (self) {
+ renderer = r;
+ mode = SelectMode;
+ state = QuietState;
+ edgeFuzz = 3.0f;
+ leaderNode = nil;
+ modifyEdge = nil;
+ selectionBoxContents = [[NSMutableSet alloc] initWithCapacity:10];
+ grabbedResizeHandle = NoHandle;
+ }
+
+ return self;
+}
+
+- (TikzDocument*) doc {
+ return [renderer document];
+}
+
+- (void) deselectAllNodes {
+ [[[self doc] pickSupport] deselectAllNodes];
+}
+
+- (void) deselectAllEdges {
+ [[[self doc] pickSupport] deselectAllEdges];
+}
+
+- (void) deselectAll {
+ [[[self doc] pickSupport] deselectAllNodes];
+ [[[self doc] pickSupport] deselectAllEdges];
+}
+
+- (void) shiftNodesByMovingLeader:(Node*)leader to:(NSPoint)to {
+ Transformer *transformer = [renderer transformer];
+
+ NSPoint from = [transformer toScreen:[leader point]];
+ //to = [[renderer grid] snapScreenPoint:to];
+ float dx = to.x - from.x;
+ float dy = to.y - from.y;
+
+ for (Node *node in [[[self doc] pickSupport] selectedNodes]) {
+ NSPoint p = [transformer toScreen:[node point]];
+ p.x += dx;
+ p.y += dy;
+ p = [[renderer grid] snapScreenPoint:p];
+ [node setPoint:[transformer fromScreen:p]];
+ }
+}
+
+- (float) edgeFuzz {
+ return edgeFuzz;
+}
+
+- (void) setEdgeFuzz:(float)fuzz {
+ edgeFuzz = fuzz;
+}
+
+- (InputMode) mode {
+ return mode;
+}
+
+- (void) resetState {
+ state = QuietState;
+}
+
+- (void) setMode:(InputMode)m {
+ if (mode != m) {
+ if (mode == BoundingBoxMode) {
+ [renderer setBoundingBoxHandlesShown:NO];
+ }
+ mode = m;
+ [self deselectAll];
+ if (m == BoundingBoxMode) {
+ [renderer setBoundingBoxHandlesShown:YES];
+ }
+ }
+}
+
+- (BOOL) circleWithCenter:(NSPoint)c andRadius:(float)r containsPoint:(NSPoint)p {
+ return (NSDistanceBetweenPoints(c, p) <= r);
+}
+
+- (void) lookForControlPointAt:(NSPoint)pos {
+ const float cpr = [Edge controlPointRadius];
+ for (Edge *e in [[[self doc] pickSupport] selectedEdges]) {
+ NSPoint cp1 = [[renderer transformer] toScreen:[e cp1]];
+ if ([self circleWithCenter:cp1 andRadius:cpr containsPoint:pos]) {
+ state = DragEdgeControlPoint1;
+ modifyEdge = e;
+ [[self doc] startModifyEdge:e];
+ return;
+ }
+ NSPoint cp2 = [[renderer transformer] toScreen:[e cp2]];
+ if ([self circleWithCenter:cp2 andRadius:cpr containsPoint:pos]) {
+ state = DragEdgeControlPoint2;
+ modifyEdge = e;
+ [[self doc] startModifyEdge:e];
+ return;
+ }
+ }
+}
+
+- (void) mousePressAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask {
+ dragOrigin = pos;
+
+ // we should already be in a quiet state, but no harm in making sure
+ state = QuietState;
+
+ if (mode == HandMode || mask == ControlMask) {
+ state = CanvasDragState;
+ oldOrigin = [[renderer transformer] origin];
+ } else if (mode == DrawEdgeMode) {
+ leaderNode = [renderer anyNodeAt:pos];
+ if (leaderNode != nil) {
+ state = EdgeDragState;
+ }
+ } else if (mode == BoundingBoxMode) {
+ state = BoundingBoxState;
+ grabbedResizeHandle = [renderer boundingBoxResizeHandleAt:pos];
+ [[self doc] startChangeBoundingBox];
+ if (grabbedResizeHandle == NoHandle) {
+ [[[self doc] graph] setBoundingBox:NSZeroRect];
+ [renderer setBoundingBoxHandlesShown:NO];
+ }
+ } else if (mode == SelectMode) {
+ modifyEdge = nil;
+ [self lookForControlPointAt:pos];
+
+ if (modifyEdge == nil) {
+ // we didn't find a control point
+
+ BOOL unionSelect = (mask & unionSelectMask);
+
+ leaderNode = [renderer anyNodeAt:pos];
+ // if we hit a node, deselect other nodes (if Shift is up) and go to move mode
+ if (leaderNode != nil) {
+ BOOL alreadySelected = [[self doc] isNodeSelected:leaderNode];
+ if (!unionSelect && !alreadySelected) {
+ [self deselectAllEdges];
+ [self deselectAllNodes];
+ }
+ if (unionSelect && alreadySelected) {
+ state = ToggleSelectState;
+ } else {
+ [[[self doc] pickSupport] selectNode:leaderNode];
+ state = MoveSelectedNodesState;
+ oldLeaderPos = [leaderNode point];
+ [[self doc] startShiftNodes:[[[self doc] pickSupport] selectedNodes]];
+ }
+ }
+
+ // if mouse did not hit a node, check if mouse hit an edge
+ if (leaderNode == nil) {
+ Edge *edge = [renderer anyEdgeAt:pos withFuzz:edgeFuzz];
+ if (edge != nil) {
+ BOOL alreadySelected = [[self doc] isEdgeSelected:edge];
+ if (!unionSelect) {
+ [self deselectAll];
+ }
+ if (unionSelect && alreadySelected) {
+ [[[self doc] pickSupport] deselectEdge:edge];
+ } else {
+ [[[self doc] pickSupport] selectEdge:edge];
+ }
+ } else {
+ // if mouse did not hit anything, put us in box mode
+ if (!unionSelect) {
+ [self deselectAll];
+ }
+ [selectionBoxContents removeAllObjects];
+ state = SelectBoxState;
+ }
+ }
+ }
+ }
+}
+
+- (void) mouseReleaseAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask {
+ if (state == SelectBoxState) {
+ BOOL shouldDeselect = !(mask & unionSelectMask);
+ if (shouldDeselect) {
+ [self deselectAllEdges];
+ }
+ [[[self doc] pickSupport] selectAllNodes:selectionBoxContents
+ replacingSelection:shouldDeselect];
+ [renderer clearSelectionBox];
+ } else if (state == ToggleSelectState) {
+ [[[self doc] pickSupport] deselectNode:leaderNode];
+ leaderNode = nil;
+ } else if (state == MoveSelectedNodesState) {
+ if (NSEqualPoints (oldLeaderPos, [leaderNode point])) {
+ [[self doc] cancelShiftNodes];
+ } else {
+ [[self doc] endShiftNodes];
+ }
+ leaderNode = nil;
+ } else if (state == DragEdgeControlPoint1 || state == DragEdgeControlPoint2) {
+ // FIXME: check if there was any real change
+ [[self doc] endModifyEdge];
+ } else if (state == EdgeDragState) {
+ [renderer clearHalfEdge];
+ Node *targ = [renderer anyNodeAt:pos];
+ if (targ != nil) {
+ [[self doc] addEdgeFrom:leaderNode to:targ];
+ }
+ } else if (state == QuietState && mode == CreateNodeMode) {
+ Transformer *transformer = [renderer transformer];
+ NSPoint nodePoint = [transformer fromScreen:[[renderer grid] snapScreenPoint:pos]];
+ [[self doc] addNodeAt:nodePoint];
+ } else if (state == BoundingBoxState) {
+ [[self doc] endChangeBoundingBox];
+ [renderer setBoundingBoxHandlesShown:YES];
+ }
+
+ state = QuietState;
+}
+
+- (void) mouseDoubleClickAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask {
+ if (mode != SelectMode) {
+ return;
+ }
+ if (state != QuietState) {
+ return;
+ }
+ // convert bend mode on edge under mouse cursor
+ Edge *edge = [renderer anyEdgeAt:pos withFuzz:edgeFuzz];
+ if (edge != nil) {
+ [[self doc] startModifyEdge:edge];
+ if ([edge bendMode]==EdgeBendModeBasic) {
+ [edge convertBendToAngles];
+ [edge setBendMode:EdgeBendModeInOut];
+ } else {
+ [edge setBendMode:EdgeBendModeBasic];
+ }
+ [[self doc] endModifyEdge];
+
+ [self deselectAllEdges];
+ [[[self doc] pickSupport] selectEdge:edge];
+ }
+}
+
+- (void) mouseMoveTo:(NSPoint)pos withButtons:(MouseButton)buttons andMask:(InputMask)mask {
+ Transformer *transformer = [renderer transformer];
+
+ if (state == ToggleSelectState) {
+ state = MoveSelectedNodesState;
+ oldLeaderPos = [leaderNode point];
+ [[self doc] startShiftNodes:[[[self doc] pickSupport] selectedNodes]];
+ }
+
+ if (state == SelectBoxState) {
+ NSRect selectionBox = NSRectAroundPoints(dragOrigin, pos);
+ [renderer setSelectionBox:selectionBox];
+
+ NSEnumerator *enumerator = [[self doc] nodeEnumerator];
+ Node *node;
+ while ((node = [enumerator nextObject]) != nil) {
+ NSPoint nodePos = [transformer toScreen:[node point]];
+ if (NSPointInRect(nodePos, selectionBox)) {
+ if (![selectionBoxContents member:node]) {
+ [selectionBoxContents addObject:node];
+ [renderer invalidateNode:node];
+ }
+ } else {
+ if ([selectionBoxContents member:node]) {
+ [selectionBoxContents removeObject:node];
+ [renderer invalidateNode:node];
+ }
+ }
+ }
+ } else if (state == MoveSelectedNodesState) {
+ if (leaderNode != nil) {
+ [self shiftNodesByMovingLeader:leaderNode to:pos];
+ NSPoint shiftSoFar;
+ shiftSoFar.x = [leaderNode point].x - oldLeaderPos.x;
+ shiftSoFar.y = [leaderNode point].y - oldLeaderPos.y;
+ [[self doc] shiftNodesUpdate:shiftSoFar];
+ }
+ } else if (state == DragEdgeControlPoint1 || state == DragEdgeControlPoint2) {
+ // invalidate once before we start changing it: we may be shrinking
+ // the control circles
+ [[self doc] modifyEdgeCheckPoint];
+ if (state == DragEdgeControlPoint1) {
+ [modifyEdge moveCp1To:[transformer fromScreen:pos]
+ withWeightCourseness:0.1f
+ andBendCourseness:15
+ forceLinkControlPoints:(mask & ControlMask)];
+ } else {
+ [modifyEdge moveCp2To:[transformer fromScreen:pos]
+ withWeightCourseness:0.1f
+ andBendCourseness:15
+ forceLinkControlPoints:(mask & ControlMask)];
+ }
+ [[self doc] modifyEdgeCheckPoint];
+ } else if (state == EdgeDragState) {
+ [renderer setHalfEdgeFrom:leaderNode to:pos];
+ } else if (state == BoundingBoxState) {
+ Grid *grid = [renderer grid];
+ Graph *graph = [[self doc] graph];
+ if (grabbedResizeHandle == NoHandle) {
+ NSRect bbox = NSRectAroundPoints(
+ [grid snapScreenPoint:dragOrigin],
+ [grid snapScreenPoint:pos]
+ );
+ [graph setBoundingBox:[transformer rectFromScreen:bbox]];
+ } else {
+ NSRect bbox = [transformer rectToScreen:[graph boundingBox]];
+ NSPoint p2 = [grid snapScreenPoint:pos];
+
+ if (grabbedResizeHandle == NorthWestHandle ||
+ grabbedResizeHandle == NorthHandle ||
+ grabbedResizeHandle == NorthEastHandle) {
+
+ float dy = p2.y - NSMinY(bbox);
+ if (dy < bbox.size.height) {
+ bbox.origin.y += dy;
+ bbox.size.height -= dy;
+ } else {
+ bbox.origin.y = NSMaxY(bbox);
+ bbox.size.height = 0;
+ }
+
+ } else if (grabbedResizeHandle == SouthWestHandle ||
+ grabbedResizeHandle == SouthHandle ||
+ grabbedResizeHandle == SouthEastHandle) {
+
+ float dy = p2.y - NSMaxY(bbox);
+ if (-dy < bbox.size.height) {
+ bbox.size.height += dy;
+ } else {
+ bbox.size.height = 0;
+ }
+ }
+
+ if (grabbedResizeHandle == NorthWestHandle ||
+ grabbedResizeHandle == WestHandle ||
+ grabbedResizeHandle == SouthWestHandle) {
+
+ float dx = p2.x - NSMinX(bbox);
+ if (dx < bbox.size.width) {
+ bbox.origin.x += dx;
+ bbox.size.width -= dx;
+ } else {
+ bbox.origin.x = NSMaxX(bbox);
+ bbox.size.width = 0;
+ }
+
+ } else if (grabbedResizeHandle == NorthEastHandle ||
+ grabbedResizeHandle == EastHandle ||
+ grabbedResizeHandle == SouthEastHandle) {
+
+ float dx = p2.x - NSMaxX(bbox);
+ if (-dx < bbox.size.width) {
+ bbox.size.width += dx;
+ } else {
+ bbox.size.width = 0;
+ }
+ }
+ [graph setBoundingBox:[transformer rectFromScreen:bbox]];
+ }
+ [[self doc] changeBoundingBoxCheckPoint];
+ } else if (state == CanvasDragState) {
+ NSPoint newOrigin = oldOrigin;
+ newOrigin.x += pos.x - dragOrigin.x;
+ newOrigin.y += pos.y - dragOrigin.y;
+ [[renderer transformer] setOrigin:newOrigin];
+ [renderer invalidateGraph];
+ }
+}
+
+- (void) mouseScrolledAt:(NSPoint)pos inDirection:(ScrollDirection)dir withMask:(InputMask)mask {
+ if (mask == ControlMask) {
+ if (dir == ScrollUp) {
+ [[renderer surface] zoomInAboutPoint:pos];
+ } else if (dir == ScrollDown) {
+ [[renderer surface] zoomOutAboutPoint:pos];
+ }
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/GraphRenderer.h b/tikzit/src/gtk/GraphRenderer.h
new file mode 100644
index 0000000..4609766
--- /dev/null
+++ b/tikzit/src/gtk/GraphRenderer.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+// classes
+#import "Graph.h"
+#import "Grid.h"
+#import "PickSupport.h"
+#import "TikzDocument.h"
+
+// protocols
+#import "Surface.h"
+
+typedef enum {
+ NoHandle,
+ EastHandle,
+ SouthEastHandle,
+ SouthHandle,
+ SouthWestHandle,
+ WestHandle,
+ NorthWestHandle,
+ NorthHandle,
+ NorthEastHandle
+} ResizeHandle;
+
+@interface GraphRenderer: NSObject <RenderDelegate> {
+ TikzDocument *doc;
+ NSObject<Surface> *surface;
+ Grid *grid;
+ NSRect selectionBox;
+ Node *halfEdgeOrigin;
+ NSPoint halfEdgeOriginPoint;
+ NSPoint halfEdgeEnd;
+ BOOL showBoundingBoxHandles;
+}
+
+- (id) initWithSurface:(NSObject <Surface> *)surface;
+- (id) initWithSurface:(NSObject <Surface> *)surface document:(TikzDocument*)document;
+- (void) renderWithContext:(id<RenderContext>)context;
+- (void) invalidateGraph;
+- (void) invalidateNode:(Node*)node;
+- (void) invalidateEdge:(Edge*)edge;
+- (void) invalidateNodesHitBy:(NSPoint)point;
+- (BOOL) point:(NSPoint)p hitsNode:(Node*)node;
+- (BOOL) point:(NSPoint)p hitsEdge:(Edge*)edge withFuzz:(float)fuzz;
+/**
+ * Finds a node at the given screen location.
+ *
+ * If there is more than one node at this point (because they overlap),
+ * an arbitrary one is returned.
+ */
+- (Node*) anyNodeAt:(NSPoint)p;
+/**
+ * Finds an edge at the given screen location.
+ *
+ * If there is more than one edge at this point (because they overlap),
+ * an arbitrary one is returned.
+ *
+ * @param fuzz the fuzz for detecting edges: this will pick up
+ * edges that are close to the point
+ */
+- (Edge*) anyEdgeAt:(NSPoint)p withFuzz:(float)fuzz;
+
+- (id<Surface>) surface;
+- (Transformer*) transformer;
+- (Grid*) grid;
+- (PickSupport*) pickSupport;
+
+- (Graph*) graph;
+
+- (TikzDocument*) document;
+- (void) setDocument:(TikzDocument*)document;
+
+- (NSRect) selectionBox;
+- (void) setSelectionBox:(NSRect)box;
+- (void) clearSelectionBox;
+
+- (void) setHalfEdgeFrom:(Node*)origin to:(NSPoint)end;
+- (void) clearHalfEdge;
+
+- (BOOL) boundingBoxHandlesShown;
+- (void) setBoundingBoxHandlesShown:(BOOL)shown;
+
+- (ResizeHandle) boundingBoxResizeHandleAt:(NSPoint)point;
+- (NSRect) boundingBoxResizeHandleRect:(ResizeHandle)handle;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/GraphRenderer.m b/tikzit/src/gtk/GraphRenderer.m
new file mode 100644
index 0000000..571390f
--- /dev/null
+++ b/tikzit/src/gtk/GraphRenderer.m
@@ -0,0 +1,607 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "GraphRenderer.h"
+#import "Edge+Render.h"
+#import "Node+Render.h"
+
+static const float size = 5.0;
+
+float sideHandleTop(NSRect bbox) {
+ return (NSMinY(bbox) + NSMaxY(bbox) - size)/2.0f;
+}
+
+float tbHandleLeft(NSRect bbox) {
+ return (NSMinX(bbox) + NSMaxX(bbox) - size)/2.0f;
+}
+void graph_renderer_expose_event(GtkWidget *widget, GdkEventExpose *event);
+
+@interface GraphRenderer (Private)
+- (BOOL) selectionBoxContainsNode:(Node*)node;
+- (BOOL) halfEdgeIncludesNode:(Node*)node;
+- (enum NodeState) nodeState:(Node*)node;
+- (void) renderBoundingBoxWithContext:(id<RenderContext>)context;
+- (void) renderSelectionBoxWithContext:(id<RenderContext>)context;
+- (void) renderImpendingEdgeWithContext:(id<RenderContext>)context;
+- (void) nodeNeedsRefreshing:(NSNotification*)notification;
+- (void) edgeNeedsRefreshing:(NSNotification*)notification;
+- (void) graphNeedsRefreshing:(NSNotification*)notification;
+- (void) graphChanged:(NSNotification*)notification;
+- (void) nodeStylePropertyChanged:(NSNotification*)notification;
+- (void) edgeStylePropertyChanged:(NSNotification*)notification;
+@end
+
+@implementation GraphRenderer
+
+- (id) initWithSurface:(NSObject <Surface> *)s {
+ self = [super init];
+
+ if (self) {
+ surface = [s retain];
+ doc = nil;
+ grid = [[Grid alloc] initWithSpacing:1.0f subdivisions:4 transformer:[s transformer]];
+ halfEdgeOrigin = nil;
+ [surface setRenderDelegate:self];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(nodeStylePropertyChanged:)
+ name:@"NodeStylePropertyChanged"
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(edgeStylePropertyChanged:)
+ name:@"EdgeStylePropertyChanged"
+ object:nil];
+ }
+
+ return self;
+}
+
+- (id) initWithSurface:(NSObject <Surface> *)s document:(TikzDocument*)document {
+ self = [self initWithSurface:s];
+
+ if (self) {
+ [self setDocument:document];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [doc release];
+ [grid release];
+ [surface release];
+
+ [super dealloc];
+}
+
+- (void) renderWithContext:(id<RenderContext>)context onSurface:(id<Surface>)surface {
+ [self renderWithContext:context];
+}
+
+- (void) renderWithContext:(id<RenderContext>)context {
+ // blank surface
+ [context paintWithColor:WhiteRColor];
+
+ // draw grid
+ [grid renderGridInContext:context];
+
+ // draw edges
+ NSEnumerator *enumerator = [doc edgeEnumerator];
+ Edge *edge;
+ while ((edge = [enumerator nextObject]) != nil) {
+ [edge renderToSurface:surface withContext:context selected:[doc isEdgeSelected:edge]];
+ }
+
+ // draw nodes
+ enumerator = [doc nodeEnumerator];
+ Node *node;
+ while ((node = [enumerator nextObject]) != nil) {
+ [node renderToSurface:surface withContext:context state:[self nodeState:node]];
+ }
+
+ [self renderBoundingBoxWithContext:context];
+ [self renderSelectionBoxWithContext:context];
+ [self renderImpendingEdgeWithContext:context];
+}
+
+- (void) invalidateGraph {
+ [surface invalidate];
+}
+
+- (void) invalidateNodes:(NSSet*)nodes {
+ for (Node *node in nodes) {
+ [self invalidateNode:node];
+ }
+}
+
+- (void) invalidateEdges:(NSSet*)edges {
+ for (Edge *edge in edges) {
+ [self invalidateEdge:edge];
+ }
+}
+
+- (void) invalidateNode:(Node*)node {
+ if (node == nil) {
+ return;
+ }
+ NSRect nodeRect = [node renderBoundsWithLabelForSurface:surface];
+ nodeRect = NSInsetRect (nodeRect, -2.0f, -2.0f);
+ [surface invalidateRect:nodeRect];
+}
+
+- (void) invalidateEdge:(Edge*)edge {
+ if (edge == nil) {
+ return;
+ }
+ BOOL selected = [doc isEdgeSelected:edge];
+ NSRect edgeRect = [edge renderedBoundsWithTransformer:[surface transformer] whenSelected:selected];
+ edgeRect = NSInsetRect (edgeRect, -2.0f, -2.0f);
+ [surface invalidateRect:edgeRect];
+}
+
+- (void) invalidateNodesHitBy:(NSPoint)point {
+ NSEnumerator *enumerator = [doc nodeEnumerator];
+ Node *node = nil;
+ while ((node = [enumerator nextObject]) != nil) {
+ if ([self point:point hitsNode:node]) {
+ [self invalidateNode:node];
+ }
+ }
+}
+
+- (BOOL) point:(NSPoint)p hitsNode:(Node*)node {
+ return [node hitByPoint:p onSurface:surface];
+}
+
+- (BOOL) point:(NSPoint)p fuzzyHitsNode:(Node*)node {
+ NSRect bounds = [node renderBoundsForSurface:surface];
+ return NSPointInRect(p, bounds);
+}
+
+- (BOOL) point:(NSPoint)p hitsEdge:(Edge*)edge withFuzz:(float)fuzz {
+ return [edge hitByPoint:p onSurface:surface withFuzz:fuzz];
+}
+
+- (Node*) anyNodeAt:(NSPoint)p {
+ NSEnumerator *enumerator = [doc nodeEnumerator];
+ Node *node;
+ while ((node = [enumerator nextObject]) != nil) {
+ if ([self point:p hitsNode:node]) {
+ return node;
+ }
+ }
+ return nil;
+}
+
+- (Edge*) anyEdgeAt:(NSPoint)p withFuzz:(float)fuzz {
+ // FIXME: is there an efficient way to find the "nearest" edge
+ // if the fuzz is the reason we hit more than one?
+ NSEnumerator *enumerator = [doc edgeEnumerator];
+ Edge *edge;
+ while ((edge = [enumerator nextObject]) != nil) {
+ if ([self point:p hitsEdge:edge withFuzz:fuzz]) {
+ return edge;
+ }
+ }
+ return nil;
+}
+
+- (id<Surface>) surface {
+ return surface;
+}
+
+- (Transformer*) transformer {
+ return [surface transformer];
+}
+
+- (Grid*) grid {
+ return grid;
+}
+
+- (PickSupport*) pickSupport {
+ return [doc pickSupport];
+}
+
+- (Graph*) graph {
+ return [doc graph];
+}
+
+- (TikzDocument*) document {
+ return doc;
+}
+
+- (void) setDocument:(TikzDocument*)document {
+ if (doc == document) {
+ return;
+ }
+
+ if (doc != nil) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:doc];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:[doc pickSupport]];
+ }
+
+ [document retain];
+ [doc release];
+ doc = document;
+
+ if (doc != nil) {
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphNeedsRefreshing:)
+ name:@"GraphReplaced" object:doc];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphChanged:)
+ name:@"GraphChanged" object:doc];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphChanged:)
+ name:@"GraphBeingChanged" object:doc];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphChanged:)
+ name:@"GraphChangeCancelled" object:doc];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(nodeNeedsRefreshing:)
+ name:@"NodeSelected" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(nodeNeedsRefreshing:)
+ name:@"NodeDeselected" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphNeedsRefreshing:)
+ name:@"NodeSelectionReplaced" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(edgeNeedsRefreshing:)
+ name:@"EdgeSelected" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(edgeNeedsRefreshing:)
+ name:@"EdgeDeselected" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(graphNeedsRefreshing:)
+ name:@"EdgeSelectionReplaced" object:[doc pickSupport]];
+ }
+ [surface invalidate];
+}
+
+- (NSRect) selectionBox {
+ return selectionBox;
+}
+
+- (void) setSelectionBox:(NSRect)box {
+ NSRect invRect = NSUnionRect (selectionBox, box);
+ selectionBox = box;
+ [surface invalidateRect:NSInsetRect (invRect, -2, -2)];
+}
+
+- (void) clearSelectionBox {
+ NSRect oldRect = selectionBox;
+
+ NSRect emptyRect;
+ selectionBox = emptyRect;
+
+ [surface invalidateRect:NSInsetRect (oldRect, -2, -2)];
+}
+
+- (void) invalidateHalfEdge {
+ if (halfEdgeOrigin != nil) {
+ NSRect invRect = NSRectAroundPoints(halfEdgeEnd, halfEdgeOriginPoint);
+ invRect = NSUnionRect(invRect, [halfEdgeOrigin renderBoundsWithLabelForSurface:surface]);
+
+ NSEnumerator *enumerator = [doc nodeEnumerator];
+ Node *node;
+ while ((node = [enumerator nextObject]) != nil) {
+ if ([self point:halfEdgeEnd fuzzyHitsNode:node]) {
+ invRect = NSUnionRect(invRect, [node renderBoundsWithLabelForSurface:surface]);
+ }
+ }
+ [surface invalidateRect:NSInsetRect (invRect, -2.0f, -2.0f)];
+ }
+}
+
+- (void) setHalfEdgeFrom:(Node*)origin to:(NSPoint)end {
+ [self invalidateHalfEdge];
+
+ if (halfEdgeOrigin != origin) {
+ [self invalidateNode:halfEdgeOrigin];
+ halfEdgeOrigin = origin;
+ halfEdgeOriginPoint = [[surface transformer] toScreen:[origin point]];
+ [self invalidateNode:origin];
+ }
+
+ if (origin != nil) {
+ halfEdgeEnd = end;
+ [self invalidateHalfEdge];
+ }
+}
+
+- (void) clearHalfEdge {
+ [self invalidateHalfEdge];
+ halfEdgeOrigin = nil;
+}
+
+- (BOOL) boundingBoxHandlesShown {
+ return showBoundingBoxHandles;
+}
+
+- (void) setBoundingBoxHandlesShown:(BOOL)shown {
+ if (showBoundingBoxHandles != shown) {
+ showBoundingBoxHandles = shown;
+ [self invalidateGraph];
+ }
+}
+
+- (ResizeHandle) boundingBoxResizeHandleAt:(NSPoint)p {
+ NSRect bbox = [[surface transformer] rectToScreen:[[self graph] boundingBox]];
+ if (p.x >= NSMaxX(bbox)) {
+ if (p.x <= NSMaxX(bbox) + size) {
+ if (p.y >= NSMaxY(bbox)) {
+ if (p.y <= NSMaxY(bbox) + size) {
+ return SouthEastHandle;
+ }
+ } else if (p.y <= NSMinY(bbox)) {
+ if (p.y >= NSMinY(bbox) - size) {
+ return NorthEastHandle;
+ }
+ } else {
+ float eastHandleTop = sideHandleTop(bbox);
+ if (p.y >= eastHandleTop && p.y <= (eastHandleTop + size)) {
+ return EastHandle;
+ }
+ }
+ }
+ } else if (p.x <= NSMinX(bbox)) {
+ if (p.x >= NSMinX(bbox) - size) {
+ if (p.y >= NSMaxY(bbox)) {
+ if (p.y <= NSMaxY(bbox) + size) {
+ return SouthWestHandle;
+ }
+ } else if (p.y <= NSMinY(bbox)) {
+ if (p.y >= NSMinY(bbox) - size) {
+ return NorthWestHandle;
+ }
+ } else {
+ float westHandleTop = sideHandleTop(bbox);
+ if (p.y >= westHandleTop && p.y <= (westHandleTop + size)) {
+ return WestHandle;
+ }
+ }
+ }
+ } else if (p.y >= NSMaxY(bbox)) {
+ if (p.y <= NSMaxY(bbox) + size) {
+ return SouthHandle;
+ }
+ } else if (p.y <= NSMinY(bbox)) {
+ if (p.y >= NSMinY(bbox) - size) {
+ return NorthHandle;
+ }
+ }
+ return NoHandle;
+}
+
+- (NSRect) boundingBoxResizeHandleRect:(ResizeHandle)handle {
+ if (![[self graph] hasBoundingBox]) {
+ return NSZeroRect;
+ }
+ NSRect bbox = [[surface transformer] rectToScreen:[[self graph] boundingBox]];
+ switch (handle) {
+ case EastHandle:
+ return NSMakeRect(NSMaxX(bbox), sideHandleTop(bbox), size, size);
+ case SouthEastHandle:
+ return NSMakeRect(NSMaxX(bbox), NSMaxY(bbox), size, size);
+ case SouthHandle:
+ return NSMakeRect(tbHandleLeft(bbox), NSMaxY(bbox), size, size);
+ case SouthWestHandle:
+ return NSMakeRect(NSMaxX(bbox), NSMinY(bbox) - size, size, size);
+ case WestHandle:
+ return NSMakeRect(NSMinX(bbox) - size, sideHandleTop(bbox), size, size);
+ case NorthWestHandle:
+ return NSMakeRect(NSMinX(bbox) - size, NSMinY(bbox) - size, size, size);
+ case NorthHandle:
+ return NSMakeRect(tbHandleLeft(bbox), NSMinY(bbox) - size, size, size);
+ case NorthEastHandle:
+ return NSMakeRect(NSMinX(bbox) - size, NSMaxY(bbox), size, size);
+ default:
+ return NSZeroRect;
+ }
+}
+
+@end
+
+@implementation GraphRenderer (Private)
+- (BOOL) selectionBoxContainsNode:(Node*)node {
+ return !NSIsEmptyRect (selectionBox)
+ && NSPointInRect([[surface transformer] toScreen:[node point]], selectionBox);
+}
+- (BOOL) halfEdgeIncludesNode:(Node*)node {
+ if (halfEdgeOrigin == nil) {
+ return FALSE;
+ }
+ return halfEdgeOrigin == node || [self point:halfEdgeEnd hitsNode:node];
+}
+- (enum NodeState) nodeState:(Node*)node {
+ if ([doc isNodeSelected:node]) {
+ return NodeSelected;
+ } else if ([self selectionBoxContainsNode:node] || [self halfEdgeIncludesNode:node]) {
+ return NodeHighlighted;
+ } else {
+ return NodeNormal;
+ }
+}
+
+- (void) renderBoundingBoxWithContext:(id<RenderContext>)context {
+ if ([[self graph] hasBoundingBox]) {
+ [context saveState];
+
+ NSRect bbox = [[surface transformer] rectToScreen:[[self graph] boundingBox]];
+
+ [context setAntialiasMode:AntialiasDisabled];
+ [context setLineWidth:1.0];
+ [context startPath];
+ [context rect:bbox];
+ [context strokePathWithColor:MakeSolidRColor (1.0, 0.7, 0.5)];
+
+ if ([self boundingBoxHandlesShown]) {
+ [context startPath];
+ [context rect:[self boundingBoxResizeHandleRect:EastHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:SouthEastHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:SouthHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:SouthWestHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:WestHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:NorthWestHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:NorthHandle]];
+ [context rect:[self boundingBoxResizeHandleRect:NorthEastHandle]];
+ [context strokePathWithColor:MakeSolidRColor (0.5, 0.5, 0.5)];
+ }
+
+ [context restoreState];
+ }
+}
+
+- (void) renderSelectionBoxWithContext:(id<RenderContext>)context {
+ if (!NSIsEmptyRect (selectionBox)) {
+ [context saveState];
+
+ [context setAntialiasMode:AntialiasDisabled];
+ [context setLineWidth:1.0];
+ [context startPath];
+ [context rect:selectionBox];
+ RColor fColor = MakeRColor (0.8, 0.8, 0.8, 0.2);
+ RColor sColor = MakeSolidRColor (0.6, 0.6, 0.6);
+ [context strokePathWithColor:sColor andFillWithColor:fColor];
+
+ [context restoreState];
+ }
+}
+
+- (void) renderImpendingEdgeWithContext:(id<RenderContext>)context {
+ if (halfEdgeOrigin == nil) {
+ return;
+ }
+ [context saveState];
+
+ [context setLineWidth:1.0];
+ [context startPath];
+ [context moveTo:halfEdgeOriginPoint];
+ [context lineTo:halfEdgeEnd];
+ [context strokePathWithColor:MakeRColor (0, 0, 0, 0.5)];
+
+ [context restoreState];
+}
+
+- (void) nodeNeedsRefreshing:(NSNotification*)notification {
+ [self invalidateNode:[[notification userInfo] objectForKey:@"node"]];
+}
+
+- (void) edgeNeedsRefreshing:(NSNotification*)notification {
+ Edge *edge = [[notification userInfo] objectForKey:@"edge"];
+ NSRect edgeRect = [edge renderedBoundsWithTransformer:[surface transformer] whenSelected:YES];
+ edgeRect = NSInsetRect (edgeRect, -2, -2);
+ [surface invalidateRect:edgeRect];
+}
+
+- (void) graphNeedsRefreshing:(NSNotification*)notification {
+ [self invalidateGraph];
+}
+
+- (void) graphChanged:(NSNotification*)notification {
+ GraphChange *change = [[notification userInfo] objectForKey:@"change"];
+ switch ([change changeType]) {
+ case GraphAddition:
+ case GraphDeletion:
+ [self invalidateNodes:[change affectedNodes]];
+ [self invalidateEdges:[change affectedEdges]];
+ break;
+ case NodePropertyChange:
+ if (!NSEqualPoints ([[change oldNode] point], [[change nwNode] point])) {
+ // if the node has moved, it may be affecting edges
+ [surface invalidate];
+ } else {
+ // invalide both old and new (old node may be larger)
+ [self invalidateNode:[change oldNode]];
+ [self invalidateNode:[change nwNode]];
+ }
+ break;
+ case EdgePropertyChange:
+ // invalide both old and new (old bend may increase bounds)
+ [self invalidateEdge:[change oldEdge]];
+ [self invalidateEdge:[change nwEdge]];
+ [self invalidateEdge:[change edgeRef]];
+ break;
+ case NodesPropertyChange:
+ {
+ NSEnumerator *enumerator = [[change oldNodeTable] keyEnumerator];
+ Node *node = nil;
+ while ((node = [enumerator nextObject]) != nil) {
+ NSPoint oldPos = [[[change oldNodeTable] objectForKey:node] point];
+ NSPoint newPos = [[[change nwNodeTable] objectForKey:node] point];
+ if (NSEqualPoints (oldPos, newPos)) {
+ [self invalidateNode:[[change oldNodeTable] objectForKey:node]];
+ [self invalidateNode:[[change nwNodeTable] objectForKey:node]];
+ } else {
+ [surface invalidate];
+ break;
+ }
+ }
+ }
+ break;
+ case NodesShift:
+ case NodesFlip:
+ case BoundingBoxChange:
+ [surface invalidate];
+ break;
+ default:
+ // unknown change
+ [surface invalidate];
+ break;
+ };
+}
+
+- (void) nodeStylePropertyChanged:(NSNotification*)notification {
+ if (![@"name" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ BOOL affected = NO;
+ for (Node *node in [[self graph] nodes]) {
+ if ([node style] == [notification object])
+ affected = YES;
+ }
+ if (affected)
+ [surface invalidate];
+ }
+}
+
+- (void) edgeStylePropertyChanged:(NSNotification*)notification {
+ if (![@"name" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ BOOL affected = NO;
+ for (Edge *edge in [[self graph] edges]) {
+ if ([edge style] == [notification object])
+ affected = YES;
+ }
+ if (affected)
+ [surface invalidate];
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/InputDelegate.h b/tikzit/src/gtk/InputDelegate.h
new file mode 100644
index 0000000..1e35223
--- /dev/null
+++ b/tikzit/src/gtk/InputDelegate.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+
+typedef enum {
+ LeftButton = 1,
+ MiddleButton = 2,
+ RightButton = 3,
+ Button4 = 4,
+ Button5 = 5
+} MouseButton;
+
+typedef enum {
+ ShiftMask = 1,
+ ControlMask = 2,
+ MetaMask = 4
+} InputMask;
+
+typedef enum {
+ ScrollUp = 1,
+ ScrollDown = 2,
+ ScrollLeft = 3,
+ ScrollRight = 4,
+} ScrollDirection;
+
+@interface NSObject (InputDelegate)
+/**
+ * A mouse button was pressed.
+ */
+- (void) mousePressAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask;
+/**
+ * A mouse button was released.
+ */
+- (void) mouseReleaseAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask;
+/**
+ * A mouse button was double-clicked.
+ *
+ * Note that mouseDown and mouseUp events will still be delivered.
+ * This will be triggered between the second mouseDown and the second
+ * mouseUp.
+ */
+- (void) mouseDoubleClickAt:(NSPoint)pos withButton:(MouseButton)button andMask:(InputMask)mask;
+/**
+ * The mouse was moved
+ */
+- (void) mouseMoveTo:(NSPoint)pos withButtons:(MouseButton)button andMask:(InputMask)mask;
+/**
+ * The mouse was scrolled
+ */
+- (void) mouseScrolledAt:(NSPoint)pos inDirection:(ScrollDirection)dir withMask:(InputMask)mask;
+/**
+ * A key was pressed
+ */
+- (void) keyPressed:(unsigned int)keyVal withMask:(InputMask)mask;
+/**
+ * A key was released
+ */
+- (void) keyReleased:(unsigned int)keyVal withMask:(InputMask)mask;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/MainWindow.h b/tikzit/src/gtk/MainWindow.h
new file mode 100644
index 0000000..27ce46d
--- /dev/null
+++ b/tikzit/src/gtk/MainWindow.h
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "WidgetSurface.h"
+
+@class Configuration;
+@class GraphRenderer;
+@class GraphInputHandler;
+@class Menu;
+@class PropertyPane;
+@class Preambles;
+@class PreambleEditor;
+@class PreviewWindow;
+@class SettingsDialog;
+@class StyleManager;
+@class StylesPane;
+@class TikzDocument;
+
+/**
+ * Manages the main application window
+ */
+@interface MainWindow: NSObject {
+ // the main application configuration
+ Configuration *configFile;
+ // maintains the known (user-defined) styles
+ StyleManager *styleManager;
+ // maintains the preambles used for previews
+ Preambles *preambles;
+
+ // GTK+ widgets
+ GtkWindow *mainWindow;
+ GtkTextBuffer *tikzBuffer;
+ GtkStatusbar *statusBar;
+ GtkPaned *propertyPaneSplitter;
+ GtkPaned *stylesPaneSplitter;
+ GtkPaned *tikzPaneSplitter;
+ GtkWidget *tikzPane;
+
+ // Classes that manage parts of the window
+ // (or other windows)
+ Menu *menu;
+ GraphRenderer *renderer;
+ GraphInputHandler *inputHandler;
+ StylesPane *stylesPane;
+ PropertyPane *propertyPane;
+ PreambleEditor *preambleWindow;
+ PreviewWindow *previewWindow;
+ SettingsDialog *settingsDialog;
+
+ WidgetSurface *surface;
+
+ // state variables
+ BOOL suppressTikzUpdates;
+ BOOL hasParseError;
+ // the last-accessed folder (for open and save dialogs)
+ NSString *lastFolder;
+ // the open (active) document
+ TikzDocument *document;
+}
+
+/**
+ * Create and show the main window.
+ */
+- (id) init;
+
+/**
+ * Open a file, asking the user which file to open
+ */
+- (void) openFile;
+/**
+ * Save the active document to the path it was opened from
+ * or last saved to, or ask the user where to save it.
+ */
+- (void) saveActiveDocument;
+/**
+ * Save the active document, asking the user where to save it.
+ */
+- (void) saveActiveDocumentAs;
+/**
+ * Save the active document as a shape, asking the user what to name it.
+ */
+- (void) saveActiveDocumentAsShape;
+/**
+ * Quit the application, confirming with the user if there are
+ * changes to an open document.
+ */
+- (void) quit;
+/**
+ * If there are changes to an open document, ask the user if they
+ * want to quit the application, discarding those changes.
+ *
+ * @result YES if there are no unsaved changes or the user is happy
+ * to discard any unsaved changes, NO if the application
+ * should not quit.
+ */
+- (BOOL) askCanQuit;
+
+/**
+ * Cut the current selection to the clipboard.
+ */
+- (void) cut;
+/**
+ * Copy the current selection to the clipboard.
+ */
+- (void) copy;
+/**
+ * Paste from the clipboard to the appropriate place.
+ */
+- (void) paste;
+
+/**
+ * Show the dialog for editing preambles.
+ */
+- (void) editPreambles;
+/**
+ * Show or update the preview window.
+ */
+- (void) showPreview;
+/**
+ * Show the settings dialog.
+ */
+- (void) showSettingsDialog;
+
+/**
+ * The graph input handler
+ */
+- (GraphInputHandler*) graphInputHandler;
+/**
+ * The GTK+ window that this class manages.
+ */
+- (GtkWindow*) gtkWindow;
+/**
+ * The main application configuration file
+ */
+- (Configuration*) mainConfiguration;
+/**
+ * The menu for the window.
+ */
+- (Menu*) menu;
+
+/**
+ * The document the user is currently editing
+ */
+- (TikzDocument*) activeDocument;
+
+/**
+ * Loads a new, empty document as the active document
+ */
+- (void) loadEmptyDocument;
+/**
+ * Loads an existing document from a file as the active document
+ *
+ * @param path the path to the tikz file containing the document
+ */
+- (void) loadDocumentFromFile:(NSString*)path;
+
+/**
+ * Present an error to the user
+ *
+ * (currently just outputs it on the command line)
+ *
+ * @param error the error to present
+ */
+- (void) presentError:(NSError*)error;
+/**
+ * Present an error to the user
+ *
+ * (currently just outputs it on the command line)
+ *
+ * @param error the error to present
+ * @param message a message to display with the error
+ */
+- (void) presentError:(NSError*)error withMessage:(NSString*)message;
+/**
+ * Present an error to the user
+ *
+ * (currently just outputs it on the command line)
+ *
+ * @param error the error to present
+ */
+- (void) presentGError:(GError*)error;
+/**
+ * Present an error to the user
+ *
+ * (currently just outputs it on the command line)
+ *
+ * @param error the error to present
+ * @param message a message to display with the error
+ */
+- (void) presentGError:(GError*)error withMessage:(NSString*)message;
+
+/**
+ * Save the application configuration to disk
+ *
+ * Should be called just before the application exits
+ */
+- (void) saveConfiguration;
+
+- (void) zoomIn;
+- (void) zoomOut;
+- (void) zoomReset;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/MainWindow.m b/tikzit/src/gtk/MainWindow.m
new file mode 100644
index 0000000..c391ea5
--- /dev/null
+++ b/tikzit/src/gtk/MainWindow.m
@@ -0,0 +1,1035 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "MainWindow.h"
+
+#import <gtk/gtk.h>
+#import "gtkhelpers.h"
+#import "clipboard.h"
+
+#import "Configuration.h"
+#import "FileChooserDialog.h"
+#import "GraphInputHandler.h"
+#import "GraphRenderer.h"
+#import "Menu.h"
+#import "PreambleEditor.h"
+#ifdef HAVE_POPPLER
+#import "Preambles.h"
+#import "Preambles+Storage.h"
+#import "PreviewWindow.h"
+#endif
+#import "PropertyPane.h"
+#import "RecentManager.h"
+#ifdef HAVE_POPPLER
+#import "SettingsDialog.h"
+#endif
+#import "Shape.h"
+#import "StyleManager.h"
+#import "StyleManager+Storage.h"
+#import "StylesPane.h"
+#import "SupportDir.h"
+#import "TikzDocument.h"
+#import "WidgetSurface.h"
+
+
+// {{{ Internal interfaces
+// {{{ Clipboard support
+
+static void clipboard_provide_data (GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ guint info,
+ gpointer clipboard_graph_data);
+static void clipboard_release_data (GtkClipboard *clipboard, gpointer clipboard_graph_data);
+static void clipboard_check_targets (GtkClipboard *clipboard,
+ GdkAtom *atoms,
+ gint n_atoms,
+ gpointer action);
+static void clipboard_paste_contents (GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ gpointer document);
+
+// }}}
+// {{{ Signals
+
+static void toolbox_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window);
+static void stylebox_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window);
+static void graph_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window);
+static void tikz_buffer_changed_cb (GtkTextBuffer *buffer, MainWindow *window);
+static gboolean main_window_delete_event_cb (GtkWidget *widget, GdkEvent *event, MainWindow *window);
+static void main_window_destroy_cb (GtkWidget *widget, MainWindow *window);
+static gboolean main_window_configure_event_cb (GtkWidget *widget, GdkEventConfigure *event, MainWindow *window);
+static void update_paste_action (GtkClipboard *clipboard, GdkEvent *event, GtkAction *action);
+
+// }}}
+
+@interface MainWindow (Notifications)
+- (void) toolboxWidthChanged:(int)newWidth;
+- (void) styleboxWidthChanged:(int)newWidth;
+- (void) tikzBufferChanged;
+- (void) windowSizeChangedWidth:(int)width height:(int)height;
+- (void) documentTikzChanged:(NSNotification*)notification;
+- (void) documentSelectionChanged:(NSNotification*)notification;
+- (void) undoStackChanged:(NSNotification*)notification;
+@end
+
+@interface MainWindow (InitHelpers)
+- (void) _loadConfig;
+- (void) _loadStyles;
+- (void) _loadPreambles;
+- (void) _loadUi;
+- (void) _restoreUiState;
+- (void) _connectSignals;
+@end
+
+@interface MainWindow (Private)
+- (BOOL) _confirmCloseDocumentTo:(NSString*)action;
+- (void) _forceLoadDocumentFromFile:(NSString*)path;
+- (void) _placeGraphOnClipboard:(Graph*)graph;
+- (void) _setHasParseError:(BOOL)hasError;
+/** Update the window title. */
+- (void) _updateTitle;
+/** Update the window status bar default text. */
+- (void) _updateStatus;
+/** Update the displayed tikz code to match the active document. */
+- (void) _updateTikz;
+/** Update the undo and redo actions to match the active document's
+ * undo stack. */
+- (void) _updateUndoActions;
+/** Set the last-accessed folder */
+- (void) _setLastFolder:(NSString*)path;
+/** Set the active document */
+- (void) _setActiveDocument:(TikzDocument*)newDoc;
+@end
+
+// }}}
+// {{{ API
+
+@implementation MainWindow
+
+- (id) init {
+ self = [super init];
+
+ if (self) {
+ document = nil;
+ preambles = nil;
+ preambleWindow = nil;
+ previewWindow = nil;
+ settingsDialog = nil;
+ suppressTikzUpdates = NO;
+ hasParseError = NO;
+
+ [self _loadConfig];
+ [self _loadStyles];
+ [self _loadPreambles];
+ lastFolder = [[configFile stringEntry:@"lastFolder" inGroup:@"Paths"] retain];
+ [self _loadUi];
+ [self _restoreUiState];
+ [self _connectSignals];
+
+ [self loadEmptyDocument];
+
+ gtk_widget_show (GTK_WIDGET (mainWindow));
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [configFile release];
+ [styleManager release];
+ [preambles release];
+ [menu release];
+ [renderer release];
+ [inputHandler release];
+ [stylesPane release];
+ [propertyPane release];
+ [preambleWindow release];
+ [previewWindow release];
+ [settingsDialog release];
+ [surface release];
+ [lastFolder release];
+ [document release];
+
+ g_object_unref (mainWindow);
+ g_object_unref (tikzBuffer);
+ g_object_unref (statusBar);
+ g_object_unref (propertyPaneSplitter);
+ g_object_unref (stylesPaneSplitter);
+ g_object_unref (tikzPaneSplitter);
+ g_object_unref (tikzPane);
+
+ [super dealloc];
+}
+
+- (void) openFile {
+ if (![self _confirmCloseDocumentTo:@"open a new document"]) {
+ return;
+ }
+ FileChooserDialog *dialog = [FileChooserDialog openDialogWithParent:mainWindow];
+ [dialog addStandardFilters];
+ if (lastFolder) {
+ [dialog setCurrentFolder:lastFolder];
+ }
+
+ if ([dialog showDialog]) {
+ [self _forceLoadDocumentFromFile:[dialog filePath]];
+ [self _setLastFolder:[dialog currentFolder]];
+ }
+ [dialog destroy];
+}
+
+- (void) saveActiveDocument {
+ if ([document path] == nil) {
+ [self saveActiveDocumentAs];
+ } else {
+ NSError *error = nil;
+ if (![document save:&error]) {
+ [self presentError:error];
+ } else {
+ [self _updateTitle];
+ }
+ }
+}
+
+- (void) saveActiveDocumentAs {
+ FileChooserDialog *dialog = [FileChooserDialog saveDialogWithParent:mainWindow];
+ [dialog addStandardFilters];
+ if ([document path] != nil) {
+ [dialog setCurrentFolder:[[document path] stringByDeletingLastPathComponent]];
+ } else if (lastFolder != nil) {
+ [dialog setCurrentFolder:lastFolder];
+ }
+ [dialog setSuggestedName:[document suggestedFileName]];
+
+ if ([dialog showDialog]) {
+ NSString *nfile = [dialog filePath];
+
+ NSError *error = nil;
+ if (![document saveToPath:nfile error:&error]) {
+ [self presentError:error];
+ } else {
+ [self _updateTitle];
+ [[RecentManager defaultManager] addRecentFile:nfile];
+ [self _setLastFolder:[dialog currentFolder]];
+ }
+ }
+ [dialog destroy];
+}
+
+- (void) saveActiveDocumentAsShape {
+ GtkWidget *dialog = gtk_dialog_new_with_buttons (
+ "Save as shape",
+ mainWindow,
+ GTK_DIALOG_MODAL,
+ GTK_STOCK_OK,
+ GTK_RESPONSE_ACCEPT,
+ GTK_STOCK_CANCEL,
+ GTK_RESPONSE_REJECT,
+ NULL);
+ GtkBox *content = GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog)));
+ GtkWidget *label1 = gtk_label_new ("Please choose a name for the shape");
+ GtkWidget *label2 = gtk_label_new ("Name:");
+ GtkWidget *input = gtk_entry_new ();
+ GtkBox *hbox = GTK_BOX (gtk_hbox_new (FALSE, 5));
+ gtk_box_pack_start (hbox, label2, FALSE, TRUE, 0);
+ gtk_box_pack_start (hbox, input, TRUE, TRUE, 0);
+ gtk_box_pack_start (content, label1, TRUE, TRUE, 5);
+ gtk_box_pack_start (content, GTK_WIDGET (hbox), TRUE, TRUE, 5);
+ gtk_widget_show_all (GTK_WIDGET (content));
+ gint response = gtk_dialog_run (GTK_DIALOG (dialog));
+ while (response == GTK_RESPONSE_ACCEPT) {
+ response = GTK_RESPONSE_NONE;
+ NSDictionary *shapeDict = [Shape shapeDictionary];
+ const gchar *dialogInput = gtk_entry_get_text (GTK_ENTRY (input));
+ NSString *shapeName = [NSString stringWithUTF8String:dialogInput];
+ BOOL doSave = NO;
+ if ([shapeName isEqual:@""]) {
+ GtkWidget *emptyStrDialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "You must specify a shape name");
+ gtk_dialog_run (GTK_DIALOG (emptyStrDialog));
+ gtk_widget_destroy (emptyStrDialog);
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+ } else if ([shapeDict objectForKey:shapeName] != nil) {
+ GtkWidget *overwriteDialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ "Do you want to replace the existing shape named '%s'?",
+ dialogInput);
+ gint overwriteResp = gtk_dialog_run (GTK_DIALOG (overwriteDialog));
+ gtk_widget_destroy (overwriteDialog);
+
+ if (overwriteResp == GTK_RESPONSE_YES) {
+ doSave = YES;
+ } else {
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+ }
+ } else {
+ doSave = YES;
+ }
+ if (doSave) {
+ NSError *error = nil;
+ NSString *userShapeDir = [[SupportDir userSupportDir] stringByAppendingPathComponent:@"shapes"];
+ NSString *file = [NSString stringWithFormat:@"%@/%@.tikz", userShapeDir, shapeName];
+ if (![[NSFileManager defaultManager] ensureDirectoryExists:userShapeDir error:&error]) {
+ [self presentError:error withMessage:@"Could not create user shape directory"];
+ } else {
+ NSLog (@"Saving shape to %@", file);
+ if (![document saveCopyToPath:file error:&error]) {
+ [self presentError:error withMessage:@"Could not save shape file"];
+ } else {
+ [Shape refreshShapeDictionary];
+ }
+ }
+ }
+ }
+ gtk_widget_destroy (dialog);
+}
+
+- (void) quit {
+ if ([self askCanQuit]) {
+ gtk_main_quit();
+ }
+}
+
+- (BOOL) askCanQuit {
+ if ([document hasUnsavedChanges]) {
+ GtkWidget *dialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ "Are you sure you want to quit without saving the current file?");
+ gint result = gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+ return (result == GTK_RESPONSE_YES) ? YES : NO;
+ }
+
+ return YES;
+}
+
+- (void) cut {
+ if ([[[document pickSupport] selectedNodes] count] > 0) {
+ [self _placeGraphOnClipboard:[document cutSelection]];
+ }
+}
+
+- (void) copy {
+ if ([[[document pickSupport] selectedNodes] count] > 0) {
+ [self _placeGraphOnClipboard:[document copySelection]];
+ }
+}
+
+- (void) paste {
+ gtk_clipboard_request_contents (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD),
+ tikzit_picture_atom,
+ clipboard_paste_contents,
+ document);
+}
+
+- (void) editPreambles {
+#ifdef HAVE_POPPLER
+ if (preambleWindow == nil) {
+ preambleWindow = [[PreambleEditor alloc] initWithPreambles:preambles];
+ [preambleWindow setParentWindow:mainWindow];
+ }
+ [preambleWindow show];
+#endif
+}
+
+- (void) showPreview {
+#ifdef HAVE_POPPLER
+ if (previewWindow == nil) {
+ previewWindow = [[PreviewWindow alloc] initWithPreambles:preambles config:configFile];
+ [previewWindow setParentWindow:mainWindow];
+ [previewWindow setDocument:document];
+ }
+ [previewWindow show];
+#endif
+}
+
+- (void) showSettingsDialog {
+#ifdef HAVE_POPPLER
+ if (settingsDialog == nil) {
+ settingsDialog = [[SettingsDialog alloc] initWithConfiguration:configFile];
+ [settingsDialog setParentWindow:mainWindow];
+ }
+ [settingsDialog show];
+#endif
+}
+
+- (GraphInputHandler*) graphInputHandler {
+ return inputHandler;
+}
+
+- (GtkWindow*) gtkWindow {
+ return mainWindow;
+}
+
+- (Configuration*) mainConfiguration {
+ return configFile;
+}
+
+- (Menu*) menu {
+ return menu;
+}
+
+- (TikzDocument*) activeDocument {
+ return document;
+}
+
+- (void) loadEmptyDocument {
+ if (![self _confirmCloseDocumentTo:@"start a new document"]) {
+ return;
+ }
+ [self _setActiveDocument:[TikzDocument documentWithStyleManager:styleManager]];
+}
+
+- (void) loadDocumentFromFile:(NSString*)path {
+ if (![self _confirmCloseDocumentTo:@"open a new document"]) {
+ return;
+ }
+ [self _forceLoadDocumentFromFile:path];
+}
+
+- (void) presentError:(NSError*)error {
+ GtkWidget *dialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s",
+ [[error localizedDescription] UTF8String]);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
+- (void) presentError:(NSError*)error withMessage:(NSString*)message {
+ GtkWidget *dialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s: %s",
+ [message UTF8String],
+ [[error localizedDescription] UTF8String]);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
+- (void) presentGError:(GError*)error {
+ GtkWidget *dialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s",
+ error->message);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
+- (void) presentGError:(GError*)error withMessage:(NSString*)message {
+ GtkWidget *dialog = gtk_message_dialog_new (mainWindow,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s: %s",
+ [message UTF8String],
+ error->message);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
+- (void) saveConfiguration {
+ NSError *error = nil;
+
+#ifdef HAVE_POPPLER
+ if (preambles != nil) {
+ NSString *preamblesDir = [[SupportDir userSupportDir] stringByAppendingPathComponent:@"preambles"];
+ // NSFileManager is slightly dodgy on Windows
+ g_mkdir_with_parents ([preamblesDir UTF8String], 700);
+ [preambles storeToDirectory:preamblesDir];
+ [configFile setStringEntry:@"selectedPreamble" inGroup:@"Preambles" value:[preambles selectedPreambleName]];
+ }
+#endif
+
+ [styleManager saveStylesUsingConfigurationName:@"styles"];
+ [propertyPane saveUiStateToConfig:configFile group:@"PropertyPane"];
+ [stylesPane saveUiStateToConfig:configFile group:@"StylesPane"];
+
+ if (lastFolder != nil) {
+ [configFile setStringEntry:@"lastFolder" inGroup:@"Paths" value:lastFolder];
+ }
+
+ if (![configFile writeToStoreWithError:&error]) {
+ logError (error, @"Could not write config file");
+ }
+}
+
+- (void) zoomIn {
+ [surface zoomIn];
+}
+
+- (void) zoomOut {
+ [surface zoomOut];
+}
+
+- (void) zoomReset {
+ [surface zoomReset];
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation MainWindow (Notifications)
+- (void) toolboxWidthChanged:(int)newWidth {
+ [configFile setIntegerEntry:@"toolboxWidth" inGroup:@"mainWindow" value:newWidth];
+}
+
+- (void) styleboxWidthChanged:(int)newWidth {
+ [configFile setIntegerEntry:@"styleboxWidth" inGroup:@"mainWindow" value:newWidth];
+}
+
+- (void) graphHeightChanged:(int)newHeight {
+ [configFile setIntegerEntry:@"graphHeight" inGroup:@"mainWindow" value:newHeight];
+}
+
+- (void) tikzBufferChanged {
+ if (!suppressTikzUpdates) {
+ suppressTikzUpdates = TRUE;
+
+ GtkTextIter start, end;
+ gtk_text_buffer_get_bounds (tikzBuffer, &start, &end);
+ gchar *text = gtk_text_buffer_get_text (tikzBuffer, &start, &end, FALSE);
+
+ BOOL success = [document setTikz:[NSString stringWithUTF8String:text]];
+ [self _setHasParseError:!success];
+
+ g_free (text);
+
+ suppressTikzUpdates = FALSE;
+ }
+}
+
+- (void) windowSizeChangedWidth:(int)width height:(int)height {
+ if (width > 0 && height > 0) {
+ NSNumber *w = [NSNumber numberWithInt:width];
+ NSNumber *h = [NSNumber numberWithInt:height];
+ NSMutableArray *size = [NSMutableArray arrayWithCapacity:2];
+ [size addObject:w];
+ [size addObject:h];
+ [configFile setIntegerListEntry:@"windowSize" inGroup:@"mainWindow" value:size];
+ }
+}
+
+- (void) documentTikzChanged:(NSNotification*)notification {
+ [self _updateTitle];
+ [self _updateTikz];
+}
+
+- (void) documentSelectionChanged:(NSNotification*)notification {
+ [self _updateStatus];
+ [menu notifySelectionChanged:[document pickSupport]];
+}
+
+- (void) undoStackChanged:(NSNotification*)notification {
+ [self _updateUndoActions];
+}
+@end
+
+// }}}
+// {{{ InitHelpers
+
+@implementation MainWindow (InitHelpers)
+
+- (void) _loadConfig {
+ NSError *error = nil;
+ configFile = [[Configuration alloc] initWithName:@"tikzit" loadError:&error];
+ if (error != nil) {
+ logError (error, @"WARNING: Failed to load configuration");
+ }
+}
+
+- (void) _loadStyles {
+ styleManager = [[StyleManager alloc] init];
+ [styleManager loadStylesUsingConfigurationName:@"styles"];
+}
+
+// must happen after _loadStyles
+- (void) _loadPreambles {
+#ifdef HAVE_POPPLER
+ NSString *preamblesDir = [[SupportDir userSupportDir] stringByAppendingPathComponent:@"preambles"];
+ preambles = [[Preambles alloc] initFromDirectory:preamblesDir];
+ [preambles setStyleManager:styleManager];
+ NSString *selectedPreamble = [configFile stringEntry:@"selectedPreamble" inGroup:@"Preambles"];
+ if (selectedPreamble != nil) {
+ [preambles setSelectedPreambleName:selectedPreamble];
+ }
+#endif
+}
+
+- (void) _loadUi {
+ mainWindow = GTK_WINDOW (gtk_window_new (GTK_WINDOW_TOPLEVEL));
+ g_object_ref_sink (mainWindow);
+ gtk_window_set_title (mainWindow, "TikZiT");
+ gtk_window_set_default_size (mainWindow, 700, 400);
+
+ GtkBox *mainLayout = GTK_BOX (gtk_vbox_new (FALSE, 0));
+ gtk_widget_show (GTK_WIDGET (mainLayout));
+ gtk_container_add (GTK_CONTAINER (mainWindow), GTK_WIDGET (mainLayout));
+
+ menu = [[Menu alloc] initForMainWindow:self];
+
+ GtkWidget *menubar = [menu menubar];
+ gtk_box_pack_start (mainLayout, menubar, FALSE, TRUE, 0);
+ gtk_box_reorder_child (mainLayout, menubar, 0);
+ gtk_widget_show (menubar);
+
+ GtkWidget *toolbarBox = gtk_handle_box_new ();
+ gtk_box_pack_start (mainLayout, toolbarBox, FALSE, TRUE, 0);
+ gtk_widget_show (toolbarBox);
+ gtk_container_add (GTK_CONTAINER (toolbarBox), [menu toolbar]);
+ gtk_widget_show ([menu toolbar]);
+
+ propertyPaneSplitter = GTK_PANED (gtk_hpaned_new ());
+ g_object_ref_sink (propertyPaneSplitter);
+ gtk_widget_show (GTK_WIDGET (propertyPaneSplitter));
+ gtk_box_pack_start (mainLayout, GTK_WIDGET (propertyPaneSplitter), TRUE, TRUE, 0);
+
+ propertyPane = [[PropertyPane alloc] init];
+ GtkWidget *propFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (propFrame), [propertyPane widget]);
+ gtk_paned_pack1 (propertyPaneSplitter, propFrame, FALSE, TRUE);
+ gtk_widget_show (propFrame);
+ gtk_widget_show ([propertyPane widget]);
+
+ stylesPaneSplitter = GTK_PANED (gtk_hpaned_new ());
+ g_object_ref_sink (stylesPaneSplitter);
+ gtk_widget_show (GTK_WIDGET (stylesPaneSplitter));
+ gtk_paned_pack2 (propertyPaneSplitter, GTK_WIDGET (stylesPaneSplitter), TRUE, TRUE);
+
+ stylesPane = [[StylesPane alloc] initWithManager:styleManager];
+ GtkWidget *stylesFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (stylesFrame), [stylesPane widget]);
+ gtk_paned_pack2 (stylesPaneSplitter, stylesFrame, FALSE, TRUE);
+ gtk_widget_show (stylesFrame);
+ gtk_widget_show ([stylesPane widget]);
+
+ tikzPaneSplitter = GTK_PANED (gtk_vpaned_new ());
+ g_object_ref_sink (tikzPaneSplitter);
+ gtk_widget_show (GTK_WIDGET (tikzPaneSplitter));
+ gtk_paned_pack1 (stylesPaneSplitter, GTK_WIDGET (tikzPaneSplitter), TRUE, TRUE);
+
+ surface = [[WidgetSurface alloc] init];
+ gtk_widget_show ([surface widget]);
+ GtkWidget *graphFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (graphFrame), [surface widget]);
+ gtk_widget_show (graphFrame);
+ gtk_paned_pack1 (tikzPaneSplitter, graphFrame, TRUE, TRUE);
+ [surface setDefaultScale:50.0f];
+ [surface setKeepCentered:YES];
+ [surface setGrabsFocusOnClick:YES];
+ renderer = [[GraphRenderer alloc] initWithSurface:surface document:document];
+
+ inputHandler = [[GraphInputHandler alloc] initWithGraphRenderer:renderer];
+ [surface setInputDelegate:inputHandler];
+
+ tikzBuffer = gtk_text_buffer_new (NULL);
+ g_object_ref_sink (tikzBuffer);
+ GtkWidget *tikzScroller = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_show (tikzScroller);
+
+ tikzPane = gtk_text_view_new_with_buffer (tikzBuffer);
+ gtk_text_view_set_left_margin (GTK_TEXT_VIEW (tikzPane), 3);
+ gtk_text_view_set_right_margin (GTK_TEXT_VIEW (tikzPane), 3);
+ g_object_ref_sink (tikzPane);
+ gtk_widget_show (tikzPane);
+ gtk_container_add (GTK_CONTAINER (tikzScroller), tikzPane);
+ GtkWidget *tikzFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (tikzFrame), tikzScroller);
+ gtk_widget_show (tikzFrame);
+ gtk_paned_pack2 (tikzPaneSplitter, tikzFrame, FALSE, TRUE);
+
+ statusBar = GTK_STATUSBAR (gtk_statusbar_new ());
+ gtk_widget_show (GTK_WIDGET (statusBar));
+ gtk_box_pack_start (mainLayout, GTK_WIDGET (statusBar), FALSE, TRUE, 0);
+
+ GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
+ update_paste_action (clipboard, NULL, [menu pasteAction]);
+}
+
+- (void) _restoreUiState {
+ NSArray *windowSize = [configFile integerListEntry:@"windowSize" inGroup:@"mainWindow"];
+ if (windowSize && [windowSize count] == 2) {
+ gint width = [[windowSize objectAtIndex:0] intValue];
+ gint height = [[windowSize objectAtIndex:1] intValue];
+ if (width > 0 && height > 0) {
+ gtk_window_set_default_size (mainWindow, width, height);
+ }
+ }
+ int panePos = [configFile integerEntry:@"toolboxWidth" inGroup:@"mainWindow"];
+ if (panePos > 0) {
+ gtk_paned_set_position (propertyPaneSplitter, panePos);
+ }
+ panePos = [configFile integerEntry:@"styleboxWidth" inGroup:@"mainWindow"];
+ if (panePos > 0) {
+ gtk_paned_set_position (stylesPaneSplitter, panePos);
+ }
+ panePos = [configFile integerEntry:@"graphHeight" inGroup:@"mainWindow"];
+ if (panePos > 0) {
+ gtk_paned_set_position (tikzPaneSplitter, panePos);
+ }
+ [propertyPane restoreUiStateFromConfig:configFile group:@"PropertyPane"];
+ [stylesPane restoreUiStateFromConfig:configFile group:@"StylesPane"];
+}
+
+- (void) _connectSignals {
+ GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
+ g_signal_connect (G_OBJECT (clipboard),
+ "owner-change",
+ G_CALLBACK (update_paste_action),
+ [menu pasteAction]);
+ g_signal_connect (G_OBJECT (mainWindow),
+ "key-press-event",
+ G_CALLBACK (tz_hijack_key_press),
+ NULL);
+ g_signal_connect (G_OBJECT (propertyPaneSplitter),
+ "notify::position",
+ G_CALLBACK (toolbox_divider_position_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (stylesPaneSplitter),
+ "notify::position",
+ G_CALLBACK (stylebox_divider_position_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (tikzPaneSplitter),
+ "notify::position",
+ G_CALLBACK (graph_divider_position_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (tikzBuffer),
+ "changed",
+ G_CALLBACK (tikz_buffer_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (mainWindow),
+ "delete-event",
+ G_CALLBACK (main_window_delete_event_cb),
+ self);
+ g_signal_connect (G_OBJECT (mainWindow),
+ "destroy",
+ G_CALLBACK (main_window_destroy_cb),
+ self);
+ g_signal_connect (G_OBJECT (mainWindow),
+ "configure-event",
+ G_CALLBACK (main_window_configure_event_cb),
+ self);
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation MainWindow (Private)
+
+- (BOOL) _confirmCloseDocumentTo:(NSString*)action {
+ BOOL proceed = YES;
+ if ([document hasUnsavedChanges]) {
+ NSString *message = [NSString stringWithFormat:@"You have unsaved changes to the current document, which will be lost if you %@. Are you sure you want to continue?", action];
+ GtkWidget *dialog = gtk_message_dialog_new (NULL,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ [message UTF8String]);
+ gtk_window_set_title(GTK_WINDOW(dialog), "Close current document?");
+ proceed = gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES;
+ gtk_widget_destroy (dialog);
+ }
+ return proceed;
+}
+
+- (void) _forceLoadDocumentFromFile:(NSString*)path {
+ NSError *error = nil;
+ TikzDocument *d = [TikzDocument documentFromFile:path styleManager:styleManager error:&error];
+ if (d != nil) {
+ [self _setActiveDocument:d];
+ [[RecentManager defaultManager] addRecentFile:path];
+ } else {
+ [self presentError:error withMessage:@"Could not open file"];
+ [[RecentManager defaultManager] removeRecentFile:path];
+ }
+}
+
+- (void) _placeGraphOnClipboard:(Graph*)graph {
+ GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
+
+ static const GtkTargetEntry targets[] = {
+ { "TIKZITPICTURE", 0, TARGET_TIKZIT_PICTURE },
+ { "UTF8_STRING", 0, TARGET_UTF8_STRING } };
+
+ gtk_clipboard_set_with_data (clipboard,
+ targets, G_N_ELEMENTS (targets),
+ clipboard_provide_data,
+ clipboard_release_data,
+ clipboard_graph_data_new (graph));
+}
+
+- (void) _setHasParseError:(BOOL)hasError {
+ if (hasError && !hasParseError) {
+ gtk_statusbar_push (statusBar, 1, "Parse error");
+ GdkColor color = {0, 65535, 61184, 61184};
+ gtk_widget_modify_base (tikzPane, GTK_STATE_NORMAL, &color);
+ } else if (!hasError && hasParseError) {
+ gtk_statusbar_pop (statusBar, 1);
+ gtk_widget_modify_base (tikzPane, GTK_STATE_NORMAL, NULL);
+ }
+ hasParseError = hasError;
+}
+
+- (void) _updateUndoActions {
+ [menu setUndoActionEnabled:[document canUndo]];
+ [menu setUndoActionDetail:[document undoName]];
+
+ [menu setRedoActionEnabled:[document canRedo]];
+ [menu setRedoActionDetail:[document redoName]];
+}
+
+- (void) _updateTitle {
+ NSString *title = [NSString stringWithFormat:@"TikZiT - %@%s",
+ [document name],
+ ([document hasUnsavedChanges] ? "*" : "")];
+ gtk_window_set_title(mainWindow, [title UTF8String]);
+}
+
+- (void) _updateStatus {
+ GString *buffer = g_string_sized_new (30);
+ gchar *nextNode = 0;
+
+ for (Node *n in [[document pickSupport] selectedNodes]) {
+ if (nextNode) {
+ if (buffer->len == 0) {
+ g_string_printf(buffer, "Nodes %s", nextNode);
+ } else {
+ g_string_append_printf(buffer, ", %s", nextNode);
+ }
+ }
+ nextNode = (gchar *)[[n name] UTF8String];
+ }
+ if (nextNode) {
+ if (buffer->len == 0) {
+ g_string_printf(buffer, "Node %s is selected", nextNode);
+ } else {
+ g_string_append_printf(buffer, " and %s are selected", nextNode);
+ }
+ }
+
+ if (buffer->len == 0) {
+ int nrNodes = [[[document graph] nodes] count];
+ int nrEdges = [[[document graph] edges] count];
+ g_string_printf(buffer, "Graph has %d node%s and %d edge%s",
+ nrNodes,
+ nrNodes!=1 ? "s" : "",
+ nrEdges,
+ nrEdges!=1 ? "s" : "");
+ }
+ gtk_statusbar_pop(statusBar, 0);
+ gtk_statusbar_push(statusBar, 0, buffer->str);
+
+ g_string_free (buffer, TRUE);
+}
+
+- (void) _updateTikz {
+ if (document != nil && !suppressTikzUpdates) {
+ suppressTikzUpdates = TRUE;
+
+ if (document != nil) {
+ const char *tikzString = [[document tikz] UTF8String];
+ gtk_text_buffer_set_text (tikzBuffer, tikzString, -1);
+ } else {
+ gtk_text_buffer_set_text (tikzBuffer, "", -1);
+ }
+ [self _setHasParseError:NO];
+
+ suppressTikzUpdates = FALSE;
+ }
+}
+
+- (void) _setLastFolder:(NSString*)path {
+ [path retain];
+ [lastFolder release];
+ lastFolder = path;
+}
+
+- (void) _setActiveDocument:(TikzDocument*)newDoc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:[document pickSupport]];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:document];
+
+ [newDoc retain];
+ [document release];
+ document = newDoc;
+
+ [renderer setDocument:document];
+ [stylesPane setDocument:document];
+ [propertyPane setDocument:document];
+#ifdef HAVE_POPPLER
+ [previewWindow setDocument:document];
+#endif
+ [self _updateTikz];
+ [self _updateTitle];
+ [self _updateStatus];
+ [self _updateUndoActions];
+ [menu notifySelectionChanged:[document pickSupport]];
+ [inputHandler resetState];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentTikzChanged:)
+ name:@"TikzChanged" object:document];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(undoStackChanged:)
+ name:@"UndoStackChanged" object:document];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentSelectionChanged:)
+ name:@"NodeSelectionChanged" object:[document pickSupport]];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentSelectionChanged:)
+ name:@"EdgeSelectionChanged" object:[document pickSupport]];
+}
+
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void toolbox_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ gint position;
+ g_object_get (gobject, "position", &position, NULL);
+ [window toolboxWidthChanged:position];
+ [pool drain];
+}
+
+static void stylebox_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ gint position;
+ g_object_get (gobject, "position", &position, NULL);
+ [window styleboxWidthChanged:position];
+ [pool drain];
+}
+
+static void graph_divider_position_changed_cb (GObject *gobject, GParamSpec *pspec, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ gint position;
+ g_object_get (gobject, "position", &position, NULL);
+ [window graphHeightChanged:position];
+ [pool drain];
+}
+
+static void tikz_buffer_changed_cb (GtkTextBuffer *buffer, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window tikzBufferChanged];
+ [pool drain];
+}
+
+static gboolean main_window_delete_event_cb (GtkWidget *widget, GdkEvent *event, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ return ![window askCanQuit];
+ [pool drain];
+}
+
+static void main_window_destroy_cb (GtkWidget *widget, MainWindow *window) {
+ gtk_main_quit();
+}
+
+static gboolean main_window_configure_event_cb (GtkWidget *widget, GdkEventConfigure *event, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window windowSizeChangedWidth:event->width height:event->height];
+ [pool drain];
+ return FALSE;
+}
+
+static void clipboard_provide_data (GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ guint info,
+ gpointer clipboard_graph_data) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ ClipboardGraphData *data = (ClipboardGraphData*)clipboard_graph_data;
+ if (info == TARGET_UTF8_STRING || info == TARGET_TIKZIT_PICTURE) {
+ clipboard_graph_data_convert (data);
+ GdkAtom target = (info == TARGET_UTF8_STRING) ? utf8_atom : tikzit_picture_atom;
+ gtk_selection_data_set (selection_data,
+ target,
+ 8*sizeof(gchar),
+ (guchar*)data->tikz,
+ data->tikz_length);
+ }
+
+ [pool drain];
+}
+
+static void clipboard_release_data (GtkClipboard *clipboard, gpointer data) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ clipboard_graph_data_free ((ClipboardGraphData*)data);
+ [pool drain];
+}
+
+static void clipboard_check_targets (GtkClipboard *clipboard,
+ GdkAtom *atoms,
+ gint n_atoms,
+ gpointer action) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ gboolean found = FALSE;
+ for (gint i = 0; i < n_atoms; ++i) {
+ if (atoms[i] == tikzit_picture_atom) {
+ found = TRUE;
+ break;
+ }
+ }
+ gtk_action_set_sensitive (GTK_ACTION (action), found);
+
+ [pool drain];
+}
+
+static void update_paste_action (GtkClipboard *clipboard, GdkEvent *event, GtkAction *action) {
+ gtk_action_set_sensitive (action, FALSE);
+ gtk_clipboard_request_targets (clipboard, clipboard_check_targets, action);
+}
+
+static void clipboard_paste_contents (GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ gpointer document) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ TikzDocument *doc = (TikzDocument*)document;
+ gint length = gtk_selection_data_get_length (selection_data);
+ if (length >= 0) {
+ const guchar *raw_data = gtk_selection_data_get_data (selection_data);
+ gchar *data = g_new (gchar, length+1);
+ g_strlcpy (data, (const gchar *)raw_data, length+1);
+ NSString *tikz = [NSString stringWithUTF8String:data];
+ if (tikz != nil) {
+ [doc pasteFromTikz:tikz];
+ }
+ g_free (data);
+ }
+
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/Menu.h b/tikzit/src/gtk/Menu.h
new file mode 100644
index 0000000..024c9e0
--- /dev/null
+++ b/tikzit/src/gtk/Menu.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * Stuff stolen from glade-window.c in Glade:
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2007 Vincent Geddes.
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class MainWindow;
+@class PickSupport;
+
+/**
+ * Manages the menu and toolbar for the main window.
+ */
+@interface Menu: NSObject {
+ MainWindow *mainWindow;
+ GtkUIManager *ui;
+ GtkActionGroup *staticActions;
+ GtkActionGroup *documentActions;
+// GtkActionGroup *documents_list_menu_actions;
+ GtkAction *undoAction;
+ GtkAction *redoAction;
+ GtkAction *pasteAction;
+ GtkAction **nodeSelBasedActions;
+ guint nodeSelBasedActionCount;
+ GtkAction **selBasedActions;
+ guint selBasedActionCount;
+}
+
+/**
+ * Constructs the menu and toolbar for @p window
+ *
+ * @param window the mainwindow that will be acted upon by the various
+ * menu items and toolbar buttons.
+ */
+- (id) initForMainWindow:(MainWindow*)window;
+
+/**
+ * The menubar widget, to be inserted into the main window
+ */
+- (GtkWidget*) menubar;
+/**
+ * The toolbar widget, to be inserted into the main window
+ */
+- (GtkWidget*) toolbar;
+/**
+ * The main window object passed to initForMainWindow
+ */
+- (MainWindow*) mainWindow;
+
+/**
+ * Enables or disables the undo action
+ */
+- (void) setUndoActionEnabled:(BOOL)enabled;
+/**
+ * Sets the text that describes what action will be undone
+ *
+ * @param detail a text description of the action, or nil
+ */
+- (void) setUndoActionDetail:(NSString*)detail;
+/**
+ * Enables or disables the redo action
+ */
+- (void) setRedoActionEnabled:(BOOL)enabled;
+/**
+ * Sets the text that describes what action will be redone
+ *
+ * @param detail a text description of the action, or nil
+ */
+- (void) setRedoActionDetail:(NSString*)detail;
+
+/**
+ * Gets the paste action
+ */
+- (GtkAction*) pasteAction;
+
+/**
+ * Enables or disables the actions that act on a selection
+ */
+- (void) notifySelectionChanged:(PickSupport*)pickSupport;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Menu.m b/tikzit/src/gtk/Menu.m
new file mode 100644
index 0000000..c8fffb9
--- /dev/null
+++ b/tikzit/src/gtk/Menu.m
@@ -0,0 +1,796 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * Stuff stolen from glade-window.c in Glade:
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2007 Vincent Geddes.
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Menu.h"
+
+#import "MainWindow.h"
+#import "GraphInputHandler.h"
+#import "Configuration.h"
+#import "PickSupport.h"
+#import "Shape.h"
+#import "TikzDocument.h"
+
+#import <glib.h>
+#ifdef _
+#undef _
+#endif
+#import <glib/gi18n.h>
+#import <gtk/gtk.h>
+
+#import "gtkhelpers.h"
+
+#define ACTION_GROUP_STATIC "TZStatic"
+#define ACTION_GROUP_DOCUMENT "TZDocument"
+#define ACTION_GROUP_DOCUMENTS_LIST_MENU "TZDocumentsList"
+
+#import "logo.h"
+#include <gdk-pixbuf/gdk-pixdata.h>
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpointer-sign"
+#import "icondata.m"
+#pragma GCC diagnostic pop
+
+
+// {{{ Callbacks
+
+static void new_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window loadEmptyDocument];
+ [pool drain];
+}
+
+static void open_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window openFile];
+ [pool drain];
+}
+
+static void save_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window saveActiveDocument];
+ [pool drain];
+}
+
+static void save_as_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window saveActiveDocumentAs];
+ [pool drain];
+}
+
+static void save_as_shape_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window saveActiveDocumentAsShape];
+ [pool drain];
+}
+
+static void refresh_shapes_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [Shape refreshShapeDictionary];
+ [pool drain];
+}
+
+static void quit_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window quit];
+ [pool drain];
+}
+
+static void help_cb (GtkAction *action, MainWindow *window) {
+ GError *gerror = NULL;
+ gtk_show_uri (NULL, "http://tikzit.sourceforge.net/manual.html", GDK_CURRENT_TIME, &gerror);
+ if (gerror != NULL) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ logGError (gerror, @"Could not show help");
+ [pool drain];
+ }
+}
+
+static void about_cb (GtkAction *action, MainWindow *window) {
+ static const gchar * const authors[] =
+ { "Aleks Kissinger <aleks0@gmail.com>",
+ "Chris Heunen <chrisheunen@gmail.com>",
+ "Alex Merry <dev@randomguy3.me.uk>",
+ NULL };
+
+ static const gchar license[] =
+ N_("TikZiT 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 2 of the "
+ "License, or (at your option) any later version."
+ "\n\n"
+ "TikZiT 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."
+ "\n\n"
+ "You should have received a copy of the GNU General Public License "
+ "along with TikZiT; if not, write to the Free Software "
+ "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, "
+ "MA 02110-1301, USA.");
+
+ static const gchar copyright[] =
+ "Copyright \xc2\xa9 2010-2011 Aleks Kissinger, Chris Heunen and Alex Merry.";
+
+ GdkPixbuf *logo = get_logo (LOGO_SIZE_128);
+ gtk_show_about_dialog (GTK_WINDOW ([window gtkWindow]),
+ "program-name", PACKAGE_NAME,
+ "logo", logo,
+ "authors", authors,
+ "translator-credits", _("translator-credits"),
+ "comments", _("A graph manipulation program for pgf/tikz graphs"),
+ "license", _(license),
+ "wrap-license", TRUE,
+ "copyright", copyright,
+ "version", PACKAGE_VERSION,
+ "website", "http://tikzit.sourceforge.net",
+ NULL);
+ g_object_unref (logo);
+}
+
+static void undo_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ TikzDocument *document = [window activeDocument];
+ if ([document canUndo]) {
+ [document undo];
+ } else {
+ g_warning ("Can't undo!\n");
+ gtk_action_set_sensitive (action, FALSE);
+ }
+
+ [pool drain];
+}
+
+static void redo_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ TikzDocument *document = [window activeDocument];
+ if ([document canRedo]) {
+ [document redo];
+ } else {
+ g_warning ("Can't redo!\n");
+ gtk_action_set_sensitive (action, FALSE);
+ }
+
+ [pool drain];
+}
+
+static void cut_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window cut];
+ [pool drain];
+}
+
+static void copy_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window copy];
+ [pool drain];
+}
+
+static void paste_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window paste];
+ [pool drain];
+}
+
+static void delete_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [[window activeDocument] removeSelected];
+ [pool drain];
+}
+
+static void select_all_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ TikzDocument *document = [window activeDocument];
+ [[document pickSupport] selectAllNodes:[NSSet setWithArray:[[document graph] nodes]]];
+ [pool drain];
+}
+
+static void deselect_all_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ TikzDocument *document = [window activeDocument];
+ [[document pickSupport] deselectAllNodes];
+ [[document pickSupport] deselectAllEdges];
+ [pool drain];
+}
+
+static void flip_horiz_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [[window activeDocument] flipSelectedNodesHorizontally];
+ [pool drain];
+}
+
+static void flip_vert_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [[window activeDocument] flipSelectedNodesVertically];
+ [pool drain];
+}
+
+#ifdef HAVE_POPPLER
+static void show_preferences_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window showSettingsDialog];
+ [pool drain];
+}
+
+static void show_preamble_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window editPreambles];
+ [pool drain];
+}
+
+static void show_preview_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window showPreview];
+ [pool drain];
+}
+#endif
+
+static void zoom_in_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window zoomIn];
+ [pool drain];
+}
+
+static void zoom_out_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window zoomOut];
+ [pool drain];
+}
+
+static void zoom_reset_cb (GtkAction *action, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [window zoomReset];
+ [pool drain];
+}
+
+static void input_mode_change_cb (GtkRadioAction *action, GtkRadioAction *current, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [[window graphInputHandler] setMode:(InputMode)gtk_radio_action_get_current_value (action)];
+ [pool drain];
+}
+
+static void toolbar_style_change_cb (GtkRadioAction *action, GtkRadioAction *current, Menu *menu) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ gint value = gtk_radio_action_get_current_value (action);
+ gtk_toolbar_set_style (GTK_TOOLBAR ([menu toolbar]), (GtkToolbarStyle)value);
+ [[[menu mainWindow] mainConfiguration] setIntegerEntry:@"toolbarStyle" inGroup:@"UI" value:value];
+
+ [pool drain];
+}
+
+static void recent_chooser_item_activated_cb (GtkRecentChooser *chooser, MainWindow *window) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ gchar *uri, *path;
+ GError *error = NULL;
+
+ uri = gtk_recent_chooser_get_current_uri (chooser);
+
+ path = g_filename_from_uri (uri, NULL, NULL);
+ if (error) {
+ g_warning ("Could not convert uri \"%s\" to a local path: %s", uri, error->message);
+ g_error_free (error);
+ return;
+ }
+
+ NSString *nspath = [NSString stringWithGlibFilename:path];
+ if (error) {
+ g_warning ("Could not convert filename \"%s\" to an NSString: %s", path, error->message);
+ g_error_free (error);
+ return;
+ }
+ [window loadDocumentFromFile:nspath];
+
+ g_free (uri);
+ g_free (path);
+
+ [pool drain];
+}
+
+
+
+// }}}
+// {{{ UI XML
+
+static const gchar ui_info[] =
+"<ui>"
+" <menubar name='MenuBar'>"
+" <menu action='FileMenu'>"
+" <menuitem action='New'/>"
+" <menuitem action='Open'/>"
+" <menuitem action='OpenRecent'/>"
+" <separator/>"
+" <menuitem action='Save'/>"
+" <menuitem action='SaveAs'/>"
+" <separator/>"
+" <menuitem action='SaveAsShape'/>"
+" <menuitem action='RefreshShapes'/>"
+" <separator/>"
+//" <menuitem action='Close'/>"
+" <menuitem action='Quit'/>"
+" </menu>"
+" <menu action='EditMenu'>"
+" <menu action='Tool'>"
+" <menuitem action='SelectMode'/>"
+" <menuitem action='CreateNodeMode'/>"
+" <menuitem action='DrawEdgeMode'/>"
+" <menuitem action='BoundingBoxMode'/>"
+" <menuitem action='HandMode'/>"
+" </menu>"
+" <separator/>"
+" <menuitem action='Undo'/>"
+" <menuitem action='Redo'/>"
+" <separator/>"
+" <menuitem action='Cut'/>"
+" <menuitem action='Copy'/>"
+" <menuitem action='Paste'/>"
+" <menuitem action='Delete'/>"
+" <separator/>"
+" <menuitem action='SelectAll'/>"
+" <menuitem action='DeselectAll'/>"
+" <separator/>"
+" <menuitem action='FlipVert'/>"
+" <menuitem action='FlipHoriz'/>"
+#ifdef HAVE_POPPLER
+" <separator/>"
+" <menuitem action='ShowPreferences'/>"
+#endif
+" </menu>"
+" <menu action='ViewMenu'>"
+" <menu action='ToolbarStyle'>"
+" <menuitem action='ToolbarIconsOnly'/>"
+" <menuitem action='ToolbarTextOnly'/>"
+" <menuitem action='ToolbarTextIcons'/>"
+" <menuitem action='ToolbarTextIconsHoriz'/>"
+" </menu>"
+/*
+" <menuitem action='ToolbarVisible'/>"
+" <menuitem action='StatusbarVisible'/>"
+*/
+#ifdef HAVE_POPPLER
+" <menuitem action='ShowPreamble'/>"
+" <menuitem action='ShowPreview'/>"
+#endif
+" <menu action='Zoom'>"
+" <menuitem action='ZoomIn'/>"
+" <menuitem action='ZoomOut'/>"
+" <menuitem action='ZoomReset'/>"
+" </menu>"
+" </menu>"
+/*
+" <menu action='Window'>"
+" <placeholder name='DocumentsListPlaceholder'/>"
+" </menu>"
+*/
+" <menu action='HelpMenu'>"
+" <menuitem action='HelpManual'/>"
+" <separator/>"
+" <menuitem action='About'/>"
+" </menu>"
+" </menubar>"
+" <toolbar name='ToolBar'>"
+" <toolitem action='New'/>"
+" <toolitem action='Open'/>"
+" <toolitem action='Save'/>"
+" <separator/>"
+" <toolitem action='Cut'/>"
+" <toolitem action='Copy'/>"
+" <toolitem action='Paste'/>"
+" <separator/>"
+" <toolitem action='SelectMode'/>"
+" <toolitem action='CreateNodeMode'/>"
+" <toolitem action='DrawEdgeMode'/>"
+" <toolitem action='BoundingBoxMode'/>"
+" <toolitem action='HandMode'/>"
+" </toolbar>"
+"</ui>";
+
+
+
+// }}}
+// {{{ Actions
+
+static GtkActionEntry static_entries[] = {
+ /*
+ Fields:
+ * action name
+ * stock id or name of icon for action
+ * label for action (mark for translation with N_)
+ * accelerator (as understood by gtk_accelerator_parse())
+ * tooltip (mark for translation with N_)
+ * callback
+ */
+ { "FileMenu", NULL, N_("_File") },
+ { "EditMenu", NULL, N_("_Edit") },
+ { "ViewMenu", NULL, N_("_View") },
+ //{ "ProjectMenu", NULL, N_("_Projects") },
+ { "HelpMenu", NULL, N_("_Help") },
+ //{ "UndoMenu", NULL, NULL },
+ //{ "RedoMenu", NULL, NULL },
+
+ /* FileMenu */
+ { "New", GTK_STOCK_NEW, NULL, "<control>N",
+ N_("Create a new graph"), G_CALLBACK (new_cb) },
+
+ { "Open", GTK_STOCK_OPEN, N_("_Open\342\200\246") ,"<control>O",
+ N_("Open a graph"), G_CALLBACK (open_cb) },
+
+ { "OpenRecent", NULL, N_("Open _Recent") },
+
+ { "RefreshShapes", NULL, N_("_Refresh shapes"), NULL,
+ N_(""), G_CALLBACK (refresh_shapes_cb) },
+
+ { "Quit", GTK_STOCK_QUIT, NULL, "<control>Q",
+ N_("Quit the program"), G_CALLBACK (quit_cb) },
+
+ /* EditMenu */
+ { "Tool", NULL, N_("_Tool") },
+
+#ifdef HAVE_POPPLER
+ { "ShowPreferences", GTK_STOCK_PREFERENCES, NULL, NULL,
+ N_("Edit the TikZiT preferences"), G_CALLBACK (show_preferences_cb) },
+#endif
+
+ /* ViewMenu */
+ { "ToolbarStyle", NULL, N_("_Toolbar style") },
+
+#ifdef HAVE_POPPLER
+ { "ShowPreamble", NULL, N_("_Edit Preambles..."), NULL,
+ N_("Edit the preambles used to generate the preview"), G_CALLBACK (show_preamble_cb) },
+#endif
+
+ { "Zoom", NULL, N_("_Zoom") },
+
+ { "ZoomIn", GTK_STOCK_ZOOM_IN, NULL, "<control>plus",
+ NULL, G_CALLBACK (zoom_in_cb) },
+
+ { "ZoomOut", GTK_STOCK_ZOOM_OUT, NULL, "<control>minus",
+ NULL, G_CALLBACK (zoom_out_cb) },
+
+ { "ZoomReset", GTK_STOCK_ZOOM_100, N_("_Reset zoom"), "<control>0",
+ NULL, G_CALLBACK (zoom_reset_cb) },
+
+ /* HelpMenu */
+ { "HelpManual", GTK_STOCK_HELP, N_("_Online manual"), "F1",
+ N_("TikZiT manual (online)"), G_CALLBACK (help_cb) },
+
+ { "About", GTK_STOCK_ABOUT, NULL, NULL,
+ N_("About this application"), G_CALLBACK (about_cb) },
+};
+
+static guint n_static_entries = G_N_ELEMENTS (static_entries);
+
+static GtkActionEntry document_entries[] = {
+
+ /* FileMenu */
+ { "Save", GTK_STOCK_SAVE, NULL, "<control>S",
+ N_("Save the current graph"), G_CALLBACK (save_cb) },
+
+ { "SaveAs", GTK_STOCK_SAVE_AS, N_("Save _As\342\200\246"), NULL,
+ N_("Save the current graph with a different name"), G_CALLBACK (save_as_cb) },
+
+ { "SaveAsShape", NULL, N_("Save As S_hape\342\200\246"), NULL,
+ N_("Save the current graph as a shape for use in styles"), G_CALLBACK (save_as_shape_cb) },
+
+/*
+ { "Close", GTK_STOCK_CLOSE, NULL, "<control>W",
+ N_("Close the current graph"), G_CALLBACK (close_cb) },
+*/
+
+ /* EditMenu */
+ { "Undo", GTK_STOCK_UNDO, NULL, "<control>Z",
+ N_("Undo the last action"), G_CALLBACK (undo_cb) },
+
+ { "Redo", GTK_STOCK_REDO, NULL, "<shift><control>Z",
+ N_("Redo the last action"), G_CALLBACK (redo_cb) },
+
+ { "Cut", GTK_STOCK_CUT, NULL, NULL,
+ N_("Cut the selection"), G_CALLBACK (cut_cb) },
+
+ { "Copy", GTK_STOCK_COPY, NULL, NULL,
+ N_("Copy the selection"), G_CALLBACK (copy_cb) },
+
+ { "Paste", GTK_STOCK_PASTE, NULL, NULL,
+ N_("Paste the clipboard"), G_CALLBACK (paste_cb) },
+
+ { "Delete", GTK_STOCK_DELETE, NULL, "Delete",
+ N_("Delete the selection"), G_CALLBACK (delete_cb) },
+
+ { "SelectAll", GTK_STOCK_SELECT_ALL, NULL, "<control>A",
+ N_("Select all nodes on the graph"), G_CALLBACK (select_all_cb) },
+
+ { "DeselectAll", NULL, N_("D_eselect all"), "<shift><control>A",
+ N_("Deselect everything"), G_CALLBACK (deselect_all_cb) },
+
+ { "FlipHoriz", NULL, N_("Flip nodes _horizonally"), NULL,
+ N_("Flip the selected nodes horizontally"), G_CALLBACK (flip_horiz_cb) },
+
+ { "FlipVert", NULL, N_("Flip nodes _vertically"), NULL,
+ N_("Flip the selected nodes vertically"), G_CALLBACK (flip_vert_cb) },
+
+ /* ViewMenu */
+#ifdef HAVE_POPPLER
+ { "ShowPreview", NULL, N_("_Preview"), "<control>L",
+ N_("See the graph as it will look when rendered in LaTeX"), G_CALLBACK (show_preview_cb) },
+#endif
+};
+static guint n_document_entries = G_N_ELEMENTS (document_entries);
+
+static GtkRadioActionEntry mode_entries[] = {
+ /*
+ Fields:
+ * action name
+ * stock id or name of icon for action
+ * label for action (mark for translation with N_)
+ * accelerator (as understood by gtk_accelerator_parse())
+ * tooltip (mark for translation with N_)
+ * value (see gtk_radio_action_get_current_value())
+ */
+
+ { "SelectMode", NULL, N_("_Select"), "<control><shift>s",
+ N_("Select, move and edit nodes and edges"), (gint)SelectMode },
+
+ { "CreateNodeMode", NULL, N_("_Create nodes"), "<control><shift>c",
+ N_("Create new nodes"), (gint)CreateNodeMode },
+
+ { "DrawEdgeMode", NULL, N_("_Draw edges"), "<control><shift>e",
+ N_("Draw new edges"), (gint)DrawEdgeMode },
+
+ { "BoundingBoxMode", NULL, N_("_Bounding box"), "<control><shift>x",
+ N_("Set the bounding box"), (gint)BoundingBoxMode },
+
+ { "HandMode", NULL, N_("_Pan"), "<control><shift>f",
+ N_("Move the diagram to view different parts"), (gint)HandMode },
+};
+static guint n_mode_entries = G_N_ELEMENTS (mode_entries);
+
+static GtkRadioActionEntry toolbar_style_entries[] = {
+ /*
+ Fields:
+ * action name
+ * stock id or name of icon for action
+ * label for action (mark for translation with N_)
+ * accelerator (as understood by gtk_accelerator_parse())
+ * tooltip (mark for translation with N_)
+ * value (see gtk_radio_action_get_current_value())
+ */
+
+ { "ToolbarIconsOnly", NULL, N_("_Icons only"), NULL,
+ N_("Show only icons on the toolbar"), (gint)GTK_TOOLBAR_ICONS },
+
+ { "ToolbarTextOnly", NULL, N_("_Text only"), NULL,
+ N_("Show only text on the toolbar"), (gint)GTK_TOOLBAR_TEXT },
+
+ { "ToolbarTextIcons", NULL, N_("Text _below icons"), NULL,
+ N_("Show icons on the toolbar with text below"), (gint)GTK_TOOLBAR_BOTH },
+
+ { "ToolbarTextIconsHoriz", NULL, N_("Text be_side icons"), NULL,
+ N_("Show icons on the toolbar with text beside"), (gint)GTK_TOOLBAR_BOTH_HORIZ },
+};
+static guint n_toolbar_style_entries = G_N_ELEMENTS (toolbar_style_entries);
+
+// }}}
+// {{{ Helper methods
+
+
+static void
+set_tool_button_image (GtkToolButton *button, const GdkPixdata *image_data)
+{
+ GtkWidget *image = NULL;
+
+ if (image_data) {
+ GdkPixbuf *buf = gdk_pixbuf_from_pixdata (image_data, FALSE, NULL);
+ image = gtk_image_new_from_pixbuf (buf);
+ g_object_unref (buf);
+ }
+
+ gtk_tool_button_set_icon_widget (button, image);
+
+ if (image) {
+ gtk_widget_show (image);
+ }
+}
+
+GtkWidget *
+create_recent_chooser_menu ()
+{
+ GtkWidget *recent_menu;
+ GtkRecentFilter *filter;
+
+ recent_menu = gtk_recent_chooser_menu_new_for_manager (gtk_recent_manager_get_default ());
+
+ gtk_recent_chooser_set_local_only (GTK_RECENT_CHOOSER (recent_menu), TRUE);
+ gtk_recent_chooser_set_show_icons (GTK_RECENT_CHOOSER (recent_menu), FALSE);
+ gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (recent_menu), GTK_RECENT_SORT_MRU);
+ gtk_recent_chooser_menu_set_show_numbers (GTK_RECENT_CHOOSER_MENU (recent_menu), TRUE);
+
+ filter = gtk_recent_filter_new ();
+ gtk_recent_filter_add_application (filter, g_get_application_name());
+ gtk_recent_chooser_set_filter (GTK_RECENT_CHOOSER (recent_menu), filter);
+
+ return recent_menu;
+}
+
+
+
+// }}}
+// {{{ API
+
+@implementation Menu
+
+- (id) initForMainWindow:(MainWindow*)window
+{
+ self = [super init];
+ if (!self) {
+ return nil;
+ }
+
+ mainWindow = window;
+
+ GError *error = NULL;
+
+ staticActions = gtk_action_group_new (ACTION_GROUP_STATIC);
+ //gtk_action_group_set_translation_domain (staticActions, GETTEXT_PACKAGE);
+
+ gtk_action_group_add_actions (staticActions,
+ static_entries,
+ n_static_entries,
+ window);
+ gtk_action_group_add_radio_actions (staticActions, mode_entries,
+ n_mode_entries, (gint)SelectMode,
+ G_CALLBACK (input_mode_change_cb), window);
+ GtkToolbarStyle style;
+ g_object_get (G_OBJECT (gtk_settings_get_default ()), "gtk-toolbar-style", &style, NULL);
+ gtk_action_group_add_radio_actions (staticActions, toolbar_style_entries,
+ n_toolbar_style_entries, style,
+ G_CALLBACK (toolbar_style_change_cb), self);
+
+ documentActions = gtk_action_group_new (ACTION_GROUP_DOCUMENT);
+ //gtk_action_group_set_translation_domain (documentActions, GETTEXT_PACKAGE);
+
+ gtk_action_group_add_actions (documentActions,
+ document_entries,
+ n_document_entries,
+ window);
+
+ /*
+ documents_list_menu_actions =
+ gtk_action_group_new (ACTION_GROUP_DOCUMENTS_LIST_MENU);
+ gtk_action_group_set_translation_domain (documents_list_menu_actions,
+ GETTEXT_PACKAGE);
+ */
+
+ ui = gtk_ui_manager_new ();
+
+ gtk_ui_manager_insert_action_group (ui, staticActions, 0);
+ gtk_ui_manager_insert_action_group (ui, documentActions, 1);
+ //gtk_ui_manager_insert_action_group (ui, documents_list_menu_actions, 3);
+
+ gtk_window_add_accel_group ([window gtkWindow], gtk_ui_manager_get_accel_group (ui));
+
+ if (!gtk_ui_manager_add_ui_from_string (ui, ui_info, -1, &error))
+ {
+ g_message ("Building menus failed: %s", error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ /* Set custom images for tool mode buttons */
+ set_tool_button_image (GTK_TOOL_BUTTON (gtk_ui_manager_get_widget (ui, "/ToolBar/SelectMode")), &select_rectangular);
+ set_tool_button_image (GTK_TOOL_BUTTON (gtk_ui_manager_get_widget (ui, "/ToolBar/CreateNodeMode")), &draw_ellipse);
+ set_tool_button_image (GTK_TOOL_BUTTON (gtk_ui_manager_get_widget (ui, "/ToolBar/DrawEdgeMode")), &draw_path);
+ set_tool_button_image (GTK_TOOL_BUTTON (gtk_ui_manager_get_widget (ui, "/ToolBar/BoundingBoxMode")), &transform_crop_and_resize);
+ set_tool_button_image (GTK_TOOL_BUTTON (gtk_ui_manager_get_widget (ui, "/ToolBar/HandMode")), &transform_move);
+
+ /* Save the undo and redo actions so they can be updated */
+ undoAction = gtk_action_group_get_action (documentActions, "Undo");
+ redoAction = gtk_action_group_get_action (documentActions, "Redo");
+ pasteAction = gtk_action_group_get_action (documentActions, "Paste");
+
+ /* Recent items */
+ GtkWidget *recentMenu = create_recent_chooser_menu();
+ GtkMenuItem *recentMenuItem = GTK_MENU_ITEM (gtk_ui_manager_get_widget (ui, "/MenuBar/FileMenu/OpenRecent"));
+ gtk_menu_item_set_submenu (recentMenuItem, recentMenu);
+ g_signal_connect (recentMenu, "item-activated", G_CALLBACK (recent_chooser_item_activated_cb), window);
+
+ nodeSelBasedActionCount = 4;
+ nodeSelBasedActions = g_new (GtkAction*, nodeSelBasedActionCount);
+ nodeSelBasedActions[0] = gtk_action_group_get_action (documentActions, "Cut");
+ nodeSelBasedActions[1] = gtk_action_group_get_action (documentActions, "Copy");
+ nodeSelBasedActions[2] = gtk_action_group_get_action (documentActions, "FlipHoriz");
+ nodeSelBasedActions[3] = gtk_action_group_get_action (documentActions, "FlipVert");
+ selBasedActionCount = 2;
+ selBasedActions = g_new (GtkAction*, selBasedActionCount);
+ selBasedActions[0] = gtk_action_group_get_action (documentActions, "Delete");
+ selBasedActions[1] = gtk_action_group_get_action (documentActions, "DeselectAll");
+
+ Configuration *configFile = [window mainConfiguration];
+ if ([configFile hasKey:@"toolbarStyle" inGroup:@"UI"]) {
+ int value = [configFile integerEntry:@"toolbarStyle" inGroup:@"UI"];
+ gtk_radio_action_set_current_value (
+ GTK_RADIO_ACTION (gtk_action_group_get_action (staticActions, "ToolbarIconsOnly")),
+ value);
+ }
+
+ return self;
+}
+
+- (GtkWidget*) menubar {
+ return gtk_ui_manager_get_widget (ui, "/MenuBar");
+}
+
+- (GtkWidget*) toolbar {
+ return gtk_ui_manager_get_widget (ui, "/ToolBar");
+}
+
+- (MainWindow*) mainWindow {
+ return mainWindow;
+}
+
+- (void) setUndoActionEnabled:(BOOL)enabled {
+ gtk_action_set_sensitive (undoAction, enabled);
+}
+
+- (void) setUndoActionDetail:(NSString*)detail {
+ gtk_action_set_detailed_label (undoAction, "_Undo", [detail UTF8String]);
+}
+
+- (void) setRedoActionEnabled:(BOOL)enabled {
+ gtk_action_set_sensitive (redoAction, enabled);
+}
+
+- (void) setRedoActionDetail:(NSString*)detail {
+ gtk_action_set_detailed_label (redoAction, "_Redo", [detail UTF8String]);
+}
+
+- (GtkAction*) pasteAction {
+ return pasteAction;
+}
+
+- (void) notifySelectionChanged:(PickSupport*)pickSupport {
+ BOOL hasSelectedNodes = [[pickSupport selectedNodes] count] > 0;
+ BOOL hasSelectedEdges = [[pickSupport selectedEdges] count] > 0;
+ for (int i = 0; i < nodeSelBasedActionCount; ++i) {
+ if (nodeSelBasedActions[i]) {
+ gtk_action_set_sensitive (nodeSelBasedActions[i], hasSelectedNodes);
+ }
+ }
+ for (int i = 0; i < selBasedActionCount; ++i) {
+ if (selBasedActions[i]) {
+ gtk_action_set_sensitive (selBasedActions[i], hasSelectedNodes || hasSelectedEdges);
+ }
+ }
+}
+
+- (void) dealloc {
+ g_free (nodeSelBasedActions);
+ g_free (selBasedActions);
+
+ [super dealloc];
+}
+
+@end
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/NSError+Glib.h b/tikzit/src/gtk/NSError+Glib.h
new file mode 100644
index 0000000..137977e
--- /dev/null
+++ b/tikzit/src/gtk/NSError+Glib.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+#import <glib.h>
+
+@interface NSError(Glib)
++ (id) errorWithGError:(GError*)gerror;
+@end
+
+void GErrorToNSError(GError *errorIn, NSError **errorOut);
+void logGError (GError *error, NSString *message);
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NSError+Glib.m b/tikzit/src/gtk/NSError+Glib.m
new file mode 100644
index 0000000..f466d9e
--- /dev/null
+++ b/tikzit/src/gtk/NSError+Glib.m
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NSError+Glib.h"
+#import "TZFoundation.h"
+
+@implementation NSError(Glib)
++ (id) errorWithGError:(GError*)gerror {
+ if (!gerror)
+ return nil;
+
+ NSString *message = [NSString stringWithUTF8String:gerror->message];
+ NSString *domain = [NSString stringWithUTF8String:g_quark_to_string(gerror->domain)];
+
+ NSMutableDictionary *errorDetail = [NSMutableDictionary dictionaryWithObject:message
+ forKey:NSLocalizedDescriptionKey];
+ return [self errorWithDomain:domain code:gerror->code userInfo:errorDetail];
+}
+@end
+
+void GErrorToNSError(GError *errorIn, NSError **errorOut)
+{
+ if (errorOut && errorIn) {
+ *errorOut = [NSError errorWithGError:errorIn];
+ }
+}
+
+void logGError (GError *error, NSString *message) {
+ if (message == nil) {
+ NSLog (@"%s", error->message);
+ } else {
+ NSLog (@"%@: %s", message, error->message);
+ }
+}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NSFileManager+Glib.h b/tikzit/src/gtk/NSFileManager+Glib.h
new file mode 100644
index 0000000..cb49fcb
--- /dev/null
+++ b/tikzit/src/gtk/NSFileManager+Glib.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface NSFileManager(Glib)
+/**
+ * Creates a directory in the system temp directory
+ */
+- (NSString*) createTempDirectoryWithError:(NSError**)error;
+/**
+ * Creates a directory in the system temp directory
+ */
+- (NSString*) createTempDirectory;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NSFileManager+Glib.m b/tikzit/src/gtk/NSFileManager+Glib.m
new file mode 100644
index 0000000..b3e9de6
--- /dev/null
+++ b/tikzit/src/gtk/NSFileManager+Glib.m
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NSFileManager+Glib.h"
+#import "TZFoundation.h"
+#import "mkdtemp.h"
+
+@implementation NSFileManager(Glib)
+
+- (NSString*) createTempDirectoryWithError:(NSError**)error {
+ NSString *result = nil;
+#if GLIB_CHECK_VERSION (2, 30, 0)
+ GError *gerror = NULL;
+ gchar *dir = g_dir_make_tmp ("tikzitXXXXXX", &gerror);
+ GErrorToNSError (gerror, error);
+ if (dir)
+ result = [NSString stringWithGlibFilename:dir];
+ g_free (dir);
+#else
+//#if (!GLIB_CHECK_VERSION (2, 26, 0))
+#define g_mkdtemp mkdtemp
+//#endif
+ gchar *dir = g_build_filename (g_get_tmp_dir(), "tikzitXXXXXX", NULL);
+ gchar *rdir = g_mkdtemp (dir);
+ if (rdir) {
+ result = [NSString stringWithGlibFilename:dir];
+ } else if (error) {
+ *error = [NSError errorWithLibcError:errno];
+ }
+ g_free (dir);
+#endif
+ return result;
+}
+
+- (NSString*) createTempDirectory {
+ return [self createTempDirectoryWithError:NULL];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NSString+Glib.h b/tikzit/src/gtk/NSString+Glib.h
new file mode 100644
index 0000000..ac59833
--- /dev/null
+++ b/tikzit/src/gtk/NSString+Glib.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+#import <glib.h>
+
+@interface NSString(Glib)
+/**
+ * Initialise a string with a string in the GLib filename encoding
+ */
+- (id) initWithGlibFilename:(const gchar *)filename;
+/**
+ * Create a string from a string in the GLib filename encoding
+ */
++ (id) stringWithGlibFilename:(const gchar *)filename;
+/**
+ * Get a copy of the string in GLib filename encoding.
+ *
+ * This will need to be freed with g_free.
+ */
+- (gchar*)glibFilename;
+/**
+ * Get a copy of the string as a GLib URI
+ *
+ * This will need to be freed with g_free.
+ */
+- (gchar*)glibUriWithError:(NSError**)error;
+/**
+ * Get a copy of the string as a GLib URI
+ *
+ * This will need to be freed with g_free.
+ */
+- (gchar*)glibUri;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NSString+Glib.m b/tikzit/src/gtk/NSString+Glib.m
new file mode 100644
index 0000000..b6dc765
--- /dev/null
+++ b/tikzit/src/gtk/NSString+Glib.m
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NSString+Glib.h"
+#import "TZFoundation.h"
+
+@implementation NSString(Glib)
++ (id) stringWithGlibFilename:(const gchar *)filename {
+ return [[[self alloc] initWithGlibFilename:filename] autorelease];
+}
+
+- (id) initWithGlibFilename:(const gchar *)filename {
+ if (self == nil) {
+ return nil;
+ }
+
+ if (filename == NULL) {
+ [self release];
+ return nil;
+ }
+
+ GError *error = NULL;
+ gchar *utf8file = g_filename_to_utf8 (filename, -1, NULL, NULL, &error);
+ if (utf8file == NULL) {
+ if (error)
+ logGError (error, @"Failed to convert a GLib filename to UTF8");
+ [self release];
+ return nil;
+ }
+
+ self = [self initWithUTF8String:utf8file];
+ g_free (utf8file);
+
+ return self;
+}
+
+- (gchar*)glibFilenameWithError:(NSError**)error {
+ GError *gerror = NULL;
+ gchar *result = g_filename_from_utf8 ([self UTF8String], -1, NULL, NULL, &gerror);
+ GErrorToNSError (gerror, error);
+ if (gerror) {
+ logGError (gerror, @"Failed to convert a UTF8 string to a GLib filename");
+ }
+ return result;
+}
+
+- (gchar*)glibFilename {
+ return [self glibFilenameWithError:NULL];
+}
+
+- (gchar*)glibUriWithError:(NSError**)error {
+ gchar *filepath;
+ gchar *uri;
+ NSError *cause = nil;
+
+ filepath = [self glibFilenameWithError:&cause];
+ if (!filepath) {
+ if (error) {
+ NSString *message = [NSString stringWithFormat:@"Could not convert \"%@\" to the GLib filename encoding", self];
+ *error = [NSError errorWithMessage:message code:TZ_ERR_OTHER cause:cause];
+ }
+ return NULL;
+ }
+
+ GError *gerror = NULL;
+ GError **gerrorptr = error ? &gerror : NULL;
+ uri = g_filename_to_uri (filepath, NULL, gerrorptr);
+ if (!uri && error) {
+ NSString *message = [NSString stringWithFormat:@"Could not convert \"%@\" to a GLib URI", self];
+ *error = [NSError errorWithMessage:message code:TZ_ERR_BADFORMAT cause:[NSError errorWithGError:gerror]];
+ }
+ g_free (filepath);
+ return uri;
+}
+
+- (gchar*)glibUri {
+ return [self glibUriWithError:NULL];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Node+Render.h b/tikzit/src/gtk/Node+Render.h
new file mode 100644
index 0000000..60d2573
--- /dev/null
+++ b/tikzit/src/gtk/Node+Render.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "Node.h"
+#import "RenderContext.h"
+#import "Surface.h"
+
+enum NodeState {
+ NodeNormal,
+ NodeSelected,
+ NodeHighlighted
+};
+
+@interface Node(Render)
+
+- (Transformer*) shapeTransformerForSurface:(id<Surface>)surface;
+// the total rendered bounds, excluding label
+- (NSRect) renderBoundsForSurface:(id<Surface>)surface;
+- (NSRect) renderBoundsWithLabelForSurface:(id<Surface>)surface;
+- (NSString*) renderedLabel;
+- (NSSize) renderedLabelSizeInContext:(id<RenderContext>)context;
+- (void) renderLabelToSurface:(id<Surface>)surface withContext:(id<RenderContext>)context;
+- (void) renderLabelAt:(NSPoint)point withContext:(id<RenderContext>)context;
+- (void) renderToSurface:(id<Surface>)surface withContext:(id<RenderContext>)context state:(enum NodeState)state;
+- (BOOL) hitByPoint:(NSPoint)p onSurface:(id<Surface>)surface;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Node+Render.m b/tikzit/src/gtk/Node+Render.m
new file mode 100644
index 0000000..907d818
--- /dev/null
+++ b/tikzit/src/gtk/Node+Render.m
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Node+Render.h"
+#import "Shape.h"
+#import "Shape+Render.h"
+#import "ShapeNames.h"
+
+#define MAX_LABEL_LENGTH 10
+#define LABEL_PADDING_X 2
+#define LABEL_PADDING_Y 2
+
+@implementation Node (Render)
+
+- (Shape*) shapeToRender {
+ if (style) {
+ return [Shape shapeForName:[style shapeName]];
+ } else {
+ return [Shape shapeForName:SHAPE_CIRCLE];
+ }
+}
+
+- (Transformer*) shapeTransformerForSurface:(id<Surface>)surface {
+ return [self shapeTransformerFromTransformer:[surface transformer]];
+}
+
+- (NSRect) renderBoundsUsingShapeTransform:(Transformer*)shapeTrans {
+ float strokeThickness = style ? [style strokeThickness] : [NodeStyle defaultStrokeThickness];
+ NSRect screenBounds = [shapeTrans rectToScreen:[[self shapeToRender] boundingRect]];
+ screenBounds = NSInsetRect(screenBounds, -strokeThickness, -strokeThickness);
+ return screenBounds;
+}
+
+- (NSRect) renderBoundsForSurface:(id<Surface>)surface {
+ return [self renderBoundsUsingShapeTransform:[self shapeTransformerForSurface:surface]];
+}
+
+- (NSRect) renderBoundsWithLabelForSurface:(id<Surface>)surface {
+ NSRect nodeBounds = [self renderBoundsForSurface:surface];
+ NSRect labelRect = NSZeroRect;
+ if (![label isEqual:@""]) {
+ id<RenderContext> cr = [surface createRenderContext];
+ labelRect.size = [self renderedLabelSizeInContext:cr];
+ NSPoint nodePos = [[surface transformer] toScreen:point];
+ labelRect.origin.x = nodePos.x - (labelRect.size.width / 2);
+ labelRect.origin.y = nodePos.y - (labelRect.size.height / 2);
+ }
+ return NSUnionRect(nodeBounds, labelRect);
+}
+
+- (RColor) strokeColor {
+ if (style) {
+ return [[style strokeColorRGB] rColor];
+ } else {
+ return MakeRColor (0.4, 0.4, 0.7, 0.8);
+ }
+}
+
+- (RColor) fillColor {
+ if (style) {
+ return [[style fillColorRGB] rColor];
+ } else {
+ return MakeRColor (0.4, 0.4, 0.7, 0.3);
+ }
+}
+
+- (NSString*) renderedLabel {
+ NSString *r_label = [label stringByExpandingLatexConstants];
+ if ([r_label length] > MAX_LABEL_LENGTH) {
+ r_label = [[[r_label substringToIndex:MAX_LABEL_LENGTH-1] stringByTrimmingSpaces] stringByAppendingString:@"..."];
+ } else {
+ r_label = [r_label stringByTrimmingSpaces];
+ }
+ return r_label;
+}
+
+- (NSSize) renderedLabelSizeInContext:(id<RenderContext>)context {
+ NSSize result = {0, 0};
+ if (![label isEqual:@""]) {
+ NSString *r_label = [self renderedLabel];
+
+ id<TextLayout> layout = [context layoutText:r_label withSize:9];
+
+ result = [layout size];
+ result.width += LABEL_PADDING_X;
+ result.height += LABEL_PADDING_Y;
+ }
+ return result;
+}
+
+- (void) renderLabelToSurface:(id <Surface>)surface withContext:(id<RenderContext>)context {
+ [self renderLabelAt:[[surface transformer] toScreen:point] withContext:context];
+}
+
+- (void) renderLabelAt:(NSPoint)p withContext:(id<RenderContext>)context {
+ // draw latex code overlayed on node
+ if (![label isEqual:@""]) {
+ [context saveState];
+
+ NSString *r_label = [self renderedLabel];
+ id<TextLayout> layout = [context layoutText:r_label withSize:9];
+
+ NSSize labelSize = [layout size];
+
+ NSRect textBounds = NSMakeRect (p.x - labelSize.width/2,
+ p.y - labelSize.height/2,
+ labelSize.width,
+ labelSize.height);
+ NSRect backRect = NSInsetRect (textBounds, -LABEL_PADDING_X, -LABEL_PADDING_Y);
+
+ [context startPath];
+ [context setLineWidth:1.0];
+ [context rect:backRect];
+ RColor fColor = MakeRColor (1.0, 1.0, 0.5, 0.7);
+ RColor sColor = MakeRColor (0.5, 0.0, 0.0, 0.7);
+ [context strokePathWithColor:sColor andFillWithColor:fColor];
+
+ [layout showTextAt:textBounds.origin withColor:BlackRColor];
+
+ [context restoreState];
+ }
+}
+
+- (void) renderToSurface:(id <Surface>)surface withContext:(id<RenderContext>)context state:(enum NodeState)state {
+ Transformer *shapeTrans = [self shapeTransformerForSurface:surface];
+ float strokeThickness = style ? [style strokeThickness] : [NodeStyle defaultStrokeThickness];
+
+ [context saveState];
+
+ [[self shapeToRender] drawPathWithTransform:shapeTrans andContext:context];
+
+ [context setLineWidth:strokeThickness];
+ if (!style) {
+ [context setLineDash:3.0];
+ }
+ [context strokePathWithColor:[self strokeColor] andFillWithColor:[self fillColor]];
+
+ if (state != NodeNormal) {
+ [context setLineWidth:strokeThickness + 4.0];
+ [context setLineDash:0.0];
+ float alpha = 0.0f;
+ if (state == NodeSelected)
+ alpha = 0.5f;
+ else if (state == NodeHighlighted)
+ alpha = 0.25f;
+ RColor selectionColor = MakeSolidRColor(0.61f, 0.735f, 1.0f);
+
+ [[self shapeToRender] drawPathWithTransform:shapeTrans andContext:context];
+ [context strokePathWithColor:selectionColor andFillWithColor:selectionColor usingAlpha:alpha];
+ }
+
+ [context restoreState];
+ [self renderLabelToSurface:surface withContext:context];
+}
+
+- (BOOL) hitByPoint:(NSPoint)p onSurface:(id<Surface>)surface {
+ Transformer *shapeTrans = [self shapeTransformerForSurface:surface];
+
+ NSRect screenBounds = [self renderBoundsUsingShapeTransform:shapeTrans];
+ if (!NSPointInRect(p, screenBounds)) {
+ return NO;
+ }
+
+ float strokeThickness = style ? [style strokeThickness] : [NodeStyle defaultStrokeThickness];
+ id<RenderContext> ctx = [surface createRenderContext];
+ [ctx setLineWidth:strokeThickness];
+ [[self shapeToRender] drawPathWithTransform:shapeTrans andContext:ctx];
+ return [ctx strokeIncludesPoint:p] || [ctx fillIncludesPoint:p];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyle+Gtk.h b/tikzit/src/gtk/NodeStyle+Gtk.h
new file mode 100644
index 0000000..4fa5edd
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyle+Gtk.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "NodeStyle.h"
+#import <gtk/gtk.h>
+
+@interface NodeStyle (Gtk)
+
+- (GdkColor) strokeColor;
+- (void) setStrokeColor:(GdkColor)color;
+- (GdkColor) fillColor;
+- (void) setFillColor:(GdkColor)color;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyle+Gtk.m b/tikzit/src/gtk/NodeStyle+Gtk.m
new file mode 100644
index 0000000..1954def
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyle+Gtk.m
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NodeStyle+Gtk.h"
+#import "ColorRGB+Gtk.h"
+
+@implementation NodeStyle (Gtk)
+
+- (GdkColor) strokeColor {
+ return [[self strokeColorRGB] gdkColor];
+}
+
+- (void) setStrokeColor:(GdkColor)color {
+ [self setStrokeColorRGB:[ColorRGB colorWithGdkColor:color]];
+}
+
+- (GdkColor) fillColor {
+ return [[self fillColorRGB] gdkColor];
+}
+
+- (void) setFillColor:(GdkColor)color {
+ [self setFillColorRGB:[ColorRGB colorWithGdkColor:color]];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyle+Render.h b/tikzit/src/gtk/NodeStyle+Render.h
new file mode 100644
index 0000000..00edd27
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyle+Render.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "NodeStyle.h"
+#import "RenderContext.h"
+#import "Surface.h"
+
+@interface NodeStyle (Render)
+
+- (void) renderToSurface:(id<Surface>)surface withContext:(id<RenderContext>)context at:(NSPoint)p;
+- (BOOL) hitByPoint:(NSPoint)p onSurface:(id<Surface>)surface;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyle+Storage.h b/tikzit/src/gtk/NodeStyle+Storage.h
new file mode 100644
index 0000000..7649414
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyle+Storage.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "NodeStyle.h"
+#import "Configuration.h"
+
+@interface NodeStyle (Storage)
+
+- (id) initFromConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile;
+- (void) storeToConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyle+Storage.m b/tikzit/src/gtk/NodeStyle+Storage.m
new file mode 100644
index 0000000..088b062
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyle+Storage.m
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ * Copyright 2010 Chris Heunen
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NodeStyle+Storage.h"
+#import "ColorRGB+IntegerListStorage.h"
+
+@implementation NodeStyle (Storage)
+
+- (id) initFromConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile {
+ self = [self init];
+
+ if (self) {
+ [self setName:[configFile stringEntry:@"Name" inGroup:groupName withDefault:name]];
+ [self setCategory:[configFile stringEntry:@"Category" inGroup:groupName withDefault:category]];
+ [self setShapeName:[configFile stringEntry:@"ShapeName" inGroup:groupName withDefault:shapeName]];
+ [self setScale:[configFile doubleEntry:@"Scale" inGroup:groupName withDefault:scale]];
+ [self setStrokeThickness:[configFile integerEntry:@"StrokeThickness"
+ inGroup:groupName
+ withDefault:strokeThickness]];
+ [self setStrokeColorRGB:
+ [ColorRGB colorFromValueList:
+ [configFile integerListEntry:@"StrokeColor"
+ inGroup:groupName
+ withDefault:[strokeColorRGB valueList]]]];
+ [self setFillColorRGB:
+ [ColorRGB colorFromValueList:
+ [configFile integerListEntry:@"FillColor"
+ inGroup:groupName
+ withDefault:[fillColorRGB valueList]]]];
+ }
+
+ return self;
+}
+
+- (void) storeToConfigurationGroup:(NSString*)groupName config:(Configuration*)configFile {
+ [configFile setStringEntry:@"Name" inGroup:groupName value:[self name]];
+ [configFile setStringEntry:@"Category" inGroup:groupName value:[self category]];
+ [configFile setStringEntry:@"ShapeName" inGroup:groupName value:[self shapeName]];
+ [configFile setDoubleEntry:@"Scale" inGroup:groupName value:[self scale]];
+ [configFile setIntegerEntry:@"StrokeThickness" inGroup:groupName value:[self strokeThickness]];
+ [configFile setIntegerListEntry:@"StrokeColor" inGroup:groupName value:[[self strokeColorRGB] valueList]];
+ [configFile setIntegerListEntry:@"FillColor" inGroup:groupName value:[[self fillColorRGB] valueList]];
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyleEditor.h b/tikzit/src/gtk/NodeStyleEditor.h
new file mode 100644
index 0000000..9bde203
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyleEditor.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class NodeStyle;
+
+@interface NodeStyleEditor: NSObject {
+ NodeStyle *style;
+ GtkTable *table;
+ GtkEntry *nameEdit;
+ GtkComboBox *shapeNameCombo;
+ GtkColorButton *strokeColorButton;
+ GtkWidget *makeStrokeTexSafeButton;
+ GtkColorButton *fillColorButton;
+ GtkWidget *makeFillTexSafeButton;
+ GtkAdjustment *scaleAdj;
+ BOOL blockSignals;
+}
+
+@property (retain) NodeStyle *style;
+@property (readonly) GtkWidget *widget;
+
+- (id) init;
+
+@end
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/NodeStyleEditor.m b/tikzit/src/gtk/NodeStyleEditor.m
new file mode 100644
index 0000000..febbf3c
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyleEditor.m
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NodeStyleEditor.h"
+#import "NodeStyle.h"
+#import "NodeStyle+Gtk.h"
+#import "Shape.h"
+
+static const guint row_count = 5;
+
+// {{{ Internal interfaces
+// {{{ GTK+ Callbacks
+static void style_name_edit_changed_cb (GtkEditable *widget, NodeStyleEditor *editor);
+static void style_shape_combo_changed_cb (GtkComboBox *widget, NodeStyleEditor *editor);
+static void stroke_color_changed_cb (GtkColorButton *widget, NodeStyleEditor *editor);
+static void fill_color_changed_cb (GtkColorButton *widget, NodeStyleEditor *editor);
+static void make_stroke_safe_button_clicked_cb (GtkButton *widget, NodeStyleEditor *editor);
+static void make_fill_safe_button_clicked_cb (GtkButton *widget, NodeStyleEditor *editor);
+static void scale_adjustment_changed_cb (GtkAdjustment *widget, NodeStyleEditor *editor);
+// }}}
+// {{{ Notifications
+
+@interface NodeStyleEditor (Notifications)
+- (void) shapeDictionaryReplaced:(NSNotification*)n;
+- (void) nameChangedTo:(NSString*)value;
+- (void) shapeNameChangedTo:(NSString*)value;
+- (void) strokeColorChangedTo:(GdkColor)value;
+- (void) makeStrokeColorTexSafe;
+- (void) fillColorChangedTo:(GdkColor)value;
+- (void) makeFillColorTexSafe;
+- (void) scaleChangedTo:(double)value;
+@end
+
+// }}}
+// {{{ Private
+
+@interface NodeStyleEditor (Private)
+- (void) loadShapeNames;
+- (void) setActiveShapeName:(NSString*)name;
+@end
+
+// }}}
+// }}}
+// {{{ API
+
+@implementation NodeStyleEditor
+
+- (void) _addWidget:(GtkWidget*)w withLabel:(gchar *)label atRow:(guint)row {
+ NSAssert(row < row_count, @"row_count is wrong!");
+
+ GtkWidget *l = gtk_label_new (label);
+ gtk_misc_set_alignment (GTK_MISC (l), 0, 0.5);
+ gtk_widget_show (l);
+ gtk_widget_show (w);
+
+ gtk_table_attach (table, l,
+ 0, 1, row, row+1, // l, r, t, b
+ GTK_FILL, // x opts
+ GTK_FILL | GTK_EXPAND, // y opts
+ 5, // x padding
+ 0); // y padding
+
+ gtk_table_attach (table, w,
+ 1, 2, row, row+1, // l, r, t, b
+ GTK_FILL | GTK_EXPAND, // x opts
+ GTK_FILL | GTK_EXPAND, // y opts
+ 0, // x padding
+ 0); // y padding
+}
+
+- (GtkWidget*) _createMakeColorTexSafeButton:(NSString*)type {
+ GtkWidget *b = gtk_button_new ();
+ GtkWidget *icon = gtk_image_new_from_stock (GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_BUTTON);
+ gtk_widget_show (icon);
+ gtk_container_add (GTK_CONTAINER (b), icon);
+ NSString *ttip = [NSString stringWithFormat:@"The %@ colour is not a predefined TeX colour.\nClick here to choose the nearest TeX-safe colour.", type];
+ gtk_widget_set_tooltip_text (b, [ttip UTF8String]);
+ return b;
+}
+
+- (id) init {
+ self = [super init];
+
+ if (self != nil) {
+ style = nil;
+ table = GTK_TABLE (gtk_table_new (row_count, 2, FALSE));
+ gtk_table_set_col_spacings (table, 6);
+ gtk_table_set_row_spacings (table, 6);
+ gtk_widget_set_sensitive (GTK_WIDGET (table), FALSE);
+ blockSignals = NO;
+
+ /**
+ * Name
+ */
+ nameEdit = GTK_ENTRY (gtk_entry_new ());
+ g_object_ref_sink (nameEdit);
+ [self _addWidget:GTK_WIDGET (nameEdit)
+ withLabel:"Name"
+ atRow:0];
+ g_signal_connect (G_OBJECT (nameEdit),
+ "changed",
+ G_CALLBACK (style_name_edit_changed_cb),
+ self);
+
+
+ /**
+ * Shape
+ */
+ GtkListStore *store = gtk_list_store_new (1, G_TYPE_STRING);
+ shapeNameCombo = GTK_COMBO_BOX (gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)));
+ GtkCellRenderer *cellRend = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (shapeNameCombo),
+ cellRend,
+ TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (shapeNameCombo), cellRend, "text", 0);
+ g_object_ref_sink (shapeNameCombo);
+ [self _addWidget:GTK_WIDGET (shapeNameCombo)
+ withLabel:"Shape"
+ atRow:1];
+ g_signal_connect (G_OBJECT (shapeNameCombo),
+ "changed",
+ G_CALLBACK (style_shape_combo_changed_cb),
+ self);
+
+
+ /**
+ * Stroke colour
+ */
+ GtkWidget *strokeBox = gtk_hbox_new (FALSE, 0);
+ [self _addWidget:strokeBox
+ withLabel:"Stroke colour"
+ atRow:2];
+ strokeColorButton = GTK_COLOR_BUTTON (gtk_color_button_new ());
+ g_object_ref_sink (strokeColorButton);
+ gtk_widget_show (GTK_WIDGET (strokeColorButton));
+ gtk_box_pack_start (GTK_BOX (strokeBox), GTK_WIDGET (strokeColorButton),
+ FALSE, FALSE, 0);
+ makeStrokeTexSafeButton = [self _createMakeColorTexSafeButton:@"stroke"];
+ g_object_ref_sink (makeStrokeTexSafeButton);
+ gtk_box_pack_start (GTK_BOX (strokeBox), makeStrokeTexSafeButton,
+ FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (strokeColorButton),
+ "color-set",
+ G_CALLBACK (stroke_color_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (makeStrokeTexSafeButton),
+ "clicked",
+ G_CALLBACK (make_stroke_safe_button_clicked_cb),
+ self);
+
+
+ /**
+ * Fill colour
+ */
+ GtkWidget *fillBox = gtk_hbox_new (FALSE, 0);
+ [self _addWidget:fillBox
+ withLabel:"Fill colour"
+ atRow:3];
+ fillColorButton = GTK_COLOR_BUTTON (gtk_color_button_new ());
+ g_object_ref_sink (fillColorButton);
+ gtk_widget_show (GTK_WIDGET (fillColorButton));
+ gtk_box_pack_start (GTK_BOX (fillBox), GTK_WIDGET (fillColorButton),
+ FALSE, FALSE, 0);
+ makeFillTexSafeButton = [self _createMakeColorTexSafeButton:@"fill"];
+ g_object_ref_sink (makeFillTexSafeButton);
+ gtk_box_pack_start (GTK_BOX (fillBox), makeFillTexSafeButton,
+ FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (fillColorButton),
+ "color-set",
+ G_CALLBACK (fill_color_changed_cb),
+ self);
+ g_signal_connect (G_OBJECT (makeFillTexSafeButton),
+ "clicked",
+ G_CALLBACK (make_fill_safe_button_clicked_cb),
+ self);
+
+
+ /**
+ * Scale
+ */
+ scaleAdj = GTK_ADJUSTMENT (gtk_adjustment_new (
+ 1.0, // value
+ 0.0, // lower
+ 50.0, // upper
+ 0.20, // step
+ 1.0, // page
+ 0.0)); // (irrelevant)
+ g_object_ref_sink (scaleAdj);
+ GtkWidget *scaleSpin = gtk_spin_button_new (scaleAdj, 0.0, 2);
+ [self _addWidget:scaleSpin
+ withLabel:"Scale"
+ atRow:4];
+ g_signal_connect (G_OBJECT (scaleAdj),
+ "value-changed",
+ G_CALLBACK (scale_adjustment_changed_cb),
+ self);
+
+ [self loadShapeNames];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(shapeDictionaryReplaced:)
+ name:@"ShapeDictionaryReplaced"
+ object:[Shape class]];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ g_object_unref (nameEdit);
+ g_object_unref (shapeNameCombo);
+ g_object_unref (strokeColorButton);
+ g_object_unref (makeStrokeTexSafeButton);
+ g_object_unref (fillColorButton);
+ g_object_unref (makeFillTexSafeButton);
+ g_object_unref (scaleAdj);
+ g_object_unref (table);
+ [style release];
+
+ [super dealloc];
+}
+
+- (NodeStyle*) style {
+ return style;
+}
+
+- (void) setStyle:(NodeStyle*)s {
+ blockSignals = YES;
+ NodeStyle *oldStyle = style;
+ style = [s retain];
+
+ if (style != nil) {
+ gtk_widget_set_sensitive (GTK_WIDGET (table), TRUE);
+
+ gtk_entry_set_text(nameEdit, [[style name] UTF8String]);
+
+ [self setActiveShapeName:[style shapeName]];
+
+ GdkColor c = [style strokeColor];
+ gtk_color_button_set_color(strokeColorButton, &c);
+
+ gtk_widget_set_visible (makeStrokeTexSafeButton, ([[style strokeColorRGB] name] == nil));
+
+ c = [style fillColor];
+ gtk_color_button_set_color(fillColorButton, &c);
+
+ gtk_widget_set_visible (makeFillTexSafeButton, ([[style fillColorRGB] name] == nil));
+
+ gtk_adjustment_set_value(scaleAdj, [style scale]);
+ } else {
+ gtk_entry_set_text(nameEdit, "");
+ [self setActiveShapeName:nil];
+ gtk_widget_set_visible (makeStrokeTexSafeButton, FALSE);
+ gtk_widget_set_visible (makeFillTexSafeButton, FALSE);
+ gtk_adjustment_set_value(scaleAdj, 1.0);
+ gtk_widget_set_sensitive (GTK_WIDGET (table), FALSE);
+ }
+
+ [oldStyle release];
+ blockSignals = NO;
+}
+
+- (GtkWidget*) widget {
+ return GTK_WIDGET (table);
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation NodeStyleEditor (Notifications)
+- (void) shapeDictionaryReplaced:(NSNotification*)n {
+ blockSignals = YES;
+
+ [self loadShapeNames];
+ [self setActiveShapeName:[style shapeName]];
+
+ blockSignals = NO;
+}
+
+- (void) nameChangedTo:(NSString*)value {
+ [style setName:value];
+}
+
+- (void) shapeNameChangedTo:(NSString*)value {
+ [style setShapeName:value];
+}
+
+- (void) strokeColorChangedTo:(GdkColor)value {
+ [style setStrokeColor:value];
+ gtk_widget_set_visible (makeStrokeTexSafeButton,
+ [[style strokeColorRGB] name] == nil);
+}
+
+- (void) makeStrokeColorTexSafe {
+ if (style != nil) {
+ [[style strokeColorRGB] setToClosestHashed];
+ GdkColor color = [style strokeColor];
+ gtk_color_button_set_color(strokeColorButton, &color);
+ gtk_widget_set_visible (makeStrokeTexSafeButton, FALSE);
+ }
+}
+
+- (void) fillColorChangedTo:(GdkColor)value {
+ [style setFillColor:value];
+ gtk_widget_set_visible (makeFillTexSafeButton,
+ [[style fillColorRGB] name] == nil);
+}
+
+- (void) makeFillColorTexSafe {
+ if (style != nil) {
+ [[style fillColorRGB] setToClosestHashed];
+ GdkColor color = [style fillColor];
+ gtk_color_button_set_color(fillColorButton, &color);
+ gtk_widget_set_visible (makeFillTexSafeButton, FALSE);
+ }
+}
+
+- (void) scaleChangedTo:(double)value {
+ [style setScale:value];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation NodeStyleEditor (Private)
+- (BOOL) signalsBlocked { return blockSignals; }
+
+- (void) loadShapeNames {
+ blockSignals = YES;
+
+ gtk_combo_box_set_active (shapeNameCombo, -1);
+
+ GtkListStore *list = GTK_LIST_STORE (gtk_combo_box_get_model (shapeNameCombo));
+ gtk_list_store_clear (list);
+
+ NSEnumerator *en = [[Shape shapeDictionary] keyEnumerator];
+ NSString *shapeName;
+ GtkTreeIter iter;
+ while ((shapeName = [en nextObject]) != nil) {
+ gtk_list_store_append (list, &iter);
+ gtk_list_store_set (list, &iter, 0, [shapeName UTF8String], -1);
+ }
+
+ blockSignals = NO;
+}
+
+- (void) setActiveShapeName:(NSString*)name {
+ if (name == nil) {
+ gtk_combo_box_set_active (shapeNameCombo, -1);
+ return;
+ }
+ const gchar *shapeName = [name UTF8String];
+
+ GtkTreeModel *model = gtk_combo_box_get_model (shapeNameCombo);
+ GtkTreeIter iter;
+ if (gtk_tree_model_get_iter_first (model, &iter)) {
+ do {
+ gchar *rowShapeName;
+ gtk_tree_model_get (model, &iter, 0, &rowShapeName, -1);
+ if (g_strcmp0 (shapeName, rowShapeName) == 0) {
+ gtk_combo_box_set_active_iter (shapeNameCombo, &iter);
+ g_free (rowShapeName);
+ return;
+ }
+ g_free (rowShapeName);
+ } while (gtk_tree_model_iter_next (model, &iter));
+ }
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void style_name_edit_changed_cb (GtkEditable *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ const gchar *contents = gtk_entry_get_text (GTK_ENTRY (widget));
+ [editor nameChangedTo:[NSString stringWithUTF8String:contents]];
+
+ [pool drain];
+}
+
+static void style_shape_combo_changed_cb (GtkComboBox *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ GtkTreeIter iter;
+ gtk_combo_box_get_active_iter (widget, &iter);
+ gchar *shapeName = NULL;
+ gtk_tree_model_get (gtk_combo_box_get_model (widget), &iter, 0, &shapeName, -1);
+ [editor shapeNameChangedTo:[NSString stringWithUTF8String:shapeName]];
+ g_free (shapeName);
+
+ [pool drain];
+}
+
+static void stroke_color_changed_cb (GtkColorButton *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ GdkColor color;
+ gtk_color_button_get_color (widget, &color);
+ [editor strokeColorChangedTo:color];
+
+ [pool drain];
+}
+
+static void fill_color_changed_cb (GtkColorButton *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ GdkColor color;
+ gtk_color_button_get_color (widget, &color);
+ [editor fillColorChangedTo:color];
+
+ [pool drain];
+}
+
+static void make_stroke_safe_button_clicked_cb (GtkButton *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor makeStrokeColorTexSafe];
+ [pool drain];
+}
+
+static void make_fill_safe_button_clicked_cb (GtkButton *widget, NodeStyleEditor *editor) {
+ if ([editor signalsBlocked])
+ return;
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor makeFillColorTexSafe];
+ [pool drain];
+}
+
+static void scale_adjustment_changed_cb (GtkAdjustment *adj, NodeStyleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor scaleChangedTo:gtk_adjustment_get_value (adj)];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/NodeStyleSelector.h b/tikzit/src/gtk/NodeStyleSelector.h
new file mode 100644
index 0000000..894d71e
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyleSelector.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "StyleManager.h"
+
+@interface NodeStyleSelector: NSObject {
+ GtkListStore *store;
+ GtkIconView *view;
+ StyleManager *styleManager;
+ BOOL linkedToActiveStyle;
+ BOOL suppressSetActiveStyle;
+}
+
+/*!
+ @property widget
+ @brief The GTK widget
+ */
+@property (readonly) GtkWidget *widget;
+
+/*!
+ @property manager
+ @brief The StyleManager to use. Default is [StyleManager manager]
+ */
+@property (retain) StyleManager *styleManager;
+
+/*!
+ @property linkedToActiveStyles
+ @brief Whether the current selection should be the same as the
+ style manager's active style
+ */
+@property (getter=isLinkedToActiveStyle) BOOL linkedToActiveStyle;
+
+/*!
+ @property selectedStyle
+ @brief The selected style. If linkedToActiveStyle is YES, this
+ will be the same as [manager activeStyle].
+
+ When this changes, a SelectedStyleChanged notification will be posted
+ */
+@property (assign) NodeStyle *selectedStyle;
+
+/*!
+ * Initialise with the default style manager
+ */
+- (id) init;
+/*!
+ * Initialise with the given style manager
+ */
+- (id) initWithStyleManager:(StyleManager*)m;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/NodeStyleSelector.m b/tikzit/src/gtk/NodeStyleSelector.m
new file mode 100644
index 0000000..7e9757a
--- /dev/null
+++ b/tikzit/src/gtk/NodeStyleSelector.m
@@ -0,0 +1,426 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NodeStyleSelector.h"
+
+#import "CairoRenderContext.h"
+#import "Shape.h"
+#import "Shape+Render.h"
+#import "ShapeNames.h"
+#import "StyleManager.h"
+
+#import <gdk-pixbuf/gdk-pixbuf.h>
+
+// {{{ Internal interfaces
+// {{{ Signals
+static void selection_changed_cb (GtkIconView *widget, NodeStyleSelector *mgr);
+// }}}
+
+enum {
+ STYLES_NAME_COL = 0,
+ STYLES_ICON_COL,
+ STYLES_PTR_COL,
+ STYLES_N_COLS
+};
+
+@interface NodeStyleSelector (Notifications)
+- (void) stylesReplaced:(NSNotification*)notification;
+- (void) styleAdded:(NSNotification*)notification;
+- (void) styleRemoved:(NSNotification*)notification;
+- (void) activeStyleChanged:(NSNotification*)notification;
+- (void) stylePropertyChanged:(NSNotification*)notification;
+- (void) shapeDictionaryReplaced:(NSNotification*)n;
+- (void) selectionChanged;
+@end
+
+@interface NodeStyleSelector (Private)
+- (cairo_surface_t*) createNodeIconSurface;
+- (GdkPixbuf*) pixbufOfNodeInStyle:(NodeStyle*)style;
+- (GdkPixbuf*) pixbufFromSurface:(cairo_surface_t*)surface;
+- (GdkPixbuf*) pixbufOfNodeInStyle:(NodeStyle*)style usingSurface:(cairo_surface_t*)surface;
+- (void) addStyle:(NodeStyle*)style;
+- (void) postSelectedStyleChanged;
+- (void) clearModel;
+- (void) reloadStyles;
+@end
+
+// }}}
+// {{{ API
+
+@implementation NodeStyleSelector
+
+- (id) init {
+ self = [self initWithStyleManager:[StyleManager manager]];
+ return self;
+}
+
+- (id) initWithStyleManager:(StyleManager*)m {
+ self = [super init];
+
+ if (self) {
+ styleManager = nil;
+ linkedToActiveStyle = YES;
+
+ store = gtk_list_store_new (STYLES_N_COLS,
+ G_TYPE_STRING,
+ GDK_TYPE_PIXBUF,
+ G_TYPE_POINTER);
+ g_object_ref (store);
+
+ view = GTK_ICON_VIEW (gtk_icon_view_new ());
+ g_object_ref (view);
+
+ gtk_icon_view_set_model (view, GTK_TREE_MODEL (store));
+ gtk_icon_view_set_pixbuf_column (view, STYLES_ICON_COL);
+ gtk_icon_view_set_tooltip_column (view, STYLES_NAME_COL);
+ gtk_icon_view_set_selection_mode (view, GTK_SELECTION_SINGLE);
+
+ g_signal_connect (G_OBJECT (view),
+ "selection-changed",
+ G_CALLBACK (selection_changed_cb),
+ self);
+
+ [self setStyleManager:m];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(stylePropertyChanged:)
+ name:@"NodeStylePropertyChanged"
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(shapeDictionaryReplaced:)
+ name:@"ShapeDictionaryReplaced"
+ object:[Shape class]];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ g_object_unref (view);
+ [self clearModel];
+ g_object_unref (store);
+ [styleManager release];
+
+ [super dealloc];
+}
+
+- (StyleManager*) styleManager {
+ return styleManager;
+}
+
+- (void) setStyleManager:(StyleManager*)m {
+ if (m == nil) {
+ [NSException raise:NSInvalidArgumentException format:@"Style manager cannot be nil"];
+ }
+ [m retain];
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:styleManager];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(stylesReplaced:)
+ name:@"NodeStylesReplaced"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(styleAdded:)
+ name:@"NodeStyleAdded"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(styleRemoved:)
+ name:@"NodeStyleRemoved"
+ object:m];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(activeStyleChanged:)
+ name:@"ActiveNodeStyleChanged"
+ object:m];
+
+ [styleManager release];
+ styleManager = m;
+
+ [self reloadStyles];
+}
+
+- (GtkWidget*) widget {
+ return GTK_WIDGET (view);
+}
+
+- (BOOL) isLinkedToActiveStyle {
+ return linkedToActiveStyle;
+}
+
+- (void) setLinkedToActiveStyle:(BOOL)linked {
+ linkedToActiveStyle = linked;
+ if (linkedToActiveStyle) {
+ NodeStyle *style = [self selectedStyle];
+ if ([styleManager activeNodeStyle] != style) {
+ [self setSelectedStyle:[styleManager activeNodeStyle]];
+ }
+ }
+}
+
+- (NodeStyle*) selectedStyle {
+ GList *list = gtk_icon_view_get_selected_items (view);
+ if (!list) {
+ return nil;
+ }
+ if (list->next != NULL) {
+ NSLog(@"Multiple selected items in NodeStyleSelector!");
+ }
+
+ GtkTreePath *path = (GtkTreePath*) list->data;
+ GtkTreeIter iter;
+ gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path);
+ NodeStyle *style = nil;
+ gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, STYLES_PTR_COL, &style, -1);
+
+ g_list_foreach (list, (GFunc)gtk_tree_path_free, NULL);
+ g_list_free (list);
+
+ return style;
+}
+
+- (void) setSelectedStyle:(NodeStyle*)style {
+ if (style == nil) {
+ gtk_icon_view_unselect_all (view);
+ return;
+ }
+
+ GtkTreeModel *m = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (m, &row)) {
+ do {
+ NodeStyle *rowStyle;
+ gtk_tree_model_get (m, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ gtk_icon_view_unselect_all (view);
+ GtkTreePath *path = gtk_tree_model_get_path (m, &row);
+ gtk_icon_view_select_path (view, path);
+ gtk_tree_path_free (path);
+ // styleManager.activeStyle will be updated by the GTK+ callback
+ return;
+ }
+ } while (gtk_tree_model_iter_next (m, &row));
+ }
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation NodeStyleSelector (Notifications)
+
+- (void) stylesReplaced:(NSNotification*)notification {
+ [self reloadStyles];
+}
+
+- (void) styleAdded:(NSNotification*)notification {
+ [self addStyle:[[notification userInfo] objectForKey:@"style"]];
+}
+
+- (void) styleRemoved:(NSNotification*)notification {
+ NodeStyle *style = [[notification userInfo] objectForKey:@"style"];
+
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ NodeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ gtk_list_store_remove (store, &row);
+ [rowStyle release];
+ return;
+ }
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+}
+
+- (void) activeStyleChanged:(NSNotification*)notification {
+ if (linkedToActiveStyle) {
+ NodeStyle *style = [self selectedStyle];
+ if ([styleManager activeNodeStyle] != style) {
+ [self setSelectedStyle:[styleManager activeNodeStyle]];
+ }
+ }
+}
+
+- (void) stylePropertyChanged:(NSNotification*)notification {
+ NodeStyle *style = [notification object];
+
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ NodeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ if (style == rowStyle) {
+ if ([@"name" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ gtk_list_store_set (store, &row, STYLES_NAME_COL, [[style name] UTF8String], -1);
+ } else if (![@"scale" isEqual:[[notification userInfo] objectForKey:@"propertyName"]]) {
+ GdkPixbuf *pixbuf = [self pixbufOfNodeInStyle:style];
+ gtk_list_store_set (store, &row, STYLES_ICON_COL, pixbuf, -1);
+ gdk_pixbuf_unref (pixbuf);
+ }
+ }
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+}
+
+- (void) shapeDictionaryReplaced:(NSNotification*)n {
+ [self reloadStyles];
+}
+
+- (void) selectionChanged {
+ if (linkedToActiveStyle) {
+ NodeStyle *style = [self selectedStyle];
+ if ([styleManager activeNodeStyle] != style) {
+ [styleManager setActiveNodeStyle:style];
+ }
+ }
+ [self postSelectedStyleChanged];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation NodeStyleSelector (Private)
+- (cairo_surface_t*) createNodeIconSurface {
+ return cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 24, 24);
+}
+
+- (GdkPixbuf*) pixbufOfNodeInStyle:(NodeStyle*)style {
+ cairo_surface_t *surface = [self createNodeIconSurface];
+ GdkPixbuf *pixbuf = [self pixbufOfNodeInStyle:style usingSurface:surface];
+ cairo_surface_destroy (surface);
+ return pixbuf;
+}
+
+// Bring on GTK+3 and gdk_pixbuf_get_from_surface()
+- (GdkPixbuf*) pixbufFromSurface:(cairo_surface_t*)surface {
+ cairo_surface_flush (surface);
+
+ int width = cairo_image_surface_get_width (surface);
+ int height = cairo_image_surface_get_height (surface);
+ int stride = cairo_image_surface_get_stride (surface);
+ unsigned char *data = cairo_image_surface_get_data (surface);
+
+ GdkPixbuf *pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
+ TRUE,
+ 8,
+ width,
+ height);
+ unsigned char *pbdata = gdk_pixbuf_get_pixels (pixbuf);
+ int pbstride = gdk_pixbuf_get_rowstride (pixbuf);
+
+ for (int y = 0; y < height; ++y) {
+ uint32_t *line = (uint32_t*)(data + y*stride);
+ unsigned char *pbline = pbdata + (y*pbstride);
+ for (int x = 0; x < width; ++x) {
+ uint32_t pixel = *(line + x);
+ unsigned char *pbpixel = pbline + (x*4);
+ // NB: We should un-pre-mult the alpha here.
+ // However, in our world, alpha is always
+ // on or off, so it doesn't really matter
+ pbpixel[3] = ((pixel & 0xff000000) >> 24);
+ pbpixel[0] = ((pixel & 0x00ff0000) >> 16);
+ pbpixel[1] = ((pixel & 0x0000ff00) >> 8);
+ pbpixel[2] = (pixel & 0x000000ff);
+ }
+ }
+
+ return pixbuf;
+}
+
+- (GdkPixbuf*) pixbufOfNodeInStyle:(NodeStyle*)style usingSurface:(cairo_surface_t*)surface {
+ Shape *shape = [Shape shapeForName:[style shapeName]];
+
+ int width = cairo_image_surface_get_width (surface);
+ int height = cairo_image_surface_get_height (surface);
+ NSRect pixbufBounds = NSMakeRect(0.0, 0.0, width, height);
+ const CGFloat lineWidth = [style strokeThickness];
+ Transformer *shapeTrans = [Transformer transformerToFit:[shape boundingRect]
+ intoScreenRect:NSInsetRect(pixbufBounds, lineWidth, lineWidth)
+ flippedAboutXAxis:YES];
+
+ CairoRenderContext *context = [[CairoRenderContext alloc] initForSurface:surface];
+ [context clearSurface];
+ [shape drawPathWithTransform:shapeTrans andContext:context];
+ [context setLineWidth:lineWidth];
+ [context strokePathWithColor:[[style strokeColorRGB] rColor]
+ andFillWithColor:[[style fillColorRGB] rColor]];
+ [context release];
+
+ return [self pixbufFromSurface:surface];
+}
+
+- (void) addStyle:(NodeStyle*)style usingSurface:(cairo_surface_t*)surface {
+ GtkTreeIter iter;
+ gtk_list_store_append (store, &iter);
+
+ GdkPixbuf *pixbuf = [self pixbufOfNodeInStyle:style usingSurface:surface];
+ gtk_list_store_set (store, &iter,
+ STYLES_NAME_COL, [[style name] UTF8String],
+ STYLES_ICON_COL, pixbuf,
+ STYLES_PTR_COL, (gpointer)[style retain],
+ -1);
+ gdk_pixbuf_unref (pixbuf);
+}
+
+- (void) addStyle:(NodeStyle*)style {
+ cairo_surface_t *surface = [self createNodeIconSurface];
+ [self addStyle:style usingSurface:surface];
+ cairo_surface_destroy (surface);
+}
+
+- (void) postSelectedStyleChanged {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SelectedStyleChanged" object:self];
+}
+
+- (void) clearModel {
+ [self setSelectedStyle:nil];
+ GtkTreeModel *model = GTK_TREE_MODEL (store);
+ GtkTreeIter row;
+ if (gtk_tree_model_get_iter_first (model, &row)) {
+ do {
+ NodeStyle *rowStyle;
+ gtk_tree_model_get (model, &row, STYLES_PTR_COL, &rowStyle, -1);
+ [rowStyle release];
+ } while (gtk_tree_model_iter_next (model, &row));
+ }
+ gtk_list_store_clear (store);
+}
+
+- (void) reloadStyles {
+ [self clearModel];
+ for (NodeStyle *style in [styleManager nodeStyles]) {
+ [self addStyle:style];
+ }
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void selection_changed_cb (GtkIconView *view, NodeStyleSelector *mgr) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [mgr selectionChanged];
+ [pool drain];
+}
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/NodeStylesPalette.h b/tikzit/src/gtk/NodeStylesPalette.h
new file mode 100644
index 0000000..0185857
--- /dev/null
+++ b/tikzit/src/gtk/NodeStylesPalette.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class StyleManager;
+@class NodeStyleSelector;
+@class NodeStyleEditor;
+@class TikzDocument;
+
+@interface NodeStylesPalette: NSObject {
+ TikzDocument *document;
+ NodeStyleSelector *selector;
+ NodeStyleEditor *editor;
+
+ GtkWidget *palette;
+
+ GtkWidget *removeStyleButton;
+ GtkWidget *applyStyleButton;
+ GtkWidget *clearStyleButton;
+}
+
+@property (retain) StyleManager *styleManager;
+@property (retain) TikzDocument *document;
+@property (readonly) GtkWidget *widget;
+
+- (id) initWithManager:(StyleManager*)m;
+
+@end
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/NodeStylesPalette.m b/tikzit/src/gtk/NodeStylesPalette.m
new file mode 100644
index 0000000..d5ac2e1
--- /dev/null
+++ b/tikzit/src/gtk/NodeStylesPalette.m
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "NodeStylesPalette.h"
+
+#import "NodeStyleSelector.h"
+#import "NodeStyleEditor.h"
+#import "StyleManager.h"
+#import "TikzDocument.h"
+
+// {{{ Internal interfaces
+// {{{ GTK+ Callbacks
+static void add_style_button_cb (GtkButton *widget, NodeStylesPalette *palette);
+static void remove_style_button_cb (GtkButton *widget, NodeStylesPalette *palette);
+static void apply_style_button_cb (GtkButton *widget, NodeStylesPalette *palette);
+static void clear_style_button_cb (GtkButton *widget, NodeStylesPalette *palette);
+// }}}
+// {{{ Notifications
+
+@interface NodeStylesPalette (Notifications)
+- (void) selectedStyleChanged:(NSNotification*)notification;
+- (void) nodeSelectionChanged:(NSNotification*)n;
+@end
+
+// }}}
+// {{{ Private
+
+@interface NodeStylesPalette (Private)
+- (void) updateButtonState;
+- (void) removeSelectedStyle;
+- (void) applySelectedStyle;
+- (void) clearSelectedStyle;
+@end
+
+// }}}
+// }}}
+// {{{ API
+
+@implementation NodeStylesPalette
+
+@synthesize widget=palette;
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithManager:(StyleManager*)m {
+ self = [super init];
+
+ if (self) {
+ document = nil;
+ selector = [[NodeStyleSelector alloc] initWithStyleManager:m];
+ editor = [[NodeStyleEditor alloc] init];
+
+ palette = gtk_vbox_new (FALSE, 0);
+ gtk_box_set_spacing (GTK_BOX (palette), 6);
+ g_object_ref_sink (palette);
+
+ gtk_box_pack_start (GTK_BOX (palette), [editor widget], FALSE, FALSE, 0);
+ gtk_widget_show ([editor widget]);
+ GtkWidget *selectorFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (selectorFrame), [selector widget]);
+ gtk_box_pack_start (GTK_BOX (palette), selectorFrame, TRUE, TRUE, 0);
+ gtk_widget_show (selectorFrame);
+ gtk_widget_show ([selector widget]);
+
+ GtkBox *buttonBox = GTK_BOX (gtk_hbox_new(FALSE, 5));
+ gtk_box_pack_start (GTK_BOX (palette), GTK_WIDGET (buttonBox), FALSE, FALSE, 0);
+
+ GtkBox *bbox1 = GTK_BOX (gtk_hbox_new(FALSE, 0));
+ gtk_box_pack_start (buttonBox, GTK_WIDGET (bbox1), FALSE, FALSE, 0);
+
+ GtkWidget *addStyleButton = gtk_button_new ();
+ gtk_widget_set_tooltip_text (addStyleButton, "Add a new style");
+ GtkWidget *addIcon = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (addStyleButton), addIcon);
+ gtk_box_pack_start (bbox1, addStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (addStyleButton),
+ "clicked",
+ G_CALLBACK (add_style_button_cb),
+ self);
+
+ removeStyleButton = gtk_button_new ();
+ g_object_ref_sink (removeStyleButton);
+ gtk_widget_set_tooltip_text (removeStyleButton, "Delete selected style");
+ GtkWidget *removeIcon = gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (removeStyleButton), removeIcon);
+ gtk_box_pack_start (bbox1, removeStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (removeStyleButton),
+ "clicked",
+ G_CALLBACK (remove_style_button_cb),
+ self);
+
+ GtkBox *bbox2 = GTK_BOX (gtk_hbox_new(FALSE, 0));
+ gtk_box_pack_start (buttonBox, GTK_WIDGET (bbox2), FALSE, FALSE, 0);
+
+ applyStyleButton = gtk_button_new_with_label ("Apply");
+ g_object_ref_sink (applyStyleButton);
+ gtk_widget_set_tooltip_text (applyStyleButton, "Apply style to selected nodes");
+ gtk_box_pack_start (bbox2, applyStyleButton, FALSE, FALSE, 5);
+ g_signal_connect (G_OBJECT (applyStyleButton),
+ "clicked",
+ G_CALLBACK (apply_style_button_cb),
+ self);
+
+ clearStyleButton = gtk_button_new_with_label ("Clear");
+ g_object_ref_sink (clearStyleButton);
+ gtk_widget_set_tooltip_text (clearStyleButton, "Clear style from selected nodes");
+ gtk_box_pack_start (bbox2, clearStyleButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (clearStyleButton),
+ "clicked",
+ G_CALLBACK (clear_style_button_cb),
+ self);
+
+ gtk_widget_show_all (GTK_WIDGET (buttonBox));
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(selectedStyleChanged:)
+ name:@"SelectedStyleChanged"
+ object:selector];
+
+ [self updateButtonState];
+ }
+
+ return self;
+}
+
+- (StyleManager*) styleManager {
+ return [selector styleManager];
+}
+
+- (void) setStyleManager:(StyleManager*)m {
+ [selector setStyleManager:m];
+}
+
+- (TikzDocument*) document {
+ return document;
+}
+
+- (void) setDocument:(TikzDocument*)doc {
+ if (document != nil) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:nil
+ object:[document pickSupport]];
+ }
+
+ [doc retain];
+ [document release];
+ document = doc;
+
+ if (document != nil) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(nodeSelectionChanged:)
+ name:@"NodeSelectionChanged"
+ object:[document pickSupport]];
+ }
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [editor release];
+ [selector release];
+ [document release];
+ g_object_unref (palette);
+ g_object_unref (removeStyleButton);
+ g_object_unref (applyStyleButton);
+ g_object_unref (clearStyleButton);
+
+ [super dealloc];
+}
+
+@end
+
+// }}}
+// {{{ Notifications
+
+@implementation NodeStylesPalette (Notifications)
+- (void) selectedStyleChanged:(NSNotification*)notification {
+ [editor setStyle:[selector selectedStyle]];
+ [self updateButtonState];
+}
+
+- (void) nodeSelectionChanged:(NSNotification*)n {
+ [self updateButtonState];
+}
+@end
+
+// }}}
+// {{{ Private
+
+@implementation NodeStylesPalette (Private)
+- (void) updateButtonState {
+ gboolean hasNodeSelection = [[[document pickSupport] selectedNodes] count] > 0;
+ gboolean hasStyleSelection = [selector selectedStyle] != nil;
+
+ gtk_widget_set_sensitive (applyStyleButton, hasNodeSelection && hasStyleSelection);
+ gtk_widget_set_sensitive (clearStyleButton, hasNodeSelection);
+ gtk_widget_set_sensitive (removeStyleButton, hasStyleSelection);
+}
+
+- (void) removeSelectedStyle {
+ NodeStyle *style = [selector selectedStyle];
+ if (style)
+ [[selector styleManager] removeNodeStyle:style];
+}
+
+- (void) applySelectedStyle {
+ [document startModifyNodes:[[document pickSupport] selectedNodes]];
+
+ NodeStyle *style = [selector selectedStyle];
+ for (Node *node in [[document pickSupport] selectedNodes]) {
+ [node setStyle:style];
+ }
+
+ [document endModifyNodes];
+}
+
+- (void) clearSelectedStyle {
+ [document startModifyNodes:[[document pickSupport] selectedNodes]];
+
+ for (Node *node in [[document pickSupport] selectedNodes]) {
+ [node setStyle:nil];
+ }
+
+ [document endModifyNodes];
+}
+
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void add_style_button_cb (GtkButton *widget, NodeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ NodeStyle *newStyle = [NodeStyle defaultNodeStyleWithName:@"newstyle"];
+ [[palette styleManager] addNodeStyle:newStyle];
+ [[palette styleManager] setActiveNodeStyle:newStyle];
+
+ [pool drain];
+}
+
+static void remove_style_button_cb (GtkButton *widget, NodeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette removeSelectedStyle];
+ [pool drain];
+}
+
+static void apply_style_button_cb (GtkButton *widget, NodeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette applySelectedStyle];
+ [pool drain];
+}
+
+static void clear_style_button_cb (GtkButton *widget, NodeStylesPalette *palette) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [palette clearSelectedStyle];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/PreambleEditor.h b/tikzit/src/gtk/PreambleEditor.h
new file mode 100644
index 0000000..b081e00
--- /dev/null
+++ b/tikzit/src/gtk/PreambleEditor.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class Preambles;
+
+@interface PreambleEditor: NSObject {
+ Preambles *preambles;
+
+ // we don't keep any refs, as we control
+ // the top window
+ GtkWindow *parentWindow;
+ GtkWindow *window;
+ GtkListStore *preambleListStore;
+ GtkTreeView *preambleSelector;
+ GtkTextView *preambleView;
+ BOOL blockSignals;
+ BOOL adding;
+}
+
+- (id) initWithPreambles:(Preambles*)p;
+
+- (void) setParentWindow:(GtkWindow*)parent;
+
+- (Preambles*) preambles;
+
+- (void) show;
+- (void) hide;
+- (BOOL) isVisible;
+- (void) setVisible:(BOOL)visible;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/PreambleEditor.m b/tikzit/src/gtk/PreambleEditor.m
new file mode 100644
index 0000000..7b299c3
--- /dev/null
+++ b/tikzit/src/gtk/PreambleEditor.m
@@ -0,0 +1,556 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "PreambleEditor.h"
+#import "Preambles.h"
+#import <gdk/gdk.h>
+
+enum {
+ NAME_COLUMN,
+ IS_CUSTOM_COLUMN,
+ N_COLUMNS
+};
+
+// {{{ Internal interfaces
+// {{{ Signals
+static gboolean window_delete_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ PreambleEditor *editor);
+static gboolean window_focus_out_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ PreambleEditor *editor);
+static void close_button_clicked_cb (GtkButton *widget, PreambleEditor *editor);
+static void add_button_clicked_cb (GtkButton *widget, PreambleEditor *editor);
+static void remove_button_clicked_cb (GtkButton *widget, PreambleEditor *editor);
+static void undo_button_clicked_cb (GtkButton *widget, PreambleEditor *editor);
+static void redo_button_clicked_cb (GtkButton *widget, PreambleEditor *editor);
+static void preamble_name_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PreambleEditor *editor);
+static void preamble_selection_changed_cb (GtkTreeSelection *treeselection,
+ PreambleEditor *editor);
+// }}}
+
+@interface PreambleEditor (Private)
+- (void) loadUi;
+- (void) save;
+- (void) revert;
+- (void) update;
+- (void) fillListStore;
+- (BOOL) isDefaultPreambleSelected;
+- (NSString*) selectedPreambleName;
+- (void) addPreamble;
+- (void) deletePreamble;
+- (void) renamePreambleAtPath:(gchar*)path to:(gchar*)newValue;
+- (void) nodeStylePropertyChanged:(NSNotification*)notification;
+- (void) edgeStylePropertyChanged:(NSNotification*)notification;
+@end
+
+// }}}
+// {{{ API
+
+@implementation PreambleEditor
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithPreambles:(Preambles*)p {
+ self = [super init];
+
+ if (self) {
+ preambles = [p retain];
+ parentWindow = NULL;
+ window = NULL;
+ preambleView = NULL;
+ preambleSelector = NULL;
+ blockSignals = NO;
+ adding = NO;
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(nodeStylePropertyChanged:)
+ name:@"NodeStylePropertyChanged"
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(edgeStylePropertyChanged:)
+ name:@"EdgeStylePropertyChanged"
+ object:nil];
+ }
+
+ return self;
+}
+
+- (Preambles*) preambles {
+ return preambles;
+}
+
+- (void) setParentWindow:(GtkWindow*)parent {
+ GtkWindow *oldParent = parentWindow;
+
+ if (parent)
+ g_object_ref (parentWindow);
+ parentWindow = parent;
+ if (oldParent)
+ g_object_unref (oldParent);
+
+ if (window) {
+ gtk_window_set_transient_for (window, parentWindow);
+ }
+}
+
+- (void) show {
+ [self loadUi];
+ gtk_widget_show (GTK_WIDGET (window));
+ [self revert];
+}
+
+- (void) hide {
+ if (!window) {
+ return;
+ }
+ [self save];
+ gtk_widget_hide (GTK_WIDGET (window));
+}
+
+- (BOOL) isVisible {
+ if (!window) {
+ return NO;
+ }
+ gboolean visible;
+ g_object_get (G_OBJECT (window), "visible", &visible, NULL);
+ return visible ? YES : NO;
+}
+
+- (void) setVisible:(BOOL)visible {
+ if (visible) {
+ [self show];
+ } else {
+ [self hide];
+ }
+}
+
+- (void) dealloc {
+ [preambles release];
+ preambles = nil;
+ if (window) {
+ gtk_widget_destroy (GTK_WIDGET (window));
+ window = NULL;
+ }
+ if (parentWindow) {
+ g_object_ref (parentWindow);
+ }
+
+ [super dealloc];
+}
+
+@end
+
+// }}}
+// {{{ Private
+
+@implementation PreambleEditor (Private)
+- (GtkWidget*) createPreambleList {
+ preambleListStore = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ preambleSelector = GTK_TREE_VIEW (gtk_tree_view_new_with_model (
+ GTK_TREE_MODEL (preambleListStore)));
+ gtk_widget_set_size_request (GTK_WIDGET (preambleSelector), 150, -1);
+ gtk_tree_view_set_headers_visible (preambleSelector, FALSE);
+
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+
+ renderer = gtk_cell_renderer_text_new ();
+ column = gtk_tree_view_column_new_with_attributes ("Preamble",
+ renderer,
+ "text", NAME_COLUMN,
+ "editable", IS_CUSTOM_COLUMN,
+ NULL);
+ gtk_tree_view_append_column (preambleSelector, column);
+ g_signal_connect (G_OBJECT (renderer),
+ "edited",
+ G_CALLBACK (preamble_name_edited_cb),
+ self);
+
+ GtkWidget *listScroller = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (listScroller),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_container_add (GTK_CONTAINER (listScroller),
+ GTK_WIDGET (preambleSelector));
+
+ [self fillListStore];
+
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (preambleSelector);
+ gtk_tree_selection_set_mode (sel, GTK_SELECTION_BROWSE);
+ g_signal_connect (G_OBJECT (sel),
+ "changed",
+ G_CALLBACK (preamble_selection_changed_cb),
+ self);
+
+ return listScroller;
+}
+
+- (void) loadUi {
+ if (window) {
+ return;
+ }
+
+ window = GTK_WINDOW (gtk_window_new (GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_title (window, "Preamble editor");
+ gtk_window_set_position (window, GTK_WIN_POS_CENTER_ON_PARENT);
+ gtk_window_set_default_size (window, 600, 400);
+ gtk_window_set_type_hint (window, GDK_WINDOW_TYPE_HINT_DIALOG);
+ if (parentWindow) {
+ gtk_window_set_transient_for (window, parentWindow);
+ }
+ GdkEventMask mask;
+ g_object_get (G_OBJECT (window), "events", &mask, NULL);
+ mask |= GDK_FOCUS_CHANGE_MASK;
+ g_object_set (G_OBJECT (window), "events", mask, NULL);
+ g_signal_connect (window,
+ "delete-event",
+ G_CALLBACK (window_delete_event_cb),
+ self);
+ g_signal_connect (window,
+ "focus-out-event",
+ G_CALLBACK (window_focus_out_event_cb),
+ self);
+
+ GtkWidget *mainBox = gtk_vbox_new (FALSE, 18);
+ gtk_container_set_border_width (GTK_CONTAINER (mainBox), 12);
+ gtk_container_add (GTK_CONTAINER (window), mainBox);
+
+ GtkPaned *paned = GTK_PANED (gtk_hpaned_new ());
+ gtk_box_pack_start (GTK_BOX (mainBox),
+ GTK_WIDGET (paned),
+ TRUE, TRUE, 0);
+
+ GtkWidget *listWidget = [self createPreambleList];
+ GtkWidget *listFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (listFrame), listWidget);
+
+ GtkBox *listBox = GTK_BOX (gtk_vbox_new (FALSE, 6));
+ gtk_box_pack_start (listBox, listFrame, TRUE, TRUE, 0);
+
+ GtkContainer *listButtonBox = GTK_CONTAINER (gtk_hbox_new (FALSE, 6));
+ gtk_box_pack_start (listBox, GTK_WIDGET (listButtonBox), FALSE, TRUE, 0);
+ GtkWidget *addButton = gtk_button_new_from_stock (GTK_STOCK_ADD);
+ g_signal_connect (addButton,
+ "clicked",
+ G_CALLBACK (add_button_clicked_cb),
+ self);
+ gtk_container_add (listButtonBox, addButton);
+ GtkWidget *removeButton = gtk_button_new_from_stock (GTK_STOCK_REMOVE);
+ g_signal_connect (removeButton,
+ "clicked",
+ G_CALLBACK (remove_button_clicked_cb),
+ self);
+ gtk_container_add (listButtonBox, removeButton);
+
+ gtk_paned_pack1 (paned, GTK_WIDGET (listBox), FALSE, TRUE);
+
+ preambleView = GTK_TEXT_VIEW (gtk_text_view_new ());
+ gtk_text_view_set_left_margin (preambleView, 3);
+ gtk_text_view_set_right_margin (preambleView, 3);
+ GtkWidget *scroller = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroller),
+ GTK_POLICY_AUTOMATIC, // horiz
+ GTK_POLICY_ALWAYS); // vert
+ gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (preambleView));
+ GtkWidget *editorFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (editorFrame), scroller);
+ gtk_paned_pack2 (paned, editorFrame, TRUE, TRUE);
+
+ GtkContainer *buttonBox = GTK_CONTAINER (gtk_hbutton_box_new ());
+ gtk_box_set_spacing (GTK_BOX (buttonBox), 6);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (buttonBox), GTK_BUTTONBOX_END);
+ gtk_box_pack_start (GTK_BOX (mainBox),
+ GTK_WIDGET (buttonBox),
+ FALSE, TRUE, 0);
+ GtkWidget *closeButton = gtk_button_new_from_stock (GTK_STOCK_CLOSE);
+ gtk_container_add (buttonBox, closeButton);
+ g_signal_connect (closeButton,
+ "clicked",
+ G_CALLBACK (close_button_clicked_cb),
+ self);
+ /*
+ GtkWidget *undoButton = gtk_button_new_from_stock (GTK_STOCK_UNDO);
+ gtk_container_add (buttonBox, undoButton);
+ gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (buttonBox),
+ undoButton,
+ TRUE);
+ g_signal_connect (undoButton,
+ "clicked",
+ G_CALLBACK (undo_button_clicked_cb),
+ self);
+ GtkWidget *redoButton = gtk_button_new_from_stock (GTK_STOCK_REDO);
+ gtk_container_add (buttonBox, redoButton);
+ gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (buttonBox),
+ redoButton,
+ TRUE);
+ g_signal_connect (redoButton,
+ "clicked",
+ G_CALLBACK (redo_button_clicked_cb),
+ self);
+ */
+ [self revert];
+
+ gtk_widget_show_all (mainBox);
+}
+
+- (void) save {
+ if (!preambleView)
+ return;
+ if ([self isDefaultPreambleSelected])
+ return;
+ GtkTextIter start,end;
+ GtkTextBuffer *preambleBuffer = gtk_text_view_get_buffer (preambleView);
+ gtk_text_buffer_get_bounds(preambleBuffer, &start, &end);
+ gchar *text = gtk_text_buffer_get_text(preambleBuffer, &start, &end, FALSE);
+ NSString *preamble = [NSString stringWithUTF8String:text];
+ g_free (text);
+ [preambles setCurrentPreamble:preamble];
+}
+
+- (void) revert {
+ if (!preambleView)
+ return;
+ GtkTextBuffer *preambleBuffer = gtk_text_view_get_buffer (preambleView);
+ gtk_text_buffer_set_text (preambleBuffer, [[preambles currentPreamble] UTF8String], -1);
+ gtk_text_view_set_editable (preambleView, ![self isDefaultPreambleSelected]);
+}
+
+- (void) update {
+ if (!blockSignals) {
+ [self save];
+ }
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (preambleSelector);
+ GtkTreeIter row;
+ GtkTreeModel *model;
+ if (gtk_tree_selection_get_selected (sel, &model, &row)) {
+ gchar *name;
+ gtk_tree_model_get (model, &row, NAME_COLUMN, &name, -1);
+ NSString *preambleName = [NSString stringWithUTF8String:name];
+ [preambles setSelectedPreambleName:preambleName];
+ g_free (name);
+ }
+ [self revert];
+}
+
+- (void) fillListStore {
+ blockSignals = YES;
+
+ GtkTreeIter row;
+ gtk_list_store_clear (preambleListStore);
+
+ gtk_list_store_append (preambleListStore, &row);
+ gtk_list_store_set (preambleListStore, &row,
+ NAME_COLUMN, [[preambles defaultPreambleName] UTF8String],
+ IS_CUSTOM_COLUMN, FALSE,
+ -1);
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (preambleSelector);
+ if ([self isDefaultPreambleSelected]) {
+ gtk_tree_selection_select_iter (sel, &row);
+ }
+
+ NSEnumerator *en = [preambles customPreambleNameEnumerator];
+ NSString *preambleName;
+ while ((preambleName = [en nextObject])) {
+ gtk_list_store_append (preambleListStore, &row);
+ gtk_list_store_set (preambleListStore, &row,
+ NAME_COLUMN, [preambleName UTF8String],
+ IS_CUSTOM_COLUMN, TRUE,
+ -1);
+ if ([preambleName isEqualToString:[self selectedPreambleName]]) {
+ gtk_tree_selection_select_iter (sel, &row);
+ }
+ }
+
+ blockSignals = NO;
+}
+
+- (BOOL) isDefaultPreambleSelected {
+ return [preambles selectedPreambleIsDefault];
+}
+
+- (NSString*) selectedPreambleName {
+ return [preambles selectedPreambleName];
+}
+
+- (void) addPreamble {
+ NSString *newName = [preambles addPreamble];
+
+ GtkTreeIter row;
+ gtk_list_store_append (preambleListStore, &row);
+ gtk_list_store_set (preambleListStore, &row,
+ NAME_COLUMN, [newName UTF8String],
+ IS_CUSTOM_COLUMN, TRUE,
+ -1);
+
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (preambleSelector);
+ gtk_tree_selection_select_iter (sel, &row);
+}
+
+- (void) deletePreamble {
+ if ([self isDefaultPreambleSelected])
+ return;
+
+ NSString *name = [self selectedPreambleName];
+
+ GtkTreeIter row;
+ GtkTreeModel *model = GTK_TREE_MODEL (preambleListStore);
+
+ gtk_tree_model_get_iter_first (model, &row);
+ // ignore first; it is the default one
+ gboolean found = FALSE;
+ while (!found && gtk_tree_model_iter_next (model, &row)) {
+ gchar *candidate;
+ gtk_tree_model_get (model, &row, NAME_COLUMN, &candidate, -1);
+ if (g_strcmp0 (candidate, [name UTF8String]) == 0) {
+ found = TRUE;
+ }
+ g_free (candidate);
+ }
+
+ if (!found)
+ return;
+
+ if (![preambles removePreamble:name])
+ return;
+
+ blockSignals = YES;
+
+ gboolean had_next = gtk_list_store_remove (preambleListStore, &row);
+ if (!had_next) {
+ // select the last item
+ gint length = gtk_tree_model_iter_n_children (model, NULL);
+ gtk_tree_model_iter_nth_child (model, &row, NULL, length - 1);
+ }
+
+ GtkTreeSelection *sel = gtk_tree_view_get_selection (preambleSelector);
+ gtk_tree_selection_select_iter (sel, &row);
+
+ [self revert];
+
+ blockSignals = NO;
+}
+
+- (void) renamePreambleAtPath:(gchar*)path to:(gchar*)newValue {
+ NSString *newName = [NSString stringWithUTF8String:newValue];
+
+ GtkTreeIter row;
+ GtkTreeModel *model = GTK_TREE_MODEL (preambleListStore);
+
+ if (!gtk_tree_model_get_iter_from_string (model, &row, path))
+ return;
+
+ gchar *oldValue;
+ gtk_tree_model_get (model, &row, NAME_COLUMN, &oldValue, -1);
+
+ NSString* oldName = [NSString stringWithUTF8String:oldValue];
+ if ([preambles renamePreambleFrom:oldName to:newName]) {
+ gtk_list_store_set (preambleListStore, &row,
+ NAME_COLUMN, newValue,
+ -1);
+ }
+}
+
+- (void) nodeStylePropertyChanged:(NSNotification*)notification {
+ if ([self isDefaultPreambleSelected]) {
+ [self revert];
+ }
+}
+
+- (void) edgeStylePropertyChanged:(NSNotification*)notification {
+ if ([self isDefaultPreambleSelected]) {
+ [self revert];
+ }
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static gboolean window_delete_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor hide];
+ [pool drain];
+ return TRUE; // we dealt with this event
+}
+
+static gboolean window_focus_out_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor save];
+ [pool drain];
+ return FALSE;
+}
+
+static void close_button_clicked_cb (GtkButton *widget, PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor hide];
+ [pool drain];
+}
+
+static void add_button_clicked_cb (GtkButton *widget, PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor addPreamble];
+ [pool drain];
+}
+
+static void remove_button_clicked_cb (GtkButton *widget, PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor deletePreamble];
+ [pool drain];
+}
+
+static void undo_button_clicked_cb (GtkButton *widget, PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSLog(@"Undo");
+ [pool drain];
+}
+
+static void redo_button_clicked_cb (GtkButton *widget, PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSLog(@"Redo");
+ [pool drain];
+}
+
+static void preamble_name_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor renamePreambleAtPath:path to:new_text];
+ [pool drain];
+}
+
+static void preamble_selection_changed_cb (GtkTreeSelection *treeselection,
+ PreambleEditor *editor) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor update];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/Preambles+Storage.h b/tikzit/src/gtk/Preambles+Storage.h
new file mode 100644
index 0000000..76f56cc
--- /dev/null
+++ b/tikzit/src/gtk/Preambles+Storage.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Preambles.h"
+
+@interface Preambles (Storage)
+
++ (Preambles*) preamblesFromDirectory:(NSString*)directory;
+- (id) initFromDirectory:(NSString*)directory;
+- (void) loadFromDirectory:(NSString*)directory;
+- (void) storeToDirectory:(NSString*)directory;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Preambles+Storage.m b/tikzit/src/gtk/Preambles+Storage.m
new file mode 100644
index 0000000..bd3ea03
--- /dev/null
+++ b/tikzit/src/gtk/Preambles+Storage.m
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Preambles+Storage.h"
+
+static NSString *ext = @"preamble";
+
+@implementation Preambles (Storage)
+
++ (Preambles*) preamblesFromDirectory:(NSString*)directory {
+ return [[[self alloc] initFromDirectory:directory] autorelease];
+}
+
+- (id) initFromDirectory:(NSString*)directory {
+ BOOL isDir = NO;
+ if ([[NSFileManager defaultManager] fileExistsAtPath:directory isDirectory:&isDir] && isDir) {
+ self = [super init];
+
+ if (self) {
+ selectedPreambleName = @"default";
+ preambleDict = nil;
+ [self loadFromDirectory:directory];
+ }
+ } else {
+ self = [self init];
+ }
+
+ return self;
+}
+
+- (void) loadFromDirectory:(NSString*)directory {
+ preambleDict = [[NSMutableDictionary alloc] initWithCapacity:1];
+ NSDirectoryEnumerator *en = [[NSFileManager defaultManager] enumeratorAtPath:directory];
+ NSString *filename;
+ while ((filename = [en nextObject]) != nil) {
+ if ([filename hasSuffix:ext] && [[en fileAttributes] fileType] == NSFileTypeRegular) {
+ NSString *path = [directory stringByAppendingPathComponent:filename];
+ NSString *contents = [NSString stringWithContentsOfFile:path];
+ if (contents) {
+ [preambleDict setObject:contents forKey:[filename stringByDeletingPathExtension]];
+ }
+ }
+ }
+}
+
+- (void) storeToDirectory:(NSString*)directory {
+ NSDirectoryEnumerator *den = [[NSFileManager defaultManager] enumeratorAtPath:directory];
+ NSString *filename;
+ while ((filename = [den nextObject]) != nil) {
+ if ([filename hasSuffix:ext] && [[den fileAttributes] fileType] == NSFileTypeRegular) {
+ NSString *path = [directory stringByAppendingPathComponent:filename];
+ NSString *entry = [filename stringByDeletingPathExtension];
+ if ([preambleDict objectForKey:entry] == nil) {
+ [[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
+ }
+ }
+ }
+
+ NSEnumerator *en = [self customPreambleNameEnumerator];
+ NSString *entry;
+ while ((entry = [en nextObject]) != nil) {
+ NSString *path = [directory stringByAppendingPathComponent:[entry stringByAppendingPathExtension:ext]];
+ NSString *contents = [preambleDict objectForKey:entry];
+ [contents writeToFile:path atomically:YES];
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PreviewRenderer.h b/tikzit/src/gtk/PreviewRenderer.h
new file mode 100644
index 0000000..d691722
--- /dev/null
+++ b/tikzit/src/gtk/PreviewRenderer.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <poppler.h>
+
+#import "Surface.h"
+
+@class Configuration;
+@class Preambles;
+@class TikzDocument;
+
+@interface PreviewRenderer: NSObject<RenderDelegate> {
+ Configuration *config;
+ Preambles *preambles;
+ TikzDocument *document;
+ PopplerDocument *pdfDocument;
+ PopplerPage *pdfPage;
+}
+
+@property (readonly) Preambles *preambles;
+@property (retain) TikzDocument *document;
+@property (readonly) double height;
+@property (readonly) double width;
+
+- (id) initWithPreambles:(Preambles*)p config:(Configuration*)c;
+
+- (BOOL) updateWithError:(NSError**)error;
+- (BOOL) update;
+- (BOOL) isValid;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PreviewRenderer.m b/tikzit/src/gtk/PreviewRenderer.m
new file mode 100644
index 0000000..98c328e
--- /dev/null
+++ b/tikzit/src/gtk/PreviewRenderer.m
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "PreviewRenderer.h"
+
+#import "CairoRenderContext.h"
+#import "Configuration.h"
+#import "Preambles.h"
+#import "TikzDocument.h"
+
+@implementation PreviewRenderer
+
+@synthesize preambles,document;
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithPreambles:(Preambles*)p config:(Configuration*)c {
+ self = [super init];
+
+ if (self) {
+ document = nil;
+ config = [c retain];
+ preambles = [p retain];
+ pdfDocument = NULL;
+ pdfPage = NULL;
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [document release];
+ [config release];
+ [preambles release];
+
+ if (pdfDocument) {
+ g_object_unref (pdfDocument);
+ pdfDocument = NULL;
+ }
+ if (pdfPage) {
+ g_object_unref (pdfPage);
+ pdfPage = NULL;
+ }
+
+ [super dealloc];
+}
+
+- (BOOL) update {
+ NSError *error = nil;
+ BOOL result = [self updateWithError:&error];
+ if (error) {
+ logError (error, @"Could not update preview");
+ if ([error code] == TZ_ERR_TOOL_FAILED) {
+ NSLog (@"Output: %@", [[error userInfo] objectForKey:TZToolOutputErrorKey]);
+ }
+ }
+ return result;
+}
+
+- (BOOL) updateWithError:(NSError**)error {
+ if (document == nil) {
+ if (error) {
+ *error = [NSError errorWithMessage:@"No document given" code:TZ_ERR_BADSTATE];
+ }
+ if (pdfDocument) {
+ g_object_unref (pdfDocument);
+ pdfDocument = NULL;
+ }
+ if (pdfPage) {
+ g_object_unref (pdfPage);
+ pdfPage = NULL;
+ }
+ return NO;
+ }
+
+ NSString *tex = [NSString stringWithFormat:@"%@%@%@",
+ [preambles currentPreamble],
+ [document tikz],
+ [preambles currentPostamble]];
+
+ NSString *tempDir = [[NSFileManager defaultManager] createTempDirectoryWithError:error];
+ if (!tempDir) {
+ if (error) {
+ *error = [NSError errorWithMessage:@"Could not create temporary directory" code:TZ_ERR_IO cause:*error];
+ }
+ return NO;
+ }
+
+ // write tex code to temporary file
+ NSString *texFile = [NSString stringWithFormat:@"%@/tikzit.tex", tempDir];
+ NSString *pdfFile = [NSString stringWithFormat:@"file://%@/tikzit.pdf", tempDir];
+ [tex writeToFile:texFile atomically:YES];
+
+ NSTask *latexTask = [[NSTask alloc] init];
+ [latexTask setCurrentDirectoryPath:tempDir];
+
+ // GNUStep is clever enough to use PATH
+ NSString *path = [config stringEntry:@"pdflatex"
+ inGroup:@"Previews"
+ withDefault:@"pdflatex"];
+ [latexTask setLaunchPath:path];
+
+ NSArray *args = [NSArray arrayWithObjects:
+ @"-fmt=latex",
+ @"-output-format=pdf",
+ @"-interaction=nonstopmode",
+ @"-halt-on-error",
+ texFile,
+ nil];
+ [latexTask setArguments:args];
+
+ NSPipe *pout = [NSPipe pipe];
+ [latexTask setStandardOutput:pout];
+
+ NSFileHandle *latexOut = [pout fileHandleForReading];
+
+ BOOL success = NO;
+
+ NS_DURING {
+ [latexTask launch];
+ [latexTask waitUntilExit];
+ } NS_HANDLER {
+ NSString *desc = [NSString stringWithFormat:@"Failed to run '%@'", path];
+ NSMutableDictionary *errorDetail = [NSMutableDictionary dictionaryWithCapacity:2];
+ [errorDetail setValue:desc forKey:NSLocalizedDescriptionKey];
+ *error = [NSError errorWithDomain:TZErrorDomain code:TZ_ERR_IO userInfo:errorDetail];
+
+ // remove all temporary files
+ [[NSFileManager defaultManager] removeFileAtPath:tempDir handler:NULL];
+
+ return NO;
+ } NS_ENDHANDLER
+
+ NSData *data = [latexOut readDataToEndOfFile];
+ NSString *str = [[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding];
+
+ if ([latexTask terminationStatus] != 0) {
+ if (error) {
+ NSMutableDictionary *errorDetail = [NSMutableDictionary dictionaryWithCapacity:2];
+ [errorDetail setValue:@"Generating a PDF file with pdflatex failed" forKey:NSLocalizedDescriptionKey];
+ [errorDetail setValue:str forKey:TZToolOutputErrorKey];
+ *error = [NSError errorWithDomain:TZErrorDomain code:TZ_ERR_TOOL_FAILED userInfo:errorDetail];
+ }
+ } else {
+ // load pdf document
+ GError* gerror = NULL;
+ pdfDocument = poppler_document_new_from_file([pdfFile UTF8String], NULL, &gerror);
+ if (!pdfDocument) {
+ if (error) {
+ *error = [NSError errorWithMessage:[NSString stringWithFormat:@"Could not load PDF document", pdfFile]
+ code:TZ_ERR_IO
+ cause:[NSError errorWithGError:gerror]];
+ }
+ g_error_free(gerror);
+ } else {
+ pdfPage = poppler_document_get_page(pdfDocument, 0);
+ if(!pdfPage) {
+ if (error) {
+ *error = [NSError errorWithMessage:@"Could not open first page of PDF document"
+ code:TZ_ERR_OTHER];
+ }
+ g_object_unref(pdfDocument);
+ } else {
+ success = YES;
+ }
+ }
+ }
+
+ // remove all temporary files
+ [[NSFileManager defaultManager] removeFileAtPath:tempDir handler:NULL];
+
+ return success;
+}
+
+- (BOOL) isValid {
+ return pdfPage ? YES : NO;
+}
+
+- (double) width {
+ double w = 0.0;
+ if (pdfPage)
+ poppler_page_get_size(pdfPage, &w, NULL);
+ return w;
+}
+
+- (double) height {
+ double h = 0.0;
+ if (pdfPage)
+ poppler_page_get_size(pdfPage, NULL, &h);
+ return h;
+}
+
+- (void) renderWithContext:(id<RenderContext>)c onSurface:(id<Surface>)surface {
+ if (document != nil) {
+ CairoRenderContext *context = (CairoRenderContext*)c;
+
+ [context saveState];
+ [context applyTransform:[surface transformer]];
+
+ // white-out
+ [context paintWithColor:WhiteRColor];
+
+ poppler_page_render (pdfPage, [context cairoContext]);
+
+ [context restoreState];
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PreviewWindow.h b/tikzit/src/gtk/PreviewWindow.h
new file mode 100644
index 0000000..ca6b30c
--- /dev/null
+++ b/tikzit/src/gtk/PreviewWindow.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class Configuration;
+@class Preambles;
+@class PreviewRenderer;
+@class TikzDocument;
+@class WidgetSurface;
+
+@interface PreviewWindow: NSObject {
+ PreviewRenderer *previewer;
+ GtkWindow *window;
+ WidgetSurface *surface;
+ GtkWindow *parent;
+}
+
+- (id) initWithPreambles:(Preambles*)p config:(Configuration*)c;
+
+- (void) setParentWindow:(GtkWindow*)parent;
+
+- (TikzDocument*) document;
+- (void) setDocument:(TikzDocument*)doc;
+
+- (BOOL) update;
+
+- (void) show;
+- (void) hide;
+- (BOOL) isVisible;
+- (void) setVisible:(BOOL)visible;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PreviewWindow.m b/tikzit/src/gtk/PreviewWindow.m
new file mode 100644
index 0000000..418886e
--- /dev/null
+++ b/tikzit/src/gtk/PreviewWindow.m
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "PreviewWindow.h"
+
+#import "Preambles.h"
+#import "PreviewRenderer.h"
+#import "TikzDocument.h"
+#import "WidgetSurface.h"
+
+#import "gtkhelpers.h"
+
+@interface PreviewWindow (Private)
+- (BOOL) updateOrShowError;
+@end
+
+// {{{ API
+@implementation PreviewWindow
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithPreambles:(Preambles*)p config:(Configuration*)c {
+ self = [super init];
+
+ if (self) {
+ parent = NULL;
+ previewer = [[PreviewRenderer alloc] initWithPreambles:p config:c];
+
+ window = GTK_WINDOW (gtk_window_new (GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_title (window, "Preview");
+ gtk_window_set_resizable (window, FALSE);
+ g_signal_connect (G_OBJECT (window),
+ "delete-event",
+ G_CALLBACK (gtk_widget_hide_on_delete),
+ NULL);
+
+ GtkWidget *pdfArea = gtk_drawing_area_new ();
+ gtk_container_add (GTK_CONTAINER (window), pdfArea);
+ gtk_widget_set_size_request (pdfArea, 150.0, 150.0);
+ gtk_widget_show (pdfArea);
+ surface = [[WidgetSurface alloc] initWithWidget:pdfArea];
+ [surface setRenderDelegate:previewer];
+ Transformer *t = [surface transformer];
+ [t setFlippedAboutXAxis:![t isFlippedAboutXAxis]];
+ }
+
+ return self;
+}
+
+- (void) setParentWindow:(GtkWindow*)p {
+ parent = p;
+ gtk_window_set_transient_for (window, p);
+ if (p != NULL) {
+ gtk_window_set_position (window, GTK_WIN_POS_CENTER_ON_PARENT);
+ }
+}
+
+- (TikzDocument*) document {
+ return [previewer document];
+}
+
+- (void) setDocument:(TikzDocument*)doc {
+ [previewer setDocument:doc];
+}
+
+- (void) updateSize {
+ double width = 150;
+ double height = 150;
+ if ([previewer isValid]) {
+ double pWidth = [previewer width];
+ double pHeight = [previewer height];
+ width = (width < pWidth + 4) ? pWidth + 4 : width;
+ height = (height < pHeight + 4) ? pHeight + 4 : height;
+ NSPoint offset;
+ offset.x = (width-pWidth)/2.0;
+ offset.y = (height-pHeight)/2.0;
+ [[surface transformer] setOrigin:offset];
+ }
+ [surface setSizeRequestWidth:width height:height];
+}
+
+- (BOOL) update {
+ if ([self updateOrShowError]) {
+ [self updateSize];
+ [surface invalidate];
+ return YES;
+ }
+
+ return NO;
+}
+
+- (void) show {
+ if ([self updateOrShowError]) {
+ [self updateSize];
+ gtk_widget_show (GTK_WIDGET (window));
+ [surface invalidate];
+ }
+}
+
+- (void) hide {
+ gtk_widget_hide (GTK_WIDGET (window));
+}
+
+- (BOOL) isVisible {
+ gboolean visible;
+ g_object_get (G_OBJECT (window), "visible", &visible, NULL);
+ return visible ? YES : NO;
+}
+
+- (void) setVisible:(BOOL)visible {
+ if (visible) {
+ [self show];
+ } else {
+ [self hide];
+ }
+}
+
+- (void) dealloc {
+ [previewer release];
+ [surface release];
+ gtk_widget_destroy (GTK_WIDGET (window));
+
+ [super dealloc];
+}
+
+@end
+// }}}
+
+@implementation PreviewWindow (Private)
+- (BOOL) updateOrShowError {
+ NSError *error = nil;
+ if (![previewer updateWithError:&error]) {
+ GtkWindow *dparent = gtk_widget_get_visible (GTK_WIDGET (window))
+ ? window
+ : parent;
+ GtkWidget *dialog = gtk_message_dialog_new (dparent,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "Failed to generate the preview: %s",
+ [[error localizedDescription] UTF8String]);
+#if GTK_CHECK_VERSION(2,22,0)
+ if ([error code] == TZ_ERR_TOOL_FAILED) {
+ GtkBox *box = GTK_BOX (gtk_message_dialog_get_message_area (GTK_MESSAGE_DIALOG (dialog)));
+ GtkWidget *label = gtk_label_new ("pdflatex said:");
+ gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5f);
+ gtk_widget_show (label);
+ gtk_box_pack_start (box, label, FALSE, TRUE, 0);
+
+ GtkWidget *view = gtk_text_view_new ();
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+ gtk_text_buffer_set_text (buffer, [[error toolOutput] UTF8String], -1);
+ gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
+ gtk_widget_show (view);
+ GtkWidget *scrolledView = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledView),
+ GTK_POLICY_NEVER, // horiz
+ GTK_POLICY_ALWAYS); // vert
+ gtk_widget_set_size_request (scrolledView, -1, 120);
+ gtk_container_add (GTK_CONTAINER (scrolledView), view);
+ gtk_widget_show (scrolledView);
+ gtk_box_pack_start (box, scrolledView, TRUE, TRUE, 0);
+ }
+#endif // GTK+ 2.22.0
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+ return NO;
+ }
+ return YES;
+}
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PropertyListEditor.h b/tikzit/src/gtk/PropertyListEditor.h
new file mode 100644
index 0000000..fd7e395
--- /dev/null
+++ b/tikzit/src/gtk/PropertyListEditor.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "GraphElementData.h"
+#import "GraphElementProperty.h"
+
+@protocol PropertyChangeDelegate
+@optional
+- (BOOL)startEdit;
+- (void)endEdit;
+- (void)cancelEdit;
+@end
+
+@interface PropertyListEditor: NSObject {
+ GtkListStore *list;
+ GtkWidget *view;
+ GraphElementData *data;
+ GtkWidget *widget;
+ NSObject<PropertyChangeDelegate> *delegate;
+}
+
+/*!
+ @property widget
+ @brief The widget displaying the editable list
+ */
+@property (readonly) GtkWidget *widget;
+
+/*!
+ @property data
+ @brief The GraphElementData that should be reflected in the list
+ */
+@property (retain) GraphElementData *data;
+
+/*!
+ @property delegate
+ @brief A delegate for dealing with property changes
+ */
+@property (retain) NSObject<PropertyChangeDelegate> *delegate;
+
+/*!
+ * Reload the properties from the data store
+ */
+- (void) reloadProperties;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/PropertyListEditor.m b/tikzit/src/gtk/PropertyListEditor.m
new file mode 100644
index 0000000..51f3cbf
--- /dev/null
+++ b/tikzit/src/gtk/PropertyListEditor.m
@@ -0,0 +1,422 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "PropertyListEditor.h"
+
+// {{{ Constants
+
+enum {
+ PLM_NAME_COL = 0,
+ PLM_VALUE_COL,
+ PLM_IS_PROPERTY_COL,
+ PLM_PROPERTY_COL,
+ PLM_N_COLS
+};
+
+// }}}
+// {{{ Internal interfaces
+// {{{ Signals
+
+static void value_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PropertyListEditor *editor);
+static void name_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PropertyListEditor *editor);
+static void add_prop_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor);
+static void add_atom_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor);
+static void remove_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor);
+
+// }}}
+// {{{ Private
+
+@interface PropertyListEditor (Private)
+- (void) updatePath:(gchar*)path withValue:(NSString*)newText;
+- (void) updatePath:(gchar*)path withName:(NSString*)newText;
+- (void) addProperty;
+- (void) addAtom;
+- (void) removeSelected;
+@end
+
+// }}}
+// }}}
+// {{{ API
+
+@implementation PropertyListEditor
+
+- (id) init {
+ self = [super init];
+
+ if (self) {
+ list = gtk_list_store_new (PLM_N_COLS,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_BOOLEAN,
+ G_TYPE_POINTER);
+ view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (list));
+ GtkWidget *scrolledview = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledview),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_container_add (GTK_CONTAINER (scrolledview), view);
+ gtk_widget_set_size_request (view, -1, 150);
+ data = nil;
+ delegate = nil;
+
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+
+ renderer = gtk_cell_renderer_text_new ();
+ g_object_set (G_OBJECT (renderer),
+ "editable", TRUE,
+ NULL);
+ column = gtk_tree_view_column_new_with_attributes ("Key/Atom",
+ renderer,
+ "text", PLM_NAME_COL,
+ NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (view), column);
+ g_signal_connect (G_OBJECT (renderer),
+ "edited",
+ G_CALLBACK (name_edited_cb),
+ self);
+
+ renderer = gtk_cell_renderer_text_new ();
+ column = gtk_tree_view_column_new_with_attributes ("Value",
+ renderer,
+ "text", PLM_VALUE_COL,
+ "editable", PLM_IS_PROPERTY_COL,
+ "sensitive", PLM_IS_PROPERTY_COL,
+ NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (view), column);
+ g_signal_connect (G_OBJECT (renderer),
+ "edited",
+ G_CALLBACK (value_edited_cb),
+ self);
+
+ widget = gtk_vbox_new (FALSE, 0);
+ gtk_box_set_spacing (GTK_BOX (widget), 6);
+ g_object_ref_sink (G_OBJECT (widget));
+
+ GtkWidget *listFrame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (listFrame), scrolledview);
+ gtk_widget_show (listFrame);
+ gtk_container_add (GTK_CONTAINER (widget), listFrame);
+
+ GtkBox *buttonBox = GTK_BOX (gtk_hbox_new(FALSE, 0));
+ gtk_box_pack_start (GTK_BOX (widget), GTK_WIDGET (buttonBox), FALSE, FALSE, 0);
+
+ GtkWidget *addPropButton = gtk_button_new ();
+ //gtk_widget_set_size_request (addPropButton, 27, 27);
+ gtk_widget_set_tooltip_text (addPropButton, "Add property");
+ GtkWidget *addPropContents = gtk_hbox_new(FALSE, 0);
+ GtkWidget *addPropIcon = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (addPropContents), addPropIcon);
+ gtk_container_add (GTK_CONTAINER (addPropContents), gtk_label_new ("P"));
+ gtk_container_add (GTK_CONTAINER (addPropButton), addPropContents);
+ gtk_box_pack_start (buttonBox, addPropButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (addPropButton),
+ "clicked",
+ G_CALLBACK (add_prop_clicked_cb),
+ self);
+
+ GtkWidget *addAtomButton = gtk_button_new ();
+ //gtk_widget_set_size_request (addAtomButton, 27, 27);
+ gtk_widget_set_tooltip_text (addAtomButton, "Add atom");
+ GtkWidget *addAtomContents = gtk_hbox_new(FALSE, 0);
+ GtkWidget *addAtomIcon = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (addAtomContents), addAtomIcon);
+ gtk_container_add (GTK_CONTAINER (addAtomContents), gtk_label_new ("A"));
+ gtk_container_add (GTK_CONTAINER (addAtomButton), addAtomContents);
+ gtk_box_pack_start (buttonBox, addAtomButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (addAtomButton),
+ "clicked",
+ G_CALLBACK (add_atom_clicked_cb),
+ self);
+
+ GtkWidget *removeButton = gtk_button_new ();
+ //gtk_widget_set_size_request (removeButton, 27, 27);
+ gtk_widget_set_tooltip_text (removeButton, "Remove selected");
+ GtkWidget *removeIcon = gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add (GTK_CONTAINER (removeButton), removeIcon);
+ gtk_box_pack_start (buttonBox, removeButton, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (removeButton),
+ "clicked",
+ G_CALLBACK (remove_clicked_cb),
+ self);
+
+ gtk_widget_show_all (GTK_WIDGET (buttonBox));
+ gtk_widget_show_all (scrolledview);
+ }
+
+ return self;
+}
+
+- (void) clearStore {
+ GtkTreeIter iter;
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (list), &iter)) {
+ do {
+ void *prop;
+ gtk_tree_model_get (GTK_TREE_MODEL (list), &iter,
+ PLM_PROPERTY_COL, &prop,
+ -1);
+ [(id)prop release];
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (list), &iter));
+ gtk_list_store_clear (list);
+ }
+}
+
+- (void) reloadProperties {
+ [self clearStore];
+ int pos = 0;
+ for (GraphElementProperty *p in data) {
+ GtkTreeIter iter;
+ [p retain];
+ gtk_list_store_insert_with_values (list, &iter, pos,
+ PLM_NAME_COL, [[p key] UTF8String],
+ PLM_VALUE_COL, [[p value] UTF8String],
+ PLM_IS_PROPERTY_COL, ![p isAtom],
+ PLM_PROPERTY_COL, (void *)p,
+ -1);
+ ++pos;
+ }
+}
+
+- (GtkWidget*) widget { return widget; }
+- (GraphElementData*) data { return data; }
+- (void) setData:(GraphElementData*)d {
+ [d retain];
+ [data release];
+ data = d;
+ [self reloadProperties];
+}
+
+- (NSObject<PropertyChangeDelegate>*) delegate {
+ return delegate;
+}
+
+- (void) setDelegate:(NSObject<PropertyChangeDelegate>*)d {
+ id oldDelegate = delegate;
+ delegate = [d retain];
+ [oldDelegate release];
+}
+
+- (void) dealloc {
+ [self clearStore];
+ [data release];
+ g_object_unref (list);
+ g_object_unref (widget);
+ [super dealloc];
+}
+
+@end
+
+// }}}
+// {{{ Private
+
+@implementation PropertyListEditor (Private)
+- (void) updatePath:(gchar*)pathStr withValue:(NSString*)newText {
+ GtkTreeIter iter;
+ GtkTreePath *path = gtk_tree_path_new_from_string (pathStr);
+
+ if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (list), &iter, path)) {
+ gtk_tree_path_free (path);
+ return;
+ }
+
+ void *propPtr;
+ gtk_tree_model_get (GTK_TREE_MODEL (list), &iter,
+ PLM_PROPERTY_COL, &propPtr,
+ -1);
+ GraphElementProperty *prop = (GraphElementProperty*)propPtr;
+
+ if (![prop isAtom]) {
+ if (![delegate respondsToSelector:@selector(startEdit)] || [delegate startEdit]) {
+ [prop setValue:newText];
+ gtk_list_store_set (list, &iter,
+ PLM_VALUE_COL, [newText UTF8String],
+ -1);
+ [delegate endEdit];
+ }
+ }
+
+ gtk_tree_path_free (path);
+}
+
+- (void) updatePath:(gchar*)pathStr withName:(NSString*)newText {
+ GtkTreeIter iter;
+ GtkTreePath *path = gtk_tree_path_new_from_string (pathStr);
+
+ if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (list), &iter, path)) {
+ gtk_tree_path_free (path);
+ return;
+ }
+
+ void *propPtr;
+ gtk_tree_model_get (GTK_TREE_MODEL (list), &iter,
+ PLM_PROPERTY_COL, &propPtr,
+ -1);
+ GraphElementProperty *prop = (GraphElementProperty*)propPtr;
+
+ if (![delegate respondsToSelector:@selector(startEdit)] || [delegate startEdit]) {
+ if ([newText isEqualToString:@""]) {
+ [data removeObjectIdenticalTo:prop];
+ gtk_list_store_remove (list, &iter);
+ [prop release];
+ } else {
+ [prop setKey:newText];
+ gtk_list_store_set (list, &iter,
+ PLM_NAME_COL, [newText UTF8String],
+ -1);
+ }
+ [delegate endEdit];
+ }
+
+ gtk_tree_path_free (path);
+}
+
+- (void) addProperty {
+ GtkTreeIter iter;
+ GraphElementProperty *p = [[GraphElementProperty alloc] initWithPropertyValue:@"" forKey:@"new property"];
+ if (![delegate respondsToSelector:@selector(startEdit)] || [delegate startEdit]) {
+ [data addObject:p];
+ gint pos = [data count] - 1;
+ gtk_list_store_insert_with_values (list, &iter, pos,
+ PLM_NAME_COL, "new property",
+ PLM_VALUE_COL, "",
+ PLM_IS_PROPERTY_COL, TRUE,
+ PLM_PROPERTY_COL, (void *)p,
+ -1);
+ [delegate endEdit];
+ } else {
+ [p release];
+ }
+}
+
+- (void) addAtom {
+ GtkTreeIter iter;
+ GraphElementProperty *p = [[GraphElementProperty alloc] initWithAtomName:@"new atom"];
+ if (![delegate respondsToSelector:@selector(startEdit)] || [delegate startEdit]) {
+ [data addObject:p];
+ gint pos = [data count] - 1;
+ gtk_list_store_insert_with_values (list, &iter, pos,
+ PLM_NAME_COL, "new atom",
+ PLM_VALUE_COL, [[p value] UTF8String],
+ PLM_IS_PROPERTY_COL, FALSE,
+ PLM_PROPERTY_COL, (void *)p,
+ -1);
+ [delegate endEdit];
+ } else {
+ [p release];
+ }
+}
+
+- (void) removeSelected {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
+ GList *selPaths = gtk_tree_selection_get_selected_rows (selection, NULL);
+ GList *selIters = NULL;
+
+ // Convert to iters, as GtkListStore has persistent iters
+ GList *curr = selPaths;
+ while (curr != NULL) {
+ GtkTreeIter iter;
+ gtk_tree_model_get_iter (GTK_TREE_MODEL (list),
+ &iter,
+ (GtkTreePath*)curr->data);
+ selIters = g_list_prepend (selIters, gtk_tree_iter_copy (&iter));
+ curr = g_list_next (curr);
+ }
+
+ // remove all iters
+ curr = selIters;
+ while (curr != NULL) {
+ GtkTreeIter *iter = (GtkTreeIter*)curr->data;
+ void *propPtr;
+ gtk_tree_model_get (GTK_TREE_MODEL (list), iter,
+ PLM_PROPERTY_COL, &propPtr,
+ -1);
+ GraphElementProperty *prop = (GraphElementProperty*)propPtr;
+ if (![delegate respondsToSelector:@selector(startEdit)] || [delegate startEdit]) {
+ [data removeObjectIdenticalTo:prop];
+ gtk_list_store_remove (list, iter);
+ [prop release];
+ [delegate endEdit];
+ }
+ curr = g_list_next (curr);
+ }
+
+ g_list_foreach (selIters, (GFunc) gtk_tree_iter_free, NULL);
+ g_list_free (selIters);
+ g_list_foreach (selPaths, (GFunc) gtk_tree_path_free, NULL);
+ g_list_free (selPaths);
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void value_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PropertyListEditor *editor)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor updatePath:path withValue:[NSString stringWithUTF8String:new_text]];
+ [pool drain];
+}
+
+static void name_edited_cb (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ PropertyListEditor *editor)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor updatePath:path withName:[NSString stringWithUTF8String:new_text]];
+ [pool drain];
+}
+
+static void add_prop_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor addProperty];
+ [pool drain];
+}
+
+static void add_atom_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor addAtom];
+ [pool drain];
+}
+
+static void remove_clicked_cb (GtkButton *button,
+ PropertyListEditor *editor)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [editor removeSelected];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/PropertyPane.h b/tikzit/src/gtk/PropertyPane.h
new file mode 100644
index 0000000..30f8e5a
--- /dev/null
+++ b/tikzit/src/gtk/PropertyPane.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "Configuration.h"
+#import "TikzDocument.h"
+
+@class PropertyListEditor;
+@class GraphPropertyDelegate;
+@class NodePropertyDelegate;
+@class EdgePropertyDelegate;
+@class EdgeNodePropertyDelegate;
+
+@interface PropertyPane: NSObject {
+ TikzDocument *document;
+ BOOL blockUpdates;
+
+ PropertyListEditor *graphProps;
+ PropertyListEditor *nodeProps;
+ PropertyListEditor *edgeProps;
+ PropertyListEditor *edgeNodeProps;
+
+ GraphPropertyDelegate *graphPropDelegate;
+ NodePropertyDelegate *nodePropDelegate;
+ EdgePropertyDelegate *edgePropDelegate;
+ EdgeNodePropertyDelegate *edgeNodePropDelegate;
+
+ GtkWidget *propertiesPane;
+
+ GtkExpander *graphPropsExpander;
+ GtkExpander *nodePropsExpander;
+ GtkExpander *edgePropsExpander;
+
+ GtkEntry *nodeLabelEntry;
+ GtkToggleButton *edgeNodeToggle;
+ GtkWidget *edgeNodePropsWidget;
+ GtkEntry *edgeNodeLabelEntry;
+}
+
+@property (readonly) GtkWidget *widget;
+@property (retain) TikzDocument *document;
+
+- (id) init;
+
+- (void) restoreUiStateFromConfig:(Configuration*)file group:(NSString*)group;
+- (void) saveUiStateToConfig:(Configuration*)file group:(NSString*)group;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/PropertyPane.m b/tikzit/src/gtk/PropertyPane.m
new file mode 100644
index 0000000..35910f7
--- /dev/null
+++ b/tikzit/src/gtk/PropertyPane.m
@@ -0,0 +1,598 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "PropertyPane.h"
+#import "PropertyListEditor.h"
+#import "GraphElementProperty.h"
+#import "gtkhelpers.h"
+
+// {{{ Internal interfaces
+// {{{ GTK+ helpers
+static GtkWidget *createLabelledEntry (const gchar *labelText, GtkEntry **entry);
+static GtkWidget *createPropsPaneWithLabelEntry (PropertyListEditor *props, GtkEntry **labelEntry);
+// }}}
+// {{{ GTK+ callbacks
+static void node_label_changed_cb (GtkEditable *widget, PropertyPane *pane);
+static void edge_node_label_changed_cb (GtkEditable *widget, PropertyPane *pane);
+static void edge_node_toggled_cb (GtkToggleButton *widget, PropertyPane *pane);
+// }}}
+
+@interface PropertyPane (Notifications)
+- (void) nodeSelectionChanged:(NSNotification*)n;
+- (void) edgeSelectionChanged:(NSNotification*)n;
+- (void) graphChanged:(NSNotification*)n;
+- (void) nodeLabelEdited:(NSString*)newValue;
+- (void) edgeNodeLabelEdited:(NSString*)newValue;
+- (void) edgeNodeToggled:(BOOL)newValue;
+@end
+
+@interface PropertyPane (Private)
+- (void) updateGraphPane;
+- (void) updateNodePane;
+- (void) updateEdgePane;
+- (void) _addSplitter;
+- (GtkExpander*) _addExpanderWithName:(const gchar*)name contents:(GtkWidget*)contents;
+@end
+
+// {{{ Delegates
+
+@interface GraphPropertyDelegate : NSObject<PropertyChangeDelegate> {
+ TikzDocument *doc;
+}
+- (void) setDocument:(TikzDocument*)d;
+@end
+
+@interface NodePropertyDelegate : NSObject<PropertyChangeDelegate> {
+ TikzDocument *doc;
+ Node *node;
+}
+- (void) setDocument:(TikzDocument*)d;
+- (void) setNode:(Node*)n;
+@end
+
+@interface EdgePropertyDelegate : NSObject<PropertyChangeDelegate> {
+ TikzDocument *doc;
+ Edge *edge;
+}
+- (void) setDocument:(TikzDocument*)d;
+- (void) setEdge:(Edge*)e;
+@end
+
+// }}}
+
+// }}}
+// {{{ API
+
+@implementation PropertyPane
+
+@synthesize widget=propertiesPane;
+
+- (id) init {
+ self = [super init];
+
+ if (self) {
+ document = nil;
+ blockUpdates = NO;
+
+ graphProps = [[PropertyListEditor alloc] init];
+ graphPropDelegate = [[GraphPropertyDelegate alloc] init];
+ [graphProps setDelegate:graphPropDelegate];
+
+ nodeProps = [[PropertyListEditor alloc] init];
+ nodePropDelegate = [[NodePropertyDelegate alloc] init];
+ [nodeProps setDelegate:nodePropDelegate];
+
+ edgeProps = [[PropertyListEditor alloc] init];
+ edgePropDelegate = [[EdgePropertyDelegate alloc] init];
+ [edgeProps setDelegate:edgePropDelegate];
+
+ edgeNodeProps = [[PropertyListEditor alloc] init];
+ [edgeNodeProps setDelegate:edgePropDelegate];
+
+ propertiesPane = gtk_vbox_new (FALSE, 0);
+ g_object_ref_sink (propertiesPane);
+
+ /*
+ * Graph properties
+ */
+ graphPropsExpander = [self _addExpanderWithName:"Graph properties"
+ contents:[graphProps widget]];
+ g_object_ref_sink (graphPropsExpander);
+
+
+ [self _addSplitter];
+
+ /*
+ * Node properties
+ */
+ GtkWidget *nodePropsWidget = createPropsPaneWithLabelEntry(nodeProps, &nodeLabelEntry);
+ g_object_ref (nodeLabelEntry);
+ nodePropsExpander = [self _addExpanderWithName:"Node properties"
+ contents:nodePropsWidget];
+ g_object_ref (nodePropsExpander);
+ g_signal_connect (G_OBJECT (nodeLabelEntry),
+ "changed",
+ G_CALLBACK (node_label_changed_cb),
+ self);
+
+
+ [self _addSplitter];
+
+ /*
+ * Edge properties
+ */
+ GtkBox *edgePropsBox = GTK_BOX (gtk_vbox_new (FALSE, 0));
+ gtk_box_set_spacing (edgePropsBox, 6);
+ edgePropsExpander = [self _addExpanderWithName:"Edge properties"
+ contents:GTK_WIDGET (edgePropsBox)];
+ g_object_ref (edgePropsExpander);
+
+ gtk_widget_show ([edgeProps widget]);
+ gtk_box_pack_start (edgePropsBox, [edgeProps widget], FALSE, TRUE, 0);
+
+ GtkWidget *split = gtk_hseparator_new ();
+ gtk_box_pack_start (edgePropsBox, split, FALSE, FALSE, 0);
+ gtk_widget_show (split);
+
+ edgeNodeToggle = GTK_TOGGLE_BUTTON (gtk_check_button_new_with_label ("Child node"));
+ g_object_ref (edgeNodeToggle);
+ gtk_widget_show (GTK_WIDGET (edgeNodeToggle));
+ gtk_box_pack_start (edgePropsBox, GTK_WIDGET (edgeNodeToggle), FALSE, TRUE, 0);
+ g_signal_connect (G_OBJECT (GTK_WIDGET (edgeNodeToggle)),
+ "toggled",
+ G_CALLBACK (edge_node_toggled_cb),
+ self);
+
+ edgeNodePropsWidget = createPropsPaneWithLabelEntry(edgeNodeProps, &edgeNodeLabelEntry);
+ g_object_ref (edgeNodePropsWidget);
+ g_object_ref (edgeNodeLabelEntry);
+ gtk_widget_show (edgeNodePropsWidget);
+ gtk_box_pack_start (edgePropsBox, edgeNodePropsWidget, FALSE, TRUE, 0);
+ g_signal_connect (G_OBJECT (edgeNodeLabelEntry),
+ "changed",
+ G_CALLBACK (edge_node_label_changed_cb),
+ self);
+
+
+ [self _addSplitter];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [document release];
+
+ g_object_unref (propertiesPane);
+ g_object_unref (graphPropsExpander);
+ g_object_unref (nodePropsExpander);
+ g_object_unref (edgePropsExpander);
+ g_object_unref (nodeLabelEntry);
+ g_object_unref (edgeNodeToggle);
+ g_object_unref (edgeNodePropsWidget);
+ g_object_unref (edgeNodeLabelEntry);
+
+ [graphProps release];
+ [nodeProps release];
+ [edgeProps release];
+ [edgeNodeProps release];
+ [graphPropDelegate release];
+ [nodePropDelegate release];
+ [edgePropDelegate release];
+
+ [super dealloc];
+}
+
+- (TikzDocument*) document {
+ return document;
+}
+
+- (void) setDocument:(TikzDocument*)doc {
+ if (document != nil) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:document];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:[document pickSupport]];
+ }
+
+ [graphPropDelegate setDocument:doc];
+ [nodePropDelegate setDocument:doc];
+ [edgePropDelegate setDocument:doc];
+
+ if (doc != nil) {
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(nodeSelectionChanged:)
+ name:@"NodeSelectionChanged" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(edgeSelectionChanged:)
+ name:@"EdgeSelectionChanged" object:[doc pickSupport]];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(graphChanged:)
+ name:@"TikzChanged" object:doc];
+ }
+
+ [self updateGraphPane];
+ [self updateNodePane];
+ [self updateEdgePane];
+
+ [doc retain];
+ [document release];
+ document = doc;
+}
+
+- (void) restoreUiStateFromConfig:(Configuration*)file group:(NSString*)group {
+ gtk_expander_set_expanded (graphPropsExpander,
+ [file booleanEntry:@"graph-props-expanded"
+ inGroup:group
+ withDefault:NO]);
+ gtk_expander_set_expanded (nodePropsExpander,
+ [file booleanEntry:@"node-props-expanded"
+ inGroup:group
+ withDefault:YES]);
+ gtk_expander_set_expanded (edgePropsExpander,
+ [file booleanEntry:@"edge-props-expanded"
+ inGroup:group
+ withDefault:NO]);
+}
+
+- (void) saveUiStateToConfig:(Configuration*)file group:(NSString*)group {
+ [file setBooleanEntry:@"graph-props-expanded"
+ inGroup:group
+ value:gtk_expander_get_expanded (graphPropsExpander)];
+ [file setBooleanEntry:@"node-props-expanded"
+ inGroup:group
+ value:gtk_expander_get_expanded (nodePropsExpander)];
+ [file setBooleanEntry:@"edge-props-expanded"
+ inGroup:group
+ value:gtk_expander_get_expanded (edgePropsExpander)];
+}
+
+@end
+// }}}
+// {{{ Notifications
+
+@implementation PropertyPane (Notifications)
+
+- (void) nodeSelectionChanged:(NSNotification*)n {
+ [self updateNodePane];
+}
+
+- (void) edgeSelectionChanged:(NSNotification*)n {
+ [self updateEdgePane];
+}
+
+- (void) graphChanged:(NSNotification*)n {
+ [self updateGraphPane];
+ [self updateNodePane];
+ [self updateEdgePane];
+}
+
+- (void) nodeLabelEdited:(NSString*)newValue {
+ if (blockUpdates)
+ return;
+
+ NSSet *sel = [[document pickSupport] selectedNodes];
+ if ([sel count] != 1) {
+ NSLog(@"Expected single node selected; got %lu", [sel count]);
+ return;
+ }
+
+ Node *node = [sel anyObject];
+ [document startModifyNode:node];
+ [node setLabel:newValue];
+ [document endModifyNode];
+}
+
+- (void) edgeNodeLabelEdited:(NSString*)newValue {
+ if (blockUpdates)
+ return;
+
+ NSSet *sel = [[document pickSupport] selectedEdges];
+ if ([sel count] != 1) {
+ NSLog(@"Expected single edge selected; got %lu", [sel count]);
+ return;
+ }
+
+ Edge *edge = [sel anyObject];
+ if (![edge hasEdgeNode]) {
+ NSLog(@"Expected edge with edge node");
+ return;
+ }
+
+ [document startModifyEdge:edge];
+ [[edge edgeNode] setLabel:newValue];
+ [document endModifyEdge];
+}
+
+- (void) edgeNodeToggled:(BOOL)newValue {
+ if (blockUpdates)
+ return;
+
+ NSSet *sel = [[document pickSupport] selectedEdges];
+ if ([sel count] != 1) {
+ NSLog(@"Expected single edge selected; got %lu", [sel count]);
+ return;
+ }
+
+ Edge *edge = [sel anyObject];
+
+ [document startModifyEdge:edge];
+ [edge setHasEdgeNode:newValue];
+ [document endModifyEdge];
+}
+
+@end
+// }}}
+// {{{ Private
+
+@implementation PropertyPane (Private)
+
+- (void) updateGraphPane {
+ blockUpdates = YES;
+
+ GraphElementData *data = [[document graph] data];
+ [graphProps setData:data];
+ gtk_widget_set_sensitive (gtk_bin_get_child (GTK_BIN (graphPropsExpander)), data != nil);
+
+ blockUpdates = NO;
+}
+
+- (void) updateNodePane {
+ blockUpdates = YES;
+
+ NSSet *sel = [[document pickSupport] selectedNodes];
+ if ([sel count] == 1) {
+ Node *n = [sel anyObject];
+ [nodePropDelegate setNode:n];
+ [nodeProps setData:[n data]];
+ gtk_entry_set_text (nodeLabelEntry, [[n label] UTF8String]);
+ gtk_widget_set_sensitive (gtk_bin_get_child (GTK_BIN (nodePropsExpander)), TRUE);
+ } else {
+ [nodePropDelegate setNode:nil];
+ [nodeProps setData:nil];
+ gtk_entry_set_text (nodeLabelEntry, "");
+ gtk_widget_set_sensitive (gtk_bin_get_child (GTK_BIN (nodePropsExpander)), FALSE);
+ }
+
+ blockUpdates = NO;
+}
+
+- (void) updateEdgePane {
+ blockUpdates = YES;
+
+ NSSet *sel = [[document pickSupport] selectedEdges];
+ if ([sel count] == 1) {
+ Edge *e = [sel anyObject];
+ [edgePropDelegate setEdge:e];
+ [edgeProps setData:[e data]];
+ gtk_widget_set_sensitive (gtk_bin_get_child (GTK_BIN (edgePropsExpander)), TRUE);
+ if ([e hasEdgeNode]) {
+ gtk_toggle_button_set_mode (edgeNodeToggle, TRUE);
+ gtk_entry_set_text (GTK_ENTRY (edgeNodeLabelEntry), [[[e edgeNode] label] UTF8String]);
+ [edgeNodeProps setData:[[e edgeNode] data]];
+ gtk_widget_set_sensitive (edgeNodePropsWidget, TRUE);
+ } else {
+ gtk_toggle_button_set_mode (edgeNodeToggle, FALSE);
+ gtk_entry_set_text (GTK_ENTRY (edgeNodeLabelEntry), "");
+ [edgeNodeProps setData:nil];
+ gtk_widget_set_sensitive (edgeNodePropsWidget, FALSE);
+ }
+ } else {
+ [edgePropDelegate setEdge:nil];
+ [edgeProps setData:nil];
+ [edgeNodeProps setData:nil];
+ gtk_entry_set_text (nodeLabelEntry, "");
+ gtk_widget_set_sensitive (gtk_bin_get_child (GTK_BIN (edgePropsExpander)), FALSE);
+ }
+
+ blockUpdates = NO;
+}
+
+- (void) _addSplitter {
+ GtkWidget *split = gtk_hseparator_new ();
+ gtk_box_pack_start (GTK_BOX (propertiesPane),
+ split,
+ FALSE, // expand
+ FALSE, // fill
+ 0); // padding
+ gtk_widget_show (split);
+}
+
+- (GtkExpander*) _addExpanderWithName:(const gchar*)name contents:(GtkWidget*)contents {
+ GtkWidget *exp = gtk_expander_new (name);
+ gtk_box_pack_start (GTK_BOX (propertiesPane),
+ exp,
+ FALSE, // expand
+ TRUE, // fill
+ 0); // padding
+ gtk_widget_show (exp);
+ gtk_container_set_border_width (GTK_CONTAINER (contents), 6);
+ gtk_container_add (GTK_CONTAINER (exp), contents);
+ gtk_widget_show (contents);
+ return GTK_EXPANDER (exp);
+}
+
+@end
+
+// }}}
+// {{{ Delegates
+
+@implementation GraphPropertyDelegate
+- (id) init {
+ self = [super init];
+ if (self) {
+ doc = nil;
+ }
+ return self;
+}
+- (void) setDocument:(TikzDocument*)d {
+ doc = d;
+}
+- (BOOL)startEdit {
+ if ([doc graph] != nil) {
+ [doc startChangeGraphProperties];
+ return YES;
+ }
+ return NO;
+}
+- (void)endEdit {
+ [doc endChangeGraphProperties];
+}
+- (void)cancelEdit {
+ [doc cancelChangeGraphProperties];
+}
+@end
+
+@implementation NodePropertyDelegate
+- (id) init {
+ self = [super init];
+ if (self) {
+ doc = nil;
+ node = nil;
+ }
+ return self;
+}
+- (void) setDocument:(TikzDocument*)d {
+ doc = d;
+ node = nil;
+}
+- (void) setNode:(Node*)n {
+ node = n;
+}
+- (BOOL)startEdit {
+ if (node != nil) {
+ [doc startModifyNode:node];
+ return YES;
+ }
+ return NO;
+}
+- (void)endEdit {
+ [doc endModifyNode];
+}
+- (void)cancelEdit {
+ [doc cancelModifyNode];
+}
+@end
+
+@implementation EdgePropertyDelegate
+- (id) init {
+ self = [super init];
+ if (self) {
+ doc = nil;
+ edge = nil;
+ }
+ return self;
+}
+- (void) setDocument:(TikzDocument*)d {
+ doc = d;
+ edge = nil;
+}
+- (void) setEdge:(Edge*)e {
+ edge = e;
+}
+- (BOOL)startEdit {
+ if (edge != nil) {
+ [doc startModifyEdge:edge];
+ return YES;
+ }
+ return NO;
+}
+- (void)endEdit {
+ [doc endModifyEdge];
+}
+- (void)cancelEdit {
+ [doc cancelModifyEdge];
+}
+@end
+
+// }}}
+// {{{ GTK+ helpers
+
+static GtkWidget *createLabelledEntry (const gchar *labelText, GtkEntry **entry) {
+ GtkBox *box = GTK_BOX (gtk_hbox_new (FALSE, 0));
+ GtkWidget *label = gtk_label_new (labelText);
+ gtk_widget_show (label);
+ GtkWidget *entryWidget = gtk_entry_new ();
+ gtk_widget_show (entryWidget);
+ // container widget expand fill pad
+ gtk_box_pack_start (box, label, FALSE, TRUE, 5);
+ gtk_box_pack_start (box, entryWidget, TRUE, TRUE, 0);
+ if (entry)
+ *entry = GTK_ENTRY (entryWidget);
+ return GTK_WIDGET (box);
+}
+
+static GtkWidget *createPropsPaneWithLabelEntry (PropertyListEditor *props, GtkEntry **labelEntry) {
+ GtkBox *box = GTK_BOX (gtk_vbox_new (FALSE, 0));
+ gtk_box_set_spacing (box, 6);
+
+ GtkWidget *labelWidget = createLabelledEntry ("Label", labelEntry);
+ gtk_widget_show (labelWidget);
+ // box widget expand fill pad
+ gtk_box_pack_start (box, labelWidget, FALSE, TRUE, 0);
+ gtk_box_pack_start (box, [props widget], FALSE, TRUE, 0);
+ gtk_widget_show ([props widget]);
+ return GTK_WIDGET (box);
+}
+
+// }}}
+// {{{ GTK+ callbacks
+
+static void node_label_changed_cb (GtkEditable *editable, PropertyPane *pane) {
+ if (!gtk_widget_is_sensitive (GTK_WIDGET (editable))) {
+ // clearly wasn't the user editing
+ return;
+ }
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ NSString *newValue = gtk_editable_get_string (editable, 0, -1);
+ [pane nodeLabelEdited:newValue];
+
+ [pool drain];
+}
+
+static void edge_node_label_changed_cb (GtkEditable *editable, PropertyPane *pane) {
+ if (!gtk_widget_is_sensitive (GTK_WIDGET (editable))) {
+ // clearly wasn't the user editing
+ return;
+ }
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ NSString *newValue = gtk_editable_get_string (editable, 0, -1);
+ [pane edgeNodeLabelEdited:newValue];
+
+ [pool drain];
+}
+
+static void edge_node_toggled_cb (GtkToggleButton *toggle, PropertyPane *pane) {
+ if (!gtk_widget_is_sensitive (GTK_WIDGET (toggle))) {
+ // clearly wasn't the user editing
+ return;
+ }
+
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ gboolean newValue = gtk_toggle_button_get_mode (toggle);
+ [pane edgeNodeToggled:newValue];
+
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/RecentManager.h b/tikzit/src/gtk/RecentManager.h
new file mode 100644
index 0000000..e2c2793
--- /dev/null
+++ b/tikzit/src/gtk/RecentManager.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+
+@interface RecentManager: NSObject {
+}
+
++ (RecentManager*) defaultManager;
+
+- (void)addRecentFile:(NSString*)path;
+- (void)removeRecentFile:(NSString*)path;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/RecentManager.m b/tikzit/src/gtk/RecentManager.m
new file mode 100644
index 0000000..c6074c6
--- /dev/null
+++ b/tikzit/src/gtk/RecentManager.m
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "RecentManager.h"
+#import <gtk/gtk.h>
+
+static RecentManager *defMan = nil;
+
+@implementation RecentManager
+- (id) init {
+ self = [super init];
+ return self;
+}
+
++ (RecentManager*) defaultManager {
+ if (defMan == nil) {
+ defMan = [[self alloc] init];
+ }
+ return defMan;
+}
+
+- (void)addRecentFile:(NSString*)path {
+ NSError *error = nil;
+ gchar *uri = [path glibUriWithError:&error];
+ if (error) {
+ logError (error, @"Could not add recent file");
+ return;
+ }
+
+ GtkRecentData recent_data;
+ recent_data.display_name = NULL;
+ recent_data.description = NULL;
+ recent_data.mime_type = "text/x-tikz";
+ recent_data.app_name = (gchar *) g_get_application_name ();
+ recent_data.app_exec = g_strjoin (" ", g_get_prgname (), "%u", NULL);
+ recent_data.groups = NULL;
+ recent_data.is_private = FALSE;
+
+ gtk_recent_manager_add_full (gtk_recent_manager_get_default(), uri, &recent_data);
+
+ g_free (uri);
+ g_free (recent_data.app_exec);
+}
+
+- (void)removeRecentFile:(NSString*)path {
+ NSError *error = nil;
+ gchar *uri = [path glibUriWithError:&error];
+ if (error) {
+ logError (error, @"Could not remove recent file");
+ return;
+ }
+
+ gtk_recent_manager_remove_item (gtk_recent_manager_get_default(), uri, NULL);
+
+ g_free (uri);
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/SettingsDialog.h b/tikzit/src/gtk/SettingsDialog.h
new file mode 100644
index 0000000..a159afb
--- /dev/null
+++ b/tikzit/src/gtk/SettingsDialog.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class Configuration;
+
+@interface SettingsDialog: NSObject {
+ Configuration *configuration;
+
+ // we don't keep any refs, as we control
+ // the top window
+ GtkWindow *parentWindow;
+ GtkWindow *window;
+ GtkEntry *pdflatexPathEntry;
+}
+
+@property (retain) Configuration* configuration;
+
+- (id) initWithConfiguration:(Configuration*)c;
+
+- (void) setParentWindow:(GtkWindow*)parent;
+
+- (void) show;
+- (void) hide;
+- (BOOL) isVisible;
+- (void) setVisible:(BOOL)visible;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/SettingsDialog.m b/tikzit/src/gtk/SettingsDialog.m
new file mode 100644
index 0000000..88c66d2
--- /dev/null
+++ b/tikzit/src/gtk/SettingsDialog.m
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "SettingsDialog.h"
+
+#import "Configuration.h"
+
+// {{{ Internal interfaces
+// {{{ Signals
+static gboolean window_delete_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ SettingsDialog *dialog);
+static void ok_button_clicked_cb (GtkButton *widget, SettingsDialog *dialog);
+static void cancel_button_clicked_cb (GtkButton *widget, SettingsDialog *dialog);
+// }}}
+
+@interface SettingsDialog (Private)
+- (void) loadUi;
+- (void) save;
+- (void) revert;
+@end
+
+// }}}
+// {{{ API
+
+@implementation SettingsDialog
+
+- (id) init {
+ [self release];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithConfiguration:(Configuration*)c {
+ self = [super init];
+
+ if (self) {
+ configuration = [c retain];
+ parentWindow = NULL;
+ window = NULL;
+ }
+
+ return self;
+}
+
+- (Configuration*) configuration {
+ return configuration;
+}
+
+- (void) setConfiguration:(Configuration*)c {
+ [c retain];
+ [configuration release];
+ configuration = c;
+ [self revert];
+}
+
+- (void) setParentWindow:(GtkWindow*)parent {
+ GtkWindow *oldParent = parentWindow;
+
+ if (parent)
+ g_object_ref (parentWindow);
+ parentWindow = parent;
+ if (oldParent)
+ g_object_unref (oldParent);
+
+ if (window) {
+ gtk_window_set_transient_for (window, parentWindow);
+ }
+}
+
+- (void) show {
+ [self loadUi];
+ [self revert];
+ gtk_widget_show (GTK_WIDGET (window));
+}
+
+- (void) hide {
+ if (!window) {
+ return;
+ }
+ gtk_widget_hide (GTK_WIDGET (window));
+}
+
+- (BOOL) isVisible {
+ if (!window) {
+ return NO;
+ }
+ gboolean visible;
+ g_object_get (G_OBJECT (window), "visible", &visible, NULL);
+ return visible ? YES : NO;
+}
+
+- (void) setVisible:(BOOL)visible {
+ if (visible) {
+ [self show];
+ } else {
+ [self hide];
+ }
+}
+
+- (void) dealloc {
+ [configuration release];
+ configuration = nil;
+
+ if (window) {
+ gtk_widget_destroy (GTK_WIDGET (window));
+ window = NULL;
+ }
+ if (parentWindow) {
+ g_object_ref (parentWindow);
+ }
+
+ [super dealloc];
+}
+
+@end
+
+// }}}
+// {{{ Private
+
+@implementation SettingsDialog (Private)
+- (void) loadUi {
+ if (window) {
+ return;
+ }
+
+ window = GTK_WINDOW (gtk_window_new (GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_title (window, "Preamble editor");
+ gtk_window_set_modal (window, TRUE);
+ gtk_window_set_position (window, GTK_WIN_POS_CENTER_ON_PARENT);
+ gtk_window_set_type_hint (window, GDK_WINDOW_TYPE_HINT_DIALOG);
+ if (parentWindow) {
+ gtk_window_set_transient_for (window, parentWindow);
+ }
+ g_signal_connect (window,
+ "delete-event",
+ G_CALLBACK (window_delete_event_cb),
+ self);
+
+ GtkWidget *mainBox = gtk_vbox_new (FALSE, 18);
+ gtk_container_set_border_width (GTK_CONTAINER (mainBox), 12);
+ gtk_container_add (GTK_CONTAINER (window), mainBox);
+
+#ifdef HAVE_POPPLER
+ /*
+ * Path for pdflatex
+ */
+
+ GtkWidget *pdflatexFrame = gtk_frame_new ("Previews");
+ gtk_box_pack_start (GTK_BOX (mainBox), pdflatexFrame, TRUE, TRUE, 0);
+
+ GtkBox *pdflatexBox = GTK_BOX (gtk_hbox_new (FALSE, 6));
+ gtk_container_add (GTK_CONTAINER (pdflatexFrame), GTK_WIDGET (pdflatexBox));
+ gtk_container_set_border_width (GTK_CONTAINER (pdflatexBox), 6);
+
+ GtkWidget *pdflatexLabel = gtk_label_new ("Path to pdflatex:");
+ gtk_misc_set_alignment (GTK_MISC (pdflatexLabel), 0, 0.5);
+ gtk_box_pack_start (pdflatexBox,
+ pdflatexLabel,
+ FALSE, TRUE, 0);
+
+ pdflatexPathEntry = GTK_ENTRY (gtk_entry_new ());
+ gtk_box_pack_start (pdflatexBox,
+ GTK_WIDGET (pdflatexPathEntry),
+ TRUE, TRUE, 0);
+#else
+ pdflatexPathEntry = NULL;
+#endif
+
+
+ /*
+ * Bottom buttons
+ */
+
+ GtkContainer *buttonBox = GTK_CONTAINER (gtk_hbutton_box_new ());
+ gtk_box_set_spacing (GTK_BOX (buttonBox), 6);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (buttonBox), GTK_BUTTONBOX_END);
+ gtk_box_pack_start (GTK_BOX (mainBox),
+ GTK_WIDGET (buttonBox),
+ FALSE, TRUE, 0);
+
+ GtkWidget *okButton = gtk_button_new_from_stock (GTK_STOCK_OK);
+ gtk_container_add (buttonBox, okButton);
+ g_signal_connect (okButton,
+ "clicked",
+ G_CALLBACK (ok_button_clicked_cb),
+ self);
+
+ GtkWidget *cancelButton = gtk_button_new_from_stock (GTK_STOCK_CANCEL);
+ gtk_container_add (buttonBox, cancelButton);
+ g_signal_connect (cancelButton,
+ "clicked",
+ G_CALLBACK (cancel_button_clicked_cb),
+ self);
+
+ [self revert];
+
+ gtk_widget_show_all (mainBox);
+}
+
+- (void) save {
+ if (!window)
+ return;
+
+#ifdef HAVE_POPPLER
+ const gchar *path = gtk_entry_get_text (pdflatexPathEntry);
+ if (path && *path) {
+ [configuration setStringEntry:@"pdflatex"
+ inGroup:@"Previews"
+ value:[NSString stringWithUTF8String:path]];
+ }
+#endif
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:@"ConfigurationChanged"
+ object:configuration];
+}
+
+- (void) revert {
+ if (!window)
+ return;
+
+#ifdef HAVE_POPPLER
+ NSString *path = [configuration stringEntry:@"pdflatex"
+ inGroup:@"Previews"
+ withDefault:@"pdflatex"];
+ gtk_entry_set_text (pdflatexPathEntry, [path UTF8String]);
+#endif
+}
+@end
+
+// }}}
+// {{{ GTK+ callbacks
+
+static gboolean window_delete_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ SettingsDialog *dialog) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [dialog hide];
+ [pool drain];
+ return TRUE; // we dealt with this event
+}
+
+static void ok_button_clicked_cb (GtkButton *widget, SettingsDialog *dialog) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [dialog save];
+ [dialog hide];
+ [pool drain];
+}
+
+static void cancel_button_clicked_cb (GtkButton *widget, SettingsDialog *dialog) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [dialog hide];
+ [pool drain];
+}
+
+// }}}
+
+// vim:ft=objc:ts=4:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/Shape+Render.h b/tikzit/src/gtk/Shape+Render.h
new file mode 100644
index 0000000..a744c77
--- /dev/null
+++ b/tikzit/src/gtk/Shape+Render.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "Shape.h"
+#import "RenderContext.h"
+#import "Surface.h"
+
+@interface Shape (Render)
+
+- (void) drawPathWithTransform:(Transformer*)transform andContext:(id<RenderContext>)context;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/Shape+Render.m b/tikzit/src/gtk/Shape+Render.m
new file mode 100644
index 0000000..924bb24
--- /dev/null
+++ b/tikzit/src/gtk/Shape+Render.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "Shape+Render.h"
+
+#import "Edge.h"
+
+// we use cairo for finding the bounding box etc.
+#import <cairo/cairo.h>
+
+@implementation Shape (Render)
+
+- (void) drawPathWithTransform:(Transformer*)transform andContext:(id<RenderContext>)context {
+ [context startPath];
+
+ for (NSArray *arr in [self paths]) {
+ BOOL fst = YES;
+ NSPoint p, cp1, cp2;
+
+ for (Edge *e in arr) {
+ if (fst) {
+ fst = NO;
+ p = [transform toScreen:[[e source] point]];
+ [context moveTo:p];
+ }
+
+ p = [transform toScreen:[[e target] point]];
+ if ([e isStraight]) {
+ [context lineTo:p];
+ } else {
+ cp1 = [transform toScreen:[e cp1]];
+ cp2 = [transform toScreen:[e cp2]];
+ [context curveTo:p withCp1:cp1 andCp2:cp2];
+ }
+ }
+
+ [context closeSubPath];
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/StyleManager+Storage.h b/tikzit/src/gtk/StyleManager+Storage.h
new file mode 100644
index 0000000..1727786
--- /dev/null
+++ b/tikzit/src/gtk/StyleManager+Storage.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "StyleManager.h"
+
+@interface StyleManager (Storage)
+- (void) loadStylesUsingConfigurationName:(NSString*)name;
+- (void) saveStylesUsingConfigurationName:(NSString*)name;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/StyleManager+Storage.m b/tikzit/src/gtk/StyleManager+Storage.m
new file mode 100644
index 0000000..112b885
--- /dev/null
+++ b/tikzit/src/gtk/StyleManager+Storage.m
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "StyleManager+Storage.h"
+#import "Configuration.h"
+#import "NodeStyle+Storage.h"
+#import "EdgeStyle+Storage.h"
+
+static NSString *nodeStyleGroupPrefix = @"Style ";
+static NSString *edgeStyleGroupPrefix = @"EdgeStyle ";
+
+@implementation StyleManager (Storage)
+
+- (void) loadStylesUsingConfigurationName:(NSString*)name {
+ if (![Configuration configurationExistsWithName:name]) {
+ return;
+ }
+ NSError *error = nil;
+ Configuration *stylesConfig = [Configuration configurationWithName:name loadError:&error];
+ if (error != nil) {
+ logError (error, @"Could not load styles configuration");
+ // stick with the default config
+ return;
+ }
+ NSArray *groups = [stylesConfig groups];
+ NSMutableArray *ns = [NSMutableArray arrayWithCapacity:[groups count]];
+ NSMutableArray *es = [NSMutableArray arrayWithCapacity:[groups count]];
+
+ for (NSString *groupName in groups) {
+ if ([groupName hasPrefix:nodeStyleGroupPrefix]) {
+ NodeStyle *style = [[NodeStyle alloc] initFromConfigurationGroup:groupName config:stylesConfig];
+ [ns addObject:style];
+ } else if ([groupName hasPrefix:edgeStyleGroupPrefix]) {
+ EdgeStyle *style = [[EdgeStyle alloc] initFromConfigurationGroup:groupName config:stylesConfig];
+ [es addObject:style];
+ }
+ }
+
+ [self _setNodeStyles:ns];
+ [self _setEdgeStyles:es];
+}
+
+- (void) saveStylesUsingConfigurationName:(NSString*)name {
+ NSError *error = nil;
+ Configuration *stylesConfig = [Configuration emptyConfigurationWithName:name];
+ NSArray *ns = [self nodeStyles];
+ NSArray *es = [self edgeStyles];
+ NSUInteger length = [ns count];
+ for (int i = 0; i < length; ++i) {
+ NodeStyle *style = [ns objectAtIndex:i];
+ NSString *groupName = [NSString stringWithFormat:@"%@%d", nodeStyleGroupPrefix, i];
+ [style storeToConfigurationGroup:groupName config:stylesConfig];
+ }
+ length = [es count];
+ for (int i = 0; i < length; ++i) {
+ EdgeStyle *style = [es objectAtIndex:i];
+ NSString *groupName = [NSString stringWithFormat:@"%@%d", edgeStyleGroupPrefix, i];
+ [style storeToConfigurationGroup:groupName config:stylesConfig];
+ }
+ if (![stylesConfig writeToStoreWithError:&error]) {
+ logError (error, @"Could not write styles configuration");
+ }
+}
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/StylesPane.h b/tikzit/src/gtk/StylesPane.h
new file mode 100644
index 0000000..c1a603d
--- /dev/null
+++ b/tikzit/src/gtk/StylesPane.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+
+@class Configuration;
+@class EdgeStylesPalette;
+@class NodeStylesPalette;
+@class StyleManager;
+@class TikzDocument;
+
+@interface StylesPane: NSObject {
+ NodeStylesPalette *nodeStyles;
+ EdgeStylesPalette *edgeStyles;
+
+ GtkWidget *stylesPane;
+
+ GtkExpander *nodeStylesExpander;
+ GtkExpander *edgeStylesExpander;
+}
+
+@property (readonly) GtkWidget *widget;
+@property (retain) TikzDocument *document;
+@property (retain) StyleManager *styleManager;
+
+- (id) initWithManager:(StyleManager*)m;
+
+- (void) restoreUiStateFromConfig:(Configuration*)file group:(NSString*)group;
+- (void) saveUiStateToConfig:(Configuration*)file group:(NSString*)group;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
+
diff --git a/tikzit/src/gtk/StylesPane.m b/tikzit/src/gtk/StylesPane.m
new file mode 100644
index 0000000..0f6adef
--- /dev/null
+++ b/tikzit/src/gtk/StylesPane.m
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2012 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "StylesPane.h"
+
+#import "Configuration.h"
+#import "NodeStylesPalette.h"
+#import "EdgeStylesPalette.h"
+
+@interface StylesPane (Private)
+- (void) _addSplitter;
+- (GtkExpander*) _addExpanderWithName:(const gchar*)name contents:(GtkWidget*)contents;
+@end
+
+@implementation StylesPane
+
+@synthesize widget=stylesPane;
+
+- (id) init {
+ [self dealloc];
+ self = nil;
+ return nil;
+}
+
+- (id) initWithManager:(StyleManager*)m {
+ self = [super init];
+
+ if (self) {
+ nodeStyles = [[NodeStylesPalette alloc] initWithManager:m];
+ edgeStyles = [[EdgeStylesPalette alloc] initWithManager:m];
+
+ stylesPane = gtk_vbox_new (FALSE, 0);
+ g_object_ref_sink (stylesPane);
+
+ nodeStylesExpander = [self _addExpanderWithName:"Node styles"
+ contents:[nodeStyles widget]];
+ g_object_ref_sink (nodeStylesExpander);
+ [self _addSplitter];
+
+ edgeStylesExpander = [self _addExpanderWithName:"Edge styles"
+ contents:[edgeStyles widget]];
+ g_object_ref_sink (edgeStylesExpander);
+ [self _addSplitter];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ g_object_unref (stylesPane);
+
+ [nodeStyles release];
+ [edgeStyles release];
+
+ [super dealloc];
+}
+
+- (TikzDocument*) document {
+ return [nodeStyles document];
+}
+
+- (void) setDocument:(TikzDocument*)doc {
+ [nodeStyles setDocument:doc];
+ [edgeStyles setDocument:doc];
+}
+
+- (StyleManager*) styleManager {
+ return [nodeStyles styleManager];
+}
+
+- (void) setStyleManager:(StyleManager*)m {
+ [nodeStyles setStyleManager:m];
+ [edgeStyles setStyleManager:m];
+}
+
+- (void) restoreUiStateFromConfig:(Configuration*)file group:(NSString*)group {
+ gtk_expander_set_expanded (nodeStylesExpander,
+ [file booleanEntry:@"node-styles-expanded"
+ inGroup:group
+ withDefault:YES]);
+ gtk_expander_set_expanded (edgeStylesExpander,
+ [file booleanEntry:@"edge-styles-expanded"
+ inGroup:group
+ withDefault:NO]);
+}
+
+- (void) saveUiStateToConfig:(Configuration*)file group:(NSString*)group {
+ [file setBooleanEntry:@"node-styles-expanded"
+ inGroup:group
+ value:gtk_expander_get_expanded (nodeStylesExpander)];
+ [file setBooleanEntry:@"edge-styles-expanded"
+ inGroup:group
+ value:gtk_expander_get_expanded (edgeStylesExpander)];
+}
+
+@end
+
+// }}}
+// {{{ Private
+
+@implementation StylesPane (Private)
+- (void) _addSplitter {
+ GtkWidget *split = gtk_hseparator_new ();
+ gtk_box_pack_start (GTK_BOX (stylesPane),
+ split,
+ FALSE, // expand
+ FALSE, // fill
+ 0); // padding
+ gtk_widget_show (split);
+}
+
+- (GtkExpander*) _addExpanderWithName:(const gchar*)name contents:(GtkWidget*)contents {
+ GtkWidget *exp = gtk_expander_new (name);
+ gtk_box_pack_start (GTK_BOX (stylesPane),
+ exp,
+ FALSE, // expand
+ TRUE, // fill
+ 0); // padding
+ gtk_widget_show (exp);
+ gtk_container_set_border_width (GTK_CONTAINER (contents), 6);
+ gtk_container_add (GTK_CONTAINER (exp), contents);
+ gtk_widget_show (contents);
+ return GTK_EXPANDER (exp);
+}
+@end
+
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/Surface.h b/tikzit/src/gtk/Surface.h
new file mode 100644
index 0000000..d2a0dba
--- /dev/null
+++ b/tikzit/src/gtk/Surface.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "RenderContext.h"
+#import "Transformer.h"
+
+@protocol Surface;
+
+@protocol RenderDelegate
+- (void) renderWithContext:(id<RenderContext>)context onSurface:(id<Surface>)surface;
+@end
+
+/**
+ * Represents a surface that can be rendered to
+ *
+ * This protocol should be implemented by drawing surfaces. It
+ * provides geometry information and methods to invalidate
+ * regions of the surface, triggering a redraw.
+ *
+ * The surface should send a "SurfaceSizeChanged" notification
+ * when the width or height changes.
+ */
+@protocol Surface
+
+/**
+ * The width of the surface, in surface units
+ *
+ * The surface should send a "SurfaceSizeChanged" notification
+ * when this property changes.
+ */
+@property (readonly) int width;
+/**
+ * The height of the surface, in surface units
+ *
+ * The surface should send a "SurfaceSizeChanged" notification
+ * when this property changes.
+ */
+@property (readonly) int height;
+/**
+ * The transformer that converts between graph units and surface units
+ */
+@property (readonly) Transformer *transformer;
+/**
+ * The render delegate.
+ *
+ * This will be used to redraw (parts of) the surface when necessary.
+ */
+@property (assign) id<RenderDelegate> renderDelegate;
+
+/**
+ * Create a render context for the surface.
+ */
+- (id<RenderContext>) createRenderContext;
+/**
+ * Invalidate a portion of the surface.
+ *
+ * This will request that part of the surface be redrawn.
+ */
+- (void) invalidateRect:(NSRect)rect;
+/**
+ * Invalidate the whole surface.
+ *
+ * This will request that the whole surface be redrawn.
+ */
+- (void) invalidate;
+
+- (void) zoomIn;
+- (void) zoomOut;
+- (void) zoomReset;
+- (void) zoomInAboutPoint:(NSPoint)p;
+- (void) zoomOutAboutPoint:(NSPoint)p;
+- (void) zoomResetAboutPoint:(NSPoint)p;
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/TZFoundation.h b/tikzit/src/gtk/TZFoundation.h
new file mode 100644
index 0000000..6f42700
--- /dev/null
+++ b/tikzit/src/gtk/TZFoundation.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import <Foundation/Foundation.h>
+#import <glib.h>
+
+#import "NSError+Glib.h"
+#import "NSError+Tikzit.h"
+#import "NSFileManager+Glib.h"
+#import "NSFileManager+Utils.h"
+#import "NSString+Glib.h"
+#import "NSString+LatexConstants.h"
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/TikzDocument.h b/tikzit/src/gtk/TikzDocument.h
new file mode 100644
index 0000000..2e29677
--- /dev/null
+++ b/tikzit/src/gtk/TikzDocument.h
@@ -0,0 +1,163 @@
+//
+// TikzDocument.h
+// TikZiT
+//
+// Copyright 2010 Chris Heunen
+// Copyright 2010 Alex Merry
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+
+#import "TZFoundation.h"
+#import <Graph.h>
+#import "PickSupport.h"
+#import "StyleManager.h"
+
+@interface TikzDocument : NSObject {
+ StyleManager *styleManager;
+ Graph *graph;
+ PickSupport *pickSupport;
+ NSUndoManager *undoManager;
+ NSString *tikz;
+ NSString *path;
+ NSSet *nodesetBeingModified;
+ NSMapTable *nodesetBeingModifiedOldCopy;
+ NSSet *edgesetBeingModified;
+ NSMapTable *edgesetBeingModifiedOldCopy;
+ NSPoint currentNodeShift;
+ Node *nodeBeingModified;
+ Node *nodeBeingModifiedOldCopy;
+ Edge *edgeBeingModified;
+ Edge *edgeBeingModifiedOldCopy;
+ NSRect oldGraphBounds;
+ GraphElementData *oldGraphData;
+ BOOL hasChanges;
+}
+
++ (TikzDocument*) documentWithStyleManager:(StyleManager*)manager;
++ (TikzDocument*) documentWithGraph:(Graph*)g styleManager:(StyleManager*)manager;
++ (TikzDocument*) documentWithTikz:(NSString*)t styleManager:(StyleManager*)manager;
++ (TikzDocument*) documentFromFile:(NSString*)path styleManager:(StyleManager*)manager error:(NSError**)error;
+
+- (id) initWithStyleManager:(StyleManager*)manager;
+- (id) initWithGraph:(Graph*)g styleManager:(StyleManager*)manager;
+- (id) initWithTikz:(NSString*)t styleManager:(StyleManager*)manager;
+- (id) initFromFile:(NSString*)path styleManager:(StyleManager*)manager error:(NSError**)error;
+
+- (Graph*) graph;
+- (PickSupport*) pickSupport;
+- (NSString*) path;
+- (NSString*) name;
+- (NSString*) suggestedFileName;
+- (BOOL) hasUnsavedChanges;
+
+- (StyleManager*) styleManager;
+- (void) setStyleManager:(StyleManager*)manager;
+
+- (NSString*) tikz;
+- (BOOL) setTikz:(NSString*)tikz;
+
+- (Graph*) cutSelection;
+- (Graph*) copySelection;
+- (void) paste:(Graph*)graph;
+- (void) pasteFromTikz:(NSString*)tikz;
+
+// some convenience methods:
+- (BOOL) isNodeSelected:(Node*)node;
+- (BOOL) isEdgeSelected:(Edge*)edge;
+- (NSEnumerator*) nodeEnumerator;
+- (NSEnumerator*) edgeEnumerator;
+
+- (BOOL) canUndo;
+- (void) undo;
+- (BOOL) canRedo;
+- (void) redo;
+- (NSString*) undoName;
+- (NSString*) redoName;
+
+- (void) startUndoGroup;
+- (void) nameAndEndUndoGroup:(NSString*)nm;
+- (void) endUndoGroup;
+
+- (void) startModifyNode:(Node*)node;
+- (void) modifyNodeCheckPoint;
+- (void) endModifyNode;
+- (void) cancelModifyNode;
+
+- (void) startModifyNodes:(NSSet*)nodes;
+- (void) modifyNodesCheckPoint;
+- (void) endModifyNodes;
+- (void) cancelModifyNodes;
+
+- (void) startShiftNodes:(NSSet*)nodes;
+- (void) shiftNodesUpdate:(NSPoint)shiftChange;
+- (void) endShiftNodes;
+- (void) cancelShiftNodes;
+
+- (void) startModifyEdge:(Edge*)edge;
+- (void) modifyEdgeCheckPoint;
+- (void) endModifyEdge;
+- (void) cancelModifyEdge;
+
+- (void) startModifyEdges:(NSSet*)edges;
+- (void) modifyEdgesCheckPoint;
+- (void) endModifyEdges;
+- (void) cancelModifyEdges;
+
+- (void) startChangeBoundingBox;
+- (void) changeBoundingBoxCheckPoint;
+- (void) endChangeBoundingBox;
+- (void) cancelChangeBoundingBox;
+
+- (void) startChangeGraphProperties;
+- (void) changeGraphPropertiesCheckPoint;
+- (void) endChangeGraphProperties;
+- (void) cancelChangeGraphProperties;
+
+- (void) removeSelected;
+- (void) addNode:(Node*)node;
+/*!
+ * Convenience function to add a node in the active style
+ * at the given point.
+ *
+ * @param pos the position (in graph co-ordinates) of the new node
+ * @return the added node
+ */
+- (Node*) addNodeAt:(NSPoint)pos;
+- (void) removeNode:(Node*)node;
+- (void) addEdge:(Edge*)edge;
+- (void) removeEdge:(Edge*)edge;
+/*!
+ * Convenience function to add an edge in the active style
+ * between the given nodes.
+ *
+ * @param source the source node
+ * @param target the target node
+ * @return the added edge
+ */
+- (Edge*) addEdgeFrom:(Node*)source to:(Node*)target;
+- (void) shiftSelectedNodesByPoint:(NSPoint)offset;
+- (void) insertGraph:(Graph*)g;
+- (void) flipSelectedNodesHorizontally;
+- (void) flipSelectedNodesVertically;
+
+- (BOOL) saveCopyToPath: (NSString*)path error: (NSError**)error;
+- (BOOL) saveToPath: (NSString*)path error: (NSError**)error;
+- (BOOL) save: (NSError**)error;
+
+@end
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/TikzDocument.m b/tikzit/src/gtk/TikzDocument.m
new file mode 100644
index 0000000..5ccdb98
--- /dev/null
+++ b/tikzit/src/gtk/TikzDocument.m
@@ -0,0 +1,807 @@
+//
+// TikzDocument.h
+// TikZiT
+//
+// Copyright 2010 Chris Heunen
+// Copyright 2010 Alex Merry
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+
+#import "TikzDocument.h"
+#import "TikzGraphAssembler.h"
+
+@interface TikzDocument (Private)
+- (void) styleRenamed:(NSNotification*)n;
+
+- (void) setPath:(NSString*)path;
+- (void) setGraph:(Graph*)g;
+
+- (void) registerUndoForChange:(GraphChange*)change;
+- (void) registerUndoGroupForChange:(GraphChange*)change withName:(NSString*)name;
+- (void) undoGraphChange:(GraphChange*)change;
+- (void) completedGraphChange:(GraphChange*)change withName:(NSString*)name;
+- (void) attachStylesToGraph:(Graph*)g;
+
+- (void) regenerateTikz;
+@end
+
+@implementation TikzDocument
+
++ (TikzDocument*) documentWithStyleManager:(StyleManager*)manager {
+ return [[[TikzDocument alloc] initWithStyleManager:manager] autorelease];
+}
+
++ (TikzDocument*) documentWithGraph:(Graph*)g styleManager:(StyleManager*)manager {
+ return [[[TikzDocument alloc] initWithGraph:g styleManager:manager] autorelease];
+}
+
++ (TikzDocument*) documentWithTikz:(NSString*)t styleManager:(StyleManager*)manager {
+ return [[[TikzDocument alloc] initWithTikz:t styleManager:manager] autorelease];
+}
+
++ (TikzDocument*) documentFromFile:(NSString*)pth styleManager:(StyleManager*)manager error:(NSError**)error {
+ return [[[TikzDocument alloc] initFromFile:pth styleManager:manager error:error] autorelease];
+}
+
+
+- (id) initWithStyleManager:(StyleManager*)manager {
+ self = [self initWithGraph:[Graph graph] styleManager:manager];
+ return self;
+}
+
+- (id) initWithGraph:(Graph*)g styleManager:(StyleManager*)manager {
+ self = [super init];
+
+ if (self) {
+ graph = nil;
+ styleManager = [manager retain];
+ pickSupport = [[PickSupport alloc] init];
+ undoManager = [[NSUndoManager alloc] init];
+ [undoManager setGroupsByEvent:NO];
+ tikz = nil;
+ path = nil;
+ nodesetBeingModified = nil;
+ nodesetBeingModifiedOldCopy = nil;
+ nodeBeingModified = nil;
+ nodeBeingModifiedOldCopy = nil;
+ edgeBeingModified = nil;
+ edgeBeingModifiedOldCopy = nil;
+
+ [undoManager disableUndoRegistration];
+ [self setGraph:g];
+ [undoManager enableUndoRegistration];
+
+ hasChanges = NO;
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(styleRenamed:)
+ name:@"NodeStyleRenamed"
+ object:styleManager];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(styleRenamed:)
+ name:@"EdgeStyleRenamed"
+ object:styleManager];
+ }
+
+ return self;
+}
+
+- (id) initWithTikz:(NSString*)t styleManager:(StyleManager*)manager {
+ self = [self initWithStyleManager:manager];
+
+ if (self) {
+ [undoManager disableUndoRegistration];
+ [self setTikz:t];
+ [undoManager enableUndoRegistration];
+ hasChanges = NO;
+ }
+
+ return self;
+}
+
+- (id) initFromFile:(NSString*)pth styleManager:(StyleManager*)manager error:(NSError**)error {
+ NSStringEncoding enc; // we can't pass in NULL here...
+ NSString *t = [NSString stringWithContentsOfFile:pth
+ usedEncoding:&enc error:error];
+ if (t == nil) {
+ [self release];
+ return nil;
+ }
+
+ self = [self initWithTikz:t styleManager:manager];
+
+ if (self) {
+ [self setPath:pth];
+ }
+ return self;
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [styleManager release];
+ [graph release];
+ [pickSupport release];
+ [undoManager release];
+ [tikz release];
+ [path release];
+ [nodesetBeingModified release];
+ [nodesetBeingModifiedOldCopy release];
+ [nodeBeingModified release];
+ [nodeBeingModifiedOldCopy release];
+ [edgeBeingModified release];
+ [edgeBeingModifiedOldCopy release];
+ [oldGraphData release];
+ [super dealloc];
+}
+
+- (Graph*) graph {
+ return graph;
+}
+
+- (PickSupport*) pickSupport {
+ return pickSupport;
+}
+
+- (NSString*) path {
+ return path;
+}
+
+- (NSString*) name {
+ if (path) {
+ return [[NSFileManager defaultManager] displayNameAtPath: path];
+ } else {
+ return @"Untitled";
+ }
+}
+
+- (NSString*) suggestedFileName {
+ if (path) {
+ return [path lastPathComponent];
+ } else {
+ return @"untitled.tikz";
+ }
+}
+
+- (BOOL) hasUnsavedChanges {
+ return hasChanges;
+}
+
+- (StyleManager*) styleManager {
+ return styleManager;
+}
+
+- (void) setStyleManager:(StyleManager*)manager {
+ StyleManager *oldManager = styleManager;
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:nil
+ object:oldManager];
+
+ styleManager = [manager retain];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(styleRenamed:)
+ name:@"NodeStyleRenamed"
+ object:styleManager];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(styleRenamed:)
+ name:@"EdgeStyleRenamed"
+ object:styleManager];
+
+ [self attachStylesToGraph:graph];
+ [oldManager release];
+}
+
+- (void) postGraphReplaced {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"GraphReplaced" object:self];
+}
+
+- (void) postGraphChange:(GraphChange*)change {
+ NSDictionary *info = [NSDictionary dictionaryWithObject:change forKey:@"change"];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"GraphChanged" object:self userInfo:info];
+}
+
+- (void) postIncompleteGraphChange:(GraphChange*)change {
+ NSDictionary *info = [NSDictionary dictionaryWithObject:change forKey:@"change"];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"GraphBeingChanged" object:self userInfo:info];
+}
+
+- (void) postCancelledGraphChange:(GraphChange*)change {
+ NSDictionary *info = [NSDictionary dictionaryWithObject:change forKey:@"change"];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"GraphChangeCancelled" object:self userInfo:info];
+}
+
+- (void) postTikzChanged {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"TikzChanged" object:self];
+}
+
+- (void) postParseError {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"ParseError" object:self];
+}
+
+- (void) postUndoStackChanged {
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"UndoStackChanged" object:self];
+}
+
+- (NSString*) tikz {
+ return tikz;
+}
+
+- (BOOL) setTikz:(NSString*)t {
+ if (t == nil) {
+ t = [NSString string];
+ }
+ if (t == tikz || [t isEqual:tikz]) {
+ return YES;
+ }
+
+ TikzGraphAssembler *a = [TikzGraphAssembler assembler];
+ BOOL success = [a parseTikz:t];
+ if (success) {
+ // setTikz actually generates a graph from the tikz,
+ // and generates the final tikz from that
+ [self startUndoGroup];
+ [self setGraph:[a graph]];
+ [self nameAndEndUndoGroup:@"Update tikz"];
+ } else {
+ [self postParseError];
+ }
+
+ return success;
+}
+
+- (Graph*) cutSelection {
+ Graph *selection = [self copySelection];
+ [self startUndoGroup];
+ [self removeSelected];
+ [self nameAndEndUndoGroup:@"Cut"];
+ return selection;
+}
+
+- (Graph*) copySelection {
+ return [[graph copyOfSubgraphWithNodes:[pickSupport selectedNodes]] autorelease];
+}
+
+- (void) paste:(Graph*)g {
+ if (g == nil || [[g nodes] count] == 0) {
+ // nothing to paste
+ return;
+ }
+
+ // place to the right of the existing graph
+ NSRect bounds = [graph bounds];
+ NSRect gBounds = [g bounds];
+ float dx = NSMaxX (bounds) - gBounds.origin.x + 0.5f;
+ [g shiftNodes:[g nodes] byPoint:NSMakePoint (dx, 0)];
+
+ GraphChange *change = [graph insertGraph:g];
+ [self completedGraphChange:change withName:@"Paste"];
+
+ // select everything from the clipboard
+ [pickSupport deselectAllEdges];
+ [pickSupport selectAllNodes:[NSSet setWithArray:[g nodes]] replacingSelection:YES];
+}
+
+- (void) pasteFromTikz:(NSString*)t {
+ TikzGraphAssembler *a = [TikzGraphAssembler assembler];
+ if ([a parseTikz:t]) {
+ Graph *clipboard = [a graph];
+ [self attachStylesToGraph:clipboard];
+ [self paste:clipboard];
+ }
+}
+
+- (BOOL) isNodeSelected:(Node*)node {
+ return [pickSupport isNodeSelected:node];
+}
+
+- (BOOL) isEdgeSelected:(Edge*)edge {
+ return [pickSupport isEdgeSelected:edge];
+}
+
+- (NSEnumerator*) nodeEnumerator {
+ return [[graph nodes] objectEnumerator];
+}
+
+- (NSEnumerator*) edgeEnumerator {
+ return [[graph edges] objectEnumerator];
+}
+
+- (BOOL) canUndo {
+ return [undoManager canUndo];
+}
+
+- (void) undo {
+ [undoManager undo];
+ [self postUndoStackChanged];
+}
+
+- (BOOL) canRedo {
+ return [undoManager canRedo];
+}
+
+- (void) redo {
+ [undoManager redo];
+ [self postUndoStackChanged];
+}
+
+- (NSString*) undoName {
+ return [undoManager undoActionName];
+}
+
+- (NSString*) redoName {
+ return [undoManager redoActionName];
+}
+
+- (void) startUndoGroup {
+ [undoManager beginUndoGrouping];
+}
+
+- (void) nameAndEndUndoGroup:(NSString*)nm {
+ [undoManager setActionName:nm];
+ [undoManager endUndoGrouping];
+ [self postUndoStackChanged];
+}
+
+- (void) endUndoGroup {
+ [undoManager endUndoGrouping];
+ [self postUndoStackChanged];
+}
+
+- (void) startModifyNode:(Node*)node {
+ if (nodeBeingModified != nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Already modifying a node"];
+ }
+ nodeBeingModified = [node retain];
+ nodeBeingModifiedOldCopy = [node copy];
+}
+
+- (void) modifyNodeCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange propertyChangeOfNode:nodeBeingModified
+ fromOld:nodeBeingModifiedOldCopy
+ toNew:[[nodeBeingModified copy] autorelease]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishModifySequence:(GraphChange*)change withName:(NSString*)chName cancelled:(BOOL)cancelled {
+ if (cancelled) {
+ change = [change invert];
+ [graph applyGraphChange:change];
+ [self regenerateTikz];
+ [self postCancelledGraphChange:change];
+ } else {
+ [self registerUndoGroupForChange:change withName:chName];
+ [self regenerateTikz];
+ [self postGraphChange:change];
+ }
+}
+
+- (void) _finishModifyNodeCancelled:(BOOL)cancelled {
+ if (nodeBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying a node"];
+ }
+
+ GraphChange *change = [GraphChange propertyChangeOfNode:nodeBeingModified
+ fromOld:nodeBeingModifiedOldCopy
+ toNew:[[nodeBeingModified copy] autorelease]];
+ [self _finishModifySequence:change withName:@"Modify node" cancelled:cancelled];
+
+ [nodeBeingModified release];
+ nodeBeingModified = nil;
+ [nodeBeingModifiedOldCopy release];
+ nodeBeingModifiedOldCopy = nil;
+}
+
+- (void) endModifyNode { [self _finishModifyNodeCancelled:NO]; }
+- (void) cancelModifyNode { [self _finishModifyNodeCancelled:YES]; }
+
+- (void) startModifyNodes:(NSSet*)nodes {
+ if (nodesetBeingModified != nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Already modifying a node set"];
+ }
+
+ nodesetBeingModified = [nodes copy];
+ nodesetBeingModifiedOldCopy = [[Graph nodeTableForNodes:nodes] retain];
+}
+
+- (void) modifyNodesCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange propertyChangeOfNodesFromOldCopies:nodesetBeingModifiedOldCopy
+ toNewCopies:[Graph nodeTableForNodes:nodesetBeingModified]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishModifyNodes:(BOOL)cancelled {
+ if (nodesetBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying a node set"];
+ }
+
+ GraphChange *change = [GraphChange propertyChangeOfNodesFromOldCopies:nodesetBeingModifiedOldCopy
+ toNewCopies:[Graph nodeTableForNodes:nodesetBeingModified]];
+ [self _finishModifySequence:change withName:@"Modify nodes" cancelled:cancelled];
+
+ [nodesetBeingModified release];
+ nodesetBeingModified = nil;
+ [nodesetBeingModifiedOldCopy release];
+ nodesetBeingModifiedOldCopy = nil;
+}
+
+- (void) endModifyNodes { [self _finishModifyNodes:NO]; }
+- (void) cancelModifyNodes { [self _finishModifyNodes:YES]; }
+
+- (void) startShiftNodes:(NSSet*)nodes {
+ if (nodesetBeingModified != nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Already modifying a node set"];
+ }
+
+ nodesetBeingModified = [nodes copy];
+ currentNodeShift = NSZeroPoint;
+}
+
+- (void) shiftNodesUpdate:(NSPoint)currentShift {
+ if (nodesetBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying a node set"];
+ }
+
+ currentNodeShift = currentShift;
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange shiftNodes:nodesetBeingModified
+ byPoint:currentNodeShift];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishShiftNodesCancelled:(BOOL)cancelled {
+ if (nodesetBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying a node set"];
+ }
+
+ if (!NSEqualPoints (currentNodeShift, NSZeroPoint)) {
+ GraphChange *change = [GraphChange shiftNodes:nodesetBeingModified
+ byPoint:currentNodeShift];
+ [self _finishModifySequence:change withName:@"Move nodes" cancelled:cancelled];
+ }
+
+ [nodesetBeingModified release];
+ nodesetBeingModified = nil;
+}
+
+- (void) endShiftNodes { [self _finishShiftNodesCancelled:NO]; }
+- (void) cancelShiftNodes { [self _finishShiftNodesCancelled:YES]; }
+
+- (void) startModifyEdge:(Edge*)edge {
+ if (edgeBeingModified != nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Already modifying an edge"];
+ }
+ edgeBeingModified = [edge retain];
+ edgeBeingModifiedOldCopy = [edge copy];
+}
+
+- (void) modifyEdgeCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange propertyChangeOfEdge:edgeBeingModified
+ fromOld:edgeBeingModifiedOldCopy
+ toNew:[[edgeBeingModified copy] autorelease]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishModifyEdgeCancelled:(BOOL)cancelled {
+ if (edgeBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying an edge"];
+ }
+
+ GraphChange *change = [GraphChange propertyChangeOfEdge:edgeBeingModified
+ fromOld:edgeBeingModifiedOldCopy
+ toNew:[[edgeBeingModified copy] autorelease]];
+ [self _finishModifySequence:change withName:@"Modify edge" cancelled:cancelled];
+
+ [edgeBeingModified release];
+ edgeBeingModified = nil;
+ [edgeBeingModifiedOldCopy release];
+ edgeBeingModifiedOldCopy = nil;
+}
+
+- (void) endModifyEdge { [self _finishModifyEdgeCancelled:NO]; }
+- (void) cancelModifyEdge { [self _finishModifyEdgeCancelled:YES]; }
+
+- (void) startModifyEdges:(NSSet*)edges {
+ if (edgesetBeingModified != nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Already modifying an edge set"];
+ }
+
+ edgesetBeingModified = [edges copy];
+ edgesetBeingModifiedOldCopy = [[Graph edgeTableForEdges:edges] retain];
+}
+
+- (void) modifyEdgesCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange propertyChangeOfEdgesFromOldCopies:edgesetBeingModifiedOldCopy
+ toNewCopies:[Graph edgeTableForEdges:edgesetBeingModified]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishModifyEdgesCancelled:(BOOL)cancelled {
+ if (edgesetBeingModified == nil) {
+ [NSException raise:@"NSInternalInconsistencyException" format:@"Not modifying an edge"];
+ }
+
+ GraphChange *change = [GraphChange propertyChangeOfEdgesFromOldCopies:edgesetBeingModifiedOldCopy
+ toNewCopies:[Graph edgeTableForEdges:edgesetBeingModified]];
+ [self _finishModifySequence:change withName:@"Modify edges" cancelled:cancelled];
+
+ [edgesetBeingModified release];
+ edgesetBeingModified = nil;
+ [edgesetBeingModifiedOldCopy release];
+ edgesetBeingModifiedOldCopy = nil;
+}
+
+- (void) endModifyEdges { [self _finishModifyEdgesCancelled:NO]; }
+- (void) cancelModifyEdges { [self _finishModifyEdgesCancelled:YES]; }
+
+- (void) startChangeBoundingBox {
+ oldGraphBounds = [graph boundingBox];
+}
+
+- (void) changeBoundingBoxCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange changeBoundingBoxFrom:oldGraphBounds
+ to:[graph boundingBox]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishChangeBoundingBoxCancelled:(BOOL)cancelled {
+ GraphChange *change = [GraphChange changeBoundingBoxFrom:oldGraphBounds
+ to:[graph boundingBox]];
+ [self _finishModifySequence:change withName:@"Set bounding box" cancelled:cancelled];
+}
+- (void) endChangeBoundingBox { [self _finishChangeBoundingBoxCancelled:NO]; }
+- (void) cancelChangeBoundingBox { [self _finishChangeBoundingBoxCancelled:YES]; }
+
+- (void) startChangeGraphProperties {
+ oldGraphData = [[graph data] copy];
+}
+
+- (void) changeGraphPropertiesCheckPoint {
+ [self regenerateTikz];
+ GraphChange *change = [GraphChange propertyChangeOfGraphFrom:oldGraphData
+ to:[graph data]];
+ [self postIncompleteGraphChange:change];
+}
+
+- (void) _finishChangeGraphPropertiesCancelled:(BOOL)cancelled {
+ GraphChange *change = [GraphChange propertyChangeOfGraphFrom:oldGraphData
+ to:[graph data]];
+ [self _finishModifySequence:change withName:@"Change graph properties" cancelled:cancelled];
+ [oldGraphData release];
+ oldGraphData = nil;
+}
+- (void) endChangeGraphProperties { [self _finishChangeGraphPropertiesCancelled:NO]; }
+- (void) cancelChangeGraphProperties { [self _finishChangeGraphPropertiesCancelled:YES]; }
+
+- (void) removeSelected {
+ NSUInteger selEdges = [[pickSupport selectedEdges] count];
+ NSUInteger selNodes = [[pickSupport selectedNodes] count];
+
+ if (selEdges == 0 && selNodes == 0) {
+ return;
+ }
+
+ NSString *actionName = @"Remove selection";
+
+ [self startUndoGroup];
+ if (selEdges > 0) {
+ GraphChange *change = [graph removeEdges:[pickSupport selectedEdges]];
+ [self registerUndoForChange:change];
+ [pickSupport deselectAllEdges];
+ [self postGraphChange:change];
+ } else {
+ actionName = (selNodes == 1 ? @"Remove node" : @"Remove nodes");
+ }
+ if (selNodes > 0) {
+ GraphChange *change = [graph removeNodes:[pickSupport selectedNodes]];
+ [self registerUndoForChange:change];
+ [pickSupport deselectAllNodes];
+ [self postGraphChange:change];
+ } else {
+ actionName = (selEdges == 1 ? @"Remove edge" : @"Remove edges");
+ }
+ [self nameAndEndUndoGroup:actionName];
+ [self regenerateTikz];
+}
+
+- (void) addNode:(Node*)node {
+ GraphChange *change = [graph addNode:node];
+ [self completedGraphChange:change withName:@"Add node"];
+}
+
+- (Node*) addNodeAt:(NSPoint)pos {
+ Node *node = [Node nodeWithPoint:pos];
+ [node setStyle:[styleManager activeNodeStyle]];
+ [self addNode:node];
+ return node;
+}
+
+- (void) removeNode:(Node*)node {
+ [pickSupport deselectNode:node];
+ GraphChange *change = [graph removeNode:node];
+ [self completedGraphChange:change withName:@"Remove node"];
+}
+
+- (void) addEdge:(Edge*)edge {
+ GraphChange *change = [graph addEdge:edge];
+ [self completedGraphChange:change withName:@"Add edge"];
+}
+
+- (void) removeEdge:(Edge*)edge {
+ [pickSupport deselectEdge:edge];
+ GraphChange *change = [graph removeEdge:edge];
+ [self completedGraphChange:change withName:@"Remove edge"];
+}
+
+- (Edge*) addEdgeFrom:(Node*)source to:(Node*)target {
+ Edge *edge = [Edge edgeWithSource:source andTarget:target];
+ [edge setStyle:[styleManager activeEdgeStyle]];
+ [self addEdge:edge];
+ return edge;
+}
+
+- (void) shiftSelectedNodesByPoint:(NSPoint)offset {
+ if ([[pickSupport selectedNodes] count] > 0) {
+ GraphChange *change = [graph shiftNodes:[pickSupport selectedNodes] byPoint:offset];
+ [self completedGraphChange:change withName:@"Move nodes"];
+ }
+}
+
+- (void) insertGraph:(Graph*)g {
+ GraphChange *change = [graph insertGraph:g];
+ [self completedGraphChange:change withName:@"Insert graph"];
+}
+
+- (void) flipSelectedNodesHorizontally {
+ if ([[pickSupport selectedNodes] count] > 0) {
+ GraphChange *change = [graph flipHorizontalNodes:[pickSupport selectedNodes]];
+ [self completedGraphChange:change withName:@"Flip nodes horizontally"];
+ }
+}
+
+- (void) flipSelectedNodesVertically {
+ if ([[pickSupport selectedNodes] count] > 0) {
+ GraphChange *change = [graph flipVerticalNodes:[pickSupport selectedNodes]];
+ [self completedGraphChange:change withName:@"Flip nodes vertically"];
+ }
+}
+
+- (BOOL) saveCopyToPath: (NSString*)p error: (NSError**)error {
+ if (!p) {
+ [NSException raise:@"No document path" format:@"No path given"];
+ }
+ // we use glib for writing the file, because GNUStep sucks in this regard
+ // (older versions don't have -[NSString writeToFile:atomically:encoding:error:])
+ GError *gerror = NULL;
+ gchar *filename = [p glibFilename];
+ BOOL success = g_file_set_contents (filename, [tikz UTF8String], -1, &gerror) ? YES : NO;
+ if (gerror) {
+ GErrorToNSError (gerror, error);
+ g_error_free (gerror);
+ }
+ g_free (filename);
+ return success;
+}
+
+- (BOOL) saveToPath: (NSString*)p error: (NSError**)error {
+ BOOL success = [self saveCopyToPath:p error:error];
+ if (success) {
+ [self setPath:p];
+ hasChanges = NO;
+ }
+ return success;
+}
+
+- (BOOL) save: (NSError**)error {
+ if (!path) {
+ [NSException raise:@"No document path" format:@"Tried to save a document when there was no path"];
+ }
+ return [self saveToPath:path error:error];
+}
+
+@end
+
+@implementation TikzDocument (Private)
+- (void) styleRenamed:(NSNotification*)n {
+ [self regenerateTikz];
+}
+
+- (void) setPath:(NSString*)p {
+ [p retain];
+ [path release];
+ path = p;
+}
+
+- (void) setGraph:(Graph*)g {
+ if (g == nil) {
+ g = [Graph graph];
+ }
+ if (g == graph) {
+ return;
+ }
+
+ [pickSupport deselectAllNodes];
+ [pickSupport deselectAllEdges];
+
+ [self startUndoGroup];
+ [undoManager registerUndoWithTarget:self selector:@selector(setGraph:) object:graph];
+ [g retain];
+ [graph release];
+ graph = g;
+
+ [self attachStylesToGraph:graph];
+
+ [self regenerateTikz];
+ [self postGraphReplaced];
+ [self nameAndEndUndoGroup:@"Replace graph"];
+}
+
+- (void) registerUndoForChange:(GraphChange*)change {
+ [undoManager registerUndoWithTarget:self
+ selector:@selector(undoGraphChange:)
+ object:change];
+}
+
+- (void) registerUndoGroupForChange:(GraphChange*)change withName:(NSString*)nm {
+ [self startUndoGroup];
+ [self registerUndoForChange:change];
+ [self nameAndEndUndoGroup:nm];
+}
+
+- (void) undoGraphChange:(GraphChange*)change {
+ GraphChange *inverse = [change invert];
+ [graph applyGraphChange:inverse];
+ [self startUndoGroup];
+ [undoManager registerUndoWithTarget:self
+ selector:@selector(undoGraphChange:)
+ object:inverse];
+ [self endUndoGroup];
+ [self regenerateTikz];
+ [self postGraphChange:change];
+}
+
+- (void) completedGraphChange:(GraphChange*)change withName:(NSString*)name {
+ [self registerUndoGroupForChange:change withName:name];
+ [self regenerateTikz];
+ [self postGraphChange:change];
+}
+
+- (void) attachStylesToGraph:(Graph*)g {
+ for (Node *n in [g nodes]) {
+ [n attachStyleFromTable:[styleManager nodeStyles]];
+ }
+}
+
+- (void) regenerateTikz {
+ [tikz release];
+ tikz = [[graph tikz] retain];
+ hasChanges = YES;
+ [self postTikzChanged];
+}
+@end
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/WidgetSurface.h b/tikzit/src/gtk/WidgetSurface.h
new file mode 100644
index 0000000..7a62660
--- /dev/null
+++ b/tikzit/src/gtk/WidgetSurface.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import <Surface.h>
+
+/**
+ * Provides a surface for rendering to a widget.
+ */
+@interface WidgetSurface: NSObject <Surface> {
+ GtkWidget *widget;
+ Transformer *transformer;
+ id <RenderDelegate> renderDelegate;
+ id inputDelegate;
+ BOOL keepCentered;
+ BOOL grabsFocusOnClick;
+ CGFloat defaultScale;
+ NSSize lastKnownSize;
+}
+
+- (id) initWithWidget:(GtkWidget*)widget;
+- (GtkWidget*) widget;
+
+- (id) inputDelegate;
+- (void) setInputDelegate:(id)delegate;
+
+- (BOOL) keepCentered;
+- (void) setKeepCentered:(BOOL)centered;
+
+- (BOOL) grabsFocusOnClick;
+- (void) setGrabsFocusOnClick:(BOOL)focusOnClick;
+
+- (CGFloat) defaultScale;
+- (void) setDefaultScale:(CGFloat)scale;
+
+- (void) setSizeRequestWidth:(double)width height:(double)height;
+
+@end
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/WidgetSurface.m b/tikzit/src/gtk/WidgetSurface.m
new file mode 100644
index 0000000..3538f72
--- /dev/null
+++ b/tikzit/src/gtk/WidgetSurface.m
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "WidgetSurface.h"
+#import "gtkhelpers.h"
+#import "InputDelegate.h"
+#import "CairoRenderContext.h"
+
+// {{{ Internal interfaces
+// {{{ GTK+ callbacks
+static gboolean configure_event_cb (GtkWidget *widget, GdkEventConfigure *event, WidgetSurface *surface);
+static void realize_cb (GtkWidget *widget, WidgetSurface *surface);
+static gboolean expose_event_cb (GtkWidget *widget, GdkEventExpose *event, WidgetSurface *surface);
+static gboolean button_press_event_cb (GtkWidget *widget, GdkEventButton *event, WidgetSurface *surface);
+static gboolean button_release_event_cb (GtkWidget *widget, GdkEventButton *event, WidgetSurface *surface);
+static gboolean motion_notify_event_cb (GtkWidget *widget, GdkEventMotion *event, WidgetSurface *surface);
+static gboolean key_press_event_cb (GtkWidget *widget, GdkEventKey *event, WidgetSurface *surface);
+static gboolean key_release_event_cb (GtkWidget *widget, GdkEventKey *event, WidgetSurface *surface);
+static gboolean scroll_event_cb (GtkWidget *widget, GdkEventScroll *event, WidgetSurface *surface);
+// }}}
+
+@interface WidgetSurface (Private)
+- (void) updateTransformer;
+- (void) widgetSizeChanged:(NSNotification*)notification;
+- (void) handleExposeEvent:(GdkEventExpose*)event;
+- (void) updateLastKnownSize;
+- (void) zoomTo:(CGFloat)scale aboutPoint:(NSPoint)p;
+- (void) zoomTo:(CGFloat)scale;
+@end
+// }}}
+// {{{ API
+@implementation WidgetSurface
+
+- (id) init {
+ return [self initWithWidget:gtk_drawing_area_new ()];
+}
+
+- (id) initWithWidget:(GtkWidget*)w {
+ self = [super init];
+
+ if (self) {
+ widget = w;
+ g_object_ref_sink (G_OBJECT (widget));
+ renderDelegate = nil;
+ inputDelegate = nil;
+ keepCentered = NO;
+ grabsFocusOnClick = NO;
+ defaultScale = 1.0f;
+ transformer = [[Transformer alloc] init];
+ [transformer setFlippedAboutXAxis:YES];
+ [self updateLastKnownSize];
+ g_object_set (G_OBJECT (widget), "events", GDK_STRUCTURE_MASK, NULL);
+ g_signal_connect (widget, "expose-event", G_CALLBACK (expose_event_cb), self);
+ g_signal_connect (widget, "configure-event", G_CALLBACK (configure_event_cb), self);
+ g_signal_connect (widget, "realize", G_CALLBACK (realize_cb), self);
+ g_signal_connect (widget, "button-press-event", G_CALLBACK (button_press_event_cb), self);
+ g_signal_connect (widget, "button-release-event", G_CALLBACK (button_release_event_cb), self);
+ g_signal_connect (widget, "motion-notify-event", G_CALLBACK (motion_notify_event_cb), self);
+ g_signal_connect (widget, "key-press-event", G_CALLBACK (key_press_event_cb), self);
+ g_signal_connect (widget, "key-release-event", G_CALLBACK (key_release_event_cb), self);
+ g_signal_connect (widget, "scroll-event", G_CALLBACK (scroll_event_cb), self);
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(widgetSizeChanged:)
+ name:@"SurfaceSizeChanged"
+ object:self];
+ }
+
+ return self;
+}
+
+- (void) invalidateRect:(NSRect)rect {
+ if (!NSIsEmptyRect (rect)) {
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ GdkRectangle g_rect = gdk_rectangle_from_ns_rect (rect);
+ gdk_window_invalidate_rect (window, &g_rect, TRUE);
+ }
+ }
+}
+
+- (void) invalidate {
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ GdkRegion *visible = gdk_drawable_get_visible_region (GDK_DRAWABLE (window));
+ gdk_window_invalidate_region (window, visible, TRUE);
+ gdk_region_destroy (visible);
+ }
+}
+
+- (id<RenderContext>) createRenderContext {
+ return [CairoRenderContext contextForWidget:widget];
+}
+
+- (int) width {
+ int width = 0;
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ gdk_drawable_get_size (window, &width, NULL);
+ }
+ return width;
+}
+
+- (int) height {
+ int height = 0;
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ gdk_drawable_get_size (window, NULL, &height);
+ }
+ return height;
+}
+
+- (void) setSizeRequestWidth:(double)width height:(double)height {
+ gtk_widget_set_size_request (widget, width, height);
+}
+
+- (Transformer*) transformer {
+ return transformer;
+}
+
+- (GtkWidget*) widget {
+ return widget;
+}
+
+- (void) addToEventMask:(GdkEventMask)value {
+ GdkEventMask mask;
+ g_object_get (G_OBJECT (widget), "events", &mask, NULL);
+ mask |= value;
+ g_object_set (G_OBJECT (widget), "events", mask, NULL);
+}
+
+- (void) removeFromEventMask:(GdkEventMask)value {
+ GdkEventMask mask;
+ g_object_get (G_OBJECT (widget), "events", &mask, NULL);
+ mask ^= value;
+ if (grabsFocusOnClick) {
+ mask |= GDK_BUTTON_PRESS_MASK;
+ }
+ g_object_set (G_OBJECT (widget), "events", mask, NULL);
+}
+
+- (void) setRenderDelegate:(id <RenderDelegate>)delegate {
+ // NB: no retention!
+ renderDelegate = delegate;
+ if (renderDelegate == nil) {
+ [self removeFromEventMask:GDK_EXPOSURE_MASK];
+ } else {
+ [self addToEventMask:GDK_EXPOSURE_MASK];
+ }
+}
+
+- (id) inputDelegate {
+ return inputDelegate;
+}
+
+- (void) setInputDelegate:(id)delegate {
+ if (delegate == inputDelegate) {
+ return;
+ }
+ if (inputDelegate != nil) {
+ [self removeFromEventMask:GDK_POINTER_MOTION_MASK
+ | GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_KEY_PRESS_MASK
+ | GDK_KEY_RELEASE_MASK];
+ }
+ inputDelegate = delegate;
+ if (delegate != nil) {
+ GdkEventMask mask = 0;
+ if ([delegate respondsToSelector:@selector(mousePressAt:withButton:andMask:)]) {
+ mask |= GDK_BUTTON_PRESS_MASK;
+ }
+ if ([delegate respondsToSelector:@selector(mouseReleaseAt:withButton:andMask:)]) {
+ mask |= GDK_BUTTON_RELEASE_MASK;
+ }
+ if ([delegate respondsToSelector:@selector(mouseDoubleClickAt:withButton:andMask:)]) {
+ mask |= GDK_BUTTON_PRESS_MASK;
+ }
+ if ([delegate respondsToSelector:@selector(mouseMoveTo:withButtons:andMask:)]) {
+ mask |= GDK_POINTER_MOTION_MASK;
+ }
+ if ([delegate respondsToSelector:@selector(keyPressed:withMask:)]) {
+ mask |= GDK_KEY_PRESS_MASK;
+ }
+ if ([delegate respondsToSelector:@selector(keyReleased:withMask:)]) {
+ mask |= GDK_KEY_RELEASE_MASK;
+ }
+ [self addToEventMask:mask];
+ }
+}
+
+- (id <RenderDelegate>) renderDelegate {
+ return renderDelegate;
+}
+
+- (void) setKeepCentered:(BOOL)centered {
+ keepCentered = centered;
+ [self updateTransformer];
+}
+
+- (BOOL) keepCentered {
+ return keepCentered;
+}
+
+- (BOOL) grabsFocusOnClick {
+ return grabsFocusOnClick;
+}
+
+- (void) setGrabsFocusOnClick:(BOOL)focus {
+ if (grabsFocusOnClick != focus) {
+ grabsFocusOnClick = focus;
+ if (grabsFocusOnClick) {
+ [self addToEventMask:GDK_BUTTON_PRESS_MASK];
+ } else {
+ [self removeFromEventMask:GDK_BUTTON_PRESS_MASK];
+ }
+ }
+}
+
+- (CGFloat) defaultScale {
+ return defaultScale;
+}
+
+- (void) setDefaultScale:(CGFloat)newDefault {
+ if (defaultScale != newDefault) {
+ CGFloat oldDefault = defaultScale;
+ defaultScale = newDefault;
+
+ CGFloat scale = [transformer scale];
+ scale *= (newDefault / oldDefault);
+ [transformer setScale:scale];
+ [self invalidate];
+ }
+}
+
+- (void) zoomIn {
+ CGFloat scale = [transformer scale];
+ scale *= 1.2f;
+ [self zoomTo:scale];
+}
+
+- (void) zoomOut {
+ CGFloat scale = [transformer scale];
+ scale /= 1.2f;
+ [self zoomTo:scale];
+}
+
+- (void) zoomReset {
+ [self zoomTo:defaultScale];
+}
+
+- (void) zoomInAboutPoint:(NSPoint)p {
+ CGFloat scale = [transformer scale];
+ scale *= 1.2f;
+ [self zoomTo:scale aboutPoint:p];
+}
+
+- (void) zoomOutAboutPoint:(NSPoint)p {
+ CGFloat scale = [transformer scale];
+ scale /= 1.2f;
+ [self zoomTo:scale aboutPoint:p];
+}
+
+- (void) zoomResetAboutPoint:(NSPoint)p {
+ [self zoomTo:defaultScale aboutPoint:p];
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [transformer release];
+ g_object_unref (G_OBJECT (widget));
+
+ [super dealloc];
+}
+
+@end
+// }}}
+// {{{ Private
+@implementation WidgetSurface (Private)
+- (void) widgetSizeChanged:(NSNotification*)notification {
+ [self updateTransformer];
+ [self updateLastKnownSize];
+}
+
+- (void) updateTransformer {
+ if (keepCentered) {
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ int width = 0;
+ int height = 0;
+ gdk_drawable_get_size (window, &width, &height);
+ NSPoint origin;
+ if (lastKnownSize.width < 1 || lastKnownSize.height < 1) {
+ origin.x = (float)width / 2.0f;
+ origin.y = (float)height / 2.0f;
+ } else {
+ origin = [transformer origin];
+ origin.x += ((float)width - lastKnownSize.width) / 2.0f;
+ origin.y += ((float)height - lastKnownSize.height) / 2.0f;
+ }
+ [transformer setOrigin:origin];
+ }
+ }
+}
+
+- (void) handleExposeEvent:(GdkEventExpose*)event {
+ if (renderDelegate != nil) {
+ NSRect area = gdk_rectangle_to_ns_rect (event->area);
+
+ id<RenderContext> context = [CairoRenderContext contextForWidget:widget];
+ [context rect:area];
+ [context clipToPath];
+ [renderDelegate renderWithContext:context onSurface:self];
+ }
+}
+
+- (void) updateLastKnownSize {
+ GdkWindow *window = gtk_widget_get_window (widget);
+ if (window) {
+ int width = 0;
+ int height = 0;
+ gdk_drawable_get_size (window, &width, &height);
+ lastKnownSize.width = (float)width;
+ lastKnownSize.height = (float)height;
+ } else {
+ lastKnownSize = NSZeroSize;
+ }
+}
+
+- (void) zoomTo:(CGFloat)scale aboutPoint:(NSPoint)p {
+ NSPoint graphP = [transformer fromScreen:p];
+
+ [transformer setScale:scale];
+
+ NSPoint newP = [transformer toScreen:graphP];
+ NSPoint origin = [transformer origin];
+ origin.x += p.x - newP.x;
+ origin.y += p.y - newP.y;
+ [transformer setOrigin:origin];
+
+ [self invalidate];
+}
+
+- (void) zoomTo:(CGFloat)scale {
+ NSPoint centre = NSMakePoint (lastKnownSize.width/2.0f, lastKnownSize.height/2.0f);
+ [self zoomTo:scale aboutPoint:centre];
+}
+
+@end
+// }}}
+// {{{ GTK+ callbacks
+static gboolean configure_event_cb(GtkWidget *widget, GdkEventConfigure *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SurfaceSizeChanged" object:surface];
+ [pool drain];
+ return FALSE;
+}
+
+static void realize_cb (GtkWidget *widget, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [surface updateTransformer];
+ [pool drain];
+}
+
+static gboolean expose_event_cb(GtkWidget *widget, GdkEventExpose *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [surface handleExposeEvent:event];
+ [pool drain];
+ return FALSE;
+}
+
+InputMask mask_from_gdk_modifier_state (GdkModifierType state) {
+ InputMask mask = 0;
+ if (state & GDK_SHIFT_MASK) {
+ mask |= ShiftMask;
+ }
+ if (state & GDK_CONTROL_MASK) {
+ mask |= ControlMask;
+ }
+ if (state & GDK_META_MASK) {
+ mask |= MetaMask;
+ }
+ return mask;
+}
+
+ScrollDirection scroll_dir_from_gdk_scroll_dir (GdkScrollDirection dir) {
+ switch (dir) {
+ case GDK_SCROLL_UP: return ScrollUp;
+ case GDK_SCROLL_DOWN: return ScrollDown;
+ case GDK_SCROLL_LEFT: return ScrollLeft;
+ case GDK_SCROLL_RIGHT: return ScrollRight;
+ default: NSLog(@"Invalid scroll direction %i", (int)dir); return ScrollDown;
+ }
+}
+
+MouseButton buttons_from_gdk_modifier_state (GdkModifierType state) {
+ MouseButton buttons = 0;
+ if (state & GDK_BUTTON1_MASK) {
+ buttons |= LeftButton;
+ }
+ if (state & GDK_BUTTON2_MASK) {
+ buttons |= MiddleButton;
+ }
+ if (state & GDK_BUTTON3_MASK) {
+ buttons |= RightButton;
+ }
+ if (state & GDK_BUTTON4_MASK) {
+ buttons |= Button4;
+ }
+ if (state & GDK_BUTTON5_MASK) {
+ buttons |= Button5;
+ }
+ return buttons;
+}
+
+static gboolean button_press_event_cb(GtkWidget *widget, GdkEventButton *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ if ([surface grabsFocusOnClick]) {
+ if (!GTK_WIDGET_HAS_FOCUS (widget)) {
+ gtk_widget_grab_focus (widget);
+ }
+ }
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ NSPoint pos = NSMakePoint (event->x, event->y);
+ MouseButton button = (MouseButton)event->button;
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ if (event->type == GDK_BUTTON_PRESS && [delegate respondsToSelector:@selector(mousePressAt:withButton:andMask:)]) {
+ [delegate mousePressAt:pos withButton:button andMask:mask];
+ }
+ if (event->type == GDK_2BUTTON_PRESS && [delegate respondsToSelector:@selector(mouseDoubleClickAt:withButton:andMask:)]) {
+ [delegate mouseDoubleClickAt:pos withButton:button andMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+
+static gboolean button_release_event_cb(GtkWidget *widget, GdkEventButton *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ if ([delegate respondsToSelector:@selector(mouseReleaseAt:withButton:andMask:)]) {
+ NSPoint pos = NSMakePoint (event->x, event->y);
+ MouseButton button = (MouseButton)event->button;
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ [delegate mouseReleaseAt:pos withButton:button andMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+
+static gboolean motion_notify_event_cb(GtkWidget *widget, GdkEventMotion *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ if ([delegate respondsToSelector:@selector(mouseMoveTo:withButtons:andMask:)]) {
+ NSPoint pos = NSMakePoint (event->x, event->y);
+ MouseButton buttons = buttons_from_gdk_modifier_state (event->state);
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ [delegate mouseMoveTo:pos withButtons:buttons andMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+
+static gboolean key_press_event_cb(GtkWidget *widget, GdkEventKey *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ if ([delegate respondsToSelector:@selector(keyPressed:withMask:)]) {
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ [delegate keyPressed:event->keyval withMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+
+static gboolean key_release_event_cb(GtkWidget *widget, GdkEventKey *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ if ([delegate respondsToSelector:@selector(keyReleased:withMask:)]) {
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ [delegate keyReleased:event->keyval withMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+
+static gboolean scroll_event_cb (GtkWidget *widget, GdkEventScroll *event, WidgetSurface *surface) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ id delegate = [surface inputDelegate];
+ if (delegate != nil) {
+ if ([delegate respondsToSelector:@selector(mouseScrolledAt:inDirection:withMask:)]) {
+ NSPoint pos = NSMakePoint (event->x, event->y);
+ InputMask mask = mask_from_gdk_modifier_state (event->state);
+ ScrollDirection dir = scroll_dir_from_gdk_scroll_dir (event->direction);
+ [delegate mouseScrolledAt:pos
+ inDirection:dir
+ withMask:mask];
+ }
+ }
+
+ [pool drain];
+ return FALSE;
+}
+// }}}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/cairo_helpers.h b/tikzit/src/gtk/cairo_helpers.h
new file mode 100644
index 0000000..e95357b
--- /dev/null
+++ b/tikzit/src/gtk/cairo_helpers.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import "RColor.h"
+#import <cairo/cairo.h>
+
+void cairo_ns_rectangle (cairo_t* cr, NSRect rect);
+void cairo_set_source_rcolor (cairo_t* cr, RColor color);
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/cairo_helpers.m b/tikzit/src/gtk/cairo_helpers.m
new file mode 100644
index 0000000..104e686
--- /dev/null
+++ b/tikzit/src/gtk/cairo_helpers.m
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "cairo_helpers.h"
+
+void cairo_ns_rectangle (cairo_t* cr, NSRect rect) {
+ cairo_rectangle (cr, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+}
+
+void cairo_set_source_rcolor (cairo_t* cr, RColor color) {
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue, color.alpha);
+}
+
+// vim:ft=objc:sts=4:sw=4:et
diff --git a/tikzit/src/gtk/clipboard.h b/tikzit/src/gtk/clipboard.h
new file mode 100644
index 0000000..568fc50
--- /dev/null
+++ b/tikzit/src/gtk/clipboard.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import <Graph.h>
+
+enum {
+ TARGET_UTF8_STRING,
+ TARGET_TIKZIT_PICTURE
+};
+typedef struct
+{
+ Graph *graph;
+ gchar *tikz;
+ gint tikz_length;
+} ClipboardGraphData;
+
+extern GdkAtom utf8_atom;
+extern GdkAtom tikzit_picture_atom;
+
+void clipboard_init ();
+ClipboardGraphData *clipboard_graph_data_new (Graph *graph);
+void clipboard_graph_data_free (ClipboardGraphData *data);
+void clipboard_graph_data_convert (ClipboardGraphData *data);
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/clipboard.m b/tikzit/src/gtk/clipboard.m
new file mode 100644
index 0000000..7001717
--- /dev/null
+++ b/tikzit/src/gtk/clipboard.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "clipboard.h"
+
+GdkAtom utf8_atom;
+GdkAtom tikzit_picture_atom;
+
+void clipboard_init () {
+ if (utf8_atom == GDK_NONE) {
+ utf8_atom = gdk_atom_intern ("UTF8_STRING", FALSE);
+ }
+ if (tikzit_picture_atom == GDK_NONE) {
+ tikzit_picture_atom = gdk_atom_intern ("TIKZITPICTURE", FALSE);
+ }
+}
+
+ClipboardGraphData *clipboard_graph_data_new (Graph *graph) {
+ ClipboardGraphData *data = g_new (ClipboardGraphData, 1);
+ data->graph = [graph retain];
+ data->tikz = NULL;
+ data->tikz_length = 0;
+ return data;
+}
+
+void clipboard_graph_data_free (ClipboardGraphData *data) {
+ [data->graph release];
+ if (data->tikz) {
+ g_free (data->tikz);
+ }
+ g_free (data);
+}
+
+void clipboard_graph_data_convert (ClipboardGraphData *data) {
+ if (data->graph != nil && !data->tikz) {
+ data->tikz = g_strdup ([[data->graph tikz] UTF8String]);
+ data->tikz_length = strlen (data->tikz);
+ [data->graph release];
+ data->graph = nil;
+ }
+}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4:foldmethod=marker
diff --git a/tikzit/src/gtk/gtkhelpers.h b/tikzit/src/gtk/gtkhelpers.h
new file mode 100644
index 0000000..418d234
--- /dev/null
+++ b/tikzit/src/gtk/gtkhelpers.h
@@ -0,0 +1,49 @@
+//
+// gtkhelpers.h
+// TikZiT
+//
+// Copyright 2010 Alex Merry. All rights reserved.
+//
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+#import "TZFoundation.h"
+#include <gtk/gtk.h>
+
+void gtk_table_adjust_attach (GtkTable *table,
+ GtkWidget *widget,
+ gint left_adjust,
+ gint right_adjust,
+ gint top_adjust,
+ gint bottom_adjust);
+void gtk_table_delete_row (GtkTable *table, guint row);
+void gtk_table_delete_rows (GtkTable *table, guint firstRow, guint count);
+
+NSString * gtk_editable_get_string (GtkEditable *editable, gint start, gint end);
+
+void gtk_entry_set_string (GtkEntry *entry, NSString *string);
+NSString * gtk_entry_get_string (GtkEntry *entry);
+
+GdkRectangle gdk_rectangle_from_ns_rect (NSRect rect);
+NSRect gdk_rectangle_to_ns_rect (GdkRectangle rect);
+
+void gtk_action_set_detailed_label (GtkAction *action, const gchar *baseLabel, const gchar *actionName);
+
+gint tz_hijack_key_press (GtkWindow *win,
+ GdkEventKey *event,
+ gpointer user_data);
+
+// vim:ft=objc:sts=2:sw=2:et
diff --git a/tikzit/src/gtk/gtkhelpers.m b/tikzit/src/gtk/gtkhelpers.m
new file mode 100644
index 0000000..164228c
--- /dev/null
+++ b/tikzit/src/gtk/gtkhelpers.m
@@ -0,0 +1,244 @@
+//
+// gtkhelpers.h
+// TikZiT
+//
+// Copyright 2010 Alex Merry. All rights reserved.
+//
+// Some code from Glade:
+// Copyright 2001 Ximian, Inc.
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+#import "gtkhelpers.h"
+#import <gdk/gdkkeysyms.h>
+
+void gtk_table_adjust_attach (GtkTable *table,
+ GtkWidget *widget,
+ gint left_adjust,
+ gint right_adjust,
+ gint top_adjust,
+ gint bottom_adjust) {
+ guint top_attach;
+ guint bottom_attach;
+ guint left_attach;
+ guint right_attach;
+ GtkAttachOptions xoptions;
+ GtkAttachOptions yoptions;
+ guint xpadding;
+ guint ypadding;
+
+ gtk_container_child_get (GTK_CONTAINER (table), widget,
+ "top-attach", &top_attach,
+ "bottom-attach", &bottom_attach,
+ "left-attach", &left_attach,
+ "right-attach", &right_attach,
+ "x-options", &xoptions,
+ "y-options", &yoptions,
+ "x-padding", &xpadding,
+ "y-padding", &ypadding,
+ NULL);
+
+ g_object_ref (G_OBJECT (widget));
+ gtk_container_remove (GTK_CONTAINER (table), widget);
+ gtk_table_attach (table, widget,
+ left_attach + left_adjust,
+ right_attach + right_adjust,
+ top_attach + top_adjust,
+ bottom_attach + bottom_adjust,
+ xoptions,
+ yoptions,
+ xpadding,
+ ypadding);
+ g_object_unref (G_OBJECT (widget));
+}
+
+/*
+ * Delete multiple table rows
+ */
+void gtk_table_delete_rows (GtkTable *table, guint firstRow, guint count) {
+ if (count == 0) {
+ return;
+ }
+ GtkContainer *tableC = GTK_CONTAINER (table);
+
+ guint n_columns;
+ guint n_rows;
+ g_object_get (G_OBJECT (table),
+ "n-columns", &n_columns,
+ "n-rows", &n_rows,
+ NULL);
+ guint topBound = firstRow;
+ guint bottomBound = firstRow + count;
+ if (bottomBound > n_rows) {
+ bottomBound = n_rows;
+ count = bottomBound - topBound;
+ }
+
+ GList *toBeDeleted = NULL;
+ GList *toBeShrunk = NULL;
+ /* indexed by top-attach */
+ GPtrArray *toBeMoved = g_ptr_array_sized_new (n_rows - topBound);
+ g_ptr_array_set_size (toBeMoved, n_rows - topBound);
+
+ GList *childIt = gtk_container_get_children (tableC);
+
+ while (childIt) {
+ GtkWidget *widget = GTK_WIDGET (childIt->data);
+ guint top_attach;
+ guint bottom_attach;
+ gtk_container_child_get (tableC, widget,
+ "top-attach", &top_attach,
+ "bottom-attach", &bottom_attach,
+ NULL);
+ if (top_attach >= topBound && bottom_attach <= bottomBound) {
+ toBeDeleted = g_list_prepend (toBeDeleted, widget);
+ } else if (top_attach <= topBound && bottom_attach > topBound) {
+ toBeShrunk = g_list_prepend (toBeShrunk, widget);
+ } else if (top_attach > topBound) {
+ GList *rowList = (GList*)g_ptr_array_index (toBeMoved, top_attach - topBound);
+ rowList = g_list_prepend (rowList, widget);
+ g_ptr_array_index (toBeMoved, top_attach - topBound) = rowList;
+ }
+ childIt = childIt->next;
+ }
+ g_list_free (childIt);
+
+ /* remove anything that is completely within the segment being deleted */
+ while (toBeDeleted) {
+ gtk_container_remove (tableC, GTK_WIDGET (toBeDeleted->data));
+ toBeDeleted = toBeDeleted->next;
+ }
+ g_list_free (toBeDeleted);
+
+ /* shrink anything that spans the segment */
+ while (toBeShrunk) {
+ GtkWidget *widget = GTK_WIDGET (toBeShrunk->data);
+ gtk_table_adjust_attach (table, widget, 0, 0, 0, -count);
+ toBeShrunk = toBeShrunk->next;
+ }
+ g_list_free (toBeShrunk);
+
+ /* move everything below the segment being deleted up, in order */
+ /* note that "n-rows" is not a valid "top-attach" */
+ for (int offset = 0; offset < (n_rows - 1) - topBound; ++offset) {
+ GList *rowList = (GList *)g_ptr_array_index (toBeMoved, offset);
+ guint top_attach = offset + topBound;
+ guint overlap = bottomBound - top_attach;
+ while (rowList) {
+ GtkWidget *widget = GTK_WIDGET (rowList->data);
+ gtk_table_adjust_attach (table, widget, 0, 0, -offset, -(offset + overlap));
+ rowList = rowList->next;
+ }
+ g_list_free (rowList);
+ g_ptr_array_index (toBeMoved, offset) = NULL;
+ }
+
+ gtk_table_resize (table, n_rows - 1, n_columns);
+}
+
+/*
+ * Delete a table row
+ */
+void gtk_table_delete_row (GtkTable *table, guint row) {
+ gtk_table_delete_rows (table, row, 1);
+}
+
+NSString * gtk_editable_get_string (GtkEditable *editable, gint start, gint end)
+{
+ gchar *text = gtk_editable_get_chars (editable, start, end);
+ NSString *string = [NSString stringWithUTF8String:text];
+ g_free (text);
+ return string;
+}
+
+void gtk_entry_set_string (GtkEntry *entry, NSString *string)
+{
+ gtk_entry_set_text (entry, string == nil ? "" : [string UTF8String]);
+}
+
+NSString * gtk_entry_get_string (GtkEntry *entry)
+{
+ return [NSString stringWithUTF8String:gtk_entry_get_text (entry)];
+}
+
+GdkRectangle gdk_rectangle_from_ns_rect (NSRect box) {
+ GdkRectangle rect;
+ rect.x = box.origin.x;
+ rect.y = box.origin.y;
+ rect.width = box.size.width;
+ rect.height = box.size.height;
+ return rect;
+}
+
+NSRect gdk_rectangle_to_ns_rect (GdkRectangle rect) {
+ NSRect result;
+ result.origin.x = rect.x;
+ result.origin.y = rect.y;
+ result.size.width = rect.width;
+ result.size.height = rect.height;
+ return result;
+}
+
+void gtk_action_set_detailed_label (GtkAction *action, const gchar *baseLabel, const gchar *actionName) {
+ if (actionName == NULL || *actionName == '\0') {
+ gtk_action_set_label (action, baseLabel);
+ } else {
+ GString *label = g_string_sized_new (30);
+ g_string_printf(label, "%s: %s", baseLabel, actionName);
+ gtk_action_set_label (action, label->str);
+ g_string_free (label, TRUE);
+ }
+}
+
+/**
+ * tz_hijack_key_press:
+ * @win: a #GtkWindow
+ * event: the GdkEventKey
+ * user_data: unused
+ *
+ * This function is meant to be attached to key-press-event of a toplevel,
+ * it simply allows the window contents to treat key events /before/
+ * accelerator keys come into play (this way widgets dont get deleted
+ * when cutting text in an entry etc.).
+ * Creates a liststore suitable for comboboxes and such to
+ * chose from a variety of types.
+ *
+ * Returns: whether the event was handled
+ */
+gint
+tz_hijack_key_press (GtkWindow *win,
+ GdkEventKey *event,
+ gpointer user_data)
+{
+ GtkWidget *focus_widget;
+
+ focus_widget = gtk_window_get_focus (win);
+ if (focus_widget &&
+ (event->keyval == GDK_Delete || /* Filter Delete from accelerator keys */
+ ((event->state & GDK_CONTROL_MASK) && /* CTRL keys... */
+ ((event->keyval == GDK_c || event->keyval == GDK_C) || /* CTRL-C (copy) */
+ (event->keyval == GDK_x || event->keyval == GDK_X) || /* CTRL-X (cut) */
+ (event->keyval == GDK_v || event->keyval == GDK_V) || /* CTRL-V (paste) */
+ (event->keyval == GDK_a || event->keyval == GDK_A) || /* CTRL-A (select-all) */
+ (event->keyval == GDK_n || event->keyval == GDK_N))))) /* CTRL-N (new document) ?? */
+ {
+ return gtk_widget_event (focus_widget,
+ (GdkEvent *)event);
+ }
+ return FALSE;
+}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/logo.h b/tikzit/src/gtk/logo.h
new file mode 100644
index 0000000..e778da9
--- /dev/null
+++ b/tikzit/src/gtk/logo.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2012 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+typedef enum {
+ LOGO_SIZE_16,
+ LOGO_SIZE_24,
+ LOGO_SIZE_32,
+ LOGO_SIZE_48,
+ LOGO_SIZE_64,
+ LOGO_SIZE_128,
+ LOGO_SIZE_COUNT
+} LogoSize;
+
+GdkPixbuf *get_logo (LogoSize size);
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/logo.m b/tikzit/src/gtk/logo.m
new file mode 100644
index 0000000..57533c7
--- /dev/null
+++ b/tikzit/src/gtk/logo.m
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012 Alex Merry <alex.merry@kdemail.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#import "logo.h"
+#include <gdk-pixbuf/gdk-pixdata.h>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpointer-sign"
+#import "logodata.m"
+#pragma GCC diagnostic pop
+
+static GdkPixbuf *pixbufCache[LOGO_SIZE_COUNT];
+
+GdkPixbuf *get_logo (LogoSize size) {
+ const GdkPixdata *data = NULL;
+ switch (size) {
+ case LOGO_SIZE_16:
+ data = &logo16;
+ break;
+ case LOGO_SIZE_24:
+ data = &logo24;
+ break;
+ case LOGO_SIZE_32:
+ data = &logo32;
+ break;
+ case LOGO_SIZE_48:
+ data = &logo48;
+ break;
+ case LOGO_SIZE_64:
+ data = &logo64;
+ break;
+ case LOGO_SIZE_128:
+ data = &logo128;
+ break;
+ default:
+ return NULL;
+ };
+ if (pixbufCache[size]) {
+ g_object_ref (pixbufCache[size]);
+ return pixbufCache[size];
+ } else {
+ GdkPixbuf *buf = gdk_pixbuf_from_pixdata (data, FALSE, NULL);
+ pixbufCache[size] = buf;
+ g_object_add_weak_pointer (G_OBJECT (buf), (void**)(&(pixbufCache[size])));
+ return buf;
+ }
+}
+
+// vim:ft=objc:ts=8:et:sts=4:sw=4
+
diff --git a/tikzit/src/gtk/main.m b/tikzit/src/gtk/main.m
new file mode 100644
index 0000000..93aa9e4
--- /dev/null
+++ b/tikzit/src/gtk/main.m
@@ -0,0 +1,75 @@
+//
+// main.m
+// TikZiT
+//
+// Copyright 2010 Chris Heunen. All rights reserved.
+//
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+
+#import "TZFoundation.h"
+#import <gtk/gtk.h>
+#import "clipboard.h"
+#import "logo.h"
+
+#import "MainWindow.h"
+#import "TikzGraphAssembler.h"
+
+void onUncaughtException(NSException* exception)
+{
+ NSLog(@"uncaught exception: %@", [exception description]);
+}
+
+int main (int argc, char *argv[]) {
+ NSSetUncaughtExceptionHandler(&onUncaughtException);
+
+ [[NSAutoreleasePool alloc] init];
+
+ gtk_init (&argc, &argv);
+
+ NSAutoreleasePool *initPool = [[NSAutoreleasePool alloc] init];
+
+#ifndef WINDOWS
+ GList *icon_list = NULL;
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_128));
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_64));
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_48));
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_32));
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_24));
+ icon_list = g_list_prepend (icon_list, get_logo(LOGO_SIZE_16));
+ gtk_window_set_default_icon_list (icon_list);
+ GList *list_head = icon_list;
+ while (list_head) {
+ g_object_unref ((GObject*)list_head->data);
+ list_head = list_head->next;
+ }
+#endif
+
+ clipboard_init();
+ [TikzGraphAssembler setup];
+ MainWindow *window = [[MainWindow alloc] init];
+
+ [initPool drain];
+
+ gtk_main ();
+
+ [window saveConfiguration];
+
+ return 0;
+}
+
+// vim:ft=objc:et:sts=4:sw=4
diff --git a/tikzit/src/gtk/mkdtemp.h b/tikzit/src/gtk/mkdtemp.h
new file mode 100644
index 0000000..65ee99e
--- /dev/null
+++ b/tikzit/src/gtk/mkdtemp.h
@@ -0,0 +1,32 @@
+/* Creating a private temporary directory.
+ Copyright (C) 2001-2002 Free Software Foundation, Inc.
+
+ 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 2, 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, write to the Free Software Foundation,
+ Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
+
+#if HAVE_MKDTEMP
+
+/* Get mkdtemp() declaration. */
+#include <stdlib.h>
+
+#else
+
+/* Create a unique temporary directory from TEMPLATE.
+ The last six characters of TEMPLATE must be "XXXXXX";
+ they are replaced with a string that makes the directory name unique.
+ Returns TEMPLATE, or a null pointer if it cannot get a unique name.
+ The directory is created mode 700. */
+extern char * mkdtemp (char *template);
+
+#endif
diff --git a/tikzit/src/gtk/mkdtemp.m b/tikzit/src/gtk/mkdtemp.m
new file mode 100644
index 0000000..0866e54
--- /dev/null
+++ b/tikzit/src/gtk/mkdtemp.m
@@ -0,0 +1,204 @@
+/* Copyright (C) 1999, 2001-2003 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public License as
+ published by the Free Software Foundation; either version 2 of the
+ License, or (at your option) any later version.
+
+ The GNU C Library 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
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with the GNU C Library; see the file COPYING.LIB. If not,
+ write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ Boston, MA 02111-1307, USA. */
+
+/* Extracted from misc/mkdtemp.c and sysdeps/posix/tempname.c. */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+/* Specification. */
+#include "mkdtemp.h"
+
+#include <errno.h>
+#ifndef __set_errno
+# define __set_errno(Val) errno = (Val)
+#endif
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <stdio.h>
+#ifndef TMP_MAX
+# define TMP_MAX 238328
+#endif
+
+#if HAVE_STDINT_H || _LIBC
+# include <stdint.h>
+#endif
+#if HAVE_INTTYPES_H
+# include <inttypes.h>
+#endif
+
+#if HAVE_UNISTD_H || _LIBC
+# include <unistd.h>
+#endif
+
+#if HAVE_GETTIMEOFDAY || _LIBC
+# if HAVE_SYS_TIME_H || _LIBC
+# include <sys/time.h>
+# endif
+#else
+# if HAVE_TIME_H || _LIBC
+# include <time.h>
+# endif
+#endif
+
+#include <sys/stat.h>
+#if STAT_MACROS_BROKEN
+# undef S_ISDIR
+#endif
+#if !defined S_ISDIR && defined S_IFDIR
+# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
+#endif
+#if !S_IRUSR && S_IREAD
+# define S_IRUSR S_IREAD
+#endif
+#if !S_IRUSR
+# define S_IRUSR 00400
+#endif
+#if !S_IWUSR && S_IWRITE
+# define S_IWUSR S_IWRITE
+#endif
+#if !S_IWUSR
+# define S_IWUSR 00200
+#endif
+#if !S_IXUSR && S_IEXEC
+# define S_IXUSR S_IEXEC
+#endif
+#if !S_IXUSR
+# define S_IXUSR 00100
+#endif
+
+#ifdef __MINGW32__
+/* mingw's mkdir() function has 1 argument, but we pass 2 arguments.
+ Therefore we have to disable the argument count checking. */
+# define mkdir ((int (*)()) mkdir)
+#endif
+
+#if !_LIBC
+# define __getpid getpid
+# define __gettimeofday gettimeofday
+# define __mkdir mkdir
+#endif
+
+/* Use the widest available unsigned type if uint64_t is not
+ available. The algorithm below extracts a number less than 62**6
+ (approximately 2**35.725) from uint64_t, so ancient hosts where
+ uintmax_t is only 32 bits lose about 3.725 bits of randomness,
+ which is better than not having mkstemp at all. */
+#if !defined UINT64_MAX && !defined uint64_t
+# define uint64_t uintmax_t
+#endif
+
+/* These are the characters used in temporary filenames. */
+static const char letters[] =
+"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+/* Generate a temporary file name based on TMPL. TMPL must match the
+ rules for mk[s]temp (i.e. end in "XXXXXX"). The name constructed
+ does not exist at the time of the call to __gen_tempname. TMPL is
+ overwritten with the result.
+
+ KIND is:
+ __GT_DIR: create a directory, which will be mode 0700.
+
+ We use a clever algorithm to get hard-to-predict names. */
+static int
+gen_tempname (char *tmpl)
+{
+ int len;
+ char *XXXXXX;
+ static uint64_t value;
+ uint64_t random_time_bits;
+ int count, fd = -1;
+ int save_errno = errno;
+
+ len = strlen (tmpl);
+ if (len < 6 || strcmp (&tmpl[len - 6], "XXXXXX"))
+ {
+ __set_errno (EINVAL);
+ return -1;
+ }
+
+ /* This is where the Xs start. */
+ XXXXXX = &tmpl[len - 6];
+
+ /* Get some more or less random data. */
+#ifdef RANDOM_BITS
+ RANDOM_BITS (random_time_bits);
+#else
+# if HAVE_GETTIMEOFDAY || _LIBC
+ {
+ struct timeval tv;
+ __gettimeofday (&tv, NULL);
+ random_time_bits = ((uint64_t) tv.tv_usec << 16) ^ tv.tv_sec;
+ }
+# else
+ random_time_bits = time (NULL);
+# endif
+#endif
+ value += random_time_bits ^ __getpid ();
+
+ for (count = 0; count < TMP_MAX; value += 7777, ++count)
+ {
+ uint64_t v = value;
+
+ /* Fill in the random bits. */
+ XXXXXX[0] = letters[v % 62];
+ v /= 62;
+ XXXXXX[1] = letters[v % 62];
+ v /= 62;
+ XXXXXX[2] = letters[v % 62];
+ v /= 62;
+ XXXXXX[3] = letters[v % 62];
+ v /= 62;
+ XXXXXX[4] = letters[v % 62];
+ v /= 62;
+ XXXXXX[5] = letters[v % 62];
+
+ fd = __mkdir (tmpl, S_IRUSR | S_IWUSR | S_IXUSR);
+
+ if (fd >= 0)
+ {
+ __set_errno (save_errno);
+ return fd;
+ }
+ else if (errno != EEXIST)
+ return -1;
+ }
+
+ /* We got out of the loop because we ran out of combinations to try. */
+ __set_errno (EEXIST);
+ return -1;
+}
+
+/* Generate a unique temporary directory from TEMPLATE.
+ The last six characters of TEMPLATE must be "XXXXXX";
+ they are replaced with a string that makes the filename unique.
+ The directory is created, mode 700, and its name is returned.
+ (This function comes from OpenBSD.) */
+char *
+mkdtemp (char *template)
+{
+ if (gen_tempname (template))
+ return NULL;
+ else
+ return template;
+}
diff --git a/tikzit/src/gtk/test/gtk.m b/tikzit/src/gtk/test/gtk.m
new file mode 100644
index 0000000..aabb0f2
--- /dev/null
+++ b/tikzit/src/gtk/test/gtk.m
@@ -0,0 +1,27 @@
+//
+// linux.m
+// TikZiT
+//
+// Copyright 2010 Aleks Kissinger. All rights reserved.
+//
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+#import "test/test.h"
+
+void testGtk() {
+
+}
diff --git a/tikzit/src/gtk/test/main.m b/tikzit/src/gtk/test/main.m
new file mode 100644
index 0000000..639a335
--- /dev/null
+++ b/tikzit/src/gtk/test/main.m
@@ -0,0 +1,50 @@
+//
+// main.m
+// TikZiT
+//
+// Copyright 2010 Aleks Kissinger. All rights reserved.
+//
+//
+// This file is part of TikZiT.
+//
+// TikZiT 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.
+//
+// TikZiT 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 TikZiT. If not, see <http://www.gnu.org/licenses/>.
+//
+#include "config.h"
+#import "test/test.h"
+#include <string.h>
+void testCommon();
+
+int main(int argc, char **argv) {
+ if (argc == 2 && strcmp(argv[1], "--disable-color")==0) {
+ setColorEnabled(NO);
+ } else {
+ setColorEnabled(YES);
+ }
+
+ PUTS(@"");
+ PUTS(@"**********************************************");
+ PUTS(@"TikZiT TESTS, LINUX VERSION %@", VERSION);
+ PUTS(@"**********************************************");
+ PUTS(@"");
+
+ startTests();
+ testCommon();
+ testLinux();
+
+ PUTS(@"");
+ PUTS(@"**********************************************");
+ endTests();
+ PUTS(@"**********************************************");
+ PUTS(@"");
+}