summaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/KeyValueMap.vala118
-rw-r--r--src/util/Util.vala17
-rw-r--r--src/util/file.vala241
-rw-r--r--src/util/image.vala364
-rw-r--r--src/util/misc.vala377
-rw-r--r--src/util/mk/util.mk34
-rw-r--r--src/util/string.vala268
-rw-r--r--src/util/system.vala40
-rw-r--r--src/util/ui.vala98
9 files changed, 1557 insertions, 0 deletions
diff --git a/src/util/KeyValueMap.vala b/src/util/KeyValueMap.vala
new file mode 100644
index 0000000..c1f5a55
--- /dev/null
+++ b/src/util/KeyValueMap.vala
@@ -0,0 +1,118 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class KeyValueMap {
+ private string group;
+ private Gee.HashMap<string, string> map = new Gee.HashMap<string, string>();
+
+ public KeyValueMap(string group) {
+ this.group = group;
+ }
+
+ public KeyValueMap copy() {
+ KeyValueMap clone = new KeyValueMap(group);
+ foreach (string key in map.keys)
+ clone.map.set(key, map.get(key));
+
+ return clone;
+ }
+
+ public string get_group() {
+ return group;
+ }
+
+ public Gee.Set<string> get_keys() {
+ return map.keys;
+ }
+
+ public bool has_key(string key) {
+ return map.has_key(key);
+ }
+
+ public void set_string(string key, string value) {
+ assert(key != null);
+
+ map.set(key, value);
+ }
+
+ public void set_int(string key, int value) {
+ assert(key != null);
+
+ map.set(key, value.to_string());
+ }
+
+ public void set_double(string key, double value) {
+ assert(key != null);
+
+ map.set(key, value.to_string());
+ }
+
+ public void set_float(string key, float value) {
+ assert(key != null);
+
+ map.set(key, value.to_string());
+ }
+
+ public void set_bool(string key, bool value) {
+ assert(key != null);
+
+ map.set(key, value.to_string());
+ }
+
+ public string get_string(string key, string? def) {
+ string value = map.get(key);
+
+ return (value != null) ? value : def;
+ }
+
+ public int get_int(string key, int def) {
+ string value = map.get(key);
+
+ return (value != null) ? int.parse(value) : def;
+ }
+
+ public double get_double(string key, double def) {
+ string value = map.get(key);
+
+ return (value != null) ? double.parse(value) : def;
+ }
+
+ public float get_float(string key, float def) {
+ string value = map.get(key);
+
+ return (value != null) ? (float) double.parse(value) : def;
+ }
+
+ public bool get_bool(string key, bool def) {
+ string value = map.get(key);
+
+ return (value != null) ? bool.parse(value) : def;
+ }
+
+ // REDEYE: redeye reduction operates on circular regions defined by
+ // (Gdk.Point, int) pairs, where the Gdk.Point specifies the
+ // bounding circle's center and the the int specifies the circle's
+ // radius so, get_point( ) and set_point( ) functions have been
+ // added here to easily encode/decode Gdk.Points as strings.
+ public Gdk.Point get_point(string key, Gdk.Point def) {
+ string value = map.get(key);
+
+ if (value == null) {
+ return def;
+ } else {
+ Gdk.Point result = {0};
+ if (value.scanf("(%d, %d)", &result.x, &result.y) == 2)
+ return result;
+ else
+ return def;
+ }
+ }
+
+ public void set_point(string key, Gdk.Point point) {
+ map.set(key, "(%d, %d)".printf(point.x, point.y));
+ }
+}
+
diff --git a/src/util/Util.vala b/src/util/Util.vala
new file mode 100644
index 0000000..c754ff8
--- /dev/null
+++ b/src/util/Util.vala
@@ -0,0 +1,17 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace Util {
+ // Use these file attributes when loading file information for a complete FileInfo objects
+ public const string FILE_ATTRIBUTES = "standard::*,time::*,id::file,id::filesystem,etag::value";
+
+ public void init() throws Error {
+ }
+
+ public void terminate() {
+ }
+}
+
diff --git a/src/util/file.vala b/src/util/file.vala
new file mode 100644
index 0000000..1b6bb6c
--- /dev/null
+++ b/src/util/file.vala
@@ -0,0 +1,241 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+// Returns true if the file is claimed, false if it exists, and throws an Error otherwise. The file
+// will be created when the function exits and should be overwritten. Note that the file is not
+// held open; claiming a file is merely based on its existence.
+//
+// This function is thread-safe.
+public bool claim_file(File file) throws Error {
+ try {
+ file.create(FileCreateFlags.NONE, null);
+
+ // created; success
+ return true;
+ } catch (Error err) {
+ // check for file-exists error
+ if (!(err is IOError.EXISTS)) {
+ warning("claim_file %s: %s", file.get_path(), err.message);
+
+ throw err;
+ }
+
+ return false;
+ }
+}
+
+// This function "claims" a file on the filesystem in the directory specified with a basename the
+// same or similar as what has been requested (adds numerals to the end of the name until a unique
+// one has been found). The file may exist when this function returns, and it should be
+// overwritten. It does *not* attempt to create the parent directory, however.
+//
+// This function is thread-safe.
+public File? generate_unique_file(File dir, string basename, out bool collision) throws Error {
+ // create the file to atomically "claim" it
+ File file = dir.get_child(basename);
+ if (claim_file(file)) {
+ collision = false;
+
+ return file;
+ }
+
+ // file exists, note collision and keep searching
+ collision = true;
+
+ string name, ext;
+ disassemble_filename(basename, out name, out ext);
+
+ // generate a unique filename
+ for (int ctr = 1; ctr < int.MAX; ctr++) {
+ string new_name = (ext != null) ? "%s_%d.%s".printf(name, ctr, ext) : "%s_%d".printf(name, ctr);
+
+ file = dir.get_child(new_name);
+ if (claim_file(file))
+ return file;
+ }
+
+ warning("generate_unique_filename %s for %s: unable to claim file", dir.get_path(), basename);
+
+ return null;
+}
+
+public void disassemble_filename(string basename, out string name, out string ext) {
+ long offset = find_last_offset(basename, '.');
+ if (offset <= 0) {
+ name = basename;
+ ext = null;
+ } else {
+ name = basename.substring(0, offset);
+ ext = basename.substring(offset + 1, -1);
+ }
+}
+
+// This function is thread-safe.
+public uint64 query_total_file_size(File file_or_dir, Cancellable? cancellable = null) throws Error {
+ FileType type = file_or_dir.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ if (type == FileType.REGULAR) {
+ FileInfo info = null;
+ try {
+ info = file_or_dir.query_info(FileAttribute.STANDARD_SIZE,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+ } catch (Error err) {
+ if (err is IOError.CANCELLED)
+ throw err;
+
+ debug("Unable to query filesize for %s: %s", file_or_dir.get_path(), err.message);
+
+ return 0;
+ }
+
+ return info.get_size();
+ } else if (type != FileType.DIRECTORY) {
+ return 0;
+ }
+
+ FileEnumerator enumerator;
+ try {
+ enumerator = file_or_dir.enumerate_children(FileAttribute.STANDARD_NAME,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+ if (enumerator == null)
+ return 0;
+ } catch (Error err) {
+ // Don't treat a permissions failure as a hard failure, just skip the directory
+ if (err is FileError.PERM || err is IOError.PERMISSION_DENIED)
+ return 0;
+
+ throw err;
+ }
+
+ uint64 total_bytes = 0;
+
+ FileInfo info = null;
+ while ((info = enumerator.next_file(cancellable)) != null)
+ total_bytes += query_total_file_size(file_or_dir.get_child(info.get_name()), cancellable);
+
+ return total_bytes;
+}
+
+// Does not currently recurse. Could be modified to do so. Does not error out on first file that
+// does not delete, but logs a warning and continues.
+// Note: if supplying a progress monitor, a file count is also required. The count_files_in_directory()
+// function below should do the trick.
+public void delete_all_files(File dir, Gee.Set<string>? exceptions = null, ProgressMonitor? monitor = null,
+ uint64 file_count = 0, Cancellable? cancellable = null) throws Error {
+ FileType type = dir.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ if (type != FileType.DIRECTORY)
+ throw new IOError.NOT_DIRECTORY("%s is not a directory".printf(dir.get_path()));
+
+ FileEnumerator enumerator = dir.enumerate_children("standard::name,standard::type",
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+ FileInfo info = null;
+ uint64 i = 0;
+ while ((info = enumerator.next_file(cancellable)) != null) {
+ if (info.get_file_type() != FileType.REGULAR)
+ continue;
+
+ if (exceptions != null && exceptions.contains(info.get_name()))
+ continue;
+
+ File file = dir.get_child(info.get_name());
+ try {
+ file.delete(cancellable);
+ } catch (Error err) {
+ warning("Unable to delete file %s: %s", file.get_path(), err.message);
+ }
+
+ if (monitor != null && file_count > 0)
+ monitor(file_count, ++i);
+ }
+}
+
+public time_t query_file_modified(File file) throws Error {
+ FileInfo info = file.query_info(FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
+ null);
+
+ return info.get_modification_time().tv_sec;
+}
+
+public bool query_is_directory(File file) {
+ return file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null) == FileType.DIRECTORY;
+}
+
+public bool query_is_directory_empty(File dir) throws Error {
+ if (dir.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null) != FileType.DIRECTORY)
+ return false;
+
+ FileEnumerator enumerator = dir.enumerate_children("standard::name",
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ if (enumerator == null)
+ return false;
+
+ return enumerator.next_file(null) == null;
+}
+
+public string get_display_pathname(File file) {
+ // attempt to replace home path with tilde in a user-pleasable way
+ string path = file.get_parse_name();
+ string home = Environment.get_home_dir();
+
+ if (path == home)
+ return "~";
+
+ if (path.has_prefix(home))
+ return "~%s".printf(path.substring(home.length));
+
+ return path;
+}
+
+public string strip_pretty_path(string path) {
+ if (!path.has_prefix("~"))
+ return path;
+
+ return Environment.get_home_dir() + path.substring(1);
+}
+
+public string? get_file_info_id(FileInfo info) {
+ return info.get_attribute_string(FileAttribute.ID_FILE);
+}
+
+// Breaks a uint64 skip amount into several smaller skips.
+public void skip_uint64(InputStream input, uint64 skip_amount) throws GLib.Error {
+ while (skip_amount > 0) {
+ // skip() throws an error if the amount is too large, so check against ssize_t.MAX
+ if (skip_amount >= ssize_t.MAX) {
+ input.skip(ssize_t.MAX);
+ skip_amount -= ssize_t.MAX;
+ } else {
+ input.skip((size_t) skip_amount);
+ skip_amount = 0;
+ }
+ }
+}
+
+// Returns the number of files (and/or directories) within a directory.
+public uint64 count_files_in_directory(File dir) throws GLib.Error {
+ if (!query_is_directory(dir))
+ return 0;
+
+ uint64 count = 0;
+ FileEnumerator enumerator = dir.enumerate_children("standard::*",
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+
+ FileInfo info = null;
+ while ((info = enumerator.next_file()) != null)
+ count++;
+
+ return count;
+}
+
+// Replacement for deprecated Gio.file_equal
+public bool file_equal(File? a, File? b) {
+ return (a != null && b != null) ? a.equal(b) : false;
+}
+
+// Replacement for deprecated Gio.file_hash
+public uint file_hash(File? file) {
+ return file != null ? file.hash() : 0;
+}
+
diff --git a/src/util/image.vala b/src/util/image.vala
new file mode 100644
index 0000000..e8f93ba
--- /dev/null
+++ b/src/util/image.vala
@@ -0,0 +1,364 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+bool is_color_parsable(string spec) {
+ Gdk.Color color;
+ return Gdk.Color.parse(spec, out color);
+}
+
+Gdk.RGBA parse_color(string spec) {
+ return fetch_color(spec);
+}
+
+Gdk.RGBA fetch_color(string spec) {
+ Gdk.RGBA rgba = Gdk.RGBA();
+ if (!rgba.parse(spec))
+ error("Can't parse color %s", spec);
+
+ return rgba;
+}
+
+void set_source_color_from_string(Cairo.Context ctx, string spec) {
+ Gdk.RGBA rgba = fetch_color(spec);
+ ctx.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha);
+}
+
+private const int MIN_SCALED_WIDTH = 10;
+private const int MIN_SCALED_HEIGHT = 10;
+
+Gdk.Pixbuf scale_pixbuf(Gdk.Pixbuf pixbuf, int scale, Gdk.InterpType interp, bool scale_up) {
+ Dimensions original = Dimensions.for_pixbuf(pixbuf);
+ Dimensions scaled = original.get_scaled(scale, scale_up);
+ if ((original.width == scaled.width) && (original.height == scaled.height))
+ return pixbuf;
+
+ // use sane minimums ... scale_simple will hang if this is too low
+ scaled = scaled.with_min(MIN_SCALED_WIDTH, MIN_SCALED_HEIGHT);
+
+ return pixbuf.scale_simple(scaled.width, scaled.height, interp);
+}
+
+Gdk.Pixbuf resize_pixbuf(Gdk.Pixbuf pixbuf, Dimensions resized, Gdk.InterpType interp) {
+ Dimensions original = Dimensions.for_pixbuf(pixbuf);
+ if (original.width == resized.width && original.height == resized.height)
+ return pixbuf;
+
+ // use sane minimums ... scale_simple will hang if this is too low
+ resized = resized.with_min(MIN_SCALED_WIDTH, MIN_SCALED_HEIGHT);
+
+ return pixbuf.scale_simple(resized.width, resized.height, interp);
+}
+
+private const double DEGREE = Math.PI / 180.0;
+
+void draw_rounded_corners_filled(Cairo.Context ctx, Dimensions dim, Gdk.Point origin,
+ double radius_proportion) {
+ context_rounded_corners(ctx, dim, origin, radius_proportion);
+ ctx.paint();
+}
+
+void context_rounded_corners(Cairo.Context cx, Dimensions dim, Gdk.Point origin,
+ double radius_proportion) {
+ // establish a reasonable range
+ radius_proportion = radius_proportion.clamp(2.0, 100.0);
+
+ double left = origin.x;
+ double top = origin.y;
+ double right = origin.x + dim.width;
+ double bottom = origin.y + dim.height;
+
+ // the radius of the corners is proportional to the distance of the minor axis
+ double radius = ((double) dim.minor_axis()) / radius_proportion;
+
+ // create context and clipping region, starting from the top right arc and working around
+ // clockwise
+ cx.move_to(left, top);
+ cx.arc(right - radius, top + radius, radius, -90 * DEGREE, 0 * DEGREE);
+ cx.arc(right - radius, bottom - radius, radius, 0 * DEGREE, 90 * DEGREE);
+ cx.arc(left + radius, bottom - radius, radius, 90 * DEGREE, 180 * DEGREE);
+ cx.arc(left + radius, top + radius, radius, 180 * DEGREE, 270 * DEGREE);
+ cx.clip();
+}
+
+inline uchar shift_color_byte(int b, int shift) {
+ return (uchar) (b + shift).clamp(0, 255);
+}
+
+public void shift_colors(Gdk.Pixbuf pixbuf, int red, int green, int blue, int alpha) {
+ assert(red >= -255 && red <= 255);
+ assert(green >= -255 && green <= 255);
+ assert(blue >= -255 && blue <= 255);
+ assert(alpha >= -255 && alpha <= 255);
+
+ int width = pixbuf.get_width();
+ int height = pixbuf.get_height();
+ int rowstride = pixbuf.get_rowstride();
+ int channels = pixbuf.get_n_channels();
+ uchar *pixels = pixbuf.get_pixels();
+
+ assert(channels >= 3);
+ assert(pixbuf.get_colorspace() == Gdk.Colorspace.RGB);
+ assert(pixbuf.get_bits_per_sample() == 8);
+
+ for (int y = 0; y < height; y++) {
+ int y_offset = y * rowstride;
+
+ for (int x = 0; x < width; x++) {
+ int offset = y_offset + (x * channels);
+
+ if (red != 0)
+ pixels[offset] = shift_color_byte(pixels[offset], red);
+
+ if (green != 0)
+ pixels[offset + 1] = shift_color_byte(pixels[offset + 1], green);
+
+ if (blue != 0)
+ pixels[offset + 2] = shift_color_byte(pixels[offset + 2], blue);
+
+ if (alpha != 0 && channels >= 4)
+ pixels[offset + 3] = shift_color_byte(pixels[offset + 3], alpha);
+ }
+ }
+}
+
+public void dim_pixbuf(Gdk.Pixbuf pixbuf) {
+ PixelTransformer transformer = new PixelTransformer();
+ SaturationTransformation sat = new SaturationTransformation(SaturationTransformation.MIN_PARAMETER);
+ transformer.attach_transformation(sat);
+ transformer.transform_pixbuf(pixbuf);
+ shift_colors(pixbuf, 0, 0, 0, -100);
+}
+
+bool coord_in_rectangle(int x, int y, Gdk.Rectangle rect) {
+ return (x >= rect.x && x < (rect.x + rect.width) && y >= rect.y && y <= (rect.y + rect.height));
+}
+
+public bool rectangles_equal(Gdk.Rectangle a, Gdk.Rectangle b) {
+ return (a.x == b.x) && (a.y == b.y) && (a.width == b.width) && (a.height == b.height);
+}
+
+public string rectangle_to_string(Gdk.Rectangle rect) {
+ return "%d,%d %dx%d".printf(rect.x, rect.y, rect.width, rect.height);
+}
+
+public Gdk.Rectangle clamp_rectangle(Gdk.Rectangle original, Dimensions max) {
+ Gdk.Rectangle rect = Gdk.Rectangle();
+ rect.x = original.x.clamp(0, max.width);
+ rect.y = original.y.clamp(0, max.height);
+ rect.width = original.width.clamp(0, max.width);
+ rect.height = original.height.clamp(0, max.height);
+
+ return rect;
+}
+
+public Gdk.Point scale_point(Gdk.Point p, double factor) {
+ Gdk.Point result = {0};
+ result.x = (int) (factor * p.x + 0.5);
+ result.y = (int) (factor * p.y + 0.5);
+
+ return result;
+}
+
+public Gdk.Point add_points(Gdk.Point p1, Gdk.Point p2) {
+ Gdk.Point result = {0};
+ result.x = p1.x + p2.x;
+ result.y = p1.y + p2.y;
+
+ return result;
+}
+
+public Gdk.Point subtract_points(Gdk.Point p1, Gdk.Point p2) {
+ Gdk.Point result = {0};
+ result.x = p1.x - p2.x;
+ result.y = p1.y - p2.y;
+
+ return result;
+}
+
+// Converts XRGB/ARGB (Cairo)-formatted pixels to RGBA (GDK).
+void fix_cairo_pixbuf(Gdk.Pixbuf pixbuf) {
+ uchar *gdk_pixels = pixbuf.pixels;
+ for (int j = 0 ; j < pixbuf.height; ++j) {
+ uchar *p = gdk_pixels;
+ uchar *end = p + 4 * pixbuf.width;
+
+ while (p < end) {
+ uchar tmp = p[0];
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ p[0] = p[2];
+ p[2] = tmp;
+#else
+ p[0] = p[1];
+ p[1] = p[2];
+ p[2] = p[3];
+ p[3] = tmp;
+#endif
+ p += 4;
+ }
+
+ gdk_pixels += pixbuf.rowstride;
+ }
+}
+
+/**
+ * Finds the size of the smallest axially-aligned rectangle that could contain
+ * a rectangle src_width by src_height, rotated by angle.
+ *
+ * @param src_width The width of the incoming rectangle.
+ * @param src_height The height of the incoming rectangle.
+ * @param angle The amount to rotate by, given in degrees.
+ * @param dest_width The width of the computed rectangle.
+ * @param dest_height The height of the computed rectangle.
+ */
+void compute_arb_rotated_size(double src_width, double src_height, double angle,
+ out double dest_width, out double dest_height) {
+
+ angle = Math.fabs(degrees_to_radians(angle));
+ assert(angle <= Math.PI_2);
+ dest_width = src_width * Math.cos(angle) + src_height * Math.sin(angle);
+ dest_height = src_height * Math.cos(angle) + src_width * Math.sin(angle);
+}
+
+/**
+ * @brief Rotates a pixbuf to an arbitrary angle, given in degrees, and returns the rotated pixbuf.
+ *
+ * @param source_pixbuf The source image that needs to be angled.
+ * @param angle The angle the source image should be rotated by.
+ */
+Gdk.Pixbuf rotate_arb(Gdk.Pixbuf source_pixbuf, double angle) {
+ // if the straightening angle has been reset
+ // or was never set in the first place, nothing
+ // needs to be done to the source image.
+ if (angle == 0.0) {
+ return source_pixbuf;
+ }
+
+ // Compute how much the corners of the source image will
+ // move by to determine how big the dest pixbuf should be.
+
+ double x_tmp, y_tmp;
+ compute_arb_rotated_size(source_pixbuf.width, source_pixbuf.height, angle,
+ out x_tmp, out y_tmp);
+
+ Gdk.Pixbuf dest_pixbuf = new Gdk.Pixbuf(
+ Gdk.Colorspace.RGB, true, 8, (int) Math.round(x_tmp), (int) Math.round(y_tmp));
+
+ Cairo.ImageSurface surface = new Cairo.ImageSurface.for_data(
+ (uchar []) dest_pixbuf.pixels,
+ source_pixbuf.has_alpha ? Cairo.Format.ARGB32 : Cairo.Format.RGB24,
+ dest_pixbuf.width, dest_pixbuf.height, dest_pixbuf.rowstride);
+
+ Cairo.Context context = new Cairo.Context(surface);
+
+ context.set_source_rgb(0, 0, 0);
+ context.rectangle(0, 0, dest_pixbuf.width, dest_pixbuf.height);
+ context.fill();
+
+ context.translate(dest_pixbuf.width / 2, dest_pixbuf.height / 2);
+ context.rotate(degrees_to_radians(angle));
+ context.translate(- source_pixbuf.width / 2, - source_pixbuf.height / 2);
+
+ Gdk.cairo_set_source_pixbuf(context, source_pixbuf, 0, 0);
+ context.get_source().set_filter(Cairo.Filter.BEST);
+ context.paint();
+
+ // prepare the newly-drawn image for use by
+ // the rest of the pipeline.
+ fix_cairo_pixbuf(dest_pixbuf);
+
+ return dest_pixbuf;
+}
+
+/**
+ * @brief Rotates a point around the upper left corner of an image to an arbitrary angle,
+ * given in degrees, and returns the rotated point, translated such that it, along with its attendant
+ * image, are in positive x, positive y.
+ *
+ * @note May be subject to slight inaccuracy as Gdk points' coordinates may only be in whole pixels,
+ * so the fractional component is lost.
+ *
+ * @param source_point The point to be rotated and scaled.
+ * @param img_w The width of the source image (unrotated).
+ * @param img_h The height of the source image (unrotated).
+ * @param angle The angle the source image is to be rotated by to straighten it.
+ */
+Gdk.Point rotate_point_arb(Gdk.Point source_point, int img_w, int img_h, double angle,
+ bool invert = false) {
+ // angle of 0 degrees or angle was never set?
+ if (angle == 0.0) {
+ // nothing needs to be done.
+ return source_point;
+ }
+
+ double dest_width;
+ double dest_height;
+ compute_arb_rotated_size(img_w, img_h, angle, out dest_width, out dest_height);
+
+ Cairo.Matrix matrix = Cairo.Matrix.identity();
+ matrix.translate(dest_width / 2, dest_height / 2);
+ matrix.rotate(degrees_to_radians(angle));
+ matrix.translate(- img_w / 2, - img_h / 2);
+ if (invert)
+ assert(matrix.invert() == Cairo.Status.SUCCESS);
+
+ double dest_x = source_point.x;
+ double dest_y = source_point.y;
+ matrix.transform_point(ref dest_x, ref dest_y);
+
+ return { (int) dest_x, (int) dest_y };
+}
+
+/**
+ * @brief <u>De</u>rotates a point around the upper left corner of an image from an arbitrary angle,
+ * given in degrees, and returns the de-rotated point, taking into account any translation necessary
+ * to make sure all of the rotated image stays in positive x, positive y.
+ *
+ * @note May be subject to slight inaccuracy as Gdk points' coordinates may only be in whole pixels,
+ * so the fractional component is lost.
+ *
+ * @param source_point The point to be de-rotated.
+ * @param img_w The width of the source image (unrotated).
+ * @param img_h The height of the source image (unrotated).
+ * @param angle The angle the source image is to be rotated by to straighten it.
+ */
+Gdk.Point derotate_point_arb(Gdk.Point source_point, int img_w, int img_h, double angle) {
+ return rotate_point_arb(source_point, img_w, img_h, angle, true);
+}
+
+
+// Force an axially-aligned box to be inside a rotated rectangle.
+Box clamp_inside_rotated_image(Box src, int img_w, int img_h, double angle_deg,
+ bool preserve_geom) {
+
+ Gdk.Point top_left = derotate_point_arb({src.left, src.top}, img_w, img_h, angle_deg);
+ Gdk.Point top_right = derotate_point_arb({src.right, src.top}, img_w, img_h, angle_deg);
+ Gdk.Point bottom_left = derotate_point_arb({src.left, src.bottom}, img_w, img_h, angle_deg);
+ Gdk.Point bottom_right = derotate_point_arb({src.right, src.bottom}, img_w, img_h, angle_deg);
+
+ double angle = degrees_to_radians(angle_deg);
+ int top_offset = 0, bottom_offset = 0, left_offset = 0, right_offset = 0;
+
+ int top = int.min(top_left.y, top_right.y);
+ if (top < 0)
+ top_offset = (int) ((0 - top) * Math.cos(angle));
+
+ int bottom = int.max(bottom_left.y, bottom_right.y);
+ if (bottom > img_h)
+ bottom_offset = (int) ((img_h - bottom) * Math.cos(angle));
+
+ int left = int.min(top_left.x, bottom_left.x);
+ if (left < 0)
+ left_offset = (int) ((0 - left) * Math.cos(angle));
+
+ int right = int.max(top_right.x, bottom_right.x);
+ if (right > img_w)
+ right_offset = (int) ((img_w - right) * Math.cos(angle));
+
+ return preserve_geom ? src.get_offset(left_offset + right_offset, top_offset + bottom_offset)
+ : Box(src.left + left_offset, src.top + top_offset,
+ src.right + right_offset, src.bottom + bottom_offset);
+}
+
diff --git a/src/util/misc.vala b/src/util/misc.vala
new file mode 100644
index 0000000..73ce428
--- /dev/null
+++ b/src/util/misc.vala
@@ -0,0 +1,377 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public uint int64_hash(int64? n) {
+ // Rotating XOR hash
+ uint8 *u8 = (uint8 *) n;
+ uint hash = 0;
+ for (int ctr = 0; ctr < (sizeof(int64) / sizeof(uint8)); ctr++) {
+ hash = (hash << 4) ^ (hash >> 28) ^ (*u8++);
+ }
+
+ return hash;
+}
+
+public bool int64_equal(int64? a, int64? b) {
+ int64 *bia = (int64 *) a;
+ int64 *bib = (int64 *) b;
+
+ return (*bia) == (*bib);
+}
+
+public int int64_compare(int64? a, int64? b) {
+ int64 diff = *((int64 *) a) - *((int64 *) b);
+ if (diff < 0)
+ return -1;
+ else if (diff > 0)
+ return 1;
+ else
+ return 0;
+}
+
+public int uint64_compare(uint64? a, uint64? b) {
+ uint64 a64 = *((uint64 *) a);
+ uint64 b64 = *((uint64 *) b);
+
+ if (a64 < b64)
+ return -1;
+ else if (a64 > b64)
+ return 1;
+ else
+ return 0;
+}
+
+public delegate bool ValueEqualFunc(Value a, Value b);
+
+public bool bool_value_equals(Value a, Value b) {
+ return (bool) a == (bool) b;
+}
+
+public bool int_value_equals(Value a, Value b) {
+ return (int) a == (int) b;
+}
+
+public ulong timeval_to_ms(TimeVal time_val) {
+ return (((ulong) time_val.tv_sec) * 1000) + (((ulong) time_val.tv_usec) / 1000);
+}
+
+public ulong now_ms() {
+ return timeval_to_ms(TimeVal());
+}
+
+public ulong now_sec() {
+ TimeVal time_val = TimeVal();
+
+ return time_val.tv_sec;
+}
+
+public inline time_t now_time_t() {
+ return (time_t) now_sec();
+}
+
+public string md5_binary(uint8 *buffer, size_t length) {
+ assert(length != 0);
+
+ Checksum md5 = new Checksum(ChecksumType.MD5);
+ md5.update((uchar []) buffer, length);
+
+ return md5.get_string();
+}
+
+public string md5_file(File file) throws Error {
+ Checksum md5 = new Checksum(ChecksumType.MD5);
+ uint8[] buffer = new uint8[64 * 1024];
+
+ FileInputStream fins = file.read(null);
+ for (;;) {
+ size_t bytes_read = fins.read(buffer, null);
+ if (bytes_read <= 0)
+ break;
+
+ md5.update((uchar[]) buffer, bytes_read);
+ }
+
+ try {
+ fins.close(null);
+ } catch (Error err) {
+ warning("Unable to close MD5 input stream for %s: %s", file.get_path(), err.message);
+ }
+
+ return md5.get_string();
+}
+
+// Once generic functions are available in Vala, this could be genericized.
+public bool equal_sets(Gee.Set<string>? a, Gee.Set<string>? b) {
+ if ((a != null && a.size == 0) && (b == null))
+ return true;
+
+ if ((a == null) && (b != null && b.size == 0))
+ return true;
+
+ if ((a == null && b != null) || (a != null && b == null))
+ return false;
+
+ if (a == null && b == null)
+ return true;
+
+ if (a.size != b.size)
+ return false;
+
+ // because they're sets and the same size, only need to iterate over one set to know
+ // it is equal to the other
+ foreach (string element in a) {
+ if (!b.contains(element))
+ return false;
+ }
+
+ return true;
+}
+
+// Once generic functions are available in Vala, this could be genericized.
+public Gee.Set<string>? intersection_of_sets(Gee.Set<string>? a, Gee.Set<string>? b,
+ Gee.Set<string>? excluded) {
+ if (a != null && b == null) {
+ if (excluded != null)
+ excluded.add_all(a);
+
+ return null;
+ }
+
+ if (a == null && b != null) {
+ if (excluded != null)
+ excluded.add_all(b);
+
+ return null;
+ }
+
+ Gee.Set<string> intersection = new Gee.HashSet<string>();
+
+ foreach (string element in a) {
+ if (b.contains(element))
+ intersection.add(element);
+ else if (excluded != null)
+ excluded.add(element);
+ }
+
+ foreach (string element in b) {
+ if (a.contains(element))
+ intersection.add(element);
+ else if (excluded != null)
+ excluded.add(element);
+ }
+
+ return intersection.size > 0 ? intersection : null;
+}
+
+public uchar[] serialize_photo_ids(Gee.Collection<Photo> photos) {
+ int64[] ids = new int64[photos.size];
+ int ctr = 0;
+ foreach (Photo photo in photos)
+ ids[ctr++] = photo.get_photo_id().id;
+
+ size_t bytes = photos.size * sizeof(int64);
+ uchar[] serialized = new uchar[bytes];
+ Memory.copy(serialized, ids, bytes);
+
+ return serialized;
+}
+
+public Gee.List<PhotoID?>? unserialize_photo_ids(uchar* serialized, int size) {
+ size_t count = (size / sizeof(int64));
+ if (count <= 0 || serialized == null)
+ return null;
+
+ int64[] ids = new int64[count];
+ Memory.copy(ids, serialized, size);
+
+ Gee.ArrayList<PhotoID?> list = new Gee.ArrayList<PhotoID?>();
+ foreach (int64 id in ids)
+ list.add(PhotoID(id));
+
+ return list;
+}
+
+public uchar[] serialize_media_sources(Gee.Collection<MediaSource> media) {
+ Gdk.Atom[] atoms = new Gdk.Atom[media.size];
+ int ctr = 0;
+ foreach (MediaSource current_media in media)
+ atoms[ctr++] = Gdk.Atom.intern(current_media.get_source_id(), false);
+
+ size_t bytes = media.size * sizeof(Gdk.Atom);
+ uchar[] serialized = new uchar[bytes];
+ Memory.copy(serialized, atoms, bytes);
+
+ return serialized;
+}
+
+public Gee.List<MediaSource>? unserialize_media_sources(uchar* serialized, int size) {
+ size_t count = (size / sizeof(Gdk.Atom));
+ if (count <= 0 || serialized == null)
+ return null;
+
+ Gdk.Atom[] atoms = new Gdk.Atom[count];
+ Memory.copy(atoms, serialized, size);
+
+ Gee.ArrayList<MediaSource> list = new Gee.ArrayList<MediaSource>();
+ foreach (Gdk.Atom current_atom in atoms) {
+ MediaSource media = MediaCollectionRegistry.get_instance().fetch_media(current_atom.name());
+ assert(media != null);
+ list.add(media);
+ }
+
+ return list;
+}
+
+public string format_local_datespan(Time from_date, Time to_date) {
+ string from_format, to_format;
+
+ // Ticket #3240 - Change the way date ranges are pretty-
+ // printed if the start and end date occur on consecutive days.
+ if (from_date.year == to_date.year) {
+ // are these consecutive dates?
+ if ((from_date.month == to_date.month) && (from_date.day == (to_date.day - 1))) {
+ // Yes; display like so: Sat, July 4 - 5, 20X6
+ from_format = Resources.get_start_multiday_span_format_string();
+ to_format = Resources.get_end_multiday_span_format_string();
+ } else {
+ // No, but they're in the same year; display in shortened
+ // form: Sat, July 4 - Mon, July 6, 20X6
+ from_format = Resources.get_start_multimonth_span_format_string();
+ to_format = Resources.get_end_multimonth_span_format_string();
+ }
+ } else {
+ // Span crosses a year boundary, use long form dates
+ // for both start and end date.
+ from_format = Resources.get_long_date_format_string();
+ to_format = Resources.get_long_date_format_string();
+ }
+
+ return String.strip_leading_zeroes("%s - %s".printf(from_date.format(from_format),
+ to_date.format(to_format)));
+}
+
+public string format_local_date(Time date) {
+ return String.strip_leading_zeroes(date.format(Resources.get_long_date_format_string()));
+}
+
+public delegate void OneShotCallback();
+
+public class OneShotScheduler {
+ private string name;
+ private unowned OneShotCallback callback;
+ private uint scheduled = 0;
+
+ public OneShotScheduler(string name, OneShotCallback callback) {
+ this.name = name;
+ this.callback = callback;
+ }
+
+ ~OneShotScheduler() {
+#if TRACE_DTORS
+ debug("DTOR: OneShotScheduler for %s", name);
+#endif
+
+ cancel();
+ }
+
+ public bool is_scheduled() {
+ return scheduled != 0;
+ }
+
+ public void at_idle() {
+ at_priority_idle(Priority.DEFAULT_IDLE);
+ }
+
+ public void at_priority_idle(int priority) {
+ if (scheduled == 0)
+ scheduled = Idle.add_full(priority, callback_wrapper);
+ }
+
+ public void after_timeout(uint msec, bool reschedule) {
+ priority_after_timeout(Priority.DEFAULT, msec, reschedule);
+ }
+
+ public void priority_after_timeout(int priority, uint msec, bool reschedule) {
+ if (scheduled != 0 && !reschedule)
+ return;
+
+ if (scheduled != 0)
+ Source.remove(scheduled);
+
+ scheduled = Timeout.add_full(priority, msec, callback_wrapper);
+ }
+
+ public void cancel() {
+ if (scheduled == 0)
+ return;
+
+ Source.remove(scheduled);
+ scheduled = 0;
+ }
+
+ private bool callback_wrapper() {
+ scheduled = 0;
+ callback();
+
+ return false;
+ }
+}
+
+public class OpTimer {
+ private string name;
+ private Timer timer = new Timer();
+ private long count = 0;
+ private double elapsed = 0;
+ private double shortest = double.MAX;
+ private double longest = double.MIN;
+
+ public OpTimer(string name) {
+ this.name = name;
+ }
+
+ public void start() {
+ timer.start();
+ }
+
+ public void stop() {
+ double time = timer.elapsed();
+
+ elapsed += time;
+
+ if (time < shortest)
+ shortest = time;
+
+ if (time > longest)
+ longest = time;
+
+ count++;
+ }
+
+ public string to_string() {
+ if (count > 0) {
+ return "%s: count=%ld elapsed=%.03lfs min/avg/max=%.03lf/%.03lf/%.03lf".printf(name,
+ count, elapsed, shortest, elapsed / (double) count, longest);
+ } else {
+ return "%s: no operations".printf(name);
+ }
+ }
+}
+
+// Dummy function for suppressing 'could not stat file' errors
+// generated when saving into a previously non-existent file -
+// please see https://bugzilla.gnome.org/show_bug.cgi?id=662814
+// and to work around a spurious warning given by GDK when a
+// key press event is passed from a child class' event handler
+// to a parent's; (gnome bug pending, but see https://bugzilla.redhat.com/show_bug.cgi?id=665568).
+public void suppress_warnings(string? log_domain, LogLevelFlags log_levels, string message) {
+ // do nothing.
+}
+
+public bool is_twentyfour_hr_time_system() {
+ // if no AM/PM designation is found, the location is set to use a 24 hr time system
+ return is_string_empty(Time.local(0).format("%p"));
+}
+
diff --git a/src/util/mk/util.mk b/src/util/mk/util.mk
new file mode 100644
index 0000000..86a8bd5
--- /dev/null
+++ b/src/util/mk/util.mk
@@ -0,0 +1,34 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Util
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := util
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ file.vala \
+ image.vala \
+ misc.vala \
+ string.vala \
+ system.vala \
+ KeyValueMap.vala \
+ ui.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES :=
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+
diff --git a/src/util/string.vala b/src/util/string.vala
new file mode 100644
index 0000000..9fda007
--- /dev/null
+++ b/src/util/string.vala
@@ -0,0 +1,268 @@
+/* Copyright 2010-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+extern int64 g_ascii_strtoll(string str, out char *endptr, uint num_base);
+
+public const int DEFAULT_USER_TEXT_INPUT_LENGTH = 1024;
+
+public inline bool is_string_empty(string? s) {
+ return (s == null || s[0] == '\0');
+}
+
+// utf8 case sensitive compare
+public int utf8_cs_compare(void *a, void *b) {
+ return ((string) a).collate((string) b);
+}
+
+// utf8 case insensitive compare
+public int utf8_ci_compare(void *a, void *b) {
+ return ((string) a).down().collate(((string) b).down());
+}
+
+// utf8 array to string
+public string uchar_array_to_string(uchar[] data, int length = -1) {
+ if (length < 0)
+ length = data.length;
+
+ StringBuilder builder = new StringBuilder();
+ for (int ctr = 0; ctr < length; ctr++) {
+ if (data[ctr] != '\0')
+ builder.append_c((char) data[ctr]);
+ else
+ break;
+ }
+
+ return builder.str;
+}
+
+// string to uchar array
+public uchar[] string_to_uchar_array(string str) {
+ uchar[] data = new uchar[0];
+ for (int ctr = 0; ctr < str.length; ctr++)
+ data += (uchar) str[ctr];
+
+ return data;
+}
+
+// Markup.escape_text() will crash if the UTF-8 text is not valid; it relies on a call to
+// g_utf8_next_char(), which demands that the string be validated before use, which escape_text()
+// does not do. This handles this problem by kicking back an empty string if the text is not
+// valid. Text should be validated upon entry to the system as well to guard against this
+// problem.
+//
+// Null strings are accepted; they will result in an empty string returned.
+public inline string guarded_markup_escape_text(string? plain) {
+ return (!is_string_empty(plain) && plain.validate()) ? Markup.escape_text(plain) : "";
+}
+
+public long find_last_offset(string str, char c) {
+ long offset = str.length;
+ while (--offset >= 0) {
+ if (str[offset] == c)
+ return offset;
+ }
+
+ return -1;
+}
+
+// Helper function for searching an array of case-insensitive strings. The array should be
+// all lowercase.
+public bool is_in_ci_array(string str, string[] strings) {
+ string strdown = str.down();
+ foreach (string str_element in strings) {
+ if (strdown == str_element)
+ return true;
+ }
+
+ return false;
+}
+
+[Flags]
+public enum PrepareInputTextOptions {
+ EMPTY_IS_NULL,
+ VALIDATE,
+ INVALID_IS_NULL,
+ STRIP,
+ STRIP_CRLF,
+ NORMALIZE,
+ DEFAULT = EMPTY_IS_NULL | VALIDATE | INVALID_IS_NULL | STRIP_CRLF | STRIP | NORMALIZE;
+}
+
+public string? prepare_input_text(string? text, PrepareInputTextOptions options, int dest_length) {
+ if (text == null)
+ return null;
+
+ if ((options & PrepareInputTextOptions.VALIDATE) != 0 && !text.validate())
+ return (options & PrepareInputTextOptions.INVALID_IS_NULL) != 0 ? null : "";
+
+ string prepped = text;
+
+ // Using composed form rather than GLib's default (decomposed) as NFC is the preferred form in
+ // Linux and WWW. More importantly, Pango seems to have serious problems displaying decomposed
+ // forms of Korean language glyphs (and perhaps others). See:
+ // http://trac.yorba.org/ticket/2952
+ if ((options & PrepareInputTextOptions.NORMALIZE) != 0)
+ prepped = prepped.normalize(-1, NormalizeMode.NFC);
+
+ if ((options & PrepareInputTextOptions.STRIP) != 0)
+ prepped = prepped.strip();
+
+ // Ticket #3245 - Prevent carriage return mayhem
+ // in image titles, tag names, etc.
+ if ((options & PrepareInputTextOptions.STRIP_CRLF) != 0)
+ prepped = prepped.delimit("\n\r", ' ');
+
+ if ((options & PrepareInputTextOptions.EMPTY_IS_NULL) != 0 && is_string_empty(prepped))
+ return null;
+
+ // Ticket #3196 - Allow calling functions to limit the length of the
+ // string we return to them. Passing any negative value is interpreted
+ // as 'do not truncate'.
+ if (dest_length >= 0) {
+ StringBuilder sb = new StringBuilder(prepped);
+ sb.truncate(dest_length);
+ return sb.str;
+ }
+
+ // otherwise, return normally.
+ return prepped;
+}
+
+public int64 parse_int64(string str, int num_base) {
+ return g_ascii_strtoll(str, null, num_base);
+}
+
+namespace String {
+
+public inline bool contains_char(string haystack, unichar needle) {
+ return haystack.index_of_char(needle) >= 0;
+}
+
+public inline bool contains_str(string haystack, string needle) {
+ return haystack.index_of(needle) >= 0;
+}
+
+public inline string? sliced_at(string str, int index) {
+ return (index >= 0) ? str[index:str.length] : null;
+}
+
+public inline string? sliced_at_first_str(string haystack, string needle, int start_index = 0) {
+ return sliced_at(haystack, haystack.index_of(needle, start_index));
+}
+
+public inline string? sliced_at_last_str(string haystack, string needle, int start_index = 0) {
+ return sliced_at(haystack, haystack.last_index_of(needle, start_index));
+}
+
+public inline string? sliced_at_first_char(string haystack, unichar ch, int start_index = 0) {
+ return sliced_at(haystack, haystack.index_of_char(ch, start_index));
+}
+
+public inline string? sliced_at_last_char(string haystack, unichar ch, int start_index = 0) {
+ return sliced_at(haystack, haystack.last_index_of_char(ch, start_index));
+}
+
+// Note that this method currently turns a word of all zeros into empty space ("000" -> "")
+public string strip_leading_zeroes(string str) {
+ StringBuilder stripped = new StringBuilder();
+ bool prev_is_space = true;
+ for (unowned string iter = str; iter.get_char() != 0; iter = iter.next_char()) {
+ unichar ch = iter.get_char();
+
+ if (!prev_is_space || ch != '0') {
+ stripped.append_unichar(ch);
+ prev_is_space = ch.isspace();
+ }
+ }
+
+ return stripped.str;
+}
+
+public string remove_diacritics(string istring) {
+ var builder = new StringBuilder ();
+ unichar ch;
+ int i = 0;
+ while(istring.normalize().get_next_char(ref i, out ch)) {
+ switch(ch.type()) {
+ case UnicodeType.CONTROL:
+ case UnicodeType.FORMAT:
+ case UnicodeType.UNASSIGNED:
+ case UnicodeType.NON_SPACING_MARK:
+ case UnicodeType.COMBINING_MARK:
+ case UnicodeType.ENCLOSING_MARK:
+ // Ignore those
+ continue;
+ }
+ builder.append_unichar(ch);
+ }
+ return builder.str;
+}
+
+public string to_hex_string(string str) {
+ StringBuilder builder = new StringBuilder();
+
+ uint8 *data = (uint8 *) str;
+ while (*data != 0)
+ builder.append_printf("%02Xh%s", *data++, (*data != 0) ? " " : "");
+
+ return builder.str;
+}
+
+// A note on the collated_* and precollated_* methods:
+//
+// A bug report (http://trac.yorba.org/ticket/3152) indicated that two different Hirigana characters
+// as Tag names would trigger an assertion. Investigation showed that the characters' collation
+// keys computed as equal when the locale was set to anything but the default locale (C) or
+// Japanese. A related bug was that another hash table was using str_equal, which does not use
+// collation, meaning that in one table the strings were seen as the same and in another as
+// different.
+//
+// The solution we arrived at is to use collation whenever possible, but if two strings have the
+// same collation, then fall back on strcmp(), which looks for byte-for-byte comparisons. Note
+// that this technique requires that both strings have been properly composed (use
+// prepare_input_text() for that task) so that equal UTF-8 strings are byte-for-byte equal as
+// well.
+
+// See note above.
+public uint collated_hash(void *ptr) {
+ string str = (string) ptr;
+
+ return str_hash(str.collate_key());
+}
+
+// See note above.
+public uint precollated_hash(void *ptr) {
+ return str_hash((string) ptr);
+}
+
+// See note above.
+public int collated_compare(void *a, void *b) {
+ string astr = (string) a;
+ string bstr = (string) b;
+
+ int result = astr.collate(bstr);
+
+ return (result != 0) ? result : strcmp(astr, bstr);
+}
+
+// See note above.
+public int precollated_compare(string astr, string akey, string bstr, string bkey) {
+ int result = strcmp(akey, bkey);
+
+ return (result != 0) ? result : strcmp(astr, bstr);
+}
+
+// See note above.
+public bool collated_equals(void *a, void *b) {
+ return collated_compare(a, b) == 0;
+}
+
+// See note above.
+public bool precollated_equals(string astr, string akey, string bstr, string bkey) {
+ return precollated_compare(astr, akey, bstr, bkey) == 0;
+}
+
+}
diff --git a/src/util/system.vala b/src/util/system.vala
new file mode 100644
index 0000000..9407405
--- /dev/null
+++ b/src/util/system.vala
@@ -0,0 +1,40 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+int number_of_processors() {
+ int n = (int) ExtendedPosix.sysconf(ExtendedPosix.ConfName._SC_NPROCESSORS_ONLN);
+ return n <= 0 ? 1 : n;
+}
+
+// Return the directory in which Shotwell is installed, or null if uninstalled.
+File? get_sys_install_dir(File exec_dir) {
+ // guard against exec_dir being a symlink
+ File exec_dir1 = exec_dir;
+ try {
+ exec_dir1 = File.new_for_path(
+ FileUtils.read_link("/" + FileUtils.read_link(exec_dir.get_path())));
+ } catch (FileError e) {
+ // exec_dir is not a symlink
+ }
+ File prefix_dir = File.new_for_path(Resources.PREFIX);
+ return exec_dir1.has_prefix(prefix_dir) ? prefix_dir : null;
+}
+
+string get_nautilus_install_location() {
+ return Environment.find_program_in_path("nautilus");
+}
+
+void sys_show_uri(Gdk.Screen screen, string uri) throws Error {
+ Gtk.show_uri(screen, uri, Gdk.CURRENT_TIME);
+}
+
+void show_file_in_nautilus(string filename) throws Error {
+ GLib.Process.spawn_command_line_async(get_nautilus_install_location() + " " + filename);
+}
+
+int posix_wexitstatus(int status) {
+ return (((status) & 0xff00) >> 8);
+}
diff --git a/src/util/ui.vala b/src/util/ui.vala
new file mode 100644
index 0000000..9f66de8
--- /dev/null
+++ b/src/util/ui.vala
@@ -0,0 +1,98 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public enum AdjustmentRelation {
+ BELOW,
+ IN_RANGE,
+ ABOVE
+}
+
+public enum CompassPoint {
+ NORTH,
+ SOUTH,
+ EAST,
+ WEST
+}
+
+public enum Direction {
+ FORWARD,
+ BACKWARD;
+
+ public Spit.Transitions.Direction to_transition_direction() {
+ switch (this) {
+ case FORWARD:
+ return Spit.Transitions.Direction.FORWARD;
+
+ case BACKWARD:
+ return Spit.Transitions.Direction.BACKWARD;
+
+ default:
+ error("Unknown Direction %s", this.to_string());
+ }
+ }
+}
+
+public void spin_event_loop() {
+ while (Gtk.events_pending())
+ Gtk.main_iteration();
+}
+
+public AdjustmentRelation get_adjustment_relation(Gtk.Adjustment adjustment, int value) {
+ if (value < (int) adjustment.get_value())
+ return AdjustmentRelation.BELOW;
+ else if (value > (int) (adjustment.get_value() + adjustment.get_page_size()))
+ return AdjustmentRelation.ABOVE;
+ else
+ return AdjustmentRelation.IN_RANGE;
+}
+
+public Gdk.Rectangle get_adjustment_page(Gtk.Adjustment hadj, Gtk.Adjustment vadj) {
+ Gdk.Rectangle rect = Gdk.Rectangle();
+ rect.x = (int) hadj.get_value();
+ rect.y = (int) vadj.get_value();
+ rect.width = (int) hadj.get_page_size();
+ rect.height = (int) vadj.get_page_size();
+
+ return rect;
+}
+
+// Verifies that only the mask bits are set in the modifier field, disregarding mouse and
+// key modifers that are not normally of concern (i.e. Num Lock, Caps Lock, etc.). Mask can be
+// one or more bits set, but should only consist of these values:
+// * Gdk.ModifierType.SHIFT_MASK
+// * Gdk.ModifierType.CONTROL_MASK
+// * Gdk.ModifierType.MOD1_MASK (Alt)
+// * Gdk.ModifierType.MOD3_MASK
+// * Gdk.ModifierType.MOD4_MASK
+// * Gdk.ModifierType.MOD5_MASK
+// * Gdk.ModifierType.SUPER_MASK
+// * Gdk.ModifierType.HYPER_MASK
+// * Gdk.ModifierType.META_MASK
+//
+// (Note: MOD2 seems to be Num Lock in GDK.)
+public bool has_only_key_modifier(Gdk.ModifierType field, Gdk.ModifierType mask) {
+ return (field
+ & (Gdk.ModifierType.SHIFT_MASK
+ | Gdk.ModifierType.CONTROL_MASK
+ | Gdk.ModifierType.MOD1_MASK
+ | Gdk.ModifierType.MOD3_MASK
+ | Gdk.ModifierType.MOD4_MASK
+ | Gdk.ModifierType.MOD5_MASK
+ | Gdk.ModifierType.SUPER_MASK
+ | Gdk.ModifierType.HYPER_MASK
+ | Gdk.ModifierType.META_MASK)) == mask;
+}
+
+public string build_dummy_ui_string(Gtk.ActionGroup[] groups) {
+ string ui_string = "<ui>";
+ foreach (Gtk.ActionGroup group in groups) {
+ foreach (Gtk.Action action in group.list_actions())
+ ui_string += "<accelerator name=\"%s\" action=\"%s\" />".printf(action.name, action.name);
+ }
+ ui_string += "</ui>";
+
+ return ui_string;
+}