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