ページ

2009年1月31日土曜日

ファイル名変更(その2)名前変更のU/I

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

SimpleViewerでファイル名を変更する U/Iを実装する。ファイル名の変更は一番身近なのはファインダのU/Iだろう。



名前をクリックするか、選択しreturnキーすると名前が変更できる(ちなみにWindowsの場合は F2 キーか右クリック)。
名前のクリックの場合は、0.5秒ぐらいのディレイがある。

Xcodeの場合は、名前をクリックするか右クリックでコンテキストメニューから「名称変更」を選ぶ。こちらもクリックの場合は若干のディレイがある。


iTunesの場合も名前クリックで少しのディレイ後に変更可能となる。


いずれの場合もreturnキーもしくはフォーカス移動で確定、ESCキーでキャンセルとなる。

SimpleViewerでもこれに似た U/Iにしよう。とりあえず、
・画面下部にファイル名を表示
・returnキー、またはダブルクリックで編集
・returnキー、フォーカス移動で確定、ESCキーでキャンセル
として実装する。

まずは表示から。こんな感じ。

2009年1月30日金曜日

ファイル名変更(その1)ファイル作成日時でソート

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

tori さんから、キャプチャ画像のファイル名の変更ができないかとの要望が来た。確かに名前が付けられると管理しやすいので便利だ。対応することにした。

ただしファイル名変更にあたっては、SimpleViewerのファイルリストの仕組みに手を入れなければならない。現在はファイル名でソートして、それを順番に SimpleViewerで見られるようにしている。SimpleCapでは SCAP-YYMMDD-nn という決まったファイル名なので、名前のソート=日付順だった。管理するには日付順が便利なのでそうしている。ところがファイル名が自由に付けられるとなると、単純な名前のソートではダメで、ちゃんとファイルの属性情報にあるファイル作成日時を見る必要がある。

そんなこともあるので、まずは SimpleViewerの(内部的な)ファイルリスト作成部分に手をいれる。フォルダ内にあるファイルを取得し、作成日時順で並び替えるようなメソッドなり関数があれば良いのだが、見当たらなかったので作ることにした。

フォルダ内のファイル一覧は NSFileManager#enumeratorAtPath: が使える。他にも contentsOfDirectoryAtPath:error: など簡易なメソッドが用意されているのだが、これらは取得できるのがファイル名だけなので今回は役不足だった。NSFileManager#enumeratorAtPath:NSDicrectoryEnumerator が取得でき、そこから #fileAttributes メソッドでファイルの属性情報(NSDictionary)を取り出すことができる。この属性情報にファイルの作成日時 NSFileCreationDate があり、これが使える。

さて実装だが、ファイルの一覧を扱うクラス FileList と、1ファイルを扱うクラス FileEntry の2つを用意した。

こんな感じ。
FileSet.h

@class FileEntry;
@interface FileList : NSObject {
NSMutableArray* _list;
}

-(void)setPath:(NSString*)path;
- (int)count;
- (int)indexWithFilename:(NSString*)filename;
- (FileEntry*)fileEntryAtIndex:(int)index;
@end


FileEntry.h
@interface FileEntry : NSObject
{
NSString* _name;
NSDate* _created;
}
@property (retain) NSString* name;
@property (retain) NSDate* created;
- (id)initWithFilename:(NSString*)filename fileAttributes:(NSDictionary*)attrs;
@end


FileEntryはプロパティ値として、ファイル名と作成日時を持つ。FileList はこの FileEntryの集まりを配列(NSMutableArray)で保持する。

ファイルリストの生成は #setPath: で行う。
FileList.m
-(void)setPath:(NSString*)path
{
[_list removeAllObjects]; // **clear**

NSFileManager* fm = [NSFileManager defaultManager];
NSDirectoryEnumerator* dir_enum = [fm enumeratorAtPath:path];
FileEntry* entry;

NSString* filename;
NSDictionary* attrs;
while (filename = [dir_enum nextObject]) {
if ([self isTargetFilename:filename]) {
attrs = [dir_enum fileAttributes];
if ([[attrs objectForKey:NSFileType] isEqualToString:NSFileTypeRegular]) {
entry = [[[FileEntry alloc] initWithFilename:filename
fileAttributes:attrs] autorelease];
[_list addObject:entry];
}
}
}
[_list sortUsingSelector:@selector(compare:)];
}


FileManager#enumeratorAtPath: を使い、指定したフォルダ内のファイルを一つ一つ取り出して FileEntryインスタンスへ格納する。#enumeratorAtPath: には画像以外のファイルや、フォルダ内のサブフォルダも含まれる場合も想定して対象となるファイルかどうかのチェックを入れている。現状は、通常のファイルでかつ拡張子が .jpg .gif .png のみ対象としている。最後の #sortUsingSelector: でファイルの作成日時昇順のソートをかけている。ここで使う compare: は FileEntry.m で実装している。

FileEntry.m
- (NSComparisonResult)compare:(FileEntry*)entry
{
return [_created compare:entry.created]; // ASC
// return [entry.created compare:_created]; // DESC
}

元々 NSDate が compare: を実装しているので、それを呼び出すだけ。

これで指定フォルダ内のファイル一覧を作成日時順で取り出すことができた。


なおキャプチャ画像の初期ファイル名は SCAP- を取り除いて元の形に戻した。
(例)090131-0001.png

名前に頼らずフォルダ内のファイルをチェックするようになったので。

- - - -
次は、名前変更のインターフェイスだな。

2009年1月29日木曜日

キー監視(で失敗)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

プリファレンス設定を変更した時に SimpleViewerの表示へ即時反映したい。


NSUserDefaultsに対してキー監視を登録してやれば良い。

[[NSUserDefaults standardUserDefaults]
addObserver:self
forKeyPath:@"ViewerOptions.ImageBounds"
options:NSKeyValueObservingOptionNew
context:nil];


とすると、NSUserDefaultsの ViewerOptions_ImageBounds 値が変更されると下記がコールバックされる。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context


だがアプリ起動時に下記エラーを出して強制終了してしまった。
[ addObserver: forKeyPath:@"ViewerOptions.ImageBounds" options:0x1 context:0x0] was sent to an object that is not KVC-compliant for the "ViewerOptions" property.


原因は Keypath文字列に . (ドット)が含まれていたこと。ドットは Keypathの区切りとして使われる特別な文字な為、別の解釈をされてしまった。 . をやめれば問題ない。

[[NSUserDefaults standardUserDefaults]
addObserver:self
forKeyPath:@"ViewerOptions_ImageBounds"  アンダースコア _ にしてみた。
options:NSKeyValueObservingOptionNew
context:nil];


- - - -
不注意にも NSUserDefaultsのキーのほとんどで . を使っていたので結局全部 _ に書き換えた。やれやれ。。

2009年1月28日水曜日

SimpleViewer - 画像の境界線を描く

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

SimpleViewerで画像の境界線を点線で描く様にした。

(上記イメージだと分かりづらいが、クリックして拡大すると点線が見える)

境界線の有無はプリファレンスで指定できる。

2009年1月27日火曜日

アプリケーションを開くを改造する(4)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

アプリケーションメニューに Mail.app を追加した。

SimpleViewerでメニューを開き、Mailを選択すると、


新規メール作成ウィンドウが開かれ、画像が添付される。

2009年1月26日月曜日

アプリケーションを開くを改造する(3)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

さて下準備ができたのでアプリ起動部分の実装に入ろう。
SimpleViewerにアプリ起動用のアイコンを復活させ、ここをクリックした時にアプリケーションメニューを表示する。メニューにはプリファレンスで設定したもの以外に、画像を開けるアプリケーションの一覧も載せておく。

で、こんな感じ。


アイコンをクリックすると最初に、プリファレンスで設定したアプリの一覧とシステムが推奨するデフォルトアプリ(通常はプレビュー)を表示する。

さらに「その他...」を選ぶと画像を開く事ができる他のアプリの一覧を表示する。



デフォルトアプリや画像が開けるアプリの取得は以前検証したことがある。
アプリケーションを開く(4)アプリの一覧表示
アプリケーションを開く(5)デフォルトアプリ表示


今回はこの時作ったクラスを少し改造するだけで簡単にメニューを用意することができた。

2009年1月25日日曜日

アプリケーションを開くを改造する(2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

続いてアプリケーションのアイコンを表示する。

まずは Interface Builder を使って名称の左側に NSImageView を配置する。


ここへアプリケーションのアイコン画像を当てはめるのだが Cocoa Bindings を使おう。いくつかあるうちの Value を使う。


"Bind to:" でユーザデフォルトへ接続し、"Model Key Path" にはユーザデフォルトのキー(例では Application5)を指定する。ただしユーザデフォルトの値はアプリケーションプログラムのパスなので、このままではアイコン画像は表示できない。そこで Value Transformer を使い、アプリケーションパスをアイコン画像(NSImage)に変換してやる。

Value Transformer は以前検証したことがある。下記を参照の事。
Cocoaの日々 - NSValueTransformer

定義したクラスは次の通り。
ApplicationTransformer.h

@interface ApplicationIconTransformer : NSValueTransformer {
}@end


ApplicationTransformer.m
@implementation ApplicationIconTransformer

+ (Class)transformedValueClass
{
return [NSImage class];
}

- (id)transformedValue:(id)value
{
if (!value) {
return nil;
}
return [[NSWorkspace sharedWorkspace] iconForFile:value];
}
@end


NSWorkspace#iconForFile: を使い、アプリケーションパスからアイコン画像(NSImage)を取り出し、これを変換後の値として返している。たったこれだけでモデルとビューの接続が完了する。

さて実行してみよう。


いい感じだ。
コードらしいコードを書いていないのだが、アプリを選択すると即時にアイコン画像が反映される。
あいかわらず Cocoa Bindingsは楽だ。

2009年1月24日土曜日

アプリケーションを開くを改造する(1)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

アプリケーションを開く機能を改造する。SimpleViewerが導入されてから位置づけが変わったこともあるが、今後の拡張性を考えて少し手を入れる。

従来はプリファレンスで一つ選択しておき、キャプチャ後に実行するというものだった。実行できるのは画像が開けるアプリだけでプルダウンから選べる様になっていた。


これを最大5個までのアプリケーションを登録し、これをキャプチャ直後に自動的に実行するのではなく SimpleViewerからアプリを選んで実行するように改造する。

まずはプリファレンスパネルから取りかかる。5つのアプリケーションを自由に選べるようにした。


Cocoa Bindings を使い Shared User Default Controller とバインドするとユーザデフォルトの値をコーディング無しで表示できる。


"Null Placeholder" の値を設定しておくと、バインド先の変数が nil の時に表示してくれる。

ただパスのままだと分かりづらいのでアプリケーション名が表示されるように工夫する。
バインディングした値を表示用に加工するには NSFormatter が使える。
(参考)NSFormatter ※過去の検証ブログ

まず NSFormatter から派生したカスタムフォーマッタクラスを作る。
ApplicationNameFormatter.h

@interface ApplicationNameFormatter : NSFormatter {
}


ApplicationNameFormatter.m
@implementation ApplicationNameFormatter
- (NSString *)stringForObjectValue:(id)anObject
{
NSString* name;
LSCopyDisplayNameForURL((CFURLRef)[NSURL fileURLWithPath:anObject], (CFStringRef *)&name);

if (!name) {
name = @"(not found)";
}
return name;
}
- (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error
{
return NO;
}
@end


渡されたパスを元に LSCopyDisplayNameForURL( ) でアプリ名を取得して戻す。

クラスを定義したら InterfaceBuilderでインスタンスとして登録し、目的のラベルの fomatter に接続してやる。



実行するとこうなる。



登録は「Choose...」が押されたらダイアログを開きアプリケーションを選択させ、その結果をユーザでフォルトへ保存する。

- - - -
ゆくゆくはブログへのアップローダを用意してキャプチャ後にそれをキックしてアップできると便利だと思う。アプリを登録してキックする簡易的な仕組みを用意しておけば、ユーザ自身が工夫次第で色々できると思う。

2009年1月23日金曜日

デフォルトの保存フォルダ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

デフォルトの保存フォルダを今までは「デスクトップ」にしていたがこれをやめる。

「SimpleCap Images」というフォルダをデフォルトにする。このフォルダはデスクトップに自動的に作成される。

2009年1月22日木曜日

プリファレンス - 内容によってウィンドウのサイズを変える

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

さて前回までの検証結果を SimpleCapへ組み込んでみよう。元々変数名なども同じにしていたのでコピー&ペーストだけであっけなく動いてしまった。以下、実行結果。

InterfaceBuilderではこんな感じになっている。


実行してプリファレンスを開くとウィンドウサイズがちょうどいい大きさになっている。


「選択範囲」を選ぶとアニメーションしながらウィンドウサイズが大きくなる。


「上級者向け」を選ぶと今度はアニメーションしながらサイズがちいさくなる。



メインの機能とは関係ないのでどーでもいいところとも言えるが、実装ができてちょっと気分がいい。

2009年1月21日水曜日

内容によってウィンドウのサイズを変える (2) NSViewAnimation

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

タブ切り替えの場合、新しいタブの内容が表示された状態でウィンドウのリサイズアニメーションが行われる。これは実際に動きを見てもらうとわかるのだが、大きいサイズから小さいサイズへアニメーションする時は下に余白が出て、逆の場合はアニメーションの過程で見えなかった領域のボタンなどのコントロールが現れてきて、あまり見た目は良くない。

Mail.app の場合はリサイズアニメーションが開始されたらタブの内容を一旦消して、アニメーションが完了した時点で内容を表示するようにしている。なるほどちゃんと作り込んである。こういう視覚効果にするにはどうしたら良いか?

NSWindowに delegateメソッド windowDidResize: が用意されているが、リサイズアニメーションの過程で何度か呼出されてしまうため、リサイズの完了を知ることができない。通常はリサイズ完了を知らなくても問題ないので手軽に使える NSWindow#setFrame:display:animate: で十分なのだが、今回の用途ではちょっと役不足のようだ。

さてどうするか。そういえば昨年フェードアウトの検証で NSViewAnimation を使ったが、あれがリサイズアニメーションをサポートしていた。このクラスはアニメーションの開始・終了の通知を delegateで受け取ることができる。

(参考)Windowアニメーション(その2)

これを使って前回のサンプルを改造してみよう。

AppController.m

- (void)update
{
 :
// 前回の実装
// [_window setFrame:window_frame display:YES animate:YES];

// NSViewAnimationを使った実装
NSMutableDictionary* dict = [NSMutableDictionary dictionary];
[dict setObject:_window forKey:NSViewAnimationTargetKey];
[dict setObject:[NSValue valueWithRect:[_window frame]]
forKey:NSViewAnimationStartFrameKey];
[dict setObject:[NSValue valueWithRect:window_frame]
forKey:NSViewAnimationEndFrameKey];
NSViewAnimation* anim = [[NSViewAnimation alloc]
initWithViewAnimations:[NSArray arrayWithObject:dict]];
[anim setDuration:0.25];
[anim setDelegate:self];
[anim startAnimation];
[anim release];
}

コード量は増えたがそれでも簡単にアニメーションを実行できる。NSMutableDictionaryに開始範囲(NSViewAnimationStartFrameKey)と終了範囲(NSViewAnimationEndFrameKey)を指定し、スタートさせるだけ。delegateで開始と終了通知を受け取れる。

- (BOOL)animationShouldStart:(NSAnimation *)animation
{
[[[_tab_view selectedTabViewItem] view] setHidden:YES];
return YES;
}
- (void)animationDidEnd:(NSAnimation *)animation
{
[[[_tab_view selectedTabViewItem] view] setHidden:NO];
}

開始と終了で新しく選択されたタブのビューの表示制御を行ってやる。


さて実行してみよう。


タブを切り替えるとウィンドウのリサイズアニメーションが始まる。この時、選択したタブの内容(ボタンB)は表示されない。


アニメーションが完了するとタブの内容が表示される。




検証ができたので次は SimpleCapのプリファレンスへ組み込む。

サンプル:ElasticWindow-2.zip

2009年1月20日火曜日

内容によってウィンドウのサイズを変える

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

SimpleCapのプリファレンスは NSTabViewを使っていて、ツールバーによって表示内容を切り替えている。


ただウィンドウのサイズを固定にしているので、表示内容が少ない時は間抜けな状態になる。



Mail.app などでは表示内容の大きさに合わせて、ウィンドウの大きさ(高さ)を調節している。




これを SimpleCapのプリファレンスでも実現したい。まずはサンプルを作って試してみよう。
まずは動いたところから。

2つのタブがあり、初期状態はボタン「A」が表示される。ここで2番目のタブを選択してみる。


するとボタン「B」が表示され、ウィンドウはそれにあった大きさに縮む。


考え方は次の通り、「A」表示から「B」表示へ移る直前は次のような状態になっている。


座標系は左下が原点なのでウィンドウのY座標は Wy となっている。「B」ボタンのビュー内の位置を (Vx, Vy)とする。ビュー内も原点は左下となる。一方、ウィンドウの大きさが伸縮する場合左上が固定点になるので、このY座標を origin_y とする。これはウィンドウのY座標 Wy と高さから求めることができる。高さはInterfaceBuilderで指定した初期値(すなわち最大の高さ)をとっておきこれ (_window_size)を使う。

さてこれをどう縮ませるか。縮んだ時の状態が次の図。


ウィンドウの高さは、ボタン「B」のY座標の Vy 分だけ縮む。これにマージン(MARGIN_BOTTOM)を考慮すると縮む高さ diff_y が求められる。これを初期高さ _window_size.height から引けば、縮んだ後の最終的な高さがも止められる。

一方、ウィンドウは左下が原点の座標系を使っているので、左上固定で縮む場合、ウィンドウのY座標も変更する必要がある。新しいウィンドウのY座標 Wy' は origin_y を求めておけば、ここから縮んだ後の高さ(_window_size.height - diff_y)を引けば求められる。


これで縮むウィンドウができた。

なお NSWindowのサイズ指定にはいくつかメソッドがあるが、setFrame:display:animate: を使うとサイズの変更がアニメーションで表示される。


最後にソースコード解説。
ElasticWindow.h

@interface AppController : NSObject {

IBOutlet NSTabView* _tab_view;
IBOutlet NSWindow* _window;

NSSize _window_size;
}
@end

IBのアウトレットでタブとウィンドウをつかんでいる。_window_sizeは初期のウィンドウサイズ。


ElasticWindow.m
@implementation AppController

#define MARGIN_BOTTOM 10

- (void)update
{
NSTabViewItem *item = [_tab_view selectedTabViewItem];
NSArray* subviews = [[item view] subviews];

NSRect view_frame = NSZeroRect;

for (NSView* view in subviews) {
view_frame = NSUnionRect(view_frame, [view frame]);
}

NSRect window_frame = [_window frame];

CGFloat diff_y = view_frame.origin.y - MARGIN_BOTTOM;

CGFloat origin_y = window_frame.origin.y + window_frame.size.height;

window_frame.size.height = _window_size.height - diff_y;
window_frame.origin.y = origin_y - window_frame.size.height;

[_window setFrame:window_frame display:YES animate:YES];
}

- (void)awakeFromNib
{
_window_size = [_window frame].size;
[self update];
}


- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem
{
[self update];
}

@end

updateメソッドが今回のキモ。上記で解説したアルゴリズムが実装されている。ビュー内のコントロール位置はサブビューを subviewsで取り出して NSUnionRectで最大領域を求め使っている。


サンプル:ElasticWindow-1.zip

今回は長かった。。

2009年1月19日月曜日

チェックボードをSimpleViewerへ組み込む (2) プリファレンスで切り替える

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

プリファレンスで背景を選べる様にした。


デフォルトは黒の半透明。


チェックボード。


白。

2009年1月18日日曜日

チェックボードをSimpleViewerへ組み込む

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

チェックボードをSimpleViewerへ組み込んでみた。コードは前回のものをそのまま流用した。

雰囲気は悪くない。チェックだと画像の明暗にかかわらず境界がわかるのがいい。


元々は黒の半透明。


試しに白背景を作ってみた。影ついている場合は実用的。


- - - -
どれも状況によっては使えるので、ボタンで切り替えられるようにしよう。
それはまた次回。

2009年1月17日土曜日

チェックボードを描く

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

チェックボードとは白と黒の四角を並べた模様。CoreImage にフィルタが用意されていて簡単に描ける。
http://www.blogger.com/img/blank.gif
サンプルソース:Checkboard-1.zip

実行結果:

http://www.blogger.com/img/blank.gif

フィルタはCICheckerboardGeneratorを使う。描画コードはこんな感じ。

- (void)drawRect:(NSRect)rect {

CIFilter *filter = [CIFilter filterWithName:@"CICheckerboardGenerator"];
[filter setDefaults];

[filter setValue:[NSNumber numberWithInt:10] forKey:@"inputWidth"];
[filter setValue:[CIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.5]
forKey:@"inputColor0"];
[filter setValue:[CIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.5]
forKey:@"inputColor1"];

CIImage *ciimage = [filter valueForKey:kCIOutputImageKey];

CIContext *context = [[NSGraphicsContext currentContext] CIContext];

[context drawImage:ciimage
atPoint:CGPointZero
fromRect:NSRectToCGRect([self bounds])];
}


とても簡単にチェック模様を描ける。


情報:

Using Core Image Filters
 Core Image フィルタのドキュメント

CICheckerboardGenerator
 CICheckboardGeneratorのリファレンス

Goofy behavior with CICheckerboardGenerator filter and transparency
 CICheckboardGeneratorを描画するサンプルが載っている。

- - - -
この模様を SimpleViewerの背景で使う。どんな感じになるだろうか。

2009年1月16日金曜日

COPYボタンの整理

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

キャプチャモードで表示していた COPYボタンをなくすことにした。

以前はこうなっていた。


取り外した状態。



理由は、あまり使っていないのと SimpleViewerにコピーボタンを実装した為。


あっても便利だが頻繁には使わない。使いたい場合は SimpleViewerまで表示すればコピーできるので機能を使ってもらおう。できるだけシンプルにして迷わないユーザインターフェイスにしたいこともあるので取り除く事にする。

ただ色のバランスが崩れたのでこれも見直す。タイマーボタンは色を変えデザインも少しだけ直した。


タイマーウィンドウのボタンの色、並びも変えた。

2009年1月15日木曜日

SimpleViewer - スライドトランジションを組み込む

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

スライドトランジションを SimpleViewerへ組み込む。現在の実装では NSViewのサブクラスである SimpleViewerImageView がキャプチャ画像の描画を行っている。


トランジションでは2つのビューの切り替えになる為、画像を描画するビューのインスタンスが2個必要になる。またこの2つを管理しなければならない。そこで新たに描画を担当するビュー SimpleViewerImageSubView を用意し、今までのビュー SimpleViewerImageView にはこの2つのインスタンスの管理だけを行わせることにする。


トランジション処理は SimpleViewerImageView に責務を負わせ、コントローラ向けのプログラムインターフェイスは変えない。これによってコントローラ側のコードの修正はいらなくなる。

さて動かしてみよう。






おおいい感じだ。トランジションにかかる時間は 0.25秒。いろいろ試したがこれぐらいが良さそうだ。