NSView Auto Layout

NSView のレイアウト方式いろいろ – ひ?to?り?go?と

こんなの見つけた。
windowDidResize に頼らず NSView を引き延ばす手段がこんなにあったんだ。

autoresizingMask の指定
addConstraints でアンカーの指定
layout のオーバーライド

一つづつ作って試すの面倒だから NSView の上に NSView を置いて。
上記三つを全部 PyObjC でやってみた。

#!/usr/bin/env python3

from AppKit import *

RECT = ((0, 0), (300, 100))
wins = []

class TopView(NSView):
    def initWithFrame_(self, rect):
        objc.super(TopView, self).initWithFrame_(rect)
        return self

    def drawRect_(self, rect):
        # 全体にバッテンを描く
        NSColor.darkGrayColor().set()
        path = NSBezierPath.bezierPath()
        path.setLineWidth_(10)
        path.moveToPoint_((0, 0))
        path.lineToPoint_(rect.size) # タプルなのでこれでいい
        path.moveToPoint_((0, rect.size.height))
        path.lineToPoint_((rect.size.width, 0))
        path.stroke()

class SecondView(NSView):
    def initWithFrame_(self, rect):
        objc.super(SecondView, self).initWithFrame_(rect)
        self.v = TopView.alloc().initWithFrame_(RECT)
        self.addSubview_(self.v)
        return self

    def layout(self):
        # Override
        objc.super(SecondView, self).layout()
        self.v.setFrameSize_(self.frame().size)

class ThirdView(NSView):
    def initWithFrame_(self, rect):
        objc.super(ThirdView, self).initWithFrame_(rect)
        self.v = SecondView.alloc().initWithFrame_(RECT)
        self.addSubview_(self.v)
        #
        # addConstraints
        #
        self.v.setTranslatesAutoresizingMaskIntoConstraints_(False)
        self.addConstraints_([
            self.v.leftAnchor().constraintEqualToAnchor_constant_(self.leftAnchor(), 0),
            self.v.rightAnchor().constraintEqualToAnchor_constant_(self.rightAnchor(), 0),
            self.v.topAnchor().constraintEqualToAnchor_constant_(self.topAnchor(), 0),
            self.v.bottomAnchor().constraintEqualToAnchor_constant_(self.bottomAnchor(), 0)
        ])
        return self

class MyWindow(NSWindow):
    def init(self):
        objc.super(MyWindow, self).initWithContentRect_styleMask_backing_defer_(
            RECT,
            NSTitledWindowMask | NSClosableWindowMask |
            NSResizableWindowMask | NSMiniaturizableWindowMask,
            NSBackingStoreBuffered, False)
        self.center()
        self.setTitle_('Auto Resize')
        #self.setDelegate_(self)
        # View
        self.v = ThirdView.alloc().initWithFrame_(RECT)
        self.contentView().addSubview_(self.v)
        #
        # autoresizingMask
        #
        self.v.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable) # 2+16
        #
        return self

    #def windowDidResize_(self, sender):
    #    self.v.setFrameSize_(self.contentView().frame().size)

class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, notification):
        window = MyWindow.new()
        window.makeKeyAndOrderFront_(window)
        wins.append(window)

class AppMenu(NSMenu):
    def init(self):
        objc.super(AppMenu, self).init()
        item_app  = NSMenuItem.new()
        self.addItem_(item_app)
        menu_app = NSMenu.new()
        item_app.setSubmenu_(menu_app)
        # quit menu
        item_quit = NSMenuItem.new()
        item_quit.initWithTitle_action_keyEquivalent_('Quit App', 'terminate:', 'q')
        menu_app.addItem_(item_quit)
        return self

NSApplication.sharedApplication()
NSApp.setMainMenu_(AppMenu.new())
NSApp.setDelegate_(AppDelegate.new())
NSApp.activateIgnoringOtherApps_(True)
NSApp.run()

なるほど、全部 PyObjC からでも使えますね。
でも結局 windowDidResize が一番扱いやすいような。。。。。

NSPopUpButton, NSComboBox

NSComboBox を使ってみたけど何か変だ。
システム環境設定のコンボボックスと形が違う。
タップでプルダウンも矢印の所しか反応しないし何コレ?

Views and Controls | Apple Developer Documentation
どうやらこれは NSPopUpButton というコントロールのようだ。
GTK+ みたいに Gallery が欲しいんですけど。
Widget Gallery: GTK+ 3 Reference Manual

NSComboBox が NSTextField のサブクラス。
NSPopUpButton が NSButton のサブクラス。

items = ['YAMAHA', 'SUZUKI']

class MyView(NSView):
    def initWithFrame_(self, rect):
        objc.super(MyView, self).initWithFrame_(rect)
        #
        # NSComboBox @ NSTextField init method
        cbox = NSComboBox.labelWithString_('Select')
        w, h = cbox.frameSize() # Get Control Height
        cbox.setFrame_(((10, 10), (200, h)))
        cbox.addItemsWithObjectValues_(items)
        self.addSubview_(cbox)
        #
        # NSPopUpButton
        pop = NSPopUpButton.alloc().initWithFrame_pullsDown_(((10, 40), (200, h)), False)
        pop.addItemsWithTitles_(items)
        self.addSubview_(pop)
        #
        return self

未選択状態が必要な場合は NSComboBox を利用。
みたいな使い分けでいいのかな?
どこをタップしてもいい NSPopUpButton はタッチ操作向けだよな。
もう少し弄ってみる。

pythonhosted

あれ? https://pythonhosted.org/pyobjc/index.html が消えている。

PyObjC Document はコッチに移動(?)したようだ。
Introduction ? PyObjC – the Python to Objective-C bridge
以前からあったのかどうかは知らない、基本 Fedora 屋なので。

というわけで NSTextField Tips 追加。
NSTextField – L’Isola di Niente
短いけど実用には充分にしたつもり。

しかし textDidChange: は何故デリゲートではなくメソッドなんだよ。
NSButton に至っては target, action 指定だし、この統一感の無さが macOS の中身。
macOS の洗練された見た目と統一された操作性はこの糞みたいな Cocoa で作られている。
GTK+ も GTK3 にならなかったらこんな感じになっていたんだろうな。

次は NSTextView にしようと思ったけど。
NSTextView – AppKit | Apple Developer Documentation
Rich Text のことばかりみたいだなぁ、ヤル気しねぇ。
NSSlider, NSComboBox あたりにしよう、のんびりと。

Quick Action @ change72dpi (PyObjC)

自作アプリの件が片付いたので PyObjC Tips の続きを。
NSButton – L’Isola di Niente

NSButton だけで長くなってしまったのでコレだけで一頁にした。
3D タッチ処理がこんなに簡単だとは思わなかった、AppKit スゲェ。

それはそうと、sips がおかしい、サイズが半分の半分になるんだが。
今まで下記クイックアクションに登録したコードでイケていたのに急に何故だ?
macOS をクイックアクションで拡張 – L’Isola di Niente

スクリーンショットが必要なのに困った。
緊急で自作スクリプトを作る。

#!/usr/local/bin/python3

import sys, os
from AppKit import *
from Quartz.CoreGraphics import *

os.chdir(os.path.dirname(sys.argv[1]))
args = sys.argv[1:]

for s in args:
    name = os.path.basename(s)
    src_image = NSImage.alloc().initWithContentsOfFile_(name)
    img = NSBitmapImageRep.imageRepWithData_(src_image.TIFFRepresentation()).CGImage()
    h = CGImageGetHeight(img) // 2
    w = CGImageGetWidth(img) // 2
    ctx = CGBitmapContextCreate(None, w, h, 8, 4 * w, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaPremultipliedLast)
    CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), img)
    imgref = CGBitmapContextCreateImage(ctx)
    out_image = NSImage.alloc().initWithCGImage_size_(imgref, (w, h))
    bmp = NSBitmapImageRep.imageRepWithData_(out_image.TIFFRepresentation())
    data = bmp.representationUsingType_properties_(NSBitmapImageFileTypePNG, {})
    data.writeToFile_atomically_(f'72dpi-{name}', True)

change72dpi という拡張子の無い名前にして +x パーミッション。

Comipoli で使っているコードをフルパス用に書き換えて同じ結果になるように。
クイックアクションは以前の command+shift+S でコピーを作って。

/Users/sasakima-nao/bin/change72dpi "@"

に書き換え保存。

とにかく、GUI を使うなら何をするにもフルパスが必要。
まさかのシバンまでフルパスに、いやシンボリックリンクなんだけどさすがに通った。
だからパスが通っているディレクトリに入れる必要は無いけどなんとなく。

Linux みたくフルパスを使うことのほうが珍しいにしてくれよ。
セキュリティなんて SELinux みたいなので充分だろ。

GtkPopover motion-notify-event

前回書いた GtkPopover を motion-notify-event で処理する件。
フルスクリーンかつポップオーバー表示時はシグナルを無視する。
という超単純な手段でイケた、わっはっは。

それと前々回書いた F10 でポップオーバーが斜めに出る件。
ヘッダーを出した後 g_idle_add で一旦ハンドラを抜けてから表示。
という超単純な手段でイケた、g_idle_add はマジ便利。

困ったのがポップオーバーを出した時の Esc 処理。
ポップオーバーが割り込みで非表示になるけどシグナルも飛んでくる。
上手く逃がさないとフリーズする。

class ComipoliWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.connect('motion-notify-event', self.on_motion_notify_event)
        // etc...
    def on_motion_notify_event(self, widget, event):
        if self.is_fullscreen:
            if self.menuf.popover.props.visible:
                return
            // etc...
    def key_press(self, state, keyval):
        # CapsLock?
        keymap = Gdk.Keymap.get_default()
        _state = state ^ Gdk.ModifierType.LOCK_MASK if keymap.get_caps_lock_state() else state
        # UpperCase?
        val = Gdk.keyval_to_lower(keyval) if Gdk.keyval_is_upper(keyval) else keyval
        if val == Gdk.KEY_F10:
            if self.is_fullscreen:
                self.upperbar.show()
                GLib.idle_add(self.show_fullscreen_menu)
            else:
                self.menu.clicked()
        elif val == Gdk.KEY_F11:
            self.toggle_fullscreen()
        elif val == Gdk.KEY_Escape:
            if self.upperbar.props.visible:
                self.upperbar.hide()
            elif self.is_fullscreen:
                self.toggle_fullscreen()
    def show_fullscreen_menu(self):
        self.menuf.clicked()
        return False

みたいな。

0.3.4 までボタンしか並べていなかったからこんな罠があるとは思わなかった。
不具合をなんとかするの楽しいわい、ただしマゾではない。

ところで。
Fedora 30 にアップグレードしたら Gedit 外部ツールのデフォルトが消えていた。
いや筆者はバックアップを持っているからコピーすればいいんだけど。
同じように消えてしまった人用にデフォルトコマンドを書いておきます。

# open-terminal-here (端末を開く)

#!/bin/sh
gnome-terminal --working-directory=$GEDIT_CURRENT_DOCUMENT_DIR &

# remove-trailing-spaces (末尾のスペースを取り除く)
# 入力を編集中のドキュメント、出力を置き換えるにする

#!/bin/sh
sed 's/[[:blank:]]*$//'

build, run-command, send-to-fpaste はいらないよね。
筆者は使ったことがない。