Technology Musings

May 29, 2013

Snippets / Adding a "Copy" context menu to a disabled UITextfield in iOS

JB

I have a project which has a lot of fields which, at any moment, may be enabled or disabled.  However, even when they are disabled, I want to show a context menu.  It turns out that no one has a solution, so I hacked one together for myself.  This code is adapted from a working project, but I have not tested the version presented here, but let me know if you have problems.  

The general problem you run into is this: if your UITextField is disabled, then it cannot receive touch events.  Therefore, it cannot fire the menu, even if you try to do so explicitly using Gesture recognizers.  The normal answer is to simply not allow any editing on the field.  However, while this prevents you from editing the field, it still *draws* the textfield as active.  We want it to *look* inactive as well, which you only get by disabling it.

So, what I did was to create a subclass of UITextField (JBTextField) which has a subview (a specialized subclass of UIView - JBTextFieldResponderView) that has a gesture recognizer attached.  We override hitTest on JBTextField to do this: if JBTextField is disabled, then we delegate hitTest to our JBTextFieldResponderView subclass.

As I said, this is from another project, which had a whole bunch of other stuff mixed in.  I have tried to separate out the code for you here, but I don't guarantee that it will work out-of-the-box.

@interface JBTextFieldResponderView : UIView
@property (strong, nonatomic) UIGestureRecognizer *contextGestureRecognizer;
@end

@interface JBTextField : UITextField
@property (strong, nonatomic) JBTextFieldResponderView *contextGestureRecognizerViewForDisabled;
@end
@implementation JBTextFieldResponderView
@synthesize contextGestureRecognizer = _contextGestureRecognizer;

-(id) initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];

  // Add the gesture recognizer
  self.contextGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)];
  [self addGestureRecognizer:self.contextGestureRecognizer];

  return self;
}

// First-level of response, filters out some noise
-(IBAction) longPressGestureDidFire:(id)sender {
  UILongPressGestureRecognizer *recognizer = sender;
  if(recognizer.state == UIGestureRecognizerStateBegan) { // Only fire once
    [self initiateContextMenu:sender];
  }
}

// Second level of response - actually trigger the menu
-(IBAction) initiateContextMenu:(id)sender {
  [self becomeFirstResponder]; // So the menu will be active.  We can't set the Text field to be first responder -- doesn't work if it is disabled
  UIMenuController *menu = [UIMenuController sharedMenuController];
  [menu setTarget:self.bounds inView:self];
  [menu setMenuVisible:YES animated:YES];
}

// The menu will automatically add a "Copy" command if it sees a "copy:" method.  
// See UIResponderStandardEditActions to see what other commands we can add through methods.
-(IBAction) copy:(id)sender {
  UIPasteboard *pb = [UIPasteboard generalPasteboard];
  UITextField *tf = (UITextField *)self.superview;
  [pb setString: tf.text];
}

// Normally a UIView doesn't want to become a first responder.  This forces the issue.
-(BOOL) canBecomeFirstResponder {
  return YES;
}

@end

@implementation JBTextField
@synthesize contextGestureRecognizerViewForDisabled = _contextGestureRecognizerViewForDisabled;

/* Add the recognizer view no matter which path this is initialized through */

-(id) initWithCoder:(NSCoder *)aDecoder {
  self = [super initWithCoder:aDecoder];
  [self addDisabledRecognizer];
  return self;
}

-(id) initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  [self addDisabledRecognizer];
  return self;
}

// This creates the view and adds it at the end of the view chain so it doesn't interfere, but is still present */
-(void) addDisabledRecognizer {
  self.contextGestureViewForDisabled = [[JBTextFieldResponderView alloc] initWithFrame:self.bounds];
  self.contextGestureViewForDisabled.userInteractionEnabled = NO;
  [self addSubview:self.contextGestureViewForDisabled];
}

-(void) setEnabled:(BOOL) enabled {
  [super setEnabled:enabled];
  self.contextGestureViewForDisabled.userInteractionEnabled = !enabled;
}

// This is where the magic happens
-(UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  if(self.enabled) {
    // If we are enabled, respond normally
    return [super hitTest:point withEvent:event];
  } else {
    // If we are disabled, let our specialized view determine how to respond
    UIView *v = [self.contextGestureViewForDisabled hitTest:point withEvent:event];
  }
}

@end

Now, in interface builder, we can enabled copy on disabled fields just by setting the class to JBTextField, or, if built programmatically, by replacing [UITextField alloc] with [JBTextField alloc].

I hope this helps!

Additional Resources:

 

 

 

August 05, 2012

Snippets / Simple Sage Plots for Teaching Functions

JB

I'm teaching calculus to some homeschool students this year, and I wanted to have a bunch of function plots printed out to show them.  Therefore, I turned to Sage, which I have never used.  It is great!  You have to know a few other math software pieces to really understand it, but it's not that bad.  Someone needs to do a general "how to use sage", and some day I might.

Anyway, here's my quick function for plotting graphs for students that some of you might like.

In one of the evaluation boxes, type the following:

def simpleplot(f, range=(-5,5,-5,5)):
p = plot(f, (x, range[0], range[1]))
p += text("$" + latex(f) + "$", (0, range[2] - (range[3] - range[2]) * 0.1), fontsize=30, rgbcolor=(1,0,0))
p.axes_range(range[0], range[1], range[2], range[3])
show(p, figsize=12)

What this does is create a function called "simpleplot" which will do the following:

  • Plot the given function on a default domain of -5, 5
  • Typeset the function using LaTeX, and insert the function slightly below the graph (I calculated it at 10% of the graph size).  It prints in red at 30 point size.
  • Set the axes to be -5, 5, -5, 5 (I used uniform axes on all the functions so they can compare the differences between graphs easier)
  • Draw the whole thing at 3x the normal size (figsize=12 - default is 4).

Now, I can just do:

simpleplot(1/x)

And it gives me my output!

July 23, 2012

Snippets / DVD Rescue! Getting files off of an unfinalized DVD

JB

Just wanted to shout out and say thanks to Flay for saving me!  I had a DVD that I forgot to finalize, but Flay had a nice set of instructions for how to manually copy off the files from the DVD drive.

Now I can finish editing and posting the videos for the Engineering and Metaphysics conference.

June 26, 2011

Snippets / Misc stuff I found out

JB

Objective-C PostgreSQL stuff:


Misc:
Building a shared library on OSX: (see here):
gcc -dynamiclib -install_name /usr/local/lib/libfoo.2.dylib  -compatibility_version 2.4 -current_version 2.4.5 -o libfoo.2.4.5.dylib source.o code.o

I have also seen the options -undefined suppress -flat_namespace here

Another dylib link

Caution mixing shared and archive libs

Seth Godin - how to run a useless conference

 

June 10, 2010

Snippets / Cross-browser DIV Rotation by arbitrary degrees in CSS and/or javascript

JB

I've spent most of the day on this, so I thought I would share.  Most people don't know this, but CSS3 has some *really* cool stuff coming - auto-rounded corners, gradients, and.... DIV rotations!  Unfortunately, it will be about 5 years before any of this becomes mainstream.  IE8 only has the very beginnings of CSS3 support.

So is there any way to get this on current browsers?  Yes!  The fine folks at CSS3Please.com have given us a way to do some of these cool effects using existing CSS.  If you just want, just go their, edit the stylesheet on the web page (yes! it is editable!), and copy/paste the resulting CSS into your CSS file.

But what if you want dynamic rotation?  Well, I looked at the CSS3Please source code, and figured out how it was all being calculated.  The IE6, IE7, and IE8 part is the worst, because it relies on a DirectX Matrix transform operation (isn't Internet Explorer always fun to work with?).  Not superfun, but do-able.  However, to get this calculated, we will need a Matrix library.  So, first, download and install the sylvester javascript library.  Then, put this javascript into your page:

<script src="sylvester.js"></script>
<script type="text/javascript">
function degreesToRadians(num) {
	return (num) * Math.PI / 180;
}
function createIEMatrixString(M) {
	return 'M11=' + M.e(1, 1) + ', M12=' + M.e(1,2) + ', M21=' + M.e(2,1) + ', M22=' + M.e(2,2);
}
function rotateElement(e, deg) {
	deg_str = deg + "";
	rotate_transform = "rotate(" + deg + "deg)";
	matrix_str = createIEMatrixString(Matrix.Rotation(degreesToRadians(deg)));
	filter_str = "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', " + matrix_str + ")";

	e.style["rotation"] = deg_str + "deg"; // CSS3
	e.style.MozTransform = rotate_transform; // Moz
	e.style.OTransform = rotate_transform; // Opera
	e.style.WebkitTransform = rotate_transform; // Webkit/Safari/Chrome
	e.style.filter = filter_str; // IE 6/7
	e.style.MsFilter = filter_str; // IE 8
	e.style["zoom"] = "1"; // ??? Probably IEs
}
</script>

Now, you can just do:

rotateElement(document.getElementById("whatever"), 20);

And that will rotate the element.  Note that this *sets* the rotation, so if I do this several times it will only keep the rotation at what I set it to, it won't keep on adding rotations.

Anyway, a little bit of work, a little bit of code for your client to download, but it works.

UPDATE - just found this jquery plugin that *might* do something similar, but haven't looked at it closely yet.

May 26, 2010

Snippets / A Simple HTML Geolocation Map with Twitter and Google Maps

JB

New Medio just released <a href="http://www.newmedio.com/site/postings/17?section_id=2">a cool new tool to do geolocation/geotagging maps of Twitter posts</a>.  Can be used HTML-only (no Javascript coding required!) or with a Rails plugin. 

June 26, 2009

Snippets / What's Taking So Long?

JB

There are a lot of performance analysis tools for Rails logs.  However, sometimes you just need something quick and dirty.  This one helped me out.  1-line perl script to show all Rails log lines which took more that 100milliseconds to produce:

perl -n -e 'print if(m/\((\d+\.\d+)ms\)/ && $1 > 100);' log/development.log

UPDATE - Here's another handy one:

 tail -20000 log/production.log|grep 'Completed in'|cut -d" " -f3,11|sort -n

December 18, 2008

Snippets / Docx and Open Packaging Conventions for Ruby and Rails

JB

I just finished the first iteration of my Ruby on Rails plugin for handling Microsoft's new Open Packaging Conventions file format, which is their new container format for XML-based file formats in Microsoft Office.  DOCX, Microsoft's new XML-based format for Word is probably the most widely used of these. 

Open Packaging Conventions is basically a zipfile containing one or more components, and those components' relationship to each other is defined by other files within the zipfile.

To add rubyopc to your Rails app, do:

script/plugin install http://rubyopc.googlecode.com/svn/trunk/rubyopc

Here is how you would make a DOCX file with it:

OpenPackagingConventions::Package.with_package("test.docx") do |p|
p.add_part("/word/document.xml", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", <<EOF)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
<w:body>
<w:p w:rsidR="00EA68DC" w:rsidRPr="00C703AC" w:rsidRDefault="00EA68DC" w:rsidP="00EA68DC">
<w:pPr>
<w:rPr>
<w:lang w:val="es-ES_tradnl"/>
</w:rPr>
</w:pPr>
<w:r>
<w:t>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc at risus vel erat tempus posuere. Aenean non ante. Suspendisse vehicula dolor sit amet odio. Sed at sem. Nunc fringilla. Etiam ut diam. Nunc diam neque, adipiscing sed, ultrices a, pulvinar vitae, mauris. Suspendisse at elit vitae quam volutpat dapibus. Phasellus consequat magna in tellus. Mauris mauris dolor, dapibus sed, commodo et, pharetra eget, diam.
</w:t>
</w:r>
<w:r w:rsidRPr="00C703AC">
<w:rPr>
<w:lang w:val="es-ES_tradnl"/>
</w:rPr>
<w:t>
 Nullam consequat lacus vitae mi. Sed tortor risus, posuere sed, condimentum pellentesque, pharetra eu, nisl.
</w:t>
 </w:r>
</w:p>
</w:body>
</w:document>
EOF

p.add_part_to("/word/document.xml", "/word/styles.xml", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", <<EOF)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Normal">
<w:name w:val="Normal" />
<w:rPr>
<w:b />
</w:rPr>
 </w:style>
</w:styles>
EOF

This will create a small word document called "test.docx".

You can find the official documentation for these file formats here.

 

August 25, 2008

Snippets / Rails Sorting

JB

I always have trouble formulating exactly how sorts should work on Rails controllers that are called from sortable_element.  Therefore, I created the following method for ApplicationController which helps out a lot:

  def sort_assoc_members(assoc, ary, position_method = :position)
ary.each_index do |ary_idx|
assoc.find(ary[ary_idx]).update_attributes(position_method => (ary_idx + 1))
end    
end

Then, to use it, just create a sort action like this:

def reorder
sort_assoc_members(WhateverItem, params[:whatever_list])
end

This makes life much easier.

August 13, 2008

Snippets / TinyMCE + Prototype AJAX + RAILS

JB

NOTE - I have no idea why the formatting here is all messed up. It should be usable anyway since the snippets are fairly small

Using TinyMCE from Ruby on Rails is sometimes a bit tricky.  We tend to assume that all HTML components work the same - they can be added and removed at will.  However, TinyMCE components have to be explicitly added and removed - and as such they can be really painful.

In addition, TinyMCE components have to be saved back to the form in order to be serialized by Prototype (which is used by Rails for remote forms - remote_form_for, link_to_remote, etc.).

Therefore, I came up with the following, which doesn't completely remove the pain, but it at least eases it somewhat.  First of all, it modifies the Prototype library slightly to trigger a tinyMCE.triggerSave(true, true) every time a form is serialized (i.e. for remote forms and links).  Now you don't have to remember to do that anymore! Next, it provides a simple RJS helper to unregister/reregister TinyMCE editors when doing Ajax page manipulation in Rails.

Anyway, here are the steps:

Step 1

Put the following code into application.js:

//Register all TinyMCE instances on the page
function register_all_tiny_mce_editors() {
tinyMCE.init({
mode : "textareas",
theme : "advanced",
editor_selector : "editor"
});    
//NOTE - if you have other registration functions, put them here, too.
}
//Unregister all TinyMCE instances on the page
function unregister_all_tiny_mce_editors() {
tinyMCE.triggerSave(true, true);
for(editor_id in tinyMCE.editors) {
editor_elem = $(editor_id);
if(editor_elem) {
editor = tinyMCE.editors[editor_id];
tinyMCE.remove(editor)
}
}
} 
//Convince Prototype to do a TriggerSave anytime it is doing form serialization
var original_form_serialize_elements = Form.serializeElements;
Form.serializeElements = function(elements, gethash) {
try {
//Copy TinyMCE values back onto the regular form elements
tinyMCE.triggerSave(true, true);                
} catch(err) {
//Don't do anything - just means TinyMCE isn't loaded
}
return original_form_serialize_elements(elements, gethash);
}

Step 2

Put the following code into your layout:

<%# Modify this to wherever you put TinyMCE %>
<%= javascript_include_tag "tiny_mce/tiny_mce.js" %>
<script type="text/javascript">
tinyMCE.init({
mode : "textareas",
theme : "advanced",
editor_selector : "editor"
});
<%# Add/modify the init statement to include any other options you want  %>
<%# Be sure to use the same code as you used above in the register_all_tiny_mce_editors() function %>
</script>
<%# PUT ALL OTHER JAVASCRIPT INCLUDES ___AFTER___ THIS ONE OR ELSE TINYMCE COULD BREAK!! %>

Step 3

Now, put the following in application_helper.rb:

  def tmce_update(page, &block)
page << "unregister_all_tiny_mce_editors();"
yield
page << "register_all_tiny_mce_editors();"
end

Step 4

Now, in all RJS files which could cause TinyMCE textareas to appear/disappear, you can do the following:

tmce_update(page) do
#Perform RJS operations which affect TinyMCE areas here 
end

And now all TinyMCE areas will start behaving for you!  Note that this ONLY works with RJS-based Ajax calls - it doesn't work if you use the :update parameter, but you shouldn't be using that anyway.  Always do Ajax updates with RJS!