PyObjC」タグアーカイブ

NSWindow file drop and etc

NSWindow へのファイルドロップを検索する。
NSFilenamesPboardType 指定ばかりだけどもう Deprecated だ。
てか、JXA 版から変換コピペしたらエラーになった。

Dragging Files

NSURLPboardType を現行は指定するようだ。

def windowDidExitFullScreen_(self, sender):
    self.toolbar.setVisible_(True)
    self.is_fullscreen = False

def wantsPeriodicDraggingUpdates_(self, sender):
    return True

def performDragOperation_(self, sender):
    pb = sender.draggingPasteboard()
    if pb.types().containsObject_(NSURLPboardType):
        url = NSURL.URLFromPasteboard_(pb)
        self.set_path(url.path())
    return True

def draggingEntered_(self, sender):
    return NSDragOperationCopy

です。

そんなことより。
今日はクソ寒い中バルキアのレイドに二回挑戦してどっちも逃げられてしまった!
いやそれは本当にどうでもよくて。

前回 NSPDFImageRep でいくとか書いたけど。
この方法では左右ページが同じページになってしまうと後で気が付いた。
同じ NSPDFImageRep を左ページ用にセットした後で描写するのだから当然だった。

CGAffineTransformMake という Matrix を自分で作成する関数を見つけた。
結局 Matrix で描写するはめに、勉強しておいてよかった。

def drawRect_(self, rect):
    NSColor.blackColor().set()
    NSRectFill(rect)
    NSColor.whiteColor().set()
    if self.first_page != None:
        if self.is_pdf:
            ctx = NSGraphicsContext.currentContext().CGContext()
            r = CGPDFPageGetBoxRect(self.first_page, kCGPDFMediaBox)
            if r.size.width - r.size.height > 0 or not self.spread:
                CGContextSaveGState(ctx)
                if rect.size.width * r.size.height > rect.size.height * r.size.width:
                    n = rect.size.height / r.size.height
                    matrix = CGAffineTransformMake(n, 0, 0, n, rect.size.width / 2 - r.size.width * n / 2, 0)
                else:
                    n = rect.size.width / r.size.width
                    matrix = CGAffineTransformMake(n, 0, 0, n, n, rect.size.height / 2 - r.size.height / 2)
                CGContextConcatCTM(ctx, matrix)
                NSColor.whiteColor().set()
                CGContextFillRect(ctx, r)
                CGContextClipToRect(ctx, r)
                CGContextDrawPDFPage(ctx, self.first_page)
                CGContextRestoreGState(ctx)

見開きは省略。

NSUserDefaults でウインドウサイズを記憶させるのだが。
Toolbar をヘッダーバーにしているせいか縦位置と高さがズレる。
plist を見ながら補正値を入れた、こんなんでいいのだろうか。

def windowWillClose_(self, sender):
    defaults = NSUserDefaults.alloc().initWithSuiteName_('com.sasakima.comipoli')
    defaults.setFloat_forKey_(self.contentView().frame().size.width, 'width')
    defaults.setFloat_forKey_(self.contentView().frame().size.height - 22, 'height')
    defaults.setFloat_forKey_(self.frame().origin.x, 'x')
    defaults.setFloat_forKey_(self.frame().origin.y + 38, 'y')
    defaults.setBool_forKey_(self.canvas.l_to_r, 'LtoR')
    defaults.synchronize()

強引な。

数値順(自然順とも)ソートの他に Finder は大文字小文字の区別をしない。
以下をそのまま使ったらいい感じ。

sorting Pythonには、自然言語の文字列のための関数が組み込まれていますか? – CODE Q&A 問題解決

左右ページ切り替えを comipoli オリジナルと同様に。
PyObjC 版はメニューを丸ごと入れ替えることにしてみた。

def onLRButtonClicked_(self, button):
    if self.canvas.l_to_r:
        self.canvas.l_to_r = False
        self.toolbar.lr_button.setTitle_('L<-R')
        menu = NSApp.mainMenu().itemAtIndex_(2).submenu()
        menu.itemAtIndex_(0).setKeyEquivalent_(NSLeftArrowFunctionKey)
        menu.itemAtIndex_(1).setKeyEquivalent_(NSLeftArrowFunctionKey)
        menu.itemAtIndex_(2).setKeyEquivalent_(NSRightArrowFunctionKey)
        menu.itemAtIndex_(3).setKeyEquivalent_(NSRightArrowFunctionKey)
        menu.itemAtIndex_(5).setKeyEquivalent_(NSRightArrowFunctionKey)
        menu.itemAtIndex_(6).setKeyEquivalent_(NSLeftArrowFunctionKey)
    else:
        self.canvas.l_to_r = True
        self.toolbar.lr_button.setTitle_('L->R')
        menu = NSApp.mainMenu().itemAtIndex_(2).submenu()
        menu.itemAtIndex_(0).setKeyEquivalent_(NSRightArrowFunctionKey)
        menu.itemAtIndex_(1).setKeyEquivalent_(NSRightArrowFunctionKey)
        menu.itemAtIndex_(2).setKeyEquivalent_(NSLeftArrowFunctionKey)
        menu.itemAtIndex_(3).setKeyEquivalent_(NSLeftArrowFunctionKey)
        menu.itemAtIndex_(5).setKeyEquivalent_(NSLeftArrowFunctionKey)
        menu.itemAtIndex_(6).setKeyEquivalent_(NSRightArrowFunctionKey)
    if len(self.archive) != 0:
        self.canvas.display()

アホだ。

えっと、後何かあったかな?
連休が終わったのでここからは遅いかも。

comipoli_pyobjc_2.tar.gz

ここまでのバックアップ。

PyObjC Core Graphics

macOS 版 comipoli も PDF 対応にしたい。

NSPDFImageRep – AppKit | Apple Developer Documentation

こんなものがあったのか!
しかも NSImage からの継承。
ということは NSImage のメソッドもそのまま使えるってことだ。

elif is_pdf and re.search(r'\.pdf$', path, re.I):
    self.status = 3
    self.namelist.clear()
    data = NSData.dataWithContentsOfFile_(path)
    self.pdf = NSPDFImageRep.alloc().initWithData_(data)

######

elif self.status == 3:
    self.pdf.setCurrentPage_(num)
    return self.pdf

を前々回のソースに追記するだけで本当にイケた!
ただ cairo と Poppler の時と同じように背景は白で塗りつぶす必要があった。

しかし、画像だけの PDF だとトンデモなく表示が重いんですけど。。。。。
テキストだけの PDF なら余裕なんだけど。
筆者の mac は最新機とはいえ Air だし、pro なら。。。。。

もしかして Core Graphics を直で使ったら軽くなるかな?
と思ったので試すことに。
PyObjC の CoreGraphics は以下に入っていた
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/Quartz

PDF Document Creation, Viewing, and Transforming

上記を PyObjC で簡単に使ってみる。
PATH は自前で持っている PDF に書き換えてください。

#!/usr/bin/env python3

import objc
from AppKit import *
from Quartz.CoreGraphics import *

#PATH = '/Users/sasakima-nao/Documents/ProgrammingWithObjectiveC.pdf'
PATH = '/Users/sasakima-nao/Documents/big_picture.pdf'

class ComipoliView(NSView):
    def init(self):
        objc.super(ComipoliView, self).init()
        # PDF Document
        url = NSURL.fileURLWithPath_isDirectory_(PATH, False)
        self.pdfdoc = CGPDFDocumentCreateWithURL(url)
        self.max_page = CGPDFDocumentGetNumberOfPages(self.pdfdoc)
        self.page_number = 1
        return self

    def drawRect_(self, rect):
        NSColor.blackColor().set()
        NSRectFill(rect)
        # CGContext
        ctx = NSGraphicsContext.currentContext().CGContext()
        page = CGPDFDocumentGetPage(self.pdfdoc, self.page_number)
        #
        # Draw (x1)
        #CGContextDrawPDFPage(ctx, page)
        #
        # Resize Draw
        CGContextSaveGState(ctx)
        # start
        m = CGPDFPageGetDrawingTransform(page, kCGPDFMediaBox, rect, 0, True)
        CGContextConcatCTM(ctx, m)
        # background color is white
        cgrect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox)
        NSColor.whiteColor().set()
        CGContextFillRect(ctx, cgrect)
        # clip
        CGContextClipToRect(ctx, cgrect)
        CGContextDrawPDFPage(ctx, page)
        # end
        CGContextRestoreGState(ctx)

class ComipoliWindow(NSWindow):
    def init(self):
        frame = NSMakeRect(100, 400, 900, 600)
        objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        self.setTitle_('draw PDF')
        self.setDelegate_(self)
        # view
        self.canvas = ComipoliView.new()
        self.canvas.setFrameSize_(frame.size)
        self.contentView().addSubview_(self.canvas)
        return self

    def nextPage(self):
        if self.canvas.max_page > self.canvas.page_number:
            self.canvas.page_number += 1
            self.canvas.display()
        if self.canvas.max_page == self.canvas.page_number:
            self.setTitle_('end')

    def windowDidResize_(self, sender):
        aw = self.contentView().frame().size.width
        ah = self.contentView().frame().size.height
        size = NSMakeSize(aw, ah)
        self.canvas.setFrameSize_(size)

class ComipoliMenu(NSMenu):
    def init(self):
        objc.super(ComipoliMenu, self).init()
        item_app  = NSMenuItem.new()
        self.addItem_(item_app)
        menu_app = NSMenu.new()
        item_app.setSubmenu_(menu_app)
        item_next = NSMenuItem.new()
        item_next.initWithTitle_action_keyEquivalent_('NextPage', 'nextPage:', 'n')
        item_next.setTarget_(self)
        menu_app.addItem_(item_next)
        item_quit = NSMenuItem.new()
        item_quit.initWithTitle_action_keyEquivalent_('Quit', 'terminate:', 'q')
        menu_app.addItem_(item_quit)
        return self

    def nextPage_(self, sender):
        NSApp.keyWindow().nextPage()

NSApplication.sharedApplication()
window = ComipoliWindow.new()
window.makeKeyAndOrderFront_(window)
NSApp.setMainMenu_(ComipoliMenu.new())
NSApp.activateIgnoringOtherApps_(True)
NSApp.run()

command+N で次ページ

って、ほとんど変わらないヤン!
結局この作業を NSPDFImageRep は内部でやっているだけなんだろう。
軽くしたきゃ cbz に変換してくれで済ませよう。

そんなことより。
PyObjC ってこんなことまでできるのかい!
書いた筆者が驚いた、Xcode マジでいらね。

NSWindow “New Window”

NSWindow をもう一つ作成するメニューを付ける。
実行すると一瞬表示されて即座に消えて無くなる。
NSApp.windows プロパティの数は増えていない、何故だ?

backingType – Cocoa API解説(macOS/iOS)

全然関係ないページだけどこの Swift コードは何?
作った Window は自前の配列とかで保持しなきゃいけないのかよ。

NSApp は作成した Window を保持してくれないってことみたい。
GtkApplicationWiondow と違い引数に app 指定が無いからそりゃそうかと。
つまり、ガベージコレクションで破棄されてしまうってことだ。

class ComipoliWindow(NSWindow):

    # Avoid garbage collection
    avoid_gc = []

    def init(self):
        frame = NSMakeRect(100, 400, 800, 480)
        objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        # etc...
        return self

    @classmethod
    def new_window(self):
        window = ComipoliWindow.new()
        self.avoid_gc.append(window)
        window.makeKeyAndOrderFront_(window)
        return window

グローバル変数にしたくないからこんな classmethod を作ってみた。
大量にウインドウが作られるのものではないならアプリ終了まで保持でいい。

applicationDidFinishLaunching で Window が作れないって検索で見かける。
Objective-c, Swift も参照カウンタだから同じなんだろう。
初心者コードでは NSApp.run の前にウインドウを作るから破棄されないだけ。

後、NSMenu に View を定義すると勝手に ‘Show Tab Bar’ が挿入される。
‘Enter Full Screen’ も入るけど command+control+F は使えない。

iOSな日々: macOS Sierra非互換:「表示」メニューに「タブバーを表示」項目が自動的に追加される(非表示化対応)

なんて迷惑な仕様なんだよ。
‘Enter Full Screen’ は自前で command+control+F を定義すると消える。

#!/usr/bin/env python3

from AppKit import *
from comipoli_window import ComipoliWindow
from comipoli_menu import ComipoliMenu

class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, notification):
        ComipoliWindow.new_window()

def main():
    NSApplication.sharedApplication()
    NSApp.setMainMenu_(ComipoliMenu.new())
    NSApp.setDelegate_(AppDelegate.new())
    NSApp.activateIgnoringOtherApps_(True)
    # Hide Auto Tab Bar
    NSWindow.setAllowsAutomaticWindowTabbing_(False)
    # run
    NSApp.run()

色々なんとかなった。

しかし、AppKit は GTK+ より日本語情報が多いのはいいんだけーが。
みんな Xcode の使い方ばかり勉強して肝心のコードがほとんど無いよな。

統合開発環境ってコードだけでアプリが作れる人が使って初めて便利だと思うもの。
であるはずなんだけど、これではプログラミングが面白いと思う人なんて増えないよ。

@objc.python_method

macOS に入っている unzip はパッチが当たっていない Unicode 非対応な奴。
NSTask で使うと日本語でエラーになるので制限を付ける必要があった。

PyObjC では Python3 の zipfile モジュールを使えば何も問題なくね?
と思って使ってみたら、やっぱり文字化けしたw
でもコッチなら文字化けしたままエラーにならず画像を取り出しできるじゃないの。
よし unzip 問題は解決だ。

しかし PyObjC プログラミングは結構きつい!
まったく例外を吐いてくれない、エラーになったらフリーズするだけ。
チビッと書いて動かしてを繰り替えさないとドコでエラーになったか全然ワカンネ。

例外がウルトラ親切な PyGObject ってマジ神仕様だよ。
アレで何も作れなかったら他の環境なんて絶対に無理と断言できるくらい。

それとサブクラスにはまいった。

FieldGraph ? PyObjC ? the Python ? Objective-C bridge

NSObject を継承するとメソッドの定義に制限を食らう。
つまりメソッドの定義は Objective-c の関数変換と同様にする必要あり。
引数の数だけアンダーバーが必須、最後にアンダーバーが必要だとか。

#def change_pixbuf(self, num): # Error
def changePixbuf_(self, num):

みたいな。

@objc.IBAction はデフォルトだから書く必要は無いみたい。
Python のメソッドらしいスネークケースにするには

@objc.python_method
def change_pixbuf(self, num):

のデコレーターが必須。

ハンドラと関数はやっぱり別表記にしたいよね。
それを見つけるまで半日近く、日本語情報は皆無に近いし。

Python で作っても NSOpenPanel で警告が出るじゃん。
jxa てか osascript のせいでは無かったようで。

そんなんでもなんとかココまで作れた、ページめくりもできる。
まだ超作りかけだけど参考用とバックアップを兼ねて置いとく。
comipoli_pyobjc1.tar.gz

PyObjC NSView

comipoli JXA 版の PyObjC 化を進めないと。

An introduction to PyObjC ? PyObjC ? the Python ? Objective-C bridge

Objective-c の関数は JXA ではコロンをキャメルケースで合体だった。
PyObjC ではスネークケース、最後のコロンもアンダーバー。

alloc は関数、JXA では関数に括弧不要だったけどこのほうが理解しやすい。
class の継承は Python の掟どおり、PyGObject と同様。

プロパティは何故か全部 ReadOnly になっている。
getter は関数のように括弧で呼び出し。
setter は全部 set*** メソッドを使ってくれということみたい。

上記には書いていないけど NSArray は添字アクセスできた。
もう少し詳しく解ったらまとめページでも作る、かも。

だいたい解ったところで JXA で書いた ComipoliView を書き換えてみる。

#!/usr/bin/env python3

import objc
from AppKit import *

class ComipoliView(NSView):
    def init(self):
        objc.super(ComipoliView, self).init()
        self.first_page = None
        self.second_page = None
        self.spread = False
        self.l_to_r = False
        return self

    def drawRect_(self, rect):
        NSColor.blackColor().set()
        NSRectFill(rect)
        if self.first_page != None:
            aw = rect.size.width
            ah = rect.size.height
            w = self.first_page.size().width
            h = self.first_page.size().height
            # Horizontal?
            if w - h > 0 or not self.spread:
                # Single Page
                width, height, x, y = 0
                if aw * h > ah * w:
                    width = w * ah / h
                    height = ah
                    x = (aw - width) / 2
                    y = 0
                else:
                    width = aw
                    height = h * aw / w
                    x = 0
                    y = (ah - height) / 2
                r1 = NSMakeRect(x, y, width, height)
                self.first_page.drawInRect_(r1)
            else:
                if self.second_page != None:
                    left = ah * w / h
                    if self.l_to_r:
                        r1 = NSMakeRect(aw / 2 - left, 0, left, ah)
                        self.first_page.drawInRect_(r1)
                        #
                        w2 = self.second_page.size().width
                        h2 = self.second_page.size().height
                        right = ah * w2 / h2
                        r2 = NSMakeRect(aw / 2, 0, right, ah)
                        self.second_page.drawInRect_(r2)
                    else:
                        r1 = NSMakeRect(aw / 2, 0, left, ah)
                        self.first_page.drawInRect_(r1)
                        #
                        w2 = self.second_page.size().width
                        h2 = self.second_page.size().height
                        right = ah * w2 / h2
                        r2 = NSMakeRect(aw / 2 - right, 0, right, ah)
                        self.second_page.drawInRect_(r2)

comipoli_view.py

#!/usr/bin/env python3

import objc
from AppKit import *
from comipoli_view import ComipoliView

class ComipoliWindow(NSWindow):
    def initWithFrame_(self, frame):
        #self = objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
        objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        self.setTitle_('日本語')
        self.setDelegate_(self)
        # NSView
        self.view = ComipoliView.new()
        self.view.setFrameSize_(frame.size)
        self.view.first_page = NSImage.alloc().initWithContentsOfFile_('003.png')
        self.view.second_page = NSImage.alloc().initWithContentsOfFile_('004.png')
        self.view.spread = True
        self.contentView().addSubview_(self.view)
        #
        return self

    def windowDidResize_(self, sender):
        aw = self.contentView().frame().size.width
        ah = self.contentView().frame().size.height
        size = NSMakeSize(aw, ah)
        self.view.setFrameSize_(size)

    def windowWillClose_(self, sender):
        NSApp.terminate_(sender)

class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, notification):
        pass

NSApplication.sharedApplication()

frame = NSMakeRect(100, 400, 320, 160)
window = ComipoliWindow.alloc().initWithFrame_(frame)
window.makeKeyAndOrderFront_(window)

main_menu = NSMenu.new()
item_app  = NSMenuItem.new()
main_menu.addItem_(item_app)
menu_app = NSMenu.new()
item_app.setSubmenu_(menu_app)
item_quit = NSMenuItem.new()
item_quit.initWithTitle_action_keyEquivalent_('Quit Comipoli', 'terminate:', 'q')
menu_app.addItem_(item_quit)
NSApp.setMainMenu_(main_menu)

NSApp.setDelegate_(AppDelegate.new())
NSApp.activateIgnoringOtherApps_(True)
NSApp.run()

comipoli_window.py

なるほど、これでいいんだ。
ついでに発見したけど、init で self に代入しなくてもいいのね。
というか、self に代入って変だろ。。。。。

どうでもいいおまけ。
NSMatrix ってのがあるんだ、おぉ!って思ったけど。
GTK+ でいう GtkRadioButton の grope 化だった、まぎらわしいYO!