Programming」カテゴリーアーカイブ

PyObjC NSWindow Cascade Position

macOS では二つ目以降のウインドウは規則的にズレて表示される。
でも NSWindow は NSRect を指定して作成する、どうやっているのか。

cascadeTopLeftFromPoint: – Cocoa API解説(macOS/iOS)

そういうことか、解説ありがとう。
window を作った後に自分で移動するのね。

objc.super(MyWindow, self).initWithContentRect_styleMask_backing_defer_(
    frame,
    NSTitledWindowMask |
    NSClosableWindowMask |
    NSResizableWindowMask |
    NSMiniaturizableWindowMask,
    NSBackingStoreBuffered,
    False)
# Cascade Position
if len(NSApp.windows()) > 1:
    point = NSApp.keyWindow().cascadeTopLeftFromPoint_(NSZeroPoint)
    self.setFrameTopLeftPoint_(point)

で。

うん macOS アプリっぽくなった。
次は設定ダイアログの位置決めをどうするかだ。
小ネタでした。

PyObjC NSAttributedString

NSCollectionView は色々カスタムできるようなのでとやってみたけど。。。。。
いくら探してもチュートリアル Interface Builder しか出てこない。
しかもボコボコ落ちる、無意味なクラッシュレポートを Apple に沢山送ったよ。
もうあきらめて普通の NSView として使うことにするよ。

ということで。
NSView に文字列を表示したい。

[Objective-C]NSAttributedStringの背景色あたりの話|杏z 学習帳(旧)

NSMutableAttributedString というトンデモネェものがあるようだ。
筆者の用途では継承元の NSAttributedString で十分そう。

てか UIColor なの?
PyObjC には UIKit は無いぞ、あってどうするという話だが。
NSColor で当然のようにイケた、これは Apple の解説が悪いな。

comipoli オリジナルのサムネイル文字列は赤、背景色は 66ffff で 50% 半透明。
これを再現するには。

colorWithSRGBRed:green:blue:alpha: – NSColor | Apple Developer Documentation

最大値を 1.0 にして RGBA 指定でイケるのね。
やってみる。

class ItemView(NSView):
    def init(self):
        objc.super(ItemView, self).init()
        self.image = None
        self.num = 0
        return self

    def drawRect_(self, rect):
        NSColor.blackColor().set()
        NSRectFill(rect)
        if (self.image):
            self.image.drawInRect_(rect)
            bg_color = NSColor.colorWithSRGBRed_green_blue_alpha_(0.4, 1.0, 1.0, 0.5)
            text = NSAttributedString.alloc().initWithString_attributes_(
                '{}'.format(self.num + 1),
                {
                    NSForegroundColorAttributeName: NSColor.redColor(),
                    NSBackgroundColorAttributeName: bg_color
                })
            x = rect.size.width / 2
            text.drawAtPoint_(NSMakePoint(x, 0))

オリジナル

よし完成、といいたいけどまだ超不安定。

PyObjC Background Thread

サムネイル画像を作るバックグラウンド処理。

Objective-C で NSThread および GCD で非同期処理した結果を UI に反映する処理を書いてみた – 凹みTips

こんなアホみたく簡単にバックグラウンドスレッドが動かせるのかい!
performSelectorInBackground:withObject:
を使うだけ、GCD は Python なので関係ない。

それから GUI の常識、関数を抜けるまで画像は表示されない。
普通に書くとすべての画像の読み込みが終わってから一気に表示される。
順次表示するには for 文の中で一つ読み終わる毎に関数を抜ける必要がある。

EZ-NET: UI 関連の機能はメインスレッドで実行すること : Objective-C プログラミング

バックグラウンドスレッドの中で
performSelectorOnMainThread:withObject:waitUntilDone:
を使い UniilDone を True にすればメインスレッドの関数を抜けることができる。

Objective-c スゲェ。
みんな大好き yield を使おうと思ったけど必要ないぞ。

残念なのは NSCollectionView.content が NSArray だった。
python の配列のような可変長ではないので丸ごと配列を入れ替えすることに。

#!/usr/bin/env python3

import objc, os, re
from AppKit import *

PATH = '/Users/sasakima-nao/Pictures/nae'

class ItemView(NSView):
    def init(self):
        objc.super(ItemView, self).init()
        self.image = None
        return self

    def drawRect_(self, rect):
        self.image.drawInRect_(rect)

class CollectionViewItem(NSCollectionViewItem):
    def init(self):
        objc.super(CollectionViewItem, self).init()
        self.setView_(ItemView.alloc().initWithFrame_(NSMakeRect(0, 0, 100, 100)))
        return self

    def setRepresentedObject_(self, rep):
        objc.super(CollectionViewItem, self).setRepresentedObject_(rep)
        if rep != None:
            self.view().image = rep['pic']

class ComipoliCollectionView(NSCollectionView):
    def init(self):
        objc.super(ComipoliCollectionView, self).init()
        self.pixs = []
        self.setItemPrototype_(CollectionViewItem.new())
        self.performSelectorInBackground_withObject_(self.readImage_, PATH)
        return self

    def readImage_(self, path):
        l = os.listdir(path)
        for s in l:
            if re.search(r'\.(jpg|jpeg|png|gif)$', s, re.I):
                f = '{0}/{1}'.format(PATH, s)
                image = NSImage.alloc().initWithContentsOfFile_(f)
                self.performSelectorOnMainThread_withObject_waitUntilDone_(self.addImage_, image, True)

    def addImage_(self, image):
        self.pixs.append({'pic': image})
        self.setContent_(self.pixs)

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

    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_quit = NSMenuItem.new()
        item_quit.initWithTitle_action_keyEquivalent_('Quit', 'terminate:', 'q')
        menu_app.addItem_(item_quit)
        return self

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

前回のに追加。

ウインドウが表示された後に順次画像が表示されていくのが確認できる。

PyObjC NSCollectionView

JXA で後回しにし続けていた画像サムネイル表示をそろそろ作りたい。

NSCollectionView – AppKit | Apple Developer Documentation

どうやらコレを使えばイケるらしい。
色々遠回りしたけど上手くいったコードをいきなり。

#!/usr/bin/env python3

import objc, os, re
from AppKit import *

PATH = '/Users/sasakima-nao/Pictures/nae'

class ItemView(NSView):
    def init(self):
        objc.super(ItemView, self).init()
        self.image = None
        return self

    def drawRect_(self, rect):
        self.image.drawInRect_(rect)

class CollectionViewItem(NSCollectionViewItem):
    def init(self):
        objc.super(CollectionViewItem, self).init()
        self.setView_(ItemView.alloc().initWithFrame_(NSMakeRect(0, 0, 100, 100)))
        return self

    def setRepresentedObject_(self, rep):
        objc.super(CollectionViewItem, self).setRepresentedObject_(rep)
        if rep != None:
            self.view().image = rep['pic']

class ComipoliCollectionView(NSCollectionView):
    def init(self):
        objc.super(ComipoliCollectionView, self).init()
        a = []
        l = os.listdir(PATH)
        for s in l:
            if re.search(r'\.(jpg|png)$', s, re.I):
                f = '{0}/{1}'.format(PATH, s)
                image = NSImage.alloc().initWithContentsOfFile_(f)
                a.append({'pic': image})
        self.setItemPrototype_(CollectionViewItem.new())
        self.setContent_(a)
        return self


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

    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_quit = NSMenuItem.new()
        item_quit.initWithTitle_action_keyEquivalent_('Quit', 'terminate:', 'q')
        menu_app.addItem_(item_quit)
        return self

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

NSCollectionView, NSCollectionViewItem, NSView が最小限必要。
表示したい NSImage は適当なキーで辞書にいれて配列に突っ込む。
辞書毎に setRepresentedObject_ が呼ばれるので取り出す。
他にも手段があるようだけど、一つの方法として。

後選択可能にしたりとかもやらないと。
画像や PDF のリサイズやバックグラウンド処理をやらないと。
苗ちゃんの画像が古いのは実はもうやっていないからだとか。

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

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