root/lang/ruby/Chemr/CHMDocument.rb

Revision 13618, 11.0 kB (checked in by cho45, 6 months ago)

Made faster keyword search.
Fixed crush bug on full-text search.

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