From dcf64eac4fdb246cf6fb169c1279b67966478173 Mon Sep 17 00:00:00 2001 From: randomguy3 Date: Wed, 18 Jan 2012 10:05:06 +0000 Subject: 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 --- tikzit/src/gtk/CairoRenderContext.h | 59 ++ tikzit/src/gtk/CairoRenderContext.m | 346 +++++++++ tikzit/src/gtk/ColorRGB+Gtk.h | 30 + tikzit/src/gtk/ColorRGB+Gtk.m | 46 ++ tikzit/src/gtk/ColorRGB+IntegerListStorage.h | 32 + tikzit/src/gtk/ColorRGB+IntegerListStorage.m | 57 ++ tikzit/src/gtk/Configuration.h | 447 +++++++++++ tikzit/src/gtk/Configuration.m | 446 +++++++++++ tikzit/src/gtk/Edge+Render.h | 33 + tikzit/src/gtk/Edge+Render.m | 251 +++++++ tikzit/src/gtk/EdgeStyle+Storage.h | 29 + tikzit/src/gtk/EdgeStyle+Storage.m | 49 ++ tikzit/src/gtk/EdgeStyleEditor.h | 39 + tikzit/src/gtk/EdgeStyleEditor.m | 322 ++++++++ tikzit/src/gtk/EdgeStyleSelector.h | 69 ++ tikzit/src/gtk/EdgeStyleSelector.m | 454 +++++++++++ tikzit/src/gtk/EdgeStylesPalette.h | 46 ++ tikzit/src/gtk/EdgeStylesPalette.m | 283 +++++++ tikzit/src/gtk/FileChooserDialog.h | 56 ++ tikzit/src/gtk/FileChooserDialog.m | 152 ++++ tikzit/src/gtk/GraphInputHandler.h | 69 ++ tikzit/src/gtk/GraphInputHandler.m | 410 ++++++++++ tikzit/src/gtk/GraphRenderer.h | 105 +++ tikzit/src/gtk/GraphRenderer.m | 607 +++++++++++++++ tikzit/src/gtk/InputDelegate.h | 76 ++ tikzit/src/gtk/MainWindow.h | 221 ++++++ tikzit/src/gtk/MainWindow.m | 1035 ++++++++++++++++++++++++++ tikzit/src/gtk/Menu.h | 99 +++ tikzit/src/gtk/Menu.m | 796 ++++++++++++++++++++ tikzit/src/gtk/NSError+Glib.h | 28 + tikzit/src/gtk/NSError+Glib.m | 50 ++ tikzit/src/gtk/NSFileManager+Glib.h | 31 + tikzit/src/gtk/NSFileManager+Glib.m | 55 ++ tikzit/src/gtk/NSString+Glib.h | 50 ++ tikzit/src/gtk/NSString+Glib.m | 96 +++ tikzit/src/gtk/Node+Render.h | 44 ++ tikzit/src/gtk/Node+Render.m | 188 +++++ tikzit/src/gtk/NodeStyle+Gtk.h | 31 + tikzit/src/gtk/NodeStyle+Gtk.m | 41 + tikzit/src/gtk/NodeStyle+Render.h | 30 + tikzit/src/gtk/NodeStyle+Storage.h | 29 + tikzit/src/gtk/NodeStyle+Storage.m | 62 ++ tikzit/src/gtk/NodeStyleEditor.h | 43 ++ tikzit/src/gtk/NodeStyleEditor.m | 472 ++++++++++++ tikzit/src/gtk/NodeStyleSelector.h | 69 ++ tikzit/src/gtk/NodeStyleSelector.m | 426 +++++++++++ tikzit/src/gtk/NodeStylesPalette.h | 46 ++ tikzit/src/gtk/NodeStylesPalette.m | 279 +++++++ tikzit/src/gtk/PreambleEditor.h | 50 ++ tikzit/src/gtk/PreambleEditor.m | 556 ++++++++++++++ tikzit/src/gtk/Preambles+Storage.h | 29 + tikzit/src/gtk/Preambles+Storage.m | 84 +++ tikzit/src/gtk/PreviewRenderer.h | 48 ++ tikzit/src/gtk/PreviewRenderer.m | 230 ++++++ tikzit/src/gtk/PreviewWindow.h | 50 ++ tikzit/src/gtk/PreviewWindow.m | 191 +++++ tikzit/src/gtk/PropertyListEditor.h | 63 ++ tikzit/src/gtk/PropertyListEditor.m | 422 +++++++++++ tikzit/src/gtk/PropertyPane.h | 65 ++ tikzit/src/gtk/PropertyPane.m | 598 +++++++++++++++ tikzit/src/gtk/RecentManager.h | 30 + tikzit/src/gtk/RecentManager.m | 74 ++ tikzit/src/gtk/SettingsDialog.h | 46 ++ tikzit/src/gtk/SettingsDialog.m | 273 +++++++ tikzit/src/gtk/Shape+Render.h | 29 + tikzit/src/gtk/Shape+Render.m | 57 ++ tikzit/src/gtk/StyleManager+Storage.h | 26 + tikzit/src/gtk/StyleManager+Storage.m | 81 ++ tikzit/src/gtk/StylesPane.h | 49 ++ tikzit/src/gtk/StylesPane.m | 143 ++++ tikzit/src/gtk/Surface.h | 90 +++ tikzit/src/gtk/TZFoundation.h | 28 + tikzit/src/gtk/TikzDocument.h | 163 ++++ tikzit/src/gtk/TikzDocument.m | 807 ++++++++++++++++++++ tikzit/src/gtk/WidgetSurface.h | 55 ++ tikzit/src/gtk/WidgetSurface.m | 539 ++++++++++++++ tikzit/src/gtk/cairo_helpers.h | 25 + tikzit/src/gtk/cairo_helpers.m | 28 + tikzit/src/gtk/clipboard.h | 41 + tikzit/src/gtk/clipboard.m | 57 ++ tikzit/src/gtk/gtkhelpers.h | 49 ++ tikzit/src/gtk/gtkhelpers.m | 244 ++++++ tikzit/src/gtk/logo.h | 32 + tikzit/src/gtk/logo.m | 64 ++ tikzit/src/gtk/main.m | 75 ++ tikzit/src/gtk/mkdtemp.h | 32 + tikzit/src/gtk/mkdtemp.m | 204 +++++ tikzit/src/gtk/test/gtk.m | 27 + tikzit/src/gtk/test/main.m | 50 ++ 89 files changed, 14538 insertions(+) create mode 100644 tikzit/src/gtk/CairoRenderContext.h create mode 100644 tikzit/src/gtk/CairoRenderContext.m create mode 100644 tikzit/src/gtk/ColorRGB+Gtk.h create mode 100644 tikzit/src/gtk/ColorRGB+Gtk.m create mode 100644 tikzit/src/gtk/ColorRGB+IntegerListStorage.h create mode 100644 tikzit/src/gtk/ColorRGB+IntegerListStorage.m create mode 100644 tikzit/src/gtk/Configuration.h create mode 100644 tikzit/src/gtk/Configuration.m create mode 100644 tikzit/src/gtk/Edge+Render.h create mode 100644 tikzit/src/gtk/Edge+Render.m create mode 100644 tikzit/src/gtk/EdgeStyle+Storage.h create mode 100644 tikzit/src/gtk/EdgeStyle+Storage.m create mode 100644 tikzit/src/gtk/EdgeStyleEditor.h create mode 100644 tikzit/src/gtk/EdgeStyleEditor.m create mode 100644 tikzit/src/gtk/EdgeStyleSelector.h create mode 100644 tikzit/src/gtk/EdgeStyleSelector.m create mode 100644 tikzit/src/gtk/EdgeStylesPalette.h create mode 100644 tikzit/src/gtk/EdgeStylesPalette.m create mode 100644 tikzit/src/gtk/FileChooserDialog.h create mode 100644 tikzit/src/gtk/FileChooserDialog.m create mode 100644 tikzit/src/gtk/GraphInputHandler.h create mode 100644 tikzit/src/gtk/GraphInputHandler.m create mode 100644 tikzit/src/gtk/GraphRenderer.h create mode 100644 tikzit/src/gtk/GraphRenderer.m create mode 100644 tikzit/src/gtk/InputDelegate.h create mode 100644 tikzit/src/gtk/MainWindow.h create mode 100644 tikzit/src/gtk/MainWindow.m create mode 100644 tikzit/src/gtk/Menu.h create mode 100644 tikzit/src/gtk/Menu.m create mode 100644 tikzit/src/gtk/NSError+Glib.h create mode 100644 tikzit/src/gtk/NSError+Glib.m create mode 100644 tikzit/src/gtk/NSFileManager+Glib.h create mode 100644 tikzit/src/gtk/NSFileManager+Glib.m create mode 100644 tikzit/src/gtk/NSString+Glib.h create mode 100644 tikzit/src/gtk/NSString+Glib.m create mode 100644 tikzit/src/gtk/Node+Render.h create mode 100644 tikzit/src/gtk/Node+Render.m create mode 100644 tikzit/src/gtk/NodeStyle+Gtk.h create mode 100644 tikzit/src/gtk/NodeStyle+Gtk.m create mode 100644 tikzit/src/gtk/NodeStyle+Render.h create mode 100644 tikzit/src/gtk/NodeStyle+Storage.h create mode 100644 tikzit/src/gtk/NodeStyle+Storage.m create mode 100644 tikzit/src/gtk/NodeStyleEditor.h create mode 100644 tikzit/src/gtk/NodeStyleEditor.m create mode 100644 tikzit/src/gtk/NodeStyleSelector.h create mode 100644 tikzit/src/gtk/NodeStyleSelector.m create mode 100644 tikzit/src/gtk/NodeStylesPalette.h create mode 100644 tikzit/src/gtk/NodeStylesPalette.m create mode 100644 tikzit/src/gtk/PreambleEditor.h create mode 100644 tikzit/src/gtk/PreambleEditor.m create mode 100644 tikzit/src/gtk/Preambles+Storage.h create mode 100644 tikzit/src/gtk/Preambles+Storage.m create mode 100644 tikzit/src/gtk/PreviewRenderer.h create mode 100644 tikzit/src/gtk/PreviewRenderer.m create mode 100644 tikzit/src/gtk/PreviewWindow.h create mode 100644 tikzit/src/gtk/PreviewWindow.m create mode 100644 tikzit/src/gtk/PropertyListEditor.h create mode 100644 tikzit/src/gtk/PropertyListEditor.m create mode 100644 tikzit/src/gtk/PropertyPane.h create mode 100644 tikzit/src/gtk/PropertyPane.m create mode 100644 tikzit/src/gtk/RecentManager.h create mode 100644 tikzit/src/gtk/RecentManager.m create mode 100644 tikzit/src/gtk/SettingsDialog.h create mode 100644 tikzit/src/gtk/SettingsDialog.m create mode 100644 tikzit/src/gtk/Shape+Render.h create mode 100644 tikzit/src/gtk/Shape+Render.m create mode 100644 tikzit/src/gtk/StyleManager+Storage.h create mode 100644 tikzit/src/gtk/StyleManager+Storage.m create mode 100644 tikzit/src/gtk/StylesPane.h create mode 100644 tikzit/src/gtk/StylesPane.m create mode 100644 tikzit/src/gtk/Surface.h create mode 100644 tikzit/src/gtk/TZFoundation.h create mode 100644 tikzit/src/gtk/TikzDocument.h create mode 100644 tikzit/src/gtk/TikzDocument.m create mode 100644 tikzit/src/gtk/WidgetSurface.h create mode 100644 tikzit/src/gtk/WidgetSurface.m create mode 100644 tikzit/src/gtk/cairo_helpers.h create mode 100644 tikzit/src/gtk/cairo_helpers.m create mode 100644 tikzit/src/gtk/clipboard.h create mode 100644 tikzit/src/gtk/clipboard.m create mode 100644 tikzit/src/gtk/gtkhelpers.h create mode 100644 tikzit/src/gtk/gtkhelpers.m create mode 100644 tikzit/src/gtk/logo.h create mode 100644 tikzit/src/gtk/logo.m create mode 100644 tikzit/src/gtk/main.m create mode 100644 tikzit/src/gtk/mkdtemp.h create mode 100644 tikzit/src/gtk/mkdtemp.m create mode 100644 tikzit/src/gtk/test/gtk.m create mode 100644 tikzit/src/gtk/test/main.m (limited to 'tikzit/src/gtk') 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "RenderContext.h" +#import "Transformer.h" +#import +#import +#import + +@interface PangoTextLayout: NSObject { + 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 { + 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 + * + * 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 . + */ + +#import "CairoRenderContext.h" + +#import "cairo_helpers.h" +#import "util.h" + +#import + +@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) 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "ColorRGB.h" +#import + +@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 + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * 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 . + */ + +#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 . +// + +#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 . +// + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "Edge.h" +#import "RenderContext.h" +#import "Surface.h" + +@interface Edge (Render) + ++ (float) controlPointRadius; +- (void) renderControlsToSurface:(id)surface withContext:(id)context; +- (void) renderToSurface:(id)surface withContext:(id)context selected:(BOOL)selected; +- (NSRect) renderedBoundsWithTransformer:(Transformer*)t whenSelected:(BOOL)selected; +- (BOOL) hitByPoint:(NSPoint)p onSurface:(id)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 + * 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 . + */ + +#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 withContext:(id)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)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)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 withContext:(id)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 withFuzz:(float)fuzz { + [self updateControls]; + + NSRect boundingRect = [[surface transformer] rectToScreen:[self boundingRect]]; + if (!NSPointInRect(p, NSInsetRect(boundingRect, -fuzz, -fuzz))) { + return NO; + } + + id 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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#import "EdgeStyleEditor.h" + +#import "EdgeStyle.h" +#import "Shape.h" + +#include + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#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 + * + * 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 . + */ + +#import "EdgeStyleSelector.h" + +#import "CairoRenderContext.h" +#import "Shape.h" +#import "Shape+Render.h" +#import "ShapeNames.h" +#import "StyleManager.h" + +#import + +// {{{ 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * 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 . + */ + +#import "GraphInputHandler.h" +#import +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +// 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 { + TikzDocument *doc; + NSObject *surface; + Grid *grid; + NSRect selectionBox; + Node *halfEdgeOrigin; + NSPoint halfEdgeOriginPoint; + NSPoint halfEdgeEnd; + BOOL showBoundingBoxHandles; +} + +- (id) initWithSurface:(NSObject *)surface; +- (id) initWithSurface:(NSObject *)surface document:(TikzDocument*)document; +- (void) renderWithContext:(id)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; +- (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 + * 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 . + */ + +#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)context; +- (void) renderSelectionBoxWithContext:(id)context; +- (void) renderImpendingEdgeWithContext:(id)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 *)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 *)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)context onSurface:(id)surface { + [self renderWithContext:context]; +} + +- (void) renderWithContext:(id)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 { + 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)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)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)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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#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 + * + * 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 . + */ + +#import "MainWindow.h" + +#import +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#import "Menu.h" + +#import "MainWindow.h" +#import "GraphInputHandler.h" +#import "Configuration.h" +#import "PickSupport.h" +#import "Shape.h" +#import "TikzDocument.h" + +#import +#ifdef _ +#undef _ +#endif +#import +#import + +#import "gtkhelpers.h" + +#define ACTION_GROUP_STATIC "TZStatic" +#define ACTION_GROUP_DOCUMENT "TZDocument" +#define ACTION_GROUP_DOCUMENTS_LIST_MENU "TZDocumentsList" + +#import "logo.h" +#include +#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 ", + "Chris Heunen ", + "Alex Merry ", + 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[] = +"" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +//" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +#ifdef HAVE_POPPLER +" " +" " +#endif +" " +" " +" " +" " +" " +" " +" " +" " +/* +" " +" " +*/ +#ifdef HAVE_POPPLER +" " +" " +#endif +" " +" " +" " +" " +" " +" " +/* +" " +" " +" " +*/ +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +""; + + + +// }}} +// {{{ 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, "N", + N_("Create a new graph"), G_CALLBACK (new_cb) }, + + { "Open", GTK_STOCK_OPEN, N_("_Open\342\200\246") ,"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, "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, "plus", + NULL, G_CALLBACK (zoom_in_cb) }, + + { "ZoomOut", GTK_STOCK_ZOOM_OUT, NULL, "minus", + NULL, G_CALLBACK (zoom_out_cb) }, + + { "ZoomReset", GTK_STOCK_ZOOM_100, N_("_Reset zoom"), "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, "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, "W", + N_("Close the current graph"), G_CALLBACK (close_cb) }, +*/ + + /* EditMenu */ + { "Undo", GTK_STOCK_UNDO, NULL, "Z", + N_("Undo the last action"), G_CALLBACK (undo_cb) }, + + { "Redo", GTK_STOCK_REDO, NULL, "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, "A", + N_("Select all nodes on the graph"), G_CALLBACK (select_all_cb) }, + + { "DeselectAll", NULL, N_("D_eselect all"), "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"), "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"), "s", + N_("Select, move and edit nodes and edges"), (gint)SelectMode }, + + { "CreateNodeMode", NULL, N_("_Create nodes"), "c", + N_("Create new nodes"), (gint)CreateNodeMode }, + + { "DrawEdgeMode", NULL, N_("_Draw edges"), "e", + N_("Draw new edges"), (gint)DrawEdgeMode }, + + { "BoundingBoxMode", NULL, N_("_Bounding box"), "x", + N_("Set the bounding box"), (gint)BoundingBoxMode }, + + { "HandMode", NULL, N_("_Pan"), "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 + * + * 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 . + */ + +#import +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "Node.h" +#import "RenderContext.h" +#import "Surface.h" + +enum NodeState { + NodeNormal, + NodeSelected, + NodeHighlighted +}; + +@interface Node(Render) + +- (Transformer*) shapeTransformerForSurface:(id)surface; +// the total rendered bounds, excluding label +- (NSRect) renderBoundsForSurface:(id)surface; +- (NSRect) renderBoundsWithLabelForSurface:(id)surface; +- (NSString*) renderedLabel; +- (NSSize) renderedLabelSizeInContext:(id)context; +- (void) renderLabelToSurface:(id)surface withContext:(id)context; +- (void) renderLabelAt:(NSPoint)point withContext:(id)context; +- (void) renderToSurface:(id)surface withContext:(id)context state:(enum NodeState)state; +- (BOOL) hitByPoint:(NSPoint)p onSurface:(id)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 + * 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 . + */ + +#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 { + 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 { + return [self renderBoundsUsingShapeTransform:[self shapeTransformerForSurface:surface]]; +} + +- (NSRect) renderBoundsWithLabelForSurface:(id)surface { + NSRect nodeBounds = [self renderBoundsForSurface:surface]; + NSRect labelRect = NSZeroRect; + if (![label isEqual:@""]) { + id 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)context { + NSSize result = {0, 0}; + if (![label isEqual:@""]) { + NSString *r_label = [self renderedLabel]; + + id 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 withContext:(id)context { + [self renderLabelAt:[[surface transformer] toScreen:point] withContext:context]; +} + +- (void) renderLabelAt:(NSPoint)p withContext:(id)context { + // draw latex code overlayed on node + if (![label isEqual:@""]) { + [context saveState]; + + NSString *r_label = [self renderedLabel]; + id 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 withContext:(id)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 { + Transformer *shapeTrans = [self shapeTransformerForSurface:surface]; + + NSRect screenBounds = [self renderBoundsUsingShapeTransform:shapeTrans]; + if (!NSPointInRect(p, screenBounds)) { + return NO; + } + + float strokeThickness = style ? [style strokeThickness] : [NodeStyle defaultStrokeThickness]; + id 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "NodeStyle.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "NodeStyle.h" +#import "RenderContext.h" +#import "Surface.h" + +@interface NodeStyle (Render) + +- (void) renderToSurface:(id)surface withContext:(id)context at:(NSPoint)p; +- (BOOL) hitByPoint:(NSPoint)p onSurface:(id)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 + * + * 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 . + */ + +#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 + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#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 + * + * 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 . + */ + +#import "NodeStyleSelector.h" + +#import "CairoRenderContext.h" +#import "Shape.h" +#import "Shape+Render.h" +#import "ShapeNames.h" +#import "StyleManager.h" + +#import + +// {{{ 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#import "PreambleEditor.h" +#import "Preambles.h" +#import + +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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +#import "Surface.h" + +@class Configuration; +@class Preambles; +@class TikzDocument; + +@interface PreviewRenderer: NSObject { + 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 + * + * 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 . + */ + +#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)c onSurface:(id)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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#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 *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 *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 + * + * 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 . + */ + +#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*) delegate { + return delegate; +} + +- (void) setDelegate:(NSObject*)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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#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 + * + * 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 . + */ + +#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 { + TikzDocument *doc; +} +- (void) setDocument:(TikzDocument*)d; +@end + +@interface NodePropertyDelegate : NSObject { + TikzDocument *doc; + Node *node; +} +- (void) setDocument:(TikzDocument*)d; +- (void) setNode:(Node*)n; +@end + +@interface EdgePropertyDelegate : NSObject { + 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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "RecentManager.h" +#import + +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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "Shape.h" +#import "RenderContext.h" +#import "Surface.h" + +@interface Shape (Render) + +- (void) drawPathWithTransform:(Transformer*)transform andContext:(id)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 + * + * 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 . + */ + +#import "Shape+Render.h" + +#import "Edge.h" + +// we use cairo for finding the bounding box etc. +#import + +@implementation Shape (Render) + +- (void) drawPathWithTransform:(Transformer*)transform andContext:(id)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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import + +@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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "RenderContext.h" +#import "Transformer.h" + +@protocol Surface; + +@protocol RenderDelegate +- (void) renderWithContext:(id)context onSurface:(id)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; + +/** + * Create a render context for the surface. + */ +- (id) 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 + * + * 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 . + */ + +#import +#import + +#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 . +// + +#import "TZFoundation.h" +#import +#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 . +// + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#import + +/** + * Provides a surface for rendering to a widget. + */ +@interface WidgetSurface: NSObject { + GtkWidget *widget; + Transformer *transformer; + id 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 + * + * 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 . + */ + +#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) 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 )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 { + 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 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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import "RColor.h" +#import + +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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#import "TZFoundation.h" +#import +#import + +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 + * + * 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 . + */ + +#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 . +// +#import "TZFoundation.h" +#include + +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 . +// +#import "gtkhelpers.h" +#import + +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 + * + * 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 . + */ + +#include + +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 + * + * 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 . + */ + +#import "logo.h" +#include + +#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 . +// + +#import "TZFoundation.h" +#import +#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 + +#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 +#ifndef __set_errno +# define __set_errno(Val) errno = (Val) +#endif + +#include +#include +#include + +#include +#ifndef TMP_MAX +# define TMP_MAX 238328 +#endif + +#if HAVE_STDINT_H || _LIBC +# include +#endif +#if HAVE_INTTYPES_H +# include +#endif + +#if HAVE_UNISTD_H || _LIBC +# include +#endif + +#if HAVE_GETTIMEOFDAY || _LIBC +# if HAVE_SYS_TIME_H || _LIBC +# include +# endif +#else +# if HAVE_TIME_H || _LIBC +# include +# endif +#endif + +#include +#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 . +// +#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 . +// +#include "config.h" +#import "test/test.h" +#include +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(@""); +} -- cgit v1.2.3