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

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 はいらないよね。
筆者は使ったことがない。

Clutter Fullscreen HeaderBar

フルスクリーン時にマウスカーソルを上に持って行くとヘッダーがヒョッコリ。
という処理は GTK+ なら GtkOverlay で簡単に実現できる。

GTK+ Overlay | PaePoi

けれど Y901x でやってみたらどうやっても表示してくれない。
GtkOverlay は GtkClutter.Embed の上には被せることができないようだ。

なので GtkClutter.Actor の上にヘッダーを置いて表示させるしかない。
GTK+ の上に Clutter を載せて、その上に GTK+ を置く。
という奇妙な構造だけど他に手段が無い。
下記はそのまま動くようにしたものを抜き出ししてみた。

#!/usr/bin/gjs

const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Clutter = imports.gi.Clutter;
const GtkClutter = imports.gi.GtkClutter;

var ClWindow = GObject.registerClass({
    GTypeName: 'ClWindow'
}, class ClWindow extends Gtk.Window {
    _init() {
        super._init();
        this.is_fullscreen = false;
        // actor
        this.actor = new Clutter.Actor();
        // Fullscreen Header and Actor
        this.upperbar = new Gtk.HeaderBar({
            no_show_all: true,
            valign: Gtk.Align.START
        });
        this.upperActor = new GtkClutter.Actor({
            contents: this.upperbar
        });
        this.upperActor.hide();
        // Restore Button
        let restoreButton = new Gtk.Button({
            image: Gtk.Image.new_from_icon_name('view-restore-symbolic', Gtk.IconSize.MENU),
            visible: true
        });
        restoreButton.connect('clicked', ()=> {
            this.change_fullscreen();
        });
        this.upperbar.pack_end(restoreButton);
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.actor);
        stage.add_child(this.upperActor);
        stage.connect('motion-event', (actor, event)=> {
            if (this.is_fullscreen) {
                let [, y] = event.get_coords();
                // upperbar
                if (this.is_fullscreen) {
                    if (y <= this.hb_height+10 && !this.upperbar.visible) {
                        this.upperActor.show();
                        this.upperbar.show();
                    } else if (y > this.hb_height+10 && this.upperbar.visible) {
                        this.upperActor.hide();
                        this.upperbar.hide();
                    }
                }
            }
        });
        // box
        let vbox = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
        vbox.pack_start(embed, true, true, 0);
        vbox.connect('size-allocate', (vbox, allocation)=> {
            if (this.is_fullscreen) {
                this.upperActor.set_size(allocation.width, this.hb_height);
                this.upperbar.set_size_request(allocation.width, this.hb_height);
            }
        });
        // Double Click Fullscreen
        vbox.connect('button-press-event', (widget, event)=> {
            if (event.get_event_type() == 5) {
                this.change_fullscreen();
            }
            return true;
        });
        // main
        let hb = new Gtk.HeaderBar({show_close_button: true});
        this.set_titlebar(hb);
        this.add(vbox);
        this.connect('delete-event', ()=> {Gtk.main_quit();});
        this.resize(300, 300);
        this.show_all();
        this.hb_height = hb.get_allocated_height();
    }
    change_fullscreen() {
        this.is_fullscreen = this.is_fullscreen === false;
        if (this.is_fullscreen) {
            this.fullscreen();
        } else {
            this.unfullscreen();
            if (this.upperbar.visible) {
                this.upperbar.hide();
                this.upperActor.hide();
            }
        }
    }
});
Gtk.init(null);
GtkClutter.init(null);
let w = new ClWindow();
Gtk.main();

やっぱり長いな。

細かい解説はしないから自力で解析してね。
注意点は GtkWindow の motion-notify-event で処理しないこと。
ドロップしたメニューを選ぼうとマウスを動かすとメニューが消える罠がwwwww

って Comipoli 0.3.5 がそうなっているということに今気が付いた!
Comipoli は全部 GTK+ なんだが、同じ手段は使えない。
さて困った、eog みたいにメニューを出さないという手もあるけど。