GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 10

From 흡혈양파의 번역工房
Revision as of 18:06, 9 June 2014 by Onionmixer (talk | contribs) (표 형식 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
제 10 장 데스크톱 통합

데스크톱 통합

애플리케이션을 훌륭하게 만드는 한 가지 조건으로 플랫폼과의 정확한 통합을 들 수 있다. 통합이라 함은 모양과 느낌(look and feel)을 꾸미기 위해 기능을 재구현하는 대신 플랫폼으로부터 직접 기능을 얻음으로써 플랫폼으로 끊김 없이(seamlessly) 접근할 수 있음을 의미한다.


이번 장에서 플랫폼의 컨텍스트는 GNOME 데스크톱에 해당한다. 이는 GNOME 플랫폼에서 주요점에 해당하여 하나의 장을 통해 자세히 설명할 필요가 있다고 생각되었다. 본 장의 목표는 주 목표는 데스크톱 기능 중 일부, 이름하여 세션 관리, 런처, 키링(keyring), 알림 시스템을 활용하는 방법을 학습하는 데에 있다. 구체적으로 논하게 될 내용은 다음과 같다.

  • GNOME 세션 관리와 대화하기
  • 런처 설치
  • 비밀 데이터를 GNOME Keyring에 저장하기
  • 알림 시스템
  • 통합을 정확하게 처리하는 툴인 D-Bus를 먼저 살펴보자.


D-Bus와 대화하기

D-Bus는 프로세스간 통신(IPC)과 원격 프로시저 호출(RPC) 시스템으로, freedesktop.org 데스크톱 구현에 의해 사용된다. GNOME은 해당 소프트웨어의 구현자들 중 하나에 해당한다. D-Bus는 애플리케이션들이 서로 대화할 수 있도록 만든다. D-Bus는 한 당사자가 버스에 명령이나 쿼리를 게시(post)하고 버스를 듣는 다른 당사자들은 요청된 명령에 액션을 취할 수 있는 버스 시스템을 사용한다. 여기에는 세 개의 구분된 채널이 있는데, 이는 "시스템 버스"(system bus), "세션 버스"(session bus), "프라이빗 버스"(private bus)에 해당한다.


시스템 버스는 시스템 전반적인 메시지용으로, 사용자 또는 하드웨어 알림의 생성을 예로 들 수 있다. 버스는 시스템에 의해 실행되고, 시스템에서 모든 실행자가 실행하는 D-Bus 인식 애플리케이션은 전부 버스를 듣고 응답할 수 있다. 두 번째 타입은 세션 버스로, 실행 중인 데스크톱의 사용자에 의해 실행된다. 동일한 세션 내에 동일한 사용자가 실행하는 애플리케이션은 모두 버스를 듣고 그에 응답할 수 있다. 세 번째는 프라이빗 버스로, 점대점(point-to-point) 버스이며, 연결된 당사자들만 대화할 수 있다.


각 애플리케이션은 버스로 연결을 구축할 수 있다. 각 연결의 이름은 인터넷 도메인명을 역순으로 열거한 모습으로, org.gnome.SettingsDaemon.Power 를 예로 들 수 있겠다. 애플리케이션은 이후 경로라고 불리는 식별자를 이용해 버스에서 일부 서비스를 노출시킬 수 있다. 경로는 파일시스템 경로처럼 생겼는데, /org/gnome/SettingsDaemon/Power 를 예로 들 수 있다.


애플리케이션은 org.freedesktop 인터페이스와 같은 공통 인터페이스에 따라 서비스를 노출하거나, 고유의 인터페이스를 제공할 수 있다. 인터페이스는 명세(specification)에 따라 인터페이스의 사용자와 게시자(publisher)에 의해 적절하게 구현되어야 하는 API와 그 모습이 정확히 똑같다.


구체적으로 말해, 애플리케이션은 수신자의 경로와 연결 이름을 명시함으로써 명령을 포함한 메시지를 버스로 게시(post)할 수 있다. 연결명을 소유하고 명시된 경로를 노출하는 애플리케이션은 필요할 경우 인터페이스에 따라 다른 메시지로 반응 및 응답한다.


그 외의 경우, 애플리케이션은 명시된 경로를 이용해 시그널이 발생하였다는 메시지를 방송할 수도 있다. 시그널에 관심이 있는 또 다른 애플리케이션은 버스를 도청하여 시그널의 발생을 듣고 시그널을 수신 시 어떠한 실행을 할 수 있다.


D-Bus는 GNOME 데스크톱과 꽤 훌륭한 통합을 확보하는 데에 중요한 툴들 중 하나다. 플랫폼에서 이용 가능한 많은 기능들은 D-Bus를 이용해 접근할 수 있다. D-Bus가 어떻게 GNOME 데스크톱과의 통합을 돕는지에 대한 통찰력을 얻기 위해 D-Bus 세션을 들어보자.


Gnome3 notice header.png
D-Bus에 관한 추가 정보는 http://www.freedesktop.org/wiki/Software/dbus 에서 이용할 수 있다.
Gnome3 notice footer.png


실행하기 - D-Bus 듣기

D-Bus 세션 버스에서 무슨 일이 일어나는지 확인하기 위해 아래와 같은 실험을 해보자.

  1. Terminal을 열어라.
  2. dbus-monitor를 입력하고 Enter를 눌러라.
  3. 화면은 D-Bus에서 온 메시지로 가득찰 것이다. 음향 크기를 증가시키거나 GNOME 메인 메뉴로 접근하거나 애플리케이션을 활성화하라. 당신의 액션은 모두 버스로 방송될 것이다.


무슨 일이 일어났는가?

dbus-monitor 명령은 세션 버스에 무엇이 공시되든지 내용을 듣는다. 랩탑 컴퓨터를 충전하면서 사용 중이라면 아래와 같은 내용이 뜰 것이다.

signal sender=:1.6 -> dest=(null destination) serial=192 path=/org/gnome/SettingsDaemon/Power; 
interface=org.freedesktop.DBus.Properties;
member=PropertiesChanged
    string "org.gnome.SettingsDaemon.Power"
    array [
        dict entry(
            string "Tooltip"
            variant string "Laptop battery 1 hour 10 minutes until charged (44%)" 
        )
    ]
    array [
    ]


출력 내용은 org.freedesktop.DBus.Properties 인터페이스에 PropertiesChanged 시그널을 표시하고 있다고 말한다. 인터페이스에 따르면 시그널에는 세 개의 인자, 즉 문자열, 배열, 또 다른 배열이 있다. 이러한 특정 시그널은 /org/gnome/SettingsDaemon/Power 경로에서 노출되고 모두에게 방송된다. 첫 번째 인자 org/gnome.SettingsDaemon.Power, 사전(dictionary) 기록을 포함하는 배열, 빈 배열을 전송한다.


시스템 버스에서 무슨 일이 일어나는지 관심이 있다면 시스템 인자를 제공하라.


팝 퀴즈 - 훌륭한 애플리케이션 예제?

Q1. 앞의 데이터가 버스에 항상 게시될 경우 다음 중 데이터를 소모하기에 가장 적절한 애플리케이션 예는 무엇인가?

  1. 배터리 시스템 트레이 애플릿(applet)
  2. 배터리 확인 애플리케이션


GNOME 세션 관리자

GNOME 세션 관리자는 사용자의 데스크톱 환경 세션을 책임진다. 이는 시작 애플리케이션과 데스크톱 셸을 실행하고, 사용자의 로그아웃을 가능하게 하며, 컴퓨터를 종료한다. 애플리케이션은 심지어 관리자에게 로그아웃이나 종료를 요청할 수도 있다. 예를 들어, 사용자가 로그아웃을 원하는데 애플리케이션에 저장하지 않은 문서가 있다면 사용자가 문서를 저장하거나 명시적으로 애플리케이션을 종료할 때까지 로그아웃은 임시적으로 중지될 것이다.


세션은 사용자의 존재를 추적하고, 사용자가 일반 상태인지(available), 다른 작업 중인지(busy), 유휴 상태인지(idle), 보이지 않는 상태인지(invisible)도 추적한다. 사용자는 존재의 상태를 텍스트로 설정할 수도 있다.


실행하기 - 세션 관리자에게 이야기하기

  1. session.js라고 불리는 스크립트를 생성하라 (소스 코드 배포판에서 이용 가능하다). 중요한 부분을 아래에 표시하겠다.
    var SessionManagerInterface = {
        name: "org.gnome.SessionManager",
        methods: [
            { name: 'CanShutdown', inSignature: '', outSignature: 'b' },
            { name: 'Logout', inSignature: 'u', outSignature: '' },
            { name: 'Shutdown', inSignature: '', outSignature: '' },
            { name: 'Inhibit', inSignature: 'susu', outSignature: 'u' },
            { name: 'Uninhibit', inSignature: 'u', outSignature: '' }
        ]
    }
    
    Presence.prototype = {
        _init: function() {
            DBus.session.proxifyObject(this,
                'org.gnome.SessionManager',
                '/org/gnome/SessionManager/Presence');
        }
    }
    
    var PresenceInterface = {
        name: "org.gnome.SessionManager.Presence",
        methods: [
            { name: 'SetStatus', inSignature: 'u', outSignature: '' },
            { name: 'SetStatusText', inSignature: 's', outSignature: '' },
        ]
    }
    Main = new GType({
        parent: GObject.Object.type,
        name: "Main",
        init: function(self) {
            DBus.proxifyPrototype(SessionManager.prototype, SessionManagerInterface);
            DBus.proxifyPrototype(Presence.prototype, PresenceInterface);
            this.manager = new SessionManager();
            this.presence = new Presence();
            ...
    
            var combo = ui.get_object("presenceStatus");
            cell = new Gtk.CellRendererText();
            combo.pack_start(cell);
            combo.add_attribute(cell, "text", 1);
    
            combo.signal.changed.connect(function(s) {
                var selected = {}
                s.get_active_iter(selected);
                var id = s.model.get_value(selected.iter, 0);
                self.presence.SetStatusRemote(id.value.get_int());
            });
    
            var textStatus = ui.get_object("textStatus");
            textStatus.signal.changed.connect(function(b) {
                self.presence.SetStatusTextRemote(textStatus.text);
            });
    
            var logout = ui.get_object("logOut");
            logout.signal.clicked.connect(function(b) {
                self.manager.LogoutRemoteSync(0);
            });
    
            var shutdown = ui.get_object("powerOff");
            shutdown.signal.clicked.connect(function(b) {
                self.manager.ShutdownRemoteSync();
            });
    
            var inhibit = ui.get_object("inhibit");
            inhibit.signal.toggled.connect(function(b) {
                if (inhibit.active == 1) {
                    inhibit.label = "Uninhibit";
                    var window = ui.get_object("window1");
                    var xid = window.get_window().get_xid();
                    self.inhibitCookie = self.manager.InhibitRemoteSync(applic ationId, xid, "I forbid you to logout", 1);
                } else {
                    self.manager.UninhibitRemoteSync(self.inhibitCookie);
                    inhibit.label = "Inhibit";
                }
            });
            window.show_all();
        }
    });
    
  2. 아니면 GtkBuilder를 지원하는 Vala 프로젝트를 생성하여 session-vala로 명명하라.
  3. src/Makefile.am에서 아래와 같은 행을 찾아라.
    session_vala_VALAFLAGS = \
        --pkg gtk+-3.0 --pkg gdk-x11-3.0
    
  4. 위의 행을 아래와 같이 편집하라.
    session_vala_VALAFLAGS = \
        --pkg gtk+-3.0
    
  5. 아래 코드와 같이 가장 중요한 부분이 포함된 src/session_vala.vala를 생성하라.
    using GLib;
    using Gtk;
    
    [DBus (name = "org.gnome.SessionManager")]
    interface SessionManager : GLib.Object {
        public abstract bool can_shutdown () throws IOError;
        public abstract void logout (uint32 mode) throws IOError;
        public abstract void shutdown () throws IOError;
        public abstract uint32 inhibit (string appId, uint32 xid, string reason, uint32 flags) throws IOError;
        public abstract void uninhibit (uint32 cookie) throws IOError;
    }
    [DBus (name = "org.gnome.SessionManager.Presence")]
    interface Presence: GLib.Object {
        public abstract void set_status_text (string text) throws IOError;
        public abstract void set_status (uint32 mode) throws IOError;
    }
    public class Main : Object
    {
        ...
        public Main ()
        {
                manager = Bus.get_proxy_sync(BusType.SESSION, "org.gnome.SessionManager", "/org/gnome/SessionManager");
                presence = Bus.get_proxy_sync(BusType.SESSION, "org.gnome.SessionManager", 
    "/org/gnome/SessionManager/Presence");
            ...
            var combo = builder.get_object("presenceStatus") as ComboBox;
            var cell = new CellRendererText();
            combo.pack_start(cell, true);
            combo.add_attribute(cell, "text", 1);
    
            combo.changed.connect((object) => {
                TreeIter iter;
                object.get_active_iter(out iter);
                Value value;
                object.model.get_value(iter, 0, out value);
                presence.set_status(value.get_int());
            });
    
            var textStatus = builder.get_object("textStatus") as Entry;
            textStatus.changed.connect(() => {
                presence.set_status_text(textStatus.text);
            });
    
            var logout = builder.get_object("logOut") as Button;
            logout.clicked.connect(() => {
                manager.logout(0);
            });
    
            var shutdown = builder.get_object("powerOff") as Button;
            shutdown.clicked.connect(() => {
                manager.shutdown();
            });
    
            var inhibit = builder.get_object("inhibit") as ToggleButton;
            inhibit.toggled.connect((object) => {
                if (object.active == true) {
                    object.label = "Uninhibit";
                    var window = builder.get_object("window1") as Window;
                    var xid = Gdk.X11Window.get_xid(window.get_window());
                    cookie = manager.inhibit("MyApplication", (uint32) xid, "I forbid you to logout", 1);
                } else {
                    manager.uninhibit(cookie);
                    object.label = "Inhibit";
                }
            });
        }
    }
    
  6. Glade로 UI를 생성하고 session.ui라고 불러라.
  7. Entry, ComboBox, 버튼 집합을 넣어라. 첫 번째 버튼은 ToggleButton 이고 나머지는 일반 버튼이다.
  8. Entry 에 textStatus를, ComboBox 에는 presenceStatus를, ToggleButton 에는 inhibit을, 나머지 Button 객체에는 logOutpowrOff 라는 이름을 부여하라.
  9. ComboBox 에서 타원형 버튼을 클릭하여 새로운 모델을 생성하고, 이를 liststore1이라 불러라.
  10. 제 8장 데이터 다루기에서 데이터 소스와의 상호작용 시 학습한 단계를 따라 두 개의 필드를 liststore1 로 넣어라. 첫 번째 데이터는 gint 로 표시되는 정수이고 두 번째는 gchararray 문자열이다.
  11. liststore1을 아래의 데이터 쌍으로 미리 채워라.
    0 Available
    1 Invisible
    2 Busy
    3 Idle
    
  12. presenceStatus 위젯으로 돌아가, Active 항목을 0 값으로 설정하라.
  13. UI를 저장하고 애플리케이션을 실행하라. 아래 스크린샷과 같은 모습이 보일 것이다.
    GNOME3 Chapter10 01.png
  14. 텍스트를 텍스트박스로 입력함으로써 상태 텍스트를 넣을 수도 있다. 콤보박스에서 텍스트를 선택함으로써 존재의 상태를 변경할 수도 있다. Log Out 버튼을 누르면 로그아웃을 동의하는 즉시 세션이 닫히며, Power Off 버튼 또한 동일하게 작용하는데, 차이점이 있다면 Power Off 는 컴퓨터를 종료한다는 점이다. Inhibit 버튼을 활성화하면 로그아웃과 종료 액션이 취소될 것이다.


무슨 일이 일어났는가?

우리는 D-Bus API를 이용해 GNOME 세션 관리자와 상호작용을 하였다. 첫 번째 단계는 프록시 객체를 생성하는 것이다. 이 객체는 D-Bus API와 이어주는 다리 역할을 한다. 따라서 Seed에서는 접근할 수 없는 실제 D-Bus API를 호출하는 대신 프록시를 통해 호출하는 것이다.


프록시를 준비하기 위해서는 프로토타입을 이용해 JavaScript 객체를 생성하기만 하면 된다. 객체에서 초기화 함수를 정의하면 이 함수는 DBus.session.proxifyObject 함수를 호출한다. 이 함수는 JavaScript 함수와 D-Bus 함수 호출을 연결한다.


이 실험에서 우리는 세션 관리자로부터 두 개의 API 집합으로 접근한다. 첫 번째 집합은 SessionManager이고, 두 번째는 Presence다. 즉, 애플리케이션에 두 개의 프록시 객체가 설치되어 있을 것이란 의미다.


첫 번째 프록시는 연결명, 즉 org.gnome.SessionManager, 그리고 함수가 정의 및 노출된 경로, 즉 /org/gnome/SessionManager 를 명시하여 생성된다.

function SessionManager() {
    this._init();
}

SessionManager.prototype = {
    _init: function() {
        DBus.session.proxifyObject(this,
            'org.gnome.SessionManager',
            '/org/gnome/SessionManager');
    }
}


이후 이 객체로 매핑하길 원하는 인터페이스를 구현한다. 인터페이스는 접근하고자 하는 D-Bus 세계로부터 모든 메서드, 시그널, 프로퍼티를 열거한다. 이번 예제에서는 목적을 달성하기 위해 연결명과 메서드만 정의한다. 메서드 설명에는 inSignature와 outSignature가 있음을 주목하라. inSignature는 우리가 함수로 전달하는 매개변수를 지칭하고, outSignature는 함수가 리턴하는 변수를 나타낸다.

var SessionManagerInterface = {
    name: "org.gnome.SessionManager",
    methods: [
        { name: 'CanShutdown', inSignature: '', outSignature: 'b' },
        { name: 'Logout', inSignature: 'u', outSignature: '' },
        { name: 'Shutdown', inSignature: '', outSignature: '' },
        { name: 'Inhibit', inSignature: 'susu', outSignature: 'u' },
        { name: 'Uninhibit', inSignature: 'u', outSignature: '' }
    ]
}


앞의 설명을 바탕으로 다섯 개의 함수, CanShutdown, Logout, Shutdown, Inhibit, Uninhibit를 갖는다. 이 함수들은 org.gnome.SessionManager 연결에서 정의된다.


Gnome3 notice header.png
연결에서 노출되는 함수라고 해서 모두 우리 프록시에서 정의되어야 할 필요는 없다. 사용하고자 하는 함수만 정의하면 된다.
Gnome3 notice footer.png


매개변수는 단일 문자열로 전달되는데, 각 문자는 D-Bus 규칙에 따른 변수 타입을 나타낸다. 아래 표는 D-Bus 명세 버전 0.19에서 복사한 타입을 열거한다.

일반적 이름 코드 설명
INVALID 0 (ASCII NUL) 유효하지 않은 타입 코드로, 시그니처를 종료하는 데에 사용되었다.
BYTE 121 (ASCII "y") 8-비트의 부호가 없는 정수.
BOOLEAN 98 (ASCII "b") Boolean값으로, 0은 FALSE를 의미하고 1은 TRUE를 의미한다. 나머지는 모두 유효하지 않은 값이다.
INT16 110 (ASCII "n") 16-비트의 부호가 있는 정수.
UINT16 113 (ASCII "q") 16-비트의 부호가 없는 정수.
INT32 105 (ASCII "i") 32-비트의 부호가 있는 정수.
UINT32 117 (ASCII "u") 32-비트의 부호가 없는 정수.
INT64 120 (ASCII "x") 64-비트의 부호가 있는 정수.
UINT64 116 (ASCII "t") 64-비트의 부호가 없는 정수.
DOUBLE 100 (ASCII "d") IEEE 754 double.
STRING 115 (ASCII "s") UTF-8 문자열(유효한 UTF-8이어야 한다). null로 끝나야 하고, 다른 null 바이트는 포함해선 안 된다.
OBJECT_PATH 111 (ASCII "o") 객체 인스턴스명.
SIGNATURE 103 (ASCII "g") 타입 시그니처.
ARRAY 97 (ASCII "a") 배열.
STRUCT 114 (ASCII "r"),
40 (ASCII "("),
41 (ASCII ")")
구조체, 타입 코드 114 "r"은 바인딩과 구현에서 구조체의 일반적 개념을 표현할 때 사용되도록 예약(reserved)되며, D-Bus에서 사용되는 시그니처에 표시되어선 안 된다.
VARIANT 118 (ASCII "v") Variant 타입(값의 타입이 값 자체의 일부).
DICT_ENTRY 101 (ASCII "e"),
123 (ASCII "{"),
125 (ASCII "}")
dict 또는 map의 엔트리 (키-값 쌍). 타입 코드 101 "e"는 바인딩과 구현에서 dict 또는 dict 엔트리의 일반적 개념을 표현하기 위해 사용되도록 예약되며, D-Bus에서 사용되는 시그니처에 표시되어선 안 된다.
UNIX_FD 104 (ASCII "h") Unix 파일 기술자.
(reserved) 109 (ASCII "m") GVariant의 것과 호환되는 "maybe" 타입을 위해 예약되며, 이곳에서 명시되기 전에는 D-Bus에서 사용되는 시그니처에 표시되어선 안 된다.
(reserved) 42 (ASCII "*") 바인딩과 구현에서 하나의 완전한 타입을 표현하기 위해 사용되도록 예약되며, D-Bus에서 사용되는 시그니처에 표시되어선 안 된다.
(reserved) 63 (ASCII "?") 바인딩과 구현에서 기본 타입을 표현하기 위해 사용되도록 예약되며, D-Bus에서 사용되는 시그니처에 표시되어선 안 된다.
(reserved) 64 (ASCII "@"),
38 (ASCII "&"),
94 (ASCII "^")
바인딩과 구현에서 내부적 사용을 위해 예약되며, D-Bus에서 사용되는 시그니처에 표시되어선 안 된다. GVariant는 이러한 타입 코드를 이용해 호출 규칙을 인코딩한다.


두 번째 프록시 객체는 Presence API를 위한 객체다. API는 텍스트와 수치 상태를 모두 설정하는 데에 사용된다. 수치 상태는 아래의 상수와 같다.

  • 0 은 이용 가능한 상태
  • 1 은 보이지 않는(invisible) 상태
  • 2 는 바쁨(busy) 상태
  • 3 은 유휴(idle) 상태


Gnome3 notice header.png
API의 전체 명세는 http://people.gnome.org/~mccann/gnome-session/docs/gnome-session.html 에서 읽을 수 있다.
Gnome3 notice footer.png


API는 org.gnome.SessionManager.Presence 연결에서 정의된다.

Presence.prototype = {
    _init: function() {
        DBus.session.proxifyObject(this,
            'org.gnome.SessionManager',
            '/org/gnome/SessionManager/Presence');
    }
}


인터페이스는 두 개의 함수만 필요하기 때문에 더 짧은데, 하나는 수치 상태용 SetStatus이고, 나머지 하나는 텍스트 상태용 SetStatusText이다.

var PresenceInterface = {
    name: "org.gnome.SessionManager.Presence",
    methods: [
        { name: 'SetStatus', inSignature: 'u', outSignature: '' },
        { name: 'SetStatusText', inSignature: 's', outSignature: '' },
    ]
}


프록시 객체로 함수를 매핑한 후에는 보통처럼 함수를 호출할 수 없다. Seed는 셋업 과정에서 인터페이스에 정의한 모든 함수 이름 뒤에 Remote와 RemoteSync를 추가한다. 따라서 애플리케이션은 뒤에 Remote 또는 RemoteSync가 붙은 함수명을 호출한다. RemoveSync 버전은 동기식 호출인 반면 Remote 호출은 함수와 비동기식으로 이루어진다.


셋업 과정은 아래와 같이 proxifyPrototype 함수를 호출하면 실행된다.

DBus.proxifyPrototype(SessionManager.prototype, SessionManagerInterface);
DBus.proxifyPrototype(Presence.prototype, PresenceInterface);


즉, 인터페이스 객체에 정의된 인터페이스를 바탕으로 Seed가 Remote와 RemoteSync 함수 호출을 프로토타입에서 채울 것이란 의미다. 제 3장, 프로그래밍 언어에서 학습하였듯이 JavaScript에서는 함수를 객체의 member로 추가할 수 있다.


다음은 일부 변수를 새로 생성된 프록시 객체의 인스턴스가 되도록 초기화한다.

this.manager = new SessionManager();
this.presence = new Presence();


제 8장, 데이터 다루기에서 데이터 소스를 처리하면서 데이터를 유지하기 위한 모델로 ListStore를 이용한 적이 있다. 이를 다시 구현해보자. 하지만 TreeView를 사용하는 대신 ComboBox를 이용할 수 있다. ComboBox 또한 MVC 디자인 패턴을 사용하며 ListStore를 모델로 이용한다. TreeView와 마찬가지로 화면에 사실상 데이터를 표시하기 위해선 렌더러가 필요하다. 다시 한 번 아래와 같은 코드 조각에 표시된 바와 같이 CellRendererText를 이용해 해당 작업을 실행한다.

var combo = ui.get_object("presenceStatus");
cell = new Gtk.CellRendererText();
combo.pack_start(cell);
combo.add_attribute(cell, "text", 1);


다음으로 ComboBox의 changed 시그널을 핸들러로 연결한다. 이 핸들러에서 우리가 하는 일은 ComboBox의 현재 Iter 객체를 얻고, iter 객체의 정수 값을 얻으며, Presence 프록시 객체의 SetStatusRemote를 호출하는 일이다. 앞서 논했듯이 Seed 코드에서 SetStatusRemote 함수는 사실상 D-Bus 측에서 SetStatus 함수다. 정수는 SetStatusRemote에서 전달하는데, 이는 인터페이스 객체에 대한 SetStatus 함수의 inSignature member에서 정의하였던 u의 값과 일치한다.

combo.signal.changed.connect(function(s) {
    var selected = {}
    s.get_active_iter(selected);
    var id = s.model.get_value(selected.iter, 0);
    self.presence.SetStatusRemote(id.value.get_int());
});


그 다음으로 textStatus 위젯의 변경된 시그널을 연결하고, 시그널을 수신하면 SetStatusTextRemote 함수를 호출한다. 이후 문자열에 해당하는 textStatus.text를 전달하는데, 이는 인터페이스 객체에서 정의한 SetStatus 함수의 inSignature에서 정의한 s 값과 일치한다.

var textStatus = ui.get_object("textStatus");
textStatus.signal.changed.connect(function(b) {
    self.presence.SetStatusTextRemote(textStatus.text);
});


리스트에서 다음으로 살펴볼 위젯은 logOut 위젯이다. LogoutRemoteSync를 호출함으로써 0 값을 넣어 clicked 시그널을 처리한다. GNOME Session Manager 문서에서 0은 단순히 로그아웃을 원한다는 의미다. 1은 프롬프트 없이 로그아웃 프로세스가 완료되었음을 의미한다. 마지막으로 값이 2인 경우 프롬프트 없이 모든 방해요소(inhibitor)를 무시하고 강제로 로그아웃이 이루어졌음을 뜻한다.

var logout = ui.get_object("logOut");
logout.signal.clicked.connect(function(b) {
    self.manager.LogoutRemoteSync(0);
});


다음으로는 powerOff 위젯을 살펴보겠다. 이 버튼을 누르면 종료(shutdown) 프로세스가 시작되길 원한다. 이를 위해선 ShutdownRemoteSync 함수를 호출한다.

var shutdown = ui.get_object("powerOff");
shutdown.signal.clicked.connect(function(b) {
    self.manager.ShutdownRemoteSync();
});


마지막으로 inhibit ToggleButton을 처리해야 한다. 버튼이 활성화되었을 때와 일반 상태일 때, 두 가지 상태를 처리한다. 활성화 상태에서는 라벨을 Unhibit으로 변경하여 행위가 변경되었음을 표시하길 원하고, SessionManager에게 우리를 방해요소(inhibitor)로 등록하길 요청한다. 방해요소가 있으면 어떠한 로그아웃 프로세스든 취소될 것이다. 이를 위해서는 InhibitRemoteSync를 호출한다. 리턴값은 우리가 inhibit으로 전달해야 하는 쿠키다. 이 쿠키는 ToggleButton과 반대의 상태로 UninhibitRemoteSync를 호출할 때 사용한다. 호출이 완료되면 로그아웃 프로세스는 다시 정상으로 돌아갈 것이다.

var inhibit = ui.get_object("inhibit");
inhibit.signal.toggled.connect(function(b) {
    if (inhibit.active == 1) {
        inhibit.label = "Uninhibit";
        var window = ui.get_object("window1");
        var xid = window.get_window().get_xid();
        self.inhibitCookie = self.manager.InhibitRemoteSync(application Id, xid, "I forbid you to logout", 1);
    } else {
        self.manager.UninhibitRemoteSync(self.inhibitCookie);
        inhibit.label = "Inhibit";
    }
});


이제 Vala 코드를 살펴보자. 코드가 어떻게 작동하는지는 Seed 코드와 동일하지만 D-Bus 함수를 어떻게 호출하는지를 살펴보자. 먼저 사용하길 원하는 메서드와 클래스의 인터페이스를 생성할 필요가 있다.


앞서 언급하였던 DBus 속성에 연결명이 따라온다.

[DBus (name = "org.gnome.SessionManager")]


인터페이스를 정의하고 GLib.Object를 파생하여 시그널도 처리할 수 있도록 한다. 각 메서드마다 camel case(각 단어의 첫 문자는 대문자로 쓰고 단어는 구분 문자 없이 모두 결합된다)로 쓴 원래 함수명을 밑줄이 결합된 소문자 단어로 변환한다. 가령 CanShutdown 함수는 can_shutdown으로 변환되어야 한다. 각 메서드는 IOError 예외를 던지도록 선언되어야 하며, 인자는 Vala 네이티브 데이터 타입을 이용해 Vala 스타일로 쓰여져야 한다.

interface SessionManager : GLib.Object {
    public abstract bool can_shutdown () throws IOError;
    public abstract void logout (uint32 mode) throws IOError;
    public abstract void shutdown () throws IOError;
    public abstract uint32 inhibit (string appId, uint32 xid, string reason, uint32 flags) throws IOError;
    public abstract void uninhibit (uint32 cookie) throws IOError;
}


인터페이스를 사용하기 전에 인터페이스를 모두 선언하므로 Presence 인터페이스와 SessionManager는 아래와 같이 선언되어야 한다.

[DBus (name = "org.gnome.SessionManager.Presence")]
interface Presence: GLib.Object {
    public abstract void set_status_text (string text) throws IOError;
    public abstract void set_status (uint32 mode) throws IOError;
}


다음으로 이러한 인터페이스의 프록시 객체를 생성할 필요가 있다. get_proxy_sync를 사용해 이러한 인터페이스의 경로와 연결명을 매핑한다. null을 제외한 결과는 사용할 준비가 된 것이다.

manager = Bus.get_proxy_sync(BusType.SESSION,
    "org.gnome.SessionManager",
    "/org/gnome/SessionManager");

presence = Bus.get_proxy_sync(BusType.SESSION,
    "org.gnome.SessionManager",
    "/org/gnome/SessionManager/Presence");


지금쯤이면 manager 객체와 presence 객체는 D-Bus 객체로 연결될 준비가 되었을 것이다.


Seed에 비해 Vala에서 메서드를 호출하기는 매우 간단하다. 함수명을 직접 호출해 인자가 있다면 인자를 전달하면 될 일이다. 예를 들어 shutdown 함수는 아래의 코드 조각만으로 호출할 수 있겠다.

manager.shutdown();


시도해보기 - null 확인하기

앞서 언급했듯이 모든 프록시 객체가 D-Bus로 연결되도록 보장되는 것은 아니다. 예를 들어, 특정 연결이나 경로의 서비스 제공자가 설치되어 있지 않으면 연결은 실패하고 변수값은 null이 될 것이다.


우리 코드를 다시 살펴보고 프록시 객체의 발생마다 확인해보길 바란다. 값이 null이 아니면 코드는 그대로 사용해도 좋지만 null 이라면 어떤 조치든 취해야 한다. 이러한 경우를 처리할만한 전략을 세워보라.


런처

런처는 사용자가 아이콘을 클릭하여 애플리케이션을 실행시키는 장소다. GNOME Shell에서 런처는 Activities 메뉴의 Applications 탭에서 접근할 수 있다. 애플리케이션은 범주에 따라 열거되고, 애플리케이션의 아이콘, 제목, 설명을 표시한다. 제목과 설명은 잘 구분되어(localized) 있다. 애플리케이션은 검색을 통해 필터링할 수도 있다.


GNOME의 이전 버전이나 GNOME Fallback 모드에서는 런처가 GNOME 패널에 위치한다. 애플리케이션은 역시나 범주적으로 열거되며 설명 또한 구분되어 있다.


제 9장, GNOME을 통해 HTML5 애플리케이션 활용하기에서 애플리케이션을 열거하는 방식을 살펴봤지만 GNOME Shell이나 GNOME 패널과 정확히 같지는 않다. 실험에서는 데스크톱 파일에 포함된 정보를 로딩하는 작업이 꼭 필요했다. 따라서 애플리케이션의 아이콘을 런처에 표시되도록 만들기 위해서는 그것을 위한 데스크톱 파일을 생성할 필요가 있겠다.


실행하기 - 애플리케이션을 런처에 넣기

이제 앞의 애플리케이션에 런처를 생성해보자.

  1. session.js를 session-tester로 재명명하라 (확장자 없이).
  2. /usr/share/session-manager-test/session.ui를 열기 위해 UI 로더 부분을 수정하고, 아래의 행을 수정하라.
    ui.add_from_file("session.ui");
    
  3. 위의 행을 아래로 수정하라.
    ui.add_from_file("/usr/share/session-manager-test/session.ui");
    
  4. 새로운 텍스트 파일을 준비해 session-manager-test.desktop으로 명명하고 아래의 코드로 채워라.
    [Desktop Entry]
    Name=Session Manager Test
    Comment=Testing the interaction with GNOME session manager
    OnlyShowIn=GNOME;
    Exec=session-tester
    Icon=help-browser
    StartupNotify=true
    Terminal=false
    Type=Application
    Categories=GNOME;GTK;Settings
    
  5. UI 파일을 /usr/share/session-manager-test 디렉터리에 설치하고 (디렉터리를 생성하지 않았다면 지금 생성하라!), 데스크톱 파일은 /usr/share/applications에, session-tester 스크립트는 /usr/bin에 설치하라.
  6. GNOME Shell 에서 Activities 메뉴를 열고 Applications 탭에서 Session Manager Test 를 찾아라!


GNOME3 Chapter10 02.png


무슨 일이 일어났는가?

지금까지 시스템 전체 설정에서 UI 파일을 로딩하도록 스크립트를 간단히 수정해보았다. 이러한 과정은 애플리케이션이 배포된 곳마다 실행되어야 하는데, 이 예제는 UI 파일의 경로를 하드코딩하기 때문에 바람직한 예제는 아니다. 경로의 하드코딩을 피하는 한 가지 방법은 설정 파일을 보관하는 것이다. 제 4장, GNOME 코어 라이브러리 사용하기로 돌아가 설정 시스템으로 접근해보자.


그 다음 아래를 이용해 데스크톱 파일을 생성한다.

[Desktop Entry]


이후 애플리케이션명을 명시한다. 그 다음은 런처에 표시될 텍스트를 명시한다.

Name=Session Manager Test
Comment=Testing the interaction with GNOME session manager


OnlyShowIn은 해당 애플리케이션이 표시될 런처를 정의한다. 가령 Unity를 추가하면 애플리케이션은 Unity와 GNOME에서만 표시될 것이다.

OnlyShowIn=GNOME;


다음으로 실행 파일명을 정의한다. /usr/bin으로 설치하는 애플리케이션명은 다음과 같다.

Exec=session-tester


이후 런처에서 사용되는 아이콘을 정의한다. 지금은 help-browser 애플리케이션에서 아이콘을 훔치겠다. 실제 애플리케이션에서는 우리만의 아이콘을 제공해야 한다.

Icon=help-browser
StartupNotify=true


그리고 나면 애플리케이션을 실행하는 데에 terminal이 필요하지 않음을 명시한다. 가령, bash 스크립트를 제공할 경우, hashbang이 없거나 실행 파일의 권한이 누락되면 terminal에게 해당 스크립트를 열도록 요청해야 할 수도 있다.

Terminal=false


다음으로 애플리케이션을 Application으로 정의한다.

Type=Application


마지막으로 애플리케이션을 이러한 범주로 나누어야 함을 런처에게 알린다. 애플리케이션이 Settings로 매핑하는 System Tools 에 상주함을 런처에서 간단히 확인할 수 있다.

Categories=GNOME;GTK;Settings


런처는 보통 /usr/share/applications 내의 모든 파일에 적용시킨 변경내용을 전부 듣는다. 따라서 새로운 파일을 넣거나 기존 파일을 수정할 때마다 변경내용은 즉시 런처로 자동 표시된다고 할 수 있기 때문에 데스크톱을 재시작할 필요가 없다.


데스크톱 파일의 내용에 대한 형식 명세서는 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html 에서 찾을 수 있다.


GNOME Keyring

실제 애플리케이션을 사용할 때는 비밀번호, 비밀 데이터 또는 키를 저장하는 경우가 많다. 이러한 유형의 데이터 저장공간을 구현하기란 까다로우며, 보안 분야에 특별한 기술을 요한다. GNOME Keyring은 GNOME 플랫폼에서 이용할 수 있는 비밀 데이터 저장공간 구조에 해당한다. GNOME Keyring을 사용하는 애플리케이션은 비밀번호, 비밀 데이터 또는 키를 키링으로 저장하여 추후 필요할 때 검색할 수 있다.


키링은 보호되어 있으며 사용자 계정과 연계된다. 사용자가 시스템으로 로긴할 때마다 자동으로 애플리케이션에서 키링을 이용하고 사용자가 로그아웃하면 키링 또한 닫히도록 설정할 수도 있다. 아니면 비밀번호를 이용해 키링을 열 수도 있다.


키링에는 Seahorse 라고 불리는 애플리케이션이 따라온다. 이 애플리케이션은 저장된 비밀 데이터를 사용자의 세션 내에서 표시한다. 우리가 목표로 한 통합은 Seahorse를 대체하는 것이 아니라 키링에 데이터를 안전하게 보관하는 것이다. 하지만 Seahorse 를 이용해 데이터를 검사할 수는 있겠다.


실행하기 - 비밀번호를 안전하게 저장하기

불행히도 비밀번호를 안전하게 저장하기 위해서는 Vala를 이용해야만 하는데, Seed가 필요로 하는 gir-1.2에는 비밀번호를 쉽게 저장하는 데에 필요한 함수가 포함되어 있지 않기 때문이다.

  1. GtkBuilder 없이 빈 Vala 객체를 새로 생성하고 keyring이라 부른다.
  2. configure.ac를 열고 PKG_CHECK_MODULES(KEYRING, [gtk-3.0 ])를 찾아라. 전체 행을 아래의 행으로 수정하라.
    PKG_CHECK_MODULES(KEYRING, [gnome-keyring-1 ])
    
  3. src/Makefile.am을 열고 아래의 행을 찾아라.
    keyring_SOURCES = \
        keyring.vala config.vapi
    
    keyring_VALAFLAGS = \
        --pkg gtk+-3.0
    
  4. 위의 내용을 모두 아래의 내용으로 수정하라.
    keyring_SOURCES = \
        keyring.vala config.vapi gnome-keyring.vapi
    
    keyring_VALAFLAGS = \
        --pkg gnome-keyring-1
    
  5. src/ 디렉터리에 새로운 파일을 생성하고 gnome-keyring.vapi라고 명명한 다음 아래의 내용으로 채워라.
    [CCode (cprefix = "GnomeKeyring", lower_case_cprefix = "gnome_keyring_")]
    namespace GnomeKeyringOverrides {
    [Compact]
    public struct PasswordSchemaAttribute {
        public unowned string name;
            public GnomeKeyring.AttributeType type;
                }
    
                [Compact]
                [CCode (cheader_filename = "gnome-keyring.h")]
                public struct PasswordSchema {
                    public GnomeKeyring.ItemType item_type;
                    [CCode(array_length = false)]
                    public PasswordSchemaAttribute[] attributes;
                }
                [CCode (cheader_filename = "gnome-keyring.h")]
                public static void* store_password (GnomeKeyringOverrides.
    PasswordSchema schema,
    string? keyring, string display_name, string password, owned
    GnomeKeyring.OperationDoneCallback callback, ...);
    
                [CCode (cheader_filename = "gnome-keyring.h")]
                public static void* find_password (GnomeKeyringOverrides.
    PasswordSchema schema,
    owned GnomeKeyring.OperationGetStringCallback callback, ...);
    }
    
  6. src/keyring.vala를 열고 아래의 코드를 이용하라.
    using GnomeKeyring;
    
    public class Main : Object
    {
        private const GnomeKeyringOverrides.PasswordSchema secretData =
    {
            ItemType.GENERIC_SECRET,
            {
                { "name", AttributeType.STRING },
                { null, 0}
            }
        };
    
        public void returning_password_callback(Result result, string? password) {
            if (result == Result.OK) {
                stdout.printf ("Password is: %s\n", password);
            } else {
                stdout.printf ("Failed, code: %d\n",(int) result);
            }
        }
    
        public void store_password_callback(Result result) {
            if (result == Result.OK) {
                GnomeKeyringOverrides.find_password ( secretData, returning_password_callback, "name", "myuser", null);
            } else {
                stdout.printf ("Failed, code: %d\n",(int) result);
            }
        }
    
        public Main ()
        {
            GnomeKeyringOverrides.store_password ( secretData, null, "My Application Password", "this-is-a-password", 
    store_password_callback, "name", "myuser", null);
        }
    
        static int main (string[] args)
        {
    
            var app = new Main ();
    
            var loop = new MainLoop();
                loop.run ();
            return 0;
        }
    }
    
  7. 애플리케이션을 빌드하되 아직 실행하지 말라!
  8. GNOME Shell에서 Activities 메뉴를 열고 Seahorse 애플리케이션을 찾아 실행하라. 안에 몇 개의 엔트리가 보일 것인데 (어쩌면 비어있을 수도 있다), My Application Password 란 이름의 엔트리는 보이지 않을 것이다. Seahorse를 종료하라.
  9. 이제 애플리케이션을 실행하면 아래를 표시할 것이다.
    Password is: this-is-a-password
    
  10. 계속해서 실행되지만 조금 지나면 Ctrl+C 키조합을 이용해 종료할 수 있다.
  11. Seahorse를 다시 실행하면 My Application Password 라는 엔트리가 보일 것이다. 엔트리를 클릭하면 비밀번호, 즉 this-is-a-password 를 볼 수 있다.
GNOME3 Chapter10 03.png


무슨 일이 일어났는가?

사용자 비밀번호를 GNOME Keyring 시스템으로 저장한 것이다. 이 비밀번호는 앞절에서 애플리케이션을 이용해 보인 바와 같이 후에 필요할 때 복구할 수 있다. 웹 브라우저에서 "비밀번호 기억(remember password)" 기능을 사용하는가? 이 기능을 사용하면 브라우저는 비밀번호 필드를 저장된 비밀번호로 미리 채워 놓는다. 하지만 이번 예제에서는 비밀번호를 자신만 알고 숨겨두는 대신 GNOME Keyring에 저장하는 셈이 된다.


안타깝게도 많이 사용되는 배포판에 배포되는 GNOME Keyring에 대한 Vala 함수 및 클래스 매핑은 올바르지 않은 정보를 갖고 있기 때문에 그대로 사용할 수 없는 노릇이다. 제 9장, GNOME을 통해 HTML5 활용하기에서는 원본 .vapi 파일을 커스텀 .vapi 파일로 대체하는 방법을 보인 적이 있다. 하지만 지금은 전체 파일을 대체하는 대신 올바르지 않은 정보를 수정하는 오버라이드 파일을 넣는다. gnome-keyring.vapi 라고 부르는 오버라이드 파일은 앞서 실행하기 - 비밀번호 안전하게 저장하기 절의 4번 단계에서 살펴본 것처럼 빌드 단계에 포함된다.


오버라이드 파일의 내용은 올바르지 않게 작성된 원본 버전을 대체하는 함수 및 클래스 member에 해당한다. 이번 예제에서는 store_password와 find_password 함수, 그리고 PasswordSchema 구조체에 대한 오버라이드를 갖고 있다. 원본 버전을 무작정 대체하기보다는 다른 네임스페이스를 이용할 필요가 있다. 이번 예제에서는 GnomeKeyringOverrides 네임스페이스를 사용한다. 따라서 원본 버전을 사용하길 원할 때마다 GnomeKeyring을 사용하고, 오버라이드된 버전을 사용하고 싶으면 GnomeKeyringOverrides 네임스페이스를 사용한다.


store_password와 find_password 함수를 사용하기 전에는 보관하길 원하는 데이터의 구조체를 정의해야 한다. 아래 코드 조각에서 우리는 secretData라고 부르는 구조체를 설명한다. 데이터에는 name이라는 하나의 필드만 있다. 이 데이터를 이용해 우리는 이름과 연관된 비밀번호를 저장하길 원한다. null과 0의 쌍을 구조체의 끝으로 표시하는 구조체를 종료한다.

private const GnomeKeyringOverrides.PasswordSchema secretData = {
    ItemType.GENERIC_SECRET,
    {
        { "name", AttributeType.STRING },
        { null, 0}
    }
};


앞의 예제에서는 비동기식 버전의 저장소를 사용해 비밀번호 함수를 찾는다. 즉, 이 함수를 호출하면 즉시 함수로부터 리턴한다는 뜻이다. 함수의 성공은 다른 콜백 함수에서 처리한다. 동기식 버전을 (함수명이 sync로 끝난다) 선택하였는데 GNOME Keyring 측에서 부담스러운 무언가가 실행 중이라면 연산이 완료될 때까지 애플리케이션이 freeze할 것인데, 이러한 일은 비동기식 호출에선 발생하지 않을 것이다.


앞에서 언급했듯 몇 가지 콜백 함수를 준비해야 한다. 첫 번째는 find_password 함수를 위한 것이다. result 변수에 결과를 얻고 password 변수에서 비밀번호를 얻는다. 비밀번호 타입 정의에서 물음표는 내용이 null일 수 있다는 의미임을 명심하라. 결과가 Result.OK 이외의 값일 때마다 이런 일이 발생한다. 함수는 실패 시 비밀번호 또는 오류 코드를 출력하기 위해 사용된다. 실제 애플리케이션에서는 리턴된 비밀번호를 필더 또는 비밀번호 내용을 필요로 하는 다른 요소로 넣을 것이다.

public void returning_password_callback(Result result, string?  password) {
    if (result == Result.OK) {
        stdout.printf ("Password is: %s\n", password);
    } else {
        stdout.printf ("Failed, code: %d\n",(int) result);
    }
}


다음으로 store_password 함수를 위한 콜백 함수가 있다. 이는 result 값을 확인하는데, 성공적인 값이면 find_password 함수로 비밀번호를 얻기를 시도하면 된다. 실제 애플리케이션에서는 비밀번호가 저장되었다는 알림을 표시하거나 아니면 조용히 다른 실행을 할 것이다.

public void store_password_callback(Result result) {
    if (result == Result.OK) {
        GnomeKeyringOverrides.find_password ( secretData, returning_password_callback, "name", "myuser", null);
    } else {
        stdout.printf ("Failed, code: %d\n",(int) result);
    }
}


메인 함수에서는 My Application Password 라고 불리는 영역에 this-is-a-password 비밀번호를 저장한다. 비밀번호를 myuser 문자열과 연관시킨다. 실제 애플리케이션에서 이러한 연관은 확장이 가능하다. 가령 네트워크 비밀번호의 경우 필요한 추가 데이터를 서버명, 포트 번호, 서비스 경로 등으로 확장이 가능하다. 데이터의 끝을 표시하기 위해 함수는 null로 끝난다. 확장된 데이터 구조체가 있다면 구조체에 정의된 순서대로 필요한 데이터를 계속해서 함수의 매개변수로 전달하고 null로 끝내야 한다.

public Main ()
{
    GnomeKeyringOverrides.store_password ( secretData, null, "My Application Password", "this-is-a-password", 
store_password_callback, "name", "myuser", null);
}


알림 시스템

애플리케이션은 지금 발생 중인 이벤트에 대해 사용자에게 통지해야 하는 경우가 있다. 애플리케이션이 활성화되었고 사용자가 현재 사용 중이라면 간단히 정보를 애플리케이션 안에 표시할 수 있다. 하지만 애플리케이션이 현재 실행 중이지만 바탕화면에서(in the background) 유지되거나 크기가 최소화되었다면 어떤 일이 발생할까? 사용자가 그 정보를 보지 못하는 일이 발생할 것이다. 이는 바람직하지 못하며, 특히 매우 중요한 정보를 표시한다면 더 그러하다. 이 때를 대비해 알림 시스템을 사용해야 하는데, 사용자는 언제든지 사용할 수 있다.


GNOME 에는 libnotify 라는 알림 시스템이 있다. 메인 프로세스는 절대 종료되지 않고 데스크톱이 종료되어야만 종료되는 daemon 이라는 프로그램으로서 실행된다. 이 프로그램은 알림을 표시하라는 모든 애플리케이션의 요청을 듣는다. 요청을 수신하면 화면 하단에 알림 텍스트를 표시한다. Application은 단순히 라이브러리를 이용해 알림을 데몬으로 전송한다.


실행하기 - 알림 전송하기

몇 가지 알림을 전송해보자.

  1. notification.js라는 새로운 Seed 스크립트를 생성하라.
  2. 스크립트를 아래의 코드로 채워라.
    #!/usr/bin/env seed
    
    GLib = imports.gi.GLib;
    Notify = imports.gi.Notify;
    GObject = imports.gi.GObject;
    
    Main = new GType({
        parent: GObject.Object.type,
        name: "Main",
        init: function() {
            Notify.init('Test Application');
            var n = new Notify.Notification({
                summary: 'This is a notification text',
                body: 'This is a longer version of the notification text',
                });
            n.add_action('ok-button', 'OK', function() { n.close()});
            n.show();
        }
    });
    
    var main = new Main();
    var context = GLib.main_context_default();
    var loop = new GLib.MainLoop.c_new(context);
    loop.run();
    
  3. 이를 실행하라. 알림 텍스트의 끝이 잘려 표시되지 않음을 눈치챌 것이다. 화면 하단에 잠깐 동안 표시되는데, 텍스트로 마우스를 갖다 대면 텍스트 상자가 확장되어 모든 텍스트가 표시될 것이다. OK 버튼도 표시할 것이다. 하지만 해당 영역에서 벗어나자마자 텍스트는 사라질 것이다. OK 버튼을 누르지 않으면 알림은 화면의 하단 우측 모서리에 남아있다.
GNOME3 Chapter10 04.png


무슨 일이 일어났는가?

우리는 단순히 libnotify로 호출하는 일만 실행했을 뿐이다. 먼저 Notify로부터 가져오기를 실행함으로써 선언할 필요가 있다.

Notify = imports.gi.Notify;


libnotify로부터 함수를 호출하기 전에 먼저 init 함수를 호출해야 한다. 그렇지 않으면 어떤 함수도 성공적으로 호출할 수 없다.

Notify.init('Test Application');


다음으로 새로운 알림 객체를 생성한다. 각 객체마다 하나의 알림 텍스트를 보유할 수 있다. 생성자에 텍스트, 개요(summary), 본문(body)을 설정한다. summary 부분은 짧은 텍스트로, 화면에 처음 표시될 것이다. body 부분은 개요보다 조금 긴 텍스트인데, 알림 영역 안에 마우스 커서를 위치시키면 표시된다. Seed에서는 이 텍스트를 객체로 전달할 수 있다.

var n = new Notify.Notification({
    summary: 'This is a notification text',
    body: 'This is a longer version of the notification text',
});


다음으로 OK 버튼을 추가한다. 버튼이 클릭될 때마다 알림을 닫으면 된다. 액션은 아래 정의된 것처럼 익명(anonymous) 함수를 이용해 추가된다.

n.add_action('ok-button', 'OK', function() { n.close()});


알림을 표시하고 싶으면 show 함수를 호출하기만 하면 된다.

n.show();


알림은 프로그램이 닫혀도 알림 영역에 계속 남는데, OK 버튼을 눌러야만 닫을 수 있다.


시도해보기 - 아이콘 표시하기

알림 안에 아이콘을 표시해보자. 이는 구현하기가 매우 쉬운데, 생성자에서 전달하는 객체의 icon 필드에 아이콘명을 넣으면 끝이다.


요약

이번 장에서는 데스크톱의 몇 가지 중요한 부분, 즉 세션 관리, 런처, 키링, 알림 시스템으로의 통합에 대해 학습해보았다. D-Bus에 대해서도 소개했다. 또 일반적인 라이브러리 API 뿐만 아니라 D-Bus를 이용하는 기능으로 접근하는 방법도 논했다.


이제 로그아웃 또는 종료 프로세스를 시작할 수 있는 애플리케이션을 생성하는 방법과, 세션 관리자가 프로세스를 계속하지 못하도록 만드는 방법을 이해한다. /usr/share/applications 디렉터리에 데스크톱 파일을 설치함으로써 런처에 애플리케이션을 표시하는 방법도 학습했다. 민감한 데이터를 키링에 저장하고 필요 시 찾아오는 방법도 배웠다. 마지막으로, 데스크톱에 표시되는 알림의 표시 방법에 대해서도 논했다. 이러한 경험은 애플리케이션을 GNOME 데스크톱과 훌륭하게 통합되도록 만드는 데에 도움이 될 것이다.


또 많이 사용되는 배포판에서 제공되는 올바르지 않은 .vapi 파일 문제를 오버라이드 파일을 생성함으로써 해결하는 방법도 배웠다.


다음 장에서는 애플리케이션이 전세계에서 사용될 경우 집중해야 할 측면들을 논하고자 한다. 따라서 본인의 애플리케이션이 국제적 수준에서 성공하길 원한다면 놓쳐선 안 될 내용이다.


Notes