| 1 | #!rake ;# |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | require "uri" |
|---|
| 5 | |
|---|
| 6 | class CHMWindowController < NSWindowController |
|---|
| 7 | ib_outlet :webview |
|---|
| 8 | ib_outlet :list |
|---|
| 9 | ib_outlet :tree |
|---|
| 10 | ib_outlet :drawer |
|---|
| 11 | ib_outlet :search |
|---|
| 12 | |
|---|
| 13 | def windowDidLoad |
|---|
| 14 | @chm = self.document.chm |
|---|
| 15 | uri = URI(self.document.fileURL.absoluteString) |
|---|
| 16 | browse @chm.home |
|---|
| 17 | @now = @index = @chm.index.to_a.sort_by {|k,v| k} # cache |
|---|
| 18 | @list.setDataSource(self) |
|---|
| 19 | @list.setDoubleAction("clicked_") |
|---|
| 20 | @list.setAction("clicked_") |
|---|
| 21 | |
|---|
| 22 | @tree.setAction("treeclicked_") |
|---|
| 23 | |
|---|
| 24 | @search.setDelegate(self) |
|---|
| 25 | @drawer.open |
|---|
| 26 | end |
|---|
| 27 | |
|---|
| 28 | # OutlineView |
|---|
| 29 | # * outlineView:child:ofItem: |
|---|
| 30 | # * outlineView:isItemExpandable: |
|---|
| 31 | # * outlineView:numberOfChildrenOfItem: |
|---|
| 32 | # * outlineView:objectValueForTableColumn:byItem: |
|---|
| 33 | # * outlineView:setObjectValue:forTableColumn:byItem: |
|---|
| 34 | |
|---|
| 35 | def outlineView_child_ofItem(ov, index, item) |
|---|
| 36 | (item || @topics)[:children][index] |
|---|
| 37 | end |
|---|
| 38 | |
|---|
| 39 | def outlineView_isItemExpandable(ov, item) |
|---|
| 40 | (item || @topics)[:children].length.nonzero? |
|---|
| 41 | end |
|---|
| 42 | |
|---|
| 43 | def outlineView_numberOfChildrenOfItem(ov, item) |
|---|
| 44 | (item || @topics)[:children].length |
|---|
| 45 | end |
|---|
| 46 | |
|---|
| 47 | def outlineView_objectValueForTableColumn_byItem(ov, column, item) |
|---|
| 48 | item[:name] |
|---|
| 49 | end |
|---|
| 50 | |
|---|
| 51 | def treeclicked(sender) |
|---|
| 52 | path = sender.itemAtRow(sender.selectedRow)[:local] |
|---|
| 53 | log "Tree Clicked: #{path}" |
|---|
| 54 | browse path unless path.empty? |
|---|
| 55 | end |
|---|
| 56 | |
|---|
| 57 | |
|---|
| 58 | # Tableview |
|---|
| 59 | def numberOfRowsInTableView(table) |
|---|
| 60 | @now.length |
|---|
| 61 | end |
|---|
| 62 | |
|---|
| 63 | def tableView_objectValueForTableColumn_row(table, column, row) |
|---|
| 64 | @now[row][0] |
|---|
| 65 | end |
|---|
| 66 | |
|---|
| 67 | def tableView_setObjectValue_forTableColumn_row(table, value, column, row) |
|---|
| 68 | end |
|---|
| 69 | |
|---|
| 70 | def tableView_willDisplayCell_forTableColumn_row(table, cell, column, row) |
|---|
| 71 | end |
|---|
| 72 | |
|---|
| 73 | def textShouldBeginEditing(text) |
|---|
| 74 | true |
|---|
| 75 | end |
|---|
| 76 | |
|---|
| 77 | def textShouldEndEditing(text) |
|---|
| 78 | true |
|---|
| 79 | end |
|---|
| 80 | |
|---|
| 81 | def acceptsFirstResponder |
|---|
| 82 | true |
|---|
| 83 | end |
|---|
| 84 | |
|---|
| 85 | # TabView |
|---|
| 86 | def tabView_willSelectTabViewItem(sender, item) |
|---|
| 87 | log item.label |
|---|
| 88 | if item.label == "Tree" |
|---|
| 89 | # http://subtech.g.hatena.ne.jp/cho45/20071025#c1193355031 |
|---|
| 90 | # > OutlineView は DataSource に、ノードの値が変わらない限り、 |
|---|
| 91 | # > 同じ NSString を返すように期待してるようです。 |
|---|
| 92 | # NSDictionary で保持するように |
|---|
| 93 | @topics = NSDictionary.dictionaryWithDictionary(@chm.topics) |
|---|
| 94 | @tree.setDataSource(self) |
|---|
| 95 | end |
|---|
| 96 | end |
|---|
| 97 | |
|---|
| 98 | # general |
|---|
| 99 | |
|---|
| 100 | def controlTextDidChange(anot) |
|---|
| 101 | filtering @search.stringValue |
|---|
| 102 | @list.selectRowIndexes_byExtendingSelection(NSIndexSet.alloc.initWithIndex(0), false) |
|---|
| 103 | end |
|---|
| 104 | |
|---|
| 105 | def controlTextDidEndEditing(anot) |
|---|
| 106 | log "end #{@now.first.inspect}" |
|---|
| 107 | end |
|---|
| 108 | |
|---|
| 109 | def jumpToCurrent(sender) |
|---|
| 110 | clicked(sender) |
|---|
| 111 | end |
|---|
| 112 | |
|---|
| 113 | def filtering(str) |
|---|
| 114 | str = str.to_s |
|---|
| 115 | if str =~ /[A-Z]/ |
|---|
| 116 | r = /^#{str}/ |
|---|
| 117 | else |
|---|
| 118 | r = /^#{str}/i |
|---|
| 119 | end |
|---|
| 120 | @now = @index.select {|k,v| |
|---|
| 121 | k =~ r |
|---|
| 122 | }.sort_by {|k,v| k.length } |
|---|
| 123 | |
|---|
| 124 | if @now.length.zero? |
|---|
| 125 | r = /(#{str.split(//).map {|c| Regexp.escape(c) }.join(").*?(")})/i |
|---|
| 126 | @now.concat @index.select {|k,v| |
|---|
| 127 | k =~ r |
|---|
| 128 | }.sort_by {|k,v| |
|---|
| 129 | # 文字が前のほうに集っているほど高ランクになるように |
|---|
| 130 | m = r.match(k) |
|---|
| 131 | (0...m.size).map {|i| m.begin(i) }.inject {|p,i| p + i } |
|---|
| 132 | } |
|---|
| 133 | |
|---|
| 134 | @list.usesAlternatingRowBackgroundColors = false |
|---|
| 135 | @list.backgroundColor = NSColor.objc_send( |
|---|
| 136 | :colorWithCalibratedRed, 0.95, |
|---|
| 137 | :green, 0.90, |
|---|
| 138 | :blue, 0.90, |
|---|
| 139 | :alpha, 1 |
|---|
| 140 | ) |
|---|
| 141 | else |
|---|
| 142 | @list.usesAlternatingRowBackgroundColors = true |
|---|
| 143 | end |
|---|
| 144 | |
|---|
| 145 | @list.reloadData |
|---|
| 146 | end |
|---|
| 147 | |
|---|
| 148 | def clicked(sender) |
|---|
| 149 | if @now[@list.selectedRow] |
|---|
| 150 | browse @now[@list.selectedRow][1].first |
|---|
| 151 | end |
|---|
| 152 | end |
|---|
| 153 | |
|---|
| 154 | def browse(path) |
|---|
| 155 | return unless path |
|---|
| 156 | case path |
|---|
| 157 | when /^http:/ |
|---|
| 158 | r = NSURLRequest.requestWithURL NSURL.URLWithString(path.to_s) |
|---|
| 159 | log path |
|---|
| 160 | @webview.mainFrame.loadRequest r |
|---|
| 161 | else |
|---|
| 162 | path = "/#{path}" unless path[0] == ?/ |
|---|
| 163 | h = @webview.stringByEvaluatingJavaScriptFromString("location.pathname+location.hash") |
|---|
| 164 | unless path == h |
|---|
| 165 | r = NSURLRequest.requestWithURL CHMInternalURLProtocol.url_for(@chm, path) |
|---|
| 166 | log r |
|---|
| 167 | @webview.mainFrame.loadRequest r |
|---|
| 168 | end |
|---|
| 169 | end |
|---|
| 170 | end |
|---|
| 171 | |
|---|
| 172 | def completion(sender) |
|---|
| 173 | return if @search.stringValue.empty? |
|---|
| 174 | return if @now.empty? |
|---|
| 175 | common = "" |
|---|
| 176 | keys = @now.map{|k,v| k.split(//)} |
|---|
| 177 | if @search.stringValue.to_s =~ /[A-Z]/ |
|---|
| 178 | keys[0].zip(*keys[1..-1]) do |a| |
|---|
| 179 | m = a.first |
|---|
| 180 | if a.all? {|v| m == v} |
|---|
| 181 | common << m |
|---|
| 182 | else |
|---|
| 183 | break |
|---|
| 184 | end |
|---|
| 185 | end |
|---|
| 186 | else |
|---|
| 187 | keys[0].zip(*keys[1..-1]) do |a| |
|---|
| 188 | m = a.first.downcase |
|---|
| 189 | if a.all? {|v| v && (m == v.downcase)} |
|---|
| 190 | common << m |
|---|
| 191 | else |
|---|
| 192 | break |
|---|
| 193 | end |
|---|
| 194 | end |
|---|
| 195 | end |
|---|
| 196 | if common.length > @search.stringValue.length |
|---|
| 197 | @search.stringValue = common |
|---|
| 198 | end |
|---|
| 199 | end |
|---|
| 200 | |
|---|
| 201 | # from menu |
|---|
| 202 | def searchActivate(sender) |
|---|
| 203 | log "activate" |
|---|
| 204 | if @search.window |
|---|
| 205 | @search.window.makeFirstResponder(@search) |
|---|
| 206 | end |
|---|
| 207 | end |
|---|
| 208 | |
|---|
| 209 | def nextCandidate(sender) |
|---|
| 210 | if @list.selectedRow <= @now.size |
|---|
| 211 | @list.selectRowIndexes_byExtendingSelection(NSIndexSet.alloc.initWithIndex(@list.selectedRow+1), false) |
|---|
| 212 | @list.scrollRowToVisible(@list.selectedRow) |
|---|
| 213 | clicked(nil) |
|---|
| 214 | end |
|---|
| 215 | end |
|---|
| 216 | |
|---|
| 217 | def prevCandidate(sender) |
|---|
| 218 | if @list.selectedRow > 0 |
|---|
| 219 | @list.selectRowIndexes_byExtendingSelection(NSIndexSet.alloc.initWithIndex(@list.selectedRow-1), false) |
|---|
| 220 | @list.scrollRowToVisible(@list.selectedRow) |
|---|
| 221 | clicked(nil) |
|---|
| 222 | end |
|---|
| 223 | end |
|---|
| 224 | |
|---|
| 225 | def jumpToHome(sender) |
|---|
| 226 | browse @chm.home |
|---|
| 227 | end |
|---|
| 228 | |
|---|
| 229 | def performFindPanelAction(sender) |
|---|
| 230 | log "performFindPanelAction" |
|---|
| 231 | # @webview.performFindPanelAction(sender) # なぜかうごかない |
|---|
| 232 | text = @search.stringValue |
|---|
| 233 | @webview.objc_send( |
|---|
| 234 | :searchFor, text, |
|---|
| 235 | :direction, true, |
|---|
| 236 | :caseSensitive, false, |
|---|
| 237 | :wrap, false |
|---|
| 238 | ) |
|---|
| 239 | end |
|---|
| 240 | |
|---|
| 241 | # from MySearchWindow |
|---|
| 242 | |
|---|
| 243 | def process_keybinds(e) |
|---|
| 244 | if NSInputManager.currentInputManager |
|---|
| 245 | return false unless NSInputManager.currentInputManager.markedRange.empty? |
|---|
| 246 | end |
|---|
| 247 | key = key_string(e) |
|---|
| 248 | log "keyDown (#{e.characters}:#{e.charactersIgnoringModifiers}) -> '#{key}'" |
|---|
| 249 | keybinds = { |
|---|
| 250 | "C-j" => self.method(:nextCandidate), |
|---|
| 251 | "C-n" => self.method(:nextCandidate), |
|---|
| 252 | "C-k" => self.method(:prevCandidate), |
|---|
| 253 | "C-p" => self.method(:prevCandidate), |
|---|
| 254 | "\r" => self.method(:jumpToCurrent), |
|---|
| 255 | "\t" => self.method(:completion), |
|---|
| 256 | " " => Proc.new {|s| |
|---|
| 257 | @webview.stringByEvaluatingJavaScriptFromString <<-JS |
|---|
| 258 | window.scrollBy(0, 200); |
|---|
| 259 | JS |
|---|
| 260 | }, |
|---|
| 261 | "S- " => Proc.new {|s| |
|---|
| 262 | @webview.stringByEvaluatingJavaScriptFromString <<-JS |
|---|
| 263 | window.scrollBy(0, -200); |
|---|
| 264 | JS |
|---|
| 265 | }, |
|---|
| 266 | "C-\r" => Proc.new {|s| |
|---|
| 267 | @now = @chm.search(@search.stringValue).map {|title,url| |
|---|
| 268 | [title, [url]] |
|---|
| 269 | } |
|---|
| 270 | @list.reloadData |
|---|
| 271 | }, |
|---|
| 272 | "C-u" => Proc.new {|s| |
|---|
| 273 | @search.stringValue = "" |
|---|
| 274 | }, |
|---|
| 275 | "G-[" => Proc.new {|s| |
|---|
| 276 | @webview.goBack |
|---|
| 277 | }, |
|---|
| 278 | "G-]" => Proc.new {|s| |
|---|
| 279 | @webview.goForward |
|---|
| 280 | }, |
|---|
| 281 | "G-=" => Proc.new {|s| |
|---|
| 282 | @webview.makeTextLarger(self) |
|---|
| 283 | }, |
|---|
| 284 | "G--" => Proc.new {|s| |
|---|
| 285 | @webview.makeTextSmaller(self) |
|---|
| 286 | }, |
|---|
| 287 | } |
|---|
| 288 | (1..9).each do |i| |
|---|
| 289 | keybinds["G-#{i}"] = Proc.new {|s| |
|---|
| 290 | dc = NSDocumentController.sharedDocumentController |
|---|
| 291 | if dc.documents[i-1] |
|---|
| 292 | log(dc.documents[i-1].windowControllers) |
|---|
| 293 | dc.documents[i-1].windowControllers.first.showWindow(self) |
|---|
| 294 | end |
|---|
| 295 | } |
|---|
| 296 | end |
|---|
| 297 | eval(ChemrConfig.instance.keybinds, binding) |
|---|
| 298 | if keybinds.key?(key) |
|---|
| 299 | keybinds[key].call(self) |
|---|
| 300 | true |
|---|
| 301 | else |
|---|
| 302 | false |
|---|
| 303 | end |
|---|
| 304 | end |
|---|
| 305 | |
|---|
| 306 | def key_string(e) |
|---|
| 307 | key = "" |
|---|
| 308 | m = e.modifierFlags |
|---|
| 309 | key << "S-" if m & NSShiftKeyMask > 0 |
|---|
| 310 | key << "C-" if m & NSControlKeyMask > 0 |
|---|
| 311 | key << "M-" if m & NSAlternateKeyMask > 0 |
|---|
| 312 | key << "G-" if m & NSCommandKeyMask > 0 # TODO |
|---|
| 313 | key << e.charactersIgnoringModifiers.to_s |
|---|
| 314 | key |
|---|
| 315 | end |
|---|
| 316 | |
|---|
| 317 | # webview policyDelegate |
|---|
| 318 | # def webView_decidePolicyForNavigationAction_request_frame_decisionListener( |
|---|
| 319 | # sender, |
|---|
| 320 | # actionInformation, |
|---|
| 321 | # request, |
|---|
| 322 | # frame, |
|---|
| 323 | # listener |
|---|
| 324 | # ) |
|---|
| 325 | # |
|---|
| 326 | # if CHMInternalURLProtocol.canHandleURL(request.URL) |
|---|
| 327 | # listener.use |
|---|
| 328 | # else |
|---|
| 329 | # NSWorkspace.sharedWorkspace.openURL(request.URL) |
|---|
| 330 | # listener.ignore |
|---|
| 331 | # end |
|---|
| 332 | # end |
|---|
| 333 | # |
|---|
| 334 | # def webView_decidePolicyForNewWindowAction_request_newFrameName_decisionListener( |
|---|
| 335 | # sender, |
|---|
| 336 | # actionInformation, |
|---|
| 337 | # request, |
|---|
| 338 | # frameName, |
|---|
| 339 | # listener |
|---|
| 340 | # ) |
|---|
| 341 | # if CHMInternalURLProtocol.canHandleURL(request.URL) |
|---|
| 342 | # listener.use |
|---|
| 343 | # else |
|---|
| 344 | # NSWorkspace.sharedWorkspace.openURL(request.URL) |
|---|
| 345 | # listener.ignore |
|---|
| 346 | # end |
|---|
| 347 | # end |
|---|
| 348 | |
|---|
| 349 | # webview loading delegate |
|---|
| 350 | def webView_resource_didFinishLoadingFromDataSource(sender, id, datasource) |
|---|
| 351 | # log "loaded" |
|---|
| 352 | end |
|---|
| 353 | |
|---|
| 354 | end |
|---|
| 355 | |
|---|
| 356 | class CHMDocument < NSDocument |
|---|
| 357 | attr_reader :chm |
|---|
| 358 | |
|---|
| 359 | #- (void)makeWindowControllers |
|---|
| 360 | def makeWindowControllers |
|---|
| 361 | c = CHMWindowController.alloc.initWithWindowNibName("CHMDocument") |
|---|
| 362 | self.addWindowController(c) |
|---|
| 363 | end |
|---|
| 364 | |
|---|
| 365 | #- (BOOL)readFromURL:(NSURL *)inAbsoluteURL ofType:(NSString *)inTypeName error:(NSError **)outError |
|---|
| 366 | def readFromURL_ofType_error(url, type, error) |
|---|
| 367 | path = Pathname.new(url.path.to_s) |
|---|
| 368 | if path.directory? |
|---|
| 369 | @chm = CHMBundle.new(path) |
|---|
| 370 | else |
|---|
| 371 | @chm = Chmlib::Chm.new(path.to_s) |
|---|
| 372 | end |
|---|
| 373 | true |
|---|
| 374 | end |
|---|
| 375 | |
|---|
| 376 | #- (BOOL)writeToURL:(NSURL *)inAbsoluteURL ofType:(NSString *)inTypeName error:(NSError **)outError |
|---|
| 377 | def writeToURL_ofType_error(url, type, error) |
|---|
| 378 | false |
|---|
| 379 | end |
|---|
| 380 | |
|---|
| 381 | #- (void)windowControllerDidLoadWindowNib:(NSWindowController *)windowController |
|---|
| 382 | def windowControllerDidLoadWindowNib(cont) |
|---|
| 383 | log "wCDLWN", cont |
|---|
| 384 | end |
|---|
| 385 | |
|---|
| 386 | # def dataRepresentationOfType(aType) |
|---|
| 387 | # end |
|---|
| 388 | # |
|---|
| 389 | # def loadDataRepresentation_ofType(data, aType) |
|---|
| 390 | # end |
|---|
| 391 | |
|---|
| 392 | def displayName |
|---|
| 393 | dc = NSDocumentController.sharedDocumentController |
|---|
| 394 | i = dc.documents.index(self) + 1 |
|---|
| 395 | cmd = [8984].pack("U") |
|---|
| 396 | "#{cmd}#{i}| #{@chm.title}" |
|---|
| 397 | end |
|---|
| 398 | |
|---|
| 399 | def windowControllerWillLoadNib(cont) |
|---|
| 400 | log cont |
|---|
| 401 | end |
|---|
| 402 | |
|---|
| 403 | def winwowNibName |
|---|
| 404 | "CHMDocument" |
|---|
| 405 | end |
|---|
| 406 | end |
|---|
| 407 | |
|---|
| 408 | class MySearchWindow < NSWindow |
|---|
| 409 | |
|---|
| 410 | def sendEvent(e) |
|---|
| 411 | if e.oc_type == NSKeyDown |
|---|
| 412 | return if delegate.process_keybinds(e) |
|---|
| 413 | end |
|---|
| 414 | super_sendEvent(e) |
|---|
| 415 | end |
|---|
| 416 | |
|---|
| 417 | end |
|---|
| 418 | |
|---|
| 419 | |
|---|