Canonical Voices

UbuntuTouch

在这篇文章里,我们创建一个小的RSS阅读器。当我们完成这整个过程,我们将学会如何使用最基本的控件来展示内容,并使用不同的layout。


developernews.png


让我们开始吧。

1)创建一个最基本的应用框架

首先,我们来打开自己的Qt Creator来创建一个名叫“developernews”的项目。我们使用"App with Simple UI"模版。


ubuntu-sdk-simple-project.png



如果你还没有安装好自己的SDK的话,请参照文章"Ubuntu SDK 安装"来完成自己的安装。我们可以直接运行已经创建好的应用。为了显示的更像一个是一个手机的界面,我们直接把“main.qml"中的尺寸设置如下:

    width: units.gu(50)
    height: units.gu(75)


分辨率无关

Ubuntu的用户界面工具包的重要功能是把用户定义多个设备尺寸进行匹配。采取的方法是定义一个新的单元类型,网格单元(GU在短)。网格单位转换为像素值取决于应用程序运行在屏幕上和设备的类型。下面是一些例子:

Device Conversion
Most laptops 1 gu = 8 px
Retina laptops 1 gu = 16 px
Smart phones 1 gu = 18 px
更多的关于分辨率无关的知识可以在链接找到。


运行效果(Ctrl + R) 如下图所示:




最原始的应用其实没有什么。你可以按一下按钮改变方框中的文字。下面我们来开始设计我们的应用。

2)删除我们不需要的文件及部分代码


由于最初的代码其实对我们来书没有多大的用处。我们现在来修改我们的代码:

1)删除在"main.qml"中不需要的代码,以使得代码如下图所示:



2)修改page中的title使之成为"Developer News"。
3)在项目中的"HelloComponent.qml"上点击右键,并删除该文件。

我们重新运行程序,我们将看到没有任何内容的应用



3)加入一个PageStack

PageStack可以使得我们让一个Page推到另外一个page之上。他可以跟踪这些页面的变化,并自动提供一个"back"的按钮可以让我回到以前的页面。

现在我们来使用PageStack来重新设计我们的应用。把整个在"main.qml"中的的Page代码换成如下的代码:

import QtQuick 2.0
import Ubuntu.Components 0.1
import "components"

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"
    
    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "com.ubuntu.developer.liu-xiao-guo.developernews"
    
    /*
     This property enables the application to change orientation
     when the device is rotated. The default is false.
    */
    //automaticOrientation: true
    
    width: units.gu(50)
    height: units.gu(75)
    
    PageStack {
        id: pageStack
        anchors.fill: parent
        Component.onCompleted: {
            console.log('pagestack created')
            pageStack.push(listPage)
        }
        
        Page {
            id: listPage
            title: i18n.tr("Articles")
        }
    }
}

这里,我们可以看到每个component在被装载完成之后,有一个event事件onCompleted被调用。我们可以用这个方法来初始化我们的一下需要处理的事情。这里,我们把listPage压入堆栈尽管没有任何东西。

这时如果我们重新运行程序,我们会发现界面没有任何新的变化。这是因为我们的page中没有任何的数据。我们在“Application Output”窗口会发现如下的输出:
pagestack created

这说明我们的代码是成功运行的。

4)加入我们自己的控件

我们将加入一个新的QML控件。这个控件的名称叫做“ArticleListView"。它将被定义在一个叫做"ArticleListView.qml"的文件中。控件的名字通常是以大写字母开始的。

     

我们点击项目的右键,加入一个名字叫做“ArticleListView.qml”文件。并把文件放入"components"的目录之中。在默认的情况下,"ArticeListView.qml"除了定义一个方框外,没有任何其他的东西。我们接下来向其中添加我们所需要的内容。

4)定义ArticleListView

我们将用UbuntuListView来显示来自http://developer.ubuntu.com的RSS 条目。UbuntuListView是继承于Qt中的ListView。但是它加入了一些新的一些feature。比如说pull-to-refresh。它也可以很方便地来搜索文字。下面我们来详细介绍如何做:

1)把"ArticleListView.qml"中的代码换成如下的代码:

import QtQuick 2.0
import QtQuick.XmlListModel 2.0
import Ubuntu.Components 1.1
import Ubuntu.Components.ListItems 1.0


UbuntuListView {
   id: listView
   property alias status: rssModel.status


   model: XmlListModel {
       id: rssModel
       query: "/rss/channel/item"
       XmlRole { name: "title"; query: "title/string()" }
       XmlRole { name: "published"; query: "pubDate/string()" }
       XmlRole { name: "content"; query: "*[name()='content:encoded']/string()" }
   }


   delegate: Subtitled {
       text: title
       subText: published
       progression: true
   }


   Scrollbar {
       flickableItem: listView
   }
}

这里你们可以看到我们定义了一个alias status的属性。对很多初学者来说,可能并不好理解。你可以理解为C语言中的指针虽然并不那么确切。它实际上就是把一个component中的其他item中的属性暴露出来以使得该属性在这个component(比如UbuntuListView)被引用时可以被修改或被引用。

你的第一个挑战。在XmlListModel中我们必须定义我们所需要的feed。对developer.ubuntu.com的RSS的地址是http://developer.ubuntu.com/feed/。为了能够在我们的应用中使用它,我们必须在我们的例程中定义它。具体的property,请参考API documentation of XmlListModel。tips:大家可以查看"source"属性。我们在浏览器中输入地址developer.ubuntu.com/feed,我们可以看到如下的内容



5)使用ArticleListView

我们在上节中已经设计了一个自己的component。在这节中,我们来使用它,就像原本我们已有的其他的控件一样。我们把我们设计好的ArticleListView放到我们已经设计好的“main.qml”中来。在你的main.qml中加入如下的Page:

           ArticleListView {
               id: articleList
               objectName: "articleList"
               anchors.fill: parent
               clip: true
           }

重新运行我们的应用。我们可以看到我们几乎快成功了。我们可以看到来自developer.ubuntu.com的文章列表了。如果你还没有看到这个运行的结果。请查看一下你的XmlListModel中的source是否已经设置正确。


6)创建一个新的Component

就像上面我们创建的ArticleListView一样,我们来创建一个新的ArticleContent的component。该component的文件名字叫做"ArticleContent.qml"。文件位于和ArticleListView一样的路经(components)。
下面我们来向这个新创建的component中加入我们所需要的内容。打开文件"component/ArticleContent.qml",并输入如下的代码:

import QtQuick 2.0
import Ubuntu.Components 1.1

Item {
   property alias text: content.text

   Flickable {
       id: flickableContent
       anchors.fill: parent

       Text {
           id: content
           textFormat: Text.RichText
           wrapMode: Text.WordWrap
           width: parent.width
       }

       contentWidth: parent.width
       contentHeight: content.height
       clip: true
   }


   Scrollbar {
       flickableItem: flickableContent
   }
}


这里我们可以看到,我们创建了一个最基本的继承于Item的component。我们可以在Text网址找到更多关于Text的一些属性以更好地使用它。

7)把ArticleContent和app的其它内容连起来


到目前为止,我们已经创建了一个ArticleContent的控件。我们可以在我们的应用中使用它。每当一个在ArticleListView中的一个item被点击后,我们可以用它来显示详细的内容。

首先,我们必须在ArticleListView中每个item被点击时生成一个signal,并把这个signal连接到我们需要产生的动作。我们可以定义一个名叫"clicked"的signal。

1)打开"ArticleListView.qml"文件,并定义如下的signal:

   signal clicked(var instance)

2) 在“Subtitled"项加入如下的代码:

onClicked: listView.clicked(model)



3)使用我们已经创建好的ArticleContent控件。我们在"main.qml"文件中创建一个新的Page,并使用PageStack。

    PageStack {
        id: pageStack
        anchors.fill: parent
        Component.onCompleted: {
            console.log('pagestack created')
            pageStack.push(listPage)
        }

        Page {
            id: listPage
            title: i18n.tr("Articles")

            ArticleListView {
                id: articleList
                objectName: "articleList"
                anchors.fill: parent
                clip:true

                onClicked: {
                    console.log('[flat] article clicked: '+instance.title)
                    articleContent.text = instance.content
                    pageStack.push(contentPage)
                }

            }
        }

        Page {
            id: contentPage
            title: i18n.tr("Content")

            ArticleContent {
                id: articleContent
                objectName: "articleContent"
                anchors.fill: parent
            }
        }
    }



我们这时运行程序,可以看到如下的图片。我们可以点击在主界面中的item,并查看具体的内容:

     

至此我们已经完成了第一个阶段的代码。整个程序的代码可以在如下的网址看到。

bzr branch lp:~liu-xiao-guo/debiantrial/developernews

8)添加一个reload按钮


在这里,我们想添加一个“reload"的按钮,这样我们可以随时查看最新的在developer.ubuntu.com的内容。这里我们来定义一个"reload"方法以使得它能在“main.qml"文件中被调用。它的作用是使得XmlListModel中的refresh方法被调用。

1)打开"components/ArticleListView.qml"文件,加入如下的方法到UbuntuListView中去:

   /*
      Functions can be added to any Component definition, and can be called on
      using any instance of that Component. Here we will define a 'reload'
      function that we can call from our main application file that will cause
      the interal XmlListModel to reload it's content
    */
    function reload() {
        console.log('reloading')
        rssModel.update()
    }

挑战自己:请查看XmlListModel。你将发现上面的"update()"方法其实不是真正的方法。请找出正确的方法替换它。

2)在MainView的定义中加入如下的Action

  Action {
       id: reloadAction
       text: "Reload"
       iconName: "reload"
       onTriggered: articleList.reload()
   }
Action是一个可以重复使用的控件,并可以在多处被引用
在这里定义并给于其一个id使得它可以在多处被引用。


3) 在"listPage"中,加入如下的ToolbarButton。

tools: ToolbarItems {
               ToolbarButton {
                   action: reloadAction
               }
           }
在article的contengPage,我们想显示一个toolbar按钮
在browser中打开该文章。因为我们已经定义好了一个
可以重复使用的的Action,我们只需要引用它的即可

重新运行应用,我们可以看到如下的画面。你们可以尝试点击"reload"看看有什么反应。


5.png


9)玩一玩应用的一些属性

虽然目前我们的应用已经完成了我们大部分的所需要的功能。在这里,我们来尝试修改应用的一些属性来看看有什么一些变化。

1)定义应用MainView的id以使得我们在下面的章节中被引用

  id: mainView

2)Playtime。在MainView中找到相应的一些属性来尝试修改看看应用有什么变化。尝试改变boolean值来看看应用有那些变化。

3)我们尝试改变如下的值:

   automaticOrientation: true
   useDeprecatedToolbar: false

再重新运行应用,我们发现应用在我们转动屏幕的时候会发生相应的转动。你也可以同时看到toolbar的位置也发生了相应的变化。


6.png   


至此我们整个应用在第一阶段基本已经完成了。

整个应用的源码可以在地址下载:

bzr branch lp:~liu-xiao-guo/debiantrial/developernews_step1

我们可以在下一个章节中继续学习conditional layout来完成整个的练习!


作者:UbuntuTouch 发表于2014-9-2 14:17:36 原文链接
阅读:149 评论:0 查看评论

Read more
UbuntuTouch

[原]如何在Ubuntu中使用条件布局

我们知道现代手机可以随着手持的方位发生改变而使得手机的方位也随着发生改变。对有些应用来说,我们也希望手机的布局也能跟随发生变化。另外一种情况是当我们的应用安装到不同屏幕尺寸的平台上,我们希望我们的布局会随着屏幕的尺寸不同而发生不同的变化。我们可以利用剩余的空间显示更多的内容。在Ubuntu平台中,我们使用一个称作为conditinal layout的机制来使得我们的布局发生改变。在conditional layout的上面可以阅读更多的内容。


1)下载我们在上节中设计好的应用

我们可以在如下的地址:

bzr branch lp:~liu-xiao-guo/debiantrial/developernews_step1


下载我们的源码。我们可以安装到手机上并熟悉该应用。


2)使用conditional layout

conditional layout能够使得我们根据屏幕的尺寸来安排我们的控件。下面我们来具体讲解怎么实现它:


1)在我们的main.qml中加入如下的库:

import Ubuntu.Layouts 1.0

2)在main.qml中的PageStack之前,加入如下的代码:


 Layouts {
       id: mainLayout
       anchors.fill: parent

       layouts: [
           ConditionalLayout {
               name: 'flat'
               when: mainLayout.width >= units.gu(70)
               Page {
                   id: flatPage
                   title: i18n.tr("Developer News")

                   tools: ToolbarItems {
                       ToolbarButton {
                           action: reloadAction
                       }
                   }

                   Row {
                          anchors.fill: parent





                       ItemLayout {
                           item: "articleList"
                           width: parent.width >= units.gu(100) ? units.gu(50) : parent.width/2
                           height: parent.height
                       }
                       ItemLayout {
                           item: "articleContent"
                           width: parent.width - articleList.width
                           height: parent.height
                       }
                   }
               }
           }
       ]


       onCurrentLayoutChanged: {
           if (mainLayout.currentLayout != 'flat') {
               mainView.activeLeafNode = pageStack.currentPage
           }
       }















3)在PageStack中的ArticleListView加入:

 Layouts.item: "articleList"

4)在ArticleListView中的onClicked的如下代码:

 pageStack.push(contentPage)

替换为:

                   if (mainLayout.currentLayout != "flat") {
                       contentPage.title = instance.title
                       pageStack.push(contentPage)
                   }

5)在ArticleContent的定义中加入:

Layouts.item: "articleContent"


6)在main.qml的最后端加入:

}

现在我们已经完成了我们的工作。我们现在运行一下我们的应用。




当我们把应用的尺寸变大后,应用显示为:




最终的源码在如下地址可以找到:


bzr branch lp:~liu-xiao-guo/debiantrial/developernews_step2




作者:UbuntuTouch 发表于2014-9-3 13:41:04 原文链接
阅读:227 评论:0 查看评论

Read more
UbuntuTouch

在Ubuntu平台上面,我们可以使用History API来读取电话本及短信的内容,并使用listview来显示出来。下面我们来展示该怎么做。


1)创建一个基本的QML应用

我们使用Qt Creator来创建一个最基本的QML应用。我们选择使用“App with Simple UI"模版来创建我们的应用。





为了使用History API, 我们必须引入

import Ubuntu.History 0.1

为了能够使我们读取不同的history,我们先来做一个ComboButton。它的设计如下:

            ComboButton {
                id: type
                expanded: false

                text: "Voice"
                ListView {
                    model: typemodel
                    delegate: ListItem.Standard {
                        text: modelData

                        onClicked: {
                            console.log("item is clicked!" + index + " " + name);
                            type.expanded = false;
                            type.text = text;

                            console.log("type is: " + type.text);

                            if ( name === "Voice") {
                                historyEventModel.type = HistoryThreadModel.EventTypeVoice;
                                listView.model = historyEventModel
                            } else if ( name === "Text" ) {
                                historyEventModel.type = HistoryThreadModel.EventTypeText;
                                listView.model = historyEventModel
                            } else if ( name === "Thread") {
                                listView.model = historyThreadModel
                            }

                        }
                    }
                }
            }
这里我们可以参考ComboButton来更加多了解如何使用这个控件。这里,我们提供了三个选项"Voice", "Text" 及“Thread"。同时我们也创建了俩个不同的history model。

    HistoryEventModel {
        id: historyEventModel
        filter: HistoryFilter {}
        type: HistoryThreadModel.EventTypeVoice
        sort: HistorySort {
            sortField: "timestamp"
            sortOrder: HistorySort.DescendingOrder
        }
    }

    SortProxyModel {
        id: sortProxy
        sortRole: HistoryEventModel.TimestampRole
        sourceModel: historyEventModel
        ascending: false
    }

    HistoryThreadModel {
        id: historyThreadModel        <pre name="code" class="javascript"><span style="white-space:pre">	</span>filter: HistoryFilter {}
sort: HistorySort { sortField: "lastEventTimestamp" sortOrder: HistorySort.DescendingOrder } }


由于一些性能方面的原因的考虑,目前我们必须把filter设置为空,即:

filter: HistoryFilter {}

我们同时也把上面定义的model和我们的ListView一起连接起来:

            ListView {
                id: listView

                width: parent.width
                height: parent.height - type.height

                Component {
                    id: sectionDelegate

                    Text {
                        text: section
                    }
                }

                model: historyEventModel

               delegate:
               ...
            }

目前似乎一切都已经好了。接下来我们来运行我们的应用到手机中。我们会发现在手机中没有任何的history的东西显示。问题出现在哪里呢?



我们开启一个terminal,然后打入如下的命令:

$adb shell

然后再在手机中的命令行输入:

root@ubuntu-phablet:~# grep "DENIED" /var/log/syslog 

我们可以看到如下的图片:



显然,我们遇到了一个安全的问题。为了接的这个问题,我们必须在应用中加入它所需要的policy。



我们再重新运行程序。在手机上,我们可以看到如下的画面:




我们再也没有security的问题了。整个工程的源码在如下的地址可以下载:

bzr branch lp:~liu-xiao-guo/debiantrial/history





作者:UbuntuTouch 发表于2014-9-4 12:05:55 原文链接
阅读:104 评论:0 查看评论

Read more
UbuntuTouch

QML中的Loader是用来动态地载入一个QML的Component。它可以用来载入一个QML文件(使用它的source属性)。它也可以载入一个Component(使用它的sourceComponent属性)。它适合在需要载入一个Component时才载入它,这样避免资源的浪费。它可以动态地载入按需求在需要的时候创建我们需要的Component。更多阅读,可以参照:http://qt-project.org/doc/qt-4.8/qml-loader.html


1)动态载入一个Component


我们首先来创建我们一个基本的应用。这里我们使用一个"App with Simple UI"的模版。我们首先创建一个称作为"filearray.js"的javascript文件,如下:

var filearray = [
                    ["images/fall1.jpg", "First image"],
                    ["images/fall2.jpg", "Second image"],
                    ["images/fall3.jpg", "Three image"],
                    ["images/fall4.jpg", "Fourth image"]
                ];

这里创建了一个二维的数组。为了我们能够在应用中使用。同时,我们在"main.qml"中也创建一个Component。这样它可以在我们的应用中动态地产生。代码如下:

        Component {
            id: imageText

            Rectangle {
                id:top
                anchors.fill: parent
                Image {
                    id:innerImage
                    anchors.top: parent.top
                    anchors.topMargin:30
                    anchors.horizontalCenter: parent.horizontalCenter
                    width:parent.width*0.8;
                    height: parent.height*0.8
                    source:root.currentImage

                }
                Text{
                    id:answer
                    anchors.top:innerImage.bottom
                    anchors.topMargin:30
                    horizontalAlignment: Text.AlignHCenter
                    width:parent.width;
                    text:root.currentText
                }
            }

       
这个Component非常简单。上面使用了一个Image,下面是一个Text。这两个item的内容是从root控件中的两个property中获得。我们希望这root中的这两个property改变时他们的内容也可以改变。

   Page {
        id: root;
        title: i18n.tr("QML Loader")
        property int index: 0
        property string currentImage
        property string currentText:" "

同时,为了说明问题,我们也设计了一个timer。当这个timer每次timeout时,我们希望我们的loader:

        Loader {
            id: loader
            anchors.fill: parent
            anchors.centerIn:parent
        }

的"sourceComponent"每次都能发生改变,以使得UI得到变化。这里有趣的是,我们也可以为Loader定义它的大小:

  • 如果没有定义Loader的大小的话,Loader将会自动地适配到Component的大小尺寸(当component完成装载以后)
  • 如果Loader的大小已经被定义的话,当component完成装载后,component的尺寸将自动被适配到Loader的尺寸
    function timeout() {
        console.log("root.index" + root.index);
        console.log(FileExt.filearray[root.index][0]);
        root.currentImage = FileExt.filearray[root.index][0];
        root.currentText = FileExt.filearray[root.index][1];
        loader.sourceComponent = imageText;

        root.index++;
        if ( root.index === FileExt.filearray.length) {
            root.index = 0;
        }
    }

        Timer {
            id: timer
            interval: 3000
            onTriggered: {
                timeout();
            }
            repeat: true
            triggeredOnStart: true
        }

运行的效果图如下:



我们可以看到画面中的每三次改变一次。每一次都是一个新的Component,而不是同一个Component不变,只是其中的属性发生改变。Loader很适用于在不同的场景中装载不同的component,比如在不同的菜单中,装载不同的component,以显示不同的UI。Qt SDK很多的例程就是这样写的!

整个例程的代码在如下地址找到:

bzr branch lp:~liu-xiao-guo/debiantrial/loaderqml

2)使用Loader载入qml文件

在下面的例程中,我们来完成一个使用Loader来载入qml文件。我们来做一个类似wizard的东西。当第一个页面显示完后,紧接着按下一个按钮,进入下一个页面。我们首先来创建一个简单的"App with Simple UI"的模版应用,然后,修改main.qml文件:

import QtQuick 2.0
import Ubuntu.Components 1.1
import "components"

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "com.ubuntu.developer.liu-xiao-guo.wizard"


    //automaticOrientation: true

    // Removes the old toolbar and enables new features of the new header.
    useDeprecatedToolbar: false

    width: units.gu(100)
    height: units.gu(75)

    Page {
        id: root
        title: i18n.tr("Wizard")

        Loader {
            z: 1
            id: main
            anchors.fill: parent
        }

        Image {
            source: "images/fall1.jpg"
            anchors.fill: parent
        }

        Button {
            anchors.right: parent.right
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 20
            anchors.rightMargin: 20


            text: "Go to Page 1"
            onClicked: {
                main.source = "Page1.qml"
                console.log("Clicked in main")
            }
        }

        Connections {
            target:main.item
            onHandlerLoader:{
                console.log("Something happened!")
            }
        }
    }
}


这里我们定义了一个叫做main的Loader。当我们点击按钮”Go to Page 1"时,我们使用它来装载另外一个页面“Page1.qml"。注意我们在这里设置它的"z" order值为"1”。同时,我们也可以通过如下的Connections来接受来自main Loader的一些signal来做我们所需要的一些处理。
        Connections {
            target:main.item
            onHandlerLoader:{
                console.log("Something happened!")
            }
        }

在我们的Page1.qml文件中,我们设计如下:

import QtQuick 2.0
import Ubuntu.Components 1.1

Rectangle {
    id:page1
    anchors.fill: parent

    signal handlerLoader;

    Loader{
        z: 2
        id:loader
        anchors.fill: parent
    }
    
    Image {
        source: "images/fall2.jpg"
        anchors.fill: parent
    }

    Button {
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 20
        anchors.rightMargin: 20

        text: "Go to Page 2"
        onClicked: {
            loader.source = "Page2.qml"
            handlerLoader();
        }
    }
}

在这里,我们定义了另外一个Loader, 并且设置它的“z” order值为2,使一个画面得它可以覆盖以前的页面。我们也尝试定义了一个signal "handlerLoader"。这样我们可以使得前一个面可以的得到一个响应。我们可以把我们想要的信号通过这样的方式发送出去,让需要对它感兴趣的代码利用它。

运行我们的程序,结果如下:

       

代码在如下的地址可以找到:

bzr branch lp:~liu-xiao-guo/debiantrial/wizard

作者:UbuntuTouch 发表于2014-9-9 14:07:55 原文链接
阅读:106 评论:0 查看评论

Read more
UbuntuTouch

在这篇文章里,我来介绍如何在Ubuntu  OS上上面读取电话本的信息。


1)首先我们来创建一个最基本的应用

打开我们的Qt Creator, 我们来创建一个称作为“contact1"的项目。在本项目中,我们使用"App with Simple UI"的模版。我们修改我们的“main.qml"代码如下:


import QtQuick 2.0
import Ubuntu.Components 0.1
import "components"
import QtContacts 5.0
import Ubuntu.Components.ListItems 0.1 as ListItem

/*!
    \brief MainView with a Label and Button elements.
*/

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "com.ubuntu.developer.liu-xiao-guo.contact1"

    /*
     This property enables the application to change orientation
     when the device is rotated. The default is false.
    */
    //automaticOrientation: true

    // Removes the old toolbar and enables new features of the new header.
    useDeprecatedToolbar: false

    width: units.gu(100)
    height: units.gu(75)

    Page {
        title: i18n.tr("Contact1")

        ContactModel {
            id: contactModel

            manager: "galera"
        }

        ListView {
            id: contactView

            anchors {
                left: parent.left
                right: parent.right
                top: parent.top
                bottom: buttons.bottom
            }

            model: contactModel

            delegate: ListItem.Subtitled {
                text: contact.name.firstName
                subText: contact.phoneNumber.number
            }
        }
    }
}

这里我们首先输入我们的QtContact 5.0库。在代码中我们定义了如下的ContactModel:

        ContactModel {
            id: contactModel
            manager: "galera"
        }

一定要记住使用"galera" manager。最后,我们使用一个ListView来展示我们的内容。代码显示如上所示。但是我们现在马上在手机上运行时,可能看到如下的信息:

Fail to connect with service: QDBusError("org.freedesktop.DBus.Error.AccessDenied", "An AppArmor policy prevents this sender from sending this message to this recipient, 0 matched rules; type="method_call", sender=":1.119" (uid=32011 pid=20604 comm="/usr/lib/arm-linux-gnueabihf/qt5/bin/qmlscene $@ m") interface="org.freedesktop.DBus.Introspectable" member="Introspect" error name="(unset)" destination="com.canonical.pim" (uid=32011 pid=3057 comm="/usr/lib/arm-linux-gnueabihf/address-book-service/")")

这说明,我们的程序遇到了安全的问题。我们必须在我们应用的apparmor文件中加入相应的policy以来使得我们的读取是可行的。

==ou

加入"Contacts"policy后,我们再在手机中运行,可以看到如下的画面:



我们可以在如下的地址下载我们的代码:

bzr branch lp:~liu-xiao-guo/debiantrial/contact1

2)读取favorite contact信息


我们在我们的应用中定义如下的以个favorite model

        ContactModel {
            id: favouritesContactsModel

            manager: "galera"
            sortOrders: [
                SortOrder {
                    id: sortOrder

                    detail: ContactDetail.Name
                    field: Name.FirstName
                    direction: Qt.AscendingOrder
                }
            ]

            fetchHint: FetchHint {
                detailTypesHint: [ContactDetail.Avatar,
                    ContactDetail.Name,
                    ContactDetail.PhoneNumber]
            }

            filter: DetailFilter {
                id: favouritesFilter

                detail: ContactDetail.Favorite
                field: Favorite.Favorite
                value: true
                matchFlags: DetailFilter.MatchExactly
            }

            onErrorChanged: {
                if (error) {
                    busyIndicator.busy = false
                    contactListView.error(error)
                }
            }
        }

我们在手机上使一个contact成为favorite。这时在我们的ListView中使用我们的favorite Model。代码如下:

        ListView {
            id: contactView
            anchors.fill:parent

            model: root.showFavourites ? favouritesContactsModel : contactModel

            delegate: ListItem.Subtitled {
                text: contact.name.firstName
                subText: contact.phoneNumber.number
            }
        }
我们可以看到我们的一个被设为favorite的contact被列举出来了。



代码在如下的地址可以下载:

bzr branch lp:~liu-xiao-guo/debiantrial/contact2





作者:UbuntuTouch 发表于2014-9-9 9:01:03 原文链接
阅读:84 评论:0 查看评论

Read more
UbuntuTouch

在这篇文章里,我们将学习如何使用QML的Qt.createComponent来动态生成我们所需要的Component。这是一个有趣的练习。我希望大家能跟随我一步一步地完成这个练习。最终使得大家对QML应用有更多的认识。这篇文章中我们也将使用我们的sensor来完成我们其中的一部分功能。在练习之前请大家先去按照安装Ubuntu SDK来安装好我们的环境。


1)使用Qt Creator创建一个最基本的应用


我们首先选择一个“App with Simple UI"的模版来创建我们的最基本的应用。在创建应用的时候,由于应用的package名字中不能出现大写的字母,所以我们选择使用小写的“balloon"来作为我们的工程的名字:



紧接着,我们填入所需要的信息来完成我们的应用。



把应用的大小设为如下的值:

    width: units.gu(50)
    height: units.gu(75)


运行我们的应用:



我们看到,这个应用没有什么太多的内容。我们可以尝试点击按钮,然后看见方框中的文字发生改变。

2)添加Balloon Component

我们按照如下的图,用右键点击项目"balloon",并选择“Add New"。



我们选择创建一个叫做“Balloon.qml”的文件。记住第一个字母为大写的字母。



至此我们已经创建了一个名字叫做"Balloon.qml"的component,虽然现在它做不了什么。为了测试我们刚刚已经创建好的Balloon Component,我们把我们的Balloon在main.qml中创建出来。现在我们来修改main.qml文件:

import QtQuick 2.0
import Ubuntu.Components 1.1
import "components"

MainView {
    // objectName for functional testing purposes (autopilot-qt5)
    objectName: "mainView"

    // Note! applicationName needs to match the "name" field of the click manifest
    applicationName: "com.ubuntu.developer.liu-xiao-guo.balloon"

    //automaticOrientation: true

    // Removes the old toolbar and enables new features of the new header.
    useDeprecatedToolbar: false

    width: units.gu(50)
    height: units.gu(75)

    Page {
        title: i18n.tr("Balloon")

        Balloon {
            width: parent.width/3
            height: parent.height/3
            x: (parent.width - width) / 2
            y: (parent.height - height) /2
        }
    }
}

这里我们把Balloon放到了我们主界面的正中央的位置。重新运行我们的程序,我们可以看到:



显然,我们可以看到,我们的Balloon component里没有任何的东西。只是一个白色方框在那里。但是,至少,我们可以看到确实Balloon已经能被正确地调用。在下面的章节中,我们来一步一步地完成我们的Balloon的功能。

3)完成Balloon模块

请到地址https://github.com/liu-xiao-guo/balloon下载应用,并拷贝应用的"images"目录到我们已经创建好的项目中(别的文件不要拷贝)。这样使得我们的应用有一个叫做"images"的目录,里面有我们想要的图片。



打开我们的Balloon.qml文件。我们知道Balloon component其实是一个Image的item。我们首先把它设计为如下:

import QtQuick 2.0

Image {
    id: balloon
    width: 100
    height: 250

    source: "images/red.svg"
}

我们运行程序,我们发现它的结果如下:



显然,我们已经看到了我们所需要看到的气球了。只是它的颜色是固定的红色。我们想显示不同的气球。这时我们需要加入一个颜色的属性。我们的Balloon.qml的代码如下:

Image {
    id: balloon
    width: 100
    height: 250
    property string color: "red"

    source: "images/" + color + ".svg"
}

默认的颜色我红色(在没有定义的情况下)。我们可以尝试改变我们的main.qml文件。加入color属性:

        Balloon {
            color: "green"
            width: parent.width/3
            height: parent.height/3
            x: (parent.width - width) / 2
            y: (parent.height - height) /2
        }

再重新运行应用:



我们看见了一个绿色的气球。显然它的颜色属性是起作用的。为了能够使我们拖动鼠标移动气球,我们可以在Balloon.qml的Image中加入如下的代码:

    MouseArea {
        x: 0; y: parent.height/2
        width: parent.width
        height: parent.height/2

        drag.target: parent
        drag.axis: "XandYAxis"
    }

再重新运行应用。我们尝试用鼠标点击气球的下半部,并移动气球。我们可以看到气球随着鼠标的移动而移动。下面,我们想在点击气球的上半部分时,气球发生爆炸。为了这样做,我们必须定义另外一个MouseArea来扑捉这个事件。当我们点击气球的上半部时会发出一个爆破声。

    MouseArea {
        x: 0; y: 0
        width: parent.width
        height: parent.height/2

        onClicked: {
            balloon.state = "exploded";
            player.play();
        }
    }

为了我们能够听到一个声音,我们也同时定义了一个MediaPlayer。同时记得把刚下载好的程序中的"sounds"目录考到我们的项目中,并处于和“images”相同的目录中。

import QtMultimedia 5.0

Image {
    ...

    MediaPlayer {
        id: player
        source: "sounds/blast.wav"
    }

   ...
}

为了能够使得我们的应用能够在手机上播放出声音,我们必须为它加入我们所需要的security policy。为此,我们必须修改项目的“balloon.apparmor”文件:



有了“Audio”的policy,我们就可以在我们的手机上听到一声“砰”的声音(在气球爆炸的时候)。

这里我们把Image的id定义为"balloon"。当点击气球的上半部时,我们同时设置balloon的状态为“exploded”状态。我们知道,QML设计中可以设置component为不同的状态。在不同的状态中,可以定义component中各个item的不同属性的值。默认的状态为"",即空串。我们定义的状态如下:

    states: [
        State {
            name: "exploded"

            StateChangeScript {
                script: {
 //                   particle.running = true;
                }
            }

            PropertyChanges {
                target: balloon
                visible: false
                source: "images/" + color + "_exploded.png"
            }

            PropertyChanges {
                target: balloon
                opacity: 0
            }

            PropertyChanges {
                target: balloon
                scale: 0
            }

 //           StateChangeScript { script: balloon.destroy(1000); }
        }
    ]

这时我们重新运行我们的应用,我们会发现我们可以点击球的上半部,并听到一声爆破声。随后球就消失了。我们的目的达到了,但是,还不是我们最终想要的。这是因为从气球点击到消失,速度太快了。我们机会没有看到任何的中间过程。为了我们能够看到气球是怎么爆炸的,我们必须使用一个叫做 transition的。

    transitions: [
        Transition {
            to: "exploded"
            SequentialAnimation {

                // Disappear
                NumberAnimation { target: balloon; property: "opacity"
                    to: 0; duration: 800 }
                NumberAnimation { target: balloon; property: "scale"
                    to: 0; duration: 800 }

                PropertyAction { target: balloon; property: "source"
                    value:  {
                        if ( !surprise )
                            "images/" + color + "_exploded.png"
                        else
                            "images/flower.png";
                    }
                }

                NumberAnimation { target: balloon; property: "opacity"
                    to: 1; duration: 300 }
                NumberAnimation { target: balloon; property: "scale"
                    to: 1; duration: 300 }

                PauseAnimation {
                    duration: {
                        if (surprise)
                            2000
                        else
                            800
                    }
                }

                PropertyAction { target: balloon; property: "visible"
                    value: "false"}
            }
        }
    ]

有了这个我们可以看到气球是逐渐消失的。我们在Image中也加入了如下的属性。当这个气球是一个surprise时,我们会显示一朵花:

Image {
    id: balloon
    width: 100
    height: 250
    property string color: "red"
    property bool surprise: true
    ....
}

为了达到更加逼真的效果,我也为我们的Balloon加入了一个烟花的效果:

import QtQuick.Particles 2.0
...
Image {
    ...
ParticleSystem {
        id: particle
        anchors.fill: parent
        running: false

        Emitter {
            group: "stars"
            emitRate: 800
            lifeSpan: 2400
            size: 24
            sizeVariation: 8
            anchors.fill: parent
        }

        ImageParticle {
            anchors.fill: parent
            source: "qrc:///particleresources/star.png"
            alpha: 0
            alphaVariation: 0.2
            colorVariation: 1.0
        }

        Emitter {
            anchors.centerIn: parent
            emitRate: 400
            lifeSpan: 2400
            size: 20 // 48
            sizeVariation: 8
            velocity: AngleDirection {angleVariation: 180; magnitude: 60}
        }

        Turbulence {
            anchors.fill: parent
            strength: 2
        }
    }

...
}

并在state变化时让它运行:

            StateChangeScript {
                script: {
                    particle.running = true;
                }
            }

运行应用,效果图如下:

    

至此所有的源码可以在如下的网址下载:


bzr branch lp:~liu-xiao-guo/debiantrial/balloon1

由于篇幅的原因。我们将在下一篇文章中详细介绍怎么动态创建很多个Balloon的。大家敬请期待!











            
作者:UbuntuTouch 发表于2014-9-10 11:41:52 原文链接
阅读:159 评论:0 查看评论

Read more
UbuntuTouch

上一篇文章中,我们已经生成了我们Balloon component了。现在我们来让大家怎么来动态生成很多的气球。


4)更进一步完成我们的Balloon component


为了使得我们的Balloon更加像现实生活中的气球,我们来给Balloon给予更多的属性:

    property int x1
    property int y1
    property int speed
    property bool created: false

这里x1, y1是我们让气球飞到一个目的地时的终点位置。我们可以使用如下的语句使得它具有动画的效果:

   NumberAnimation on y {
        easing.type: Easing.InOutQuad; to: y1; duration: speed
        running: created
    }

    NumberAnimation on x {
        easing.type: Easing.InOutQuad; to: x1; duration: speed
        running: created
    }

无论x, 或是y变化时,我们都做一个动画,用speed定义的时间来完成。这个动画只有在“created"为真时才起作用。这是为了能够保证我们的动画只有在Balloon被动态生成完成后才可以产生动作。“created"在我们的程序设计中,只有被动态生成时才设计为true。为了能够destroy我们动态生成的Ballloon,我们也在Transition中的部分做了如下的修改:

    transitions: [
        Transition {
            to: "exploded"
            SequentialAnimation {
                NumberAnimation { target: balloon; property: "opacity"
                    to: 0; duration: 800 }

                NumberAnimation { target: balloon; property: "scale"
                    to: 0; duration: 800 }

                PropertyAction { target: balloon; property: "source"
                    value:  {
                        if ( surprise )
                            "images/flower.png";
                        else
                            ""
                    }
                }

                NumberAnimation { target: balloon; property: "opacity"
                    to: 1; duration: 300 }
                NumberAnimation { target: balloon; property: "scale"
                    to: 1; duration: 300
                }

                PauseAnimation {
                    duration: {
                        if (surprise)
                            400
                        else
                            200
                    }
                }

                PropertyAction { target: balloon; property: "visible"
                   value: "false"}
            }
        }
    ]

5)动态生成QML Component

为了动态生成Balloon, 我们在主界面里加入一个Button。这个按钮可以帮我们生成所需要的Balloon。我们同时也为我们的主界面加上一个天空的背景:

    Page {
        id:main
        title: i18n.tr("Balloon")
        property int time: 2000
        property int rotateVal: 0

        Image {
            anchors.fill:parent
            source: "images/sky.jpg"
        }

        Balloon {
            id: red
            x: main.width/2
            y: main.height - 60
            rotation: main.rotateVal
            color: "red"
            y1: main.height/6
            x1: 0
            speed: main.time/2
            created: true
            surprise: true
        }

        Balloon {
            id: blue
            x: main.width / 3 - 60
            y: main.height - 60
            color: "blue"
            rotation: main.rotateVal
            y1: main.height/4
            x1: main.width/2 + 20
            speed: main.time/2
            created: true
        }

        Balloon {
            id: green
            x: main.width*2/3
            y: main.height - 10
            rotation: main.rotateVal
            color: "green"
            y1: main.height/5
            x1: main.width/3 + 20
            speed: main.time/3
            created: true
            surprise: true
        }

        Button {
            z: 2
            id: restartButton

            anchors.bottom: parent.bottom
            anchors.right: parent.right
            anchors.bottomMargin: 10
            anchors.rightMargin: 10

            width: 100
            height: 40
            text: "Add"
            onClicked: {
                var x = Math.random() * main.width
                var y = main.height - 60
                var colors = new Array("red","blue","green");

                var date = new Date()
                // Use miliseconds avoids the same random secuece
                // generation among calls
                var mils = date.getMilliseconds()
                var index = Math.floor((Math.random()*mils)%3)

                var obj = Logic.createBalloon( Math.floor(x), Math.floor(y),
                                              colors[index] )
            }
        }
    }

在这里,我们使用了一个天空的Image背景。同时也创建了三个不同颜色的气球。同时,我们也加入了一个按钮“Add”用来添加动态生成的气球。为了能够使用上面的“Logic”,我们也创建了一个叫做“logic.js”的文件,处于和"main.qml"同一个目录中。它的内容如下:

var component;

function addBinding (from, toObj, toProp)
{
    var bindObj = Qt.createQmlObject("import QtQuick 2.0; Binding {value:"+from+"}", main)
    if (bindObj) {
        bindObj.target = toObj
        bindObj.property = toProp
    }
    else {
        console.log("error creating binding object")
        console.log(bindObj.errorString())
        return false
    }
    return true
}


function createBalloon(x, y, color) {
    // console.log( "Color:" + color)
    if (component == null)
        component = Qt.createComponent("Balloon.qml");

    // Note that if Block.qml was not a local file, component.status would be
    // Loading and we should wait for the component's statusChanged() signal to
    // know when the file is downloaded and ready before calling createObject().
    if (component.status === Component.Ready) {
        var dynamicObject = component.createObject(main);
        if (dynamicObject === null) {
            console.log("error creating block");
            console.log(component.errorString());
            return false;
        }

        var xx = main.width*Math.random();
        var xx1 = Math.floor(Math.min(xx, main.width-dynamicObject.width));

        dynamicObject.x = x;
        dynamicObject.y = y;
        dynamicObject.x1 = xx1;
        dynamicObject.y1 = 100 * Math.random();
        dynamicObject.speed = 2000
        dynamicObject.color = color;

        with(Math) {
            var tmp = floor(random() * 10 + 1)
            dynamicObject.surprise = (tmp===10);
        }

        var bindObj = Qt.createQmlObject("import QtQuick 2.0; Binding {value: main.rotateVal}", main);

        if (bindObj) {
            bindObj.target = dynamicObject
            bindObj.property = "rotation"
        }
        else {
            console.log("error creating binding component") ;
            console.log(bindObj.errorString());
            return false;
        }

        // This should be set last
        dynamicObject.created = true;

        // addBinding( "scaleVal", dynamicObject, scale );

    } else {
        console.log("error loading block component");
        console.log(component.errorString());
        dynamicObject = null;
        return null;
    }

    return dynamicObject;
}

function playsound(surprise) {
    if ( surprise ) {
        clapPlayer.play();
    }
    else {
        player.play();
    }
}

这里,我们使用了createBalloon(x, y, color)方法在(x, y)处创建我们所需要颜色的气球。最后为了引用"logic.js",我们必须在main.qml文件的顶头部分使用如下的语句:

import "logic.js" as Logic

这样我们就可以使用这个js文件中的方法了。


    

至此,我们已经基本完成了如何自动动态地生成QML component。全部的代码在如下的地址可以下载:

bzr branch lp:~liu-xiao-guo/debiantrial/balloon2

6)加入sensor 到应用中去

接下来,我们想把sensor加入到我们的应用中去。我们想当我们倾斜我们的手机的时候,气球也要随着转动,同时当我们摇动手机的时候,我们希望气球能跟着我们的摇动不断地变大,这样能使得我们的气球直至破裂。为了我们能够使用sensor,我们必须在main.qml中加入如下的库:

import QtSensors 5.0

同时,我们也加入如下的代码:

        Accelerometer {
            id: accel
            active: true
            dataRate: 20

            onReadingChanged: {
                var x = Math.abs(accel.reading.x);
                var y = Math.abs(accel.reading.y);
                var z = Math.abs(accel.reading.z);

                if ( x > main.maxX ) {
                    main.maxX = x;
                }

                if ( y > main.maxY ) {
                    main.maxY = y;
                }

                if ( z > main.maxZ ) {
                    main.maxZ = z;
                }

                if ( x > main.threshold || y > main.threshold || z > main.threshold ) {
                    console.log("x: " + main.maxX + " y: " + main.maxY + " z: " + main.maxZ);
                    var count = main.children.length

                    for(var i=0; i < count; i++) {
                        if ( main.children[i].type !== "balloon" )
                            continue;
                        if ( main.children[i].type === "balloon") {
                            if ( main.children[i].color === "red")
                                main.children[i].scale += 0.1
                            else if ( main.children[i].color === "green" )
                                main.children[i].scale += 0.02
                            else
                                main.children[i].scale += 0.05
                        }

                        if( main.children[i].scale > 2.0 &&
                                main.children[i].state !== "exploded" &&
                                main.children[i].color === "red" &&
                                main.children[i].type === "balloon" ) {
                            main.children[i].state = "exploded"
                        }

                        if( main.children[i].scale > 3.0 &&
                                main.children[i].state !== "exploded" &&
                                main.children[i].color === "green" &&
                                main.children[i].type === "balloon" ) {
                            main.children[i].state = "exploded"
                        }

                        if( main.children[i].scale > 4.0 &&
                                main.children[i].state !== "exploded" &&
                                main.children[i].color === "blue" &&
                                main.children[i].type === "balloon" ) {
                            main.children[i].state = "exploded"
                        }
                    }
                }
            }
        }

        RotationSensor {
            id: rotation
            dataRate: 50
            onReadingChanged: {
                var count = main.children.length

                for ( var i = 0; i < count; i ++ ) {
                    if (main.children[i].type !== "balloon")
                        continue;

                    main.children[i].rotation = -rotation.reading.x;
                    main.children[i].rotation = -rotation.reading.y;
                }

            }
        }

我们使用了加速传感器和旋转传感器。这样在我们摇动的时候,我们就可以使得气球不断地变大。当我们倾斜的时候,气球也可以随着我们的倾斜而倾斜。

最终的代码可以在如下的地址下载:

bzr branch lp:~liu-xiao-guo/debiantrial/balloonfinal





作者:UbuntuTouch 发表于2014-9-10 20:39:32 原文链接
阅读:128 评论:0 查看评论

Read more
UbuntuTouch

在这篇文章里,我们将使用Ubuntu SDK从零开始来创建一个“中国天气”的Scope应用。通过这个过程,让开发者了解Scope在Ubuntu上的开发流程,以及对Scope有更深的认识。该应用完全使用Qt C++及std C++来完成的。更多关于Scope的知识,可以在网址:http://developer.ubuntu.com/scopes/。我们开发应用的最终显示图为:


  

        

上一篇文章中,我们已经使用std C++完成了一个同样的Scope应用。这里我们来使用Qt C++ APIs来完成这个同样的应用。

1)启动Ubuntu SDK来创建一个基本的Scope应用


首先,我们来打开我们的Ubuntu SDK来创建一个最基本的应用。我们选择菜单“New file or Project”或使用热键“Ctrl +N”。我们选择“Unity Scope”模版。



我们给我们的应用一个名字“ChinaWeather”。我们同事也选择template的类型为“Empty scope”

   

接下来,我们也同时选择不同的Kit,这样我们都可以在他们上面编译并部署我们的应用。




我们直接在电脑的Desktop上运行我们的应用。就像我们先前选择的一样,这个Scope没有做任何实质性的东西。你可以点击一下显示的结果。



如果你能运行到这里,说明你的安装环境是没有问题的。如果有问题的话,请参阅我的Ubuntu SDK安装文章。这个最基本的应用其实没有什么内容。在下面的章节中我们来向这里添加一些东西以实现我们所需要的一些东西。

2)加入对Qt的支持


我们可以看到在项目的“src”目录下有两个目录:apiscope。api目录下的代码主要是为了来访问我们的web service来得到一个json或是xml的数据。在这个项目中,我们并不刺昂采用这个目录中的client类。有兴趣的开发者可以尝试把自己的client和scope的代码分开。


首先我们需要在百度的开发者网站来申请我们的开发者账号。大家可以放问网站来申请账号。我们首先来做一个测试以确保我们的账号是可以工作的。按照文中所提到的,我们可以在浏览器中输入如下的地址:http://api.map.baidu.com/telematics/v3/weather?location=%E5%8C%97%E4%BA%AC&output=xml&ak=DdzwVcsGMoYpeg5xQlAFrXQt。我们可以得到如下的内容:




首先,我们可以看到API是工作的。没有任何问题。显示的架构是xml格式的。由于我们要使用Qt及Qt中的xml库来帮助我们解析我们得到的xml格式的数据,我们在项目中加入对Qt的支持。我们首先打开在“src”中的CMakeLists.txt文件,并加入如下的句子:

add_definitions(-DQT_NO_KEYWORDS)
find_package(Qt5Network REQUIRED)
find_package(Qt5Core REQUIRED)     
find_package(Qt5Xml REQUIRED)      

include_directories(${Qt5Core_INCLUDE_DIRS})    
include_directories(${Qt5Network_INCLUDE_DIRS})
include_directories(${Qt5Xml_INCLUDE_DIRS})    

....

# Build a shared library containing our scope code.
# This will be the actual plugin that is loaded.
add_library(
  scope SHARED
  $<TARGET_OBJECTS:scope-static>
)

qt5_use_modules(scope Core Xml Network) 

# Link against the object library and our external library dependencies
target_link_libraries(
  scope
  ${SCOPE_LDFLAGS}
  ${Boost_LIBRARIES}
)


我们可以看到,我们加入了对Qt Core,XML及Network库的调用。同时,我们也打开"tests/unit/CMakeLists.txt"文件,并加入“qt5_use_modules(scope-unit-tests Core Xml Network)":

# Our test executable.
# It includes the object code from the scope
add_executable(
  scope-unit-tests
  scope/test-scope.cpp
  $<TARGET_OBJECTS:scope-static>
)

# Link against the scope, and all of our test lib dependencies
target_link_libraries(
  scope-unit-tests
  ${GTEST_BOTH_LIBRARIES}
  ${GMOCK_LIBRARIES}
  ${SCOPE_LDFLAGS}
  ${TEST_LDFLAGS}
  ${Boost_LIBRARIES}
)

qt5_use_modules(scope-unit-tests Core Xml Network)

# Register the test with CTest
add_test(
  scope-unit-tests
  scope-unit-tests
)

我们再重新编译我们的应用,如果我们没有错误的话,我们的Scope可以直接在desktop下直接运行。这里我们加入了一个”QCoreApplication”变量。这主要是为了我们能够使用signal/slot机制及生成一个Qt应用。我们来修改scope.h文件,并加QoreApplication的变量app及forward申明。我们也必须同时加入一个方法"run"。

class QCoreApplication; // added

namespace scope {
class Scope: public unity::scopes::ScopeBase {
public:
    void start(std::string const&) override;
    void stop() override;
    void run(); // added
    unity::scopes::PreviewQueryBase::UPtr preview(const unity::scopes::Result&,
                                                  const unity::scopes::ActionMetadata&) override;
    unity::scopes::SearchQueryBase::UPtr search(
            unity::scopes::CannedQuery const& q,
            unity::scopes::SearchMetadata const&) override;

protected:
    api::Config::Ptr config_;
    QCoreApplication *app; //added
};

我们同时打开scope.cpp,并做如下的修改:

#include <QCoreApplication> // added

...

void Scope::stop() {
    /* The stop method should release any resources, such as network connections where applicable */
    delete app;
}

void Scope::run()
{
    int zero = 0;
    app = new QCoreApplication(zero, nullptr);
}


重新编译应用,如果有错误及时查出问题。这样我们就完成了对Qt的支持。

3)代码讲解

src/scope/scope.cpp

这个文件定义了一个unity::scopes::ScopeBase的类。它提供了客户端用来和Scope交互的起始接口。

  • 这个类定义了“start", "stop"及"run"来运行scope。绝大多数开发者并不需要修改这个类的大部分实现。在我们的例程中,我们将不做任何的修改
  • 它也同时实现了另外的两个方法:search 和 preview。我们一般来说不需要修改这俩个方法的实现。但是他们所调用的函数在具体的文件中必须实现

注意:我们可以通过研究Scope API的头文件来对API有更多的认识。更多的详细描述,开发者可以在http://developer.ubuntu.com/api/scopes/sdk-14.10/查看。

src/scope/query.cpp

这个文件定义了一个unity::scopes::SearchQueryBase类。

这个类用来产生由用户提供的查询字符串而生产的查询结果。这个结果可能是基于json或是xml的。这个类可以用来进行对返回的结果处理并显示。

  • 得到由用户输入的查询字符串
  • 向web services发送请求
  • 生成搜索的结果(根据每个不同而不同)
  • 创建搜索结果category(比如不同的layout-- grid/carousel)
  • 根据不同的搜寻结果来绑定不同的category以显示我们所需要的UI
  • 推送不同的category来显示给最终用户

创建并注册CategoryRenderers

在本例中,我们创建了两个JSON objects. 它们是最原始的字符串,如下所示,它有两个field:template及components。template是用来定义是用什么layout来显示我们所搜索到的结果。这里我们选择的是”vertical-journal"和carousel的layout。components项可以用来让我们选择预先定义好的field来显示我们所需要的结果。这里我们添加了"title"及“art"。

std::string CAT_GRID = R"(
{
    "schema-version" : 1,
    "template" : {
        "category-layout" : "vertical-journal",
        "card-layout": "horizontal",
        "card-size": "small",
        "collapsed-rows": 0
     },
    "components" : {
        "title" : "title",
        "subtitle":"subtitle",
        "summary":"summary",
        "art":{
        "field": "art2",
        "aspect-ratio": 1
        }
    }
 } )";

//Create a JSON string to be used tro create a category renderer - uses carousel layout
std::string CR_CAROUSEL = R"(
    {
        "schema-version" : 1,
        "template" : {
            "category-layout" : "carousel",
            "card-size": "large",
            "overlay" : true
        },
        "components" : {
            "title" : "title",
            "art" : {
                "field": "art",
                "aspect-ratio": 1.6,
                "fill-mode": "fit"
            }
        }
    }
)";

这里每个layout都有自己需要显示的component(title, art,subtitle等)。这些我们可以通过在scope解析我们的xml时填上去。我们在文件的开始部分加入如上的的template的定义。

void Query::run(sc::SearchReplyProxy const& reply) {
    /* This is where the actual processing of the current search query takes place.
     * It's where you may want to query a local or remote data source for results
     * matching the query.*/

    // Trim the query string of whitespace
    const CannedQuery &query(sc::SearchQueryBase::query());
    string query_string = alg::trim_copy(query.query_string());

    if ( query_string.empty() ) {
        query_string = "北京";
    }

    QString queryUri = BASE_URI.arg(query_string.c_str());
    qDebug() << "queryUrl: " << queryUri;

    // Generate a network request to the OpenClipArt server and parse the result
    QEventLoop loop;

    QNetworkAccessManager manager;
    QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
    QObject::connect(&manager, &QNetworkAccessManager::finished,
                     [reply, query_string, this](QNetworkReply *msg){
        QByteArray data = msg->readAll();

        qDebug() << "XML data is: " << data.data();

        Query::rssImporter(data,reply, QString::fromStdString(query_string));

     });

    // The query is the search string and was passed to this Query object's constructor by the client
    // Empty search string yields no results with openclipart API.
    manager.get(QNetworkRequest(QUrl(queryUri)));
    loop.exec();
}

void Query::rssImporter(QByteArray &data, unity::scopes::SearchReplyProxy const& reply, QString title) {
    QDomElement docElem;
    QDomElement rootElem;
    QDomDocument xmldoc;

    QString query = title;
    qDebug() << "query string: " << query;

    if ( !xmldoc.setContent(data) ) {
        qWarning()<<"Error importing data";
        return;
    }

    rootElem = xmldoc.documentElement();

    // Shows the CityWeatherResponse
    qDebug() << "TagName: " << rootElem.tagName();

    // Find CityWeatherResponse
    docElem = rootElem.firstChildElement("date");
    if (docElem.isNull()) {
        qWarning()<< "Error in data," << "CityWeatherResponse" << " not found";
        return;
    }

    QString date = docElem.text();
    qDebug() << "date: " << date;

    int indexYear = date.indexOf("-");
    QString year = date.left(indexYear);

    // Get the month
    int indexMonth = date.indexOf("-", indexYear + 1);
    QString month = date.mid(indexYear + 1, indexMonth - indexYear - 1);
    // Get the day
    QString day = date.right(date.length() - indexMonth - 1);
    QDate qDate( year.toInt(), month.toInt(), day.toInt());
    qDebug() << "Date: " << qDate.toString();

    docElem = rootElem.firstChildElement("results");

    QDomElement sum = docElem.firstChildElement("index");
    QString summary = getSummary(sum);
    qDebug() << "summary: " << summary;

    QString pmiIndex = docElem.firstChildElement("pm25").text();
    qDebug() << "PMI index: "  << pmiIndex;

    QDomElement cityElem = docElem.firstChildElement("currentCity");
    QString city = cityElem.text();
    qDebug() << "city: " << city;

    docElem = docElem.firstChildElement("weather_data");

    QDomNodeList dateList = docElem.elementsByTagName("date");

    // Below is also a way to get the list of the dates
    int count = dateList.count();
    for ( int i = 0; i < count; i ++ ) {
        QDomNode node = dateList.at(i);
        qDebug() << "date: " << node.toElement().text();
    }

    /* We're now registering (creating) two new categoryies, one with grid layout, the other wiht carousel.
     * Categories can be created at any point
     * during query processing inside the run method, but it's recommended
     * to create them as soon as possible (ideally as soon as they are known to the scope) */
    CategoryRenderer rdrGrid(CAT_GRID);
    CategoryRenderer rdrCarousel(CR_CAROUSEL);

    auto catCar = reply->register_category("openclipartcarousel", city.toStdString(), "", rdrCarousel);
    auto catGrid = reply->register_category("Chineweather", "", "", rdrGrid);


    QDomElement result = docElem.firstChildElement("date");
    int index = 0;

    bool done = false;

    while (!result.isNull()) {
        QString date = result.text();
        qDebug() << "date: " << date;

        QString dayPictureUrl = result.nextSiblingElement("dayPictureUrl").text();
        qDebug() << "dayPictureUrl: " << dayPictureUrl;

        QString nightPictureUrl = result.nextSiblingElement("nightPictureUrl").text();
        qDebug() << "nightPictureUrl: " << nightPictureUrl;

        QString weather = result.nextSiblingElement("weather").text();
        qDebug() << "weather: " << weather;

        QString wind = result.nextSiblingElement("wind").text();
        qDebug() << "wind: " << wind;

        QString temperature = result.nextSiblingElement("temperature").text();
        qDebug() << "temperature: " << temperature;

        result = result.nextSiblingElement("date");

        QString daytime;
        daytime.append("白天: ");
        daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));

        CategorisedResult catres(catCar);

        // Set the picture for the day
        catres.set_uri(URI.toStdString());
        catres.set_dnd_uri(URI.toStdString());

        catres.set_title(daytime.toStdString());
        catres.set_art(dayPictureUrl.toStdString());

        // Add some extra data, and they will be shown in the preview
        catres["weather"] = Variant(weather.toStdString());
        catres["temperature"] = Variant(temperature.toStdString());
        catres["wind"] = Variant(wind.toStdString());

        //push the categorized result to the client
        if (!reply->push(catres)) {
            break; // false from push() means search waas cancelled
        }

        // Set the picture for the night
        catres.set_uri(URI.toStdString());
        catres.set_dnd_uri(URI.toStdString());

        QString nighttime;
        nighttime.append("晚上: ");
        // nighttime.append(date1);
        nighttime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));

        catres.set_title(nighttime.toStdString());
        catres.set_art(nightPictureUrl.toStdString());

        //push the categorized result to the client
        if (!reply->push(catres)) {
            break; // false from push() means search waas cancelled
        }

        if ( index == 0 && !done ) {
            CategorisedResult catres(catGrid);

            // we handle it specially for today
            catres.set_uri(URI.toStdString());
            catres.set_art(dayPictureUrl.toStdString());

            QString sub = weather + " " + " " + temperature + " " + wind + "  PMI: " + pmiIndex;
            catres["subtitle"] = sub.toStdString();
            catres["weather"] = Variant(sub.toStdString());
            catres["summary"]= summary.toStdString();
            catres["wind"] = Variant(summary.toStdString());

            QDateTime current = QDateTime::currentDateTime();\
            QTime time = current.time();

            QString daytime;
            if ( time.hour() > 6 && time.hour() < 18 ) {
                catres["art2"] = dayPictureUrl.toStdString();
                daytime.append("白天: ");
                daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));
            } else {
                catres["art2"] = nightPictureUrl.toStdString();
                daytime.append("晚上: ");
                daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));
            }

            catres.set_title(daytime.toStdString());

            if (!reply->push(catres)) {
                break; // false from push() means search waas cancelled
            }

            done = true;
            continue;
        }

        index ++;

       qDebug() <<  "============================================";
    }

    qDebug()<<"parsing ended";
}

// This function is used to get the summary of the day
QString Query::getSummary(QDomElement &docElem) {
    QDomElement result = docElem.firstChildElement("title");

    QString summary;
    while (!result.isNull()) {
        summary += result.text() + ": ";
        summary += result.nextSiblingElement("zs").text() + ", ";
        summary += result.nextSiblingElement("tipt").text() + ", ";
        summary += result.nextSiblingElement("des").text() + "\n";

        result = result.nextSiblingElement("title");
    }

    return summary;
}

为了能够顺利地进行编译,我们必须修改query.h的头文件,加入我们需要的方法定义及include一些头文件:


#ifndef SCOPE_QUERY_H_
#define SCOPE_QUERY_H_

#include <api/client.h>

#include <unity/scopes/SearchQueryBase.h>
#include <unity/scopes/ReplyProxyFwd.h>

#include <QByteArray> // added
#include <QString>     // added
#include <QDomDocument> // added
#include <QDomElement>  // added

namespace scope {

/**
 * Represents an individual query.
 *
 * A new Query object will be constructed for each query. It is
 * given query information, metadata about the search, and
 * some scope-specific configuration.
 */
class Query: public unity::scopes::SearchQueryBase {
public:
    Query(const unity::scopes::CannedQuery &query,
          const unity::scopes::SearchMetadata &metadata, api::Config::Ptr config);

    ~Query() = default;

    void cancelled() override;

    void run(const unity::scopes::SearchReplyProxy &reply) override;

private:
    void rssImporter(QByteArray &data, unity::scopes::SearchReplyProxy const& reply, QString title); // added
    QString getSummary(QDomElement &docElem);  // added

private:
    api::Client client_;
};

}

#endif // SCOPE_QUERY_H_

我们同时打开scope.cpp文件,并在文件的开头部分加入如下的代码:


#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QCoreApplication>


const QString BASE_URI = "http://api.map.baidu.com/telematics/v3/weather?location=%1&output=xml&ak=DdzwVcsGMoYpeg5xQlAFrXQt";
const QString URI = "http://www.weather.com.cn/html/weather/101010100.shtml";

// add this one to avoid too many typing
using namespace unity::scopes;

重新编译我们的scope。如果有错误请及时修正。同时在desktop或emulator中运行我们的应用。我们可以看到如下的画面。我们也可以在Unity Scope Tool的输入框中输入"上海",我们可以看到内容会发生变化。


   


更多关于 CategoryRenderer 类的介绍可以在 docs找到。

我们为每个JSON Object创建了一个CategoryRenderer,并同时向reply object注册。我们修改我们的run方法来实现显示:

我们从我们的Client API中的Client::Forecast来获取我们所需要的web service的数据,把数据填入到相应的CategorisedResult中。

关于代码的部分,我们可以运行我们的Scope,并同时查看输出的结果。我已经在程序中加入了大量的输出以方便调试及查看程序的运行情况。




我们也可以尝试点击我们的画面,在另外一个画面中可以看到一个图片。到这里,我们基本上已经看到了Scope工作的了。我们下面来更进一步来在Preview中显示更多的内容。

src/scope/preview.cpp

这个文件定义了一个unity::scopes::PreviewQueryBase类。

这个类定义了一个widget及一个layout来展示我们搜索到的结果。这是一个preview结i果,就像它的名字所描述的那样。

  • 定义在preview时所需要的widget
  • 让widget和搜索到的数据field一一对应起来
  • 定义不同数量的layout列(由屏幕的尺寸来定)
  • 把不同的widget分配到layout中的不同列中
  • 把reply实例显示到layout的widget中

大多数的代码在“run&quot;中实现。跟多关于这个类的介绍可以在http://developer.ubuntu.com/api/scopes/sdk-14.10/previewwidgets/找到。

Preview

Preview需要来生成widget并连接它们的field到CategorisedResult所定义的数据项中。它同时也用来为不同的显示环境(比如屏幕尺寸)生成不同的layout。根据不同的显示环境来生成不同数量的column。

Preview Widgets

这是一组预先定义好的widgets。每个都有一个类型。更据这个类型我们可以生成它们。你可以在这里找到Preview Widget列表及它们提供的的field类型。

这个例子使用了如下的widgets

  • header:它有title及subtitle field
  • image:它有source field有来显示从哪里得到这个art
  • text:它有text field
  • action:用来展示一个有"Open"的按钮。当用户点击时,所包含的URI将被打开

如下是一个例子,它定义了一个叫做“headerId"的PreviewWidget。第二个参数是它的类型"header"。

  1. PreviewWidget w_header("headerId""header");  

最终的程序如下:

#include <QString>  // added
#include <QDebug> // added

using namespace unity::scopes; // added

....

void Preview::run(sc::PreviewReplyProxy const& reply) {
    //
    // This preview handler just reuses values of the original result via
    // add_attribute_mapping() calls, but it could also do another network
    // request for more details if needed.
    //

    // Client can display Previews differently depending on the context
    // By creates two layouts (one with one column, one with two) and then
    // adding widgets to them differently, Unity can pick the layout the
    // scope developer thinks is best for the mode
    ColumnLayout layout1col(1), layout2col(2);

    // add columns and widgets (by id) to layouts.
    // The single column layout gets one column and all widets
    layout1col.add_column({"headerId", "artId", "tempId", "windId", "actionsId"});

    // The two column layout gets two columns.
    // The first column gets the art and header widgets (by id)
    layout2col.add_column({"artId", "headerId"});
    // The second column gets the info and actions widgets
    layout2col.add_column({"infoId", "windId", "actionsId"});

    // Push the layouts into the PreviewReplyProxy intance, thus making them
    // available for use in Preview diplay
    reply->register_layout({layout1col, layout2col});

    //Create some widgets
    // header type first. note 'headerId' used in layouts
    // second field ('header) is a standard preview widget type
    PreviewWidget w_header("headerId", "header");
    // This maps the title field of the header widget (first param)  to the
    // title field in the result to be displayed in this preview, thus providing
    // the result-specific data to the preview for display
    w_header.add_attribute_mapping("title", "title"); // attribute, result field name
    // Standard subtitle field here gets our 'artist' key value
    w_header.add_attribute_mapping("subtitle", "weather");

    PreviewWidget w_art("artId", "image");
    w_art.add_attribute_mapping("source", "art"); // // key, result field name

    PreviewWidget w_info("tempId", "text");
    w_info.add_attribute_mapping("text", "temperature");

    PreviewWidget w_wind("windId", "text");
    w_wind.add_attribute_mapping("text", "wind");

    Result result = PreviewQueryBase::result();
    QString urlString(result["uri"].get_string().c_str());
    qDebug() << "[Details] GET " << urlString;
    // QUrl url = QUrl(urlString);

    // Create an Open button and provide the URI to open for this preview result
    PreviewWidget w_actions("actionsId", "actions");
    VariantBuilder builder;
    builder.add_tuple({
            {"id", Variant("open")},
            {"label", Variant("Open")},
            {"uri", Variant(urlString.toStdString())} // uri set, this action will be handled by the Dash
        });
    w_actions.add_attribute_value("actions", builder.end());

    // Bundle out widgets as required into a PreviewWidgetList
    PreviewWidgetList widgets({w_header, w_art, w_info, w_wind, w_actions});
    // And push them to the PreviewReplyProxy as needed for use in the preview
    reply->push(widgets);
}


我们再重新运行程序,我们可以看到如下的画面。点击左边图下面的小图标,即可看到右边的图的当天的天气的详细情况。

 


为了能够使得我们的应用更加像我们自己的Scope,我们可以修改在“data”目录下的icon.png及logo.png文件。我们在手机上运行:



显示的图片如下。我们可以看到我们的图片已经发生变化了。

  


所有的程序代码可以在如下的网址找到:

bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherqtxml

该应用的另外一个使用json的Qt开发范例可以在如下的网址找到:

bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherfinal

4)调试应用

当我们在开发应用时,我们可以通过上面的“cerr”在“Application Output”输出结果来查看结果。当在手机运行时,我们也可以通过查看如下的文件来看Scope的运行情况:



我们可以通过查看在手机或emulator中的文件“~/.cache/upstart/scope-registry.log”来看最新的Scope的运行情况。



作者:UbuntuTouch 发表于2014-10-9 17:48:05 原文链接
阅读:73 评论:0 查看评论

Read more
Ryan Beisner

Agenda

  • Review ACTION points from previous meeting
  • rbasak to review mysql-5.6 transition plans with ABI breaks with infinity
  • blueprint updating
  • U Development
  • Server & Cloud Bugs (caribou)
  • Weekly Updates & Questions for the QA Team (psivaa)
  • Weekly Updates & Questions for the Kernel Team (smb, sforshee)
  • Ubuntu Server Team Events
  • Open Discussion
  • Announce next meeting date, time and chair
  • ACTION: meeting chair (of this meeting, not the next one) to carry out post-meeting procedure (minutes, etc) documented at https://wiki.ubuntu.com/ServerTeam/KnowledgeBase

Minutes

  • REVIEW ACTION POINTS FROM PREVIOUS MEETING
    • re: rbasak noted that regarding mysql mysql-5.6 transition / abi infinity action, we decided to defer the 5.6 move for this cycle, as we felt it was too late given the ABI concerns.
  • UTOPIC DEVELOPMENT
    • LINK: https://wiki.ubuntu.com/UtopicUnicorn/ReleaseSchedule
    • LINK: http://reqorts.qa.ubuntu.com/reports/rls-mgr/rls-u-tracking-bug-tasks.html#ubuntu-server
    • LINK: http://status.ubuntu.com/ubuntu-u/group/topic-u-server.html
    • LINK: https://blueprints.launchpad.net/ubuntu/+spec/topic-u-server
  • SERVER & CLOUD BUGS (CARIBOU)
    • Nothing to report.
  • WEEKLY UPDATES & QUESTIONS FOR THE QA TEAM (PSIVAA)
    • Nothing to report.
  • WEEKLY UPDATES & QUESTIONS FOR THE KERNEL TEAM (SMB, SFORSHEE)
    • smb reports that he is digging into a potential race between libvirt and xen init
  • UBUNTU SERVER TEAM EVENTS
    • None to report.
  • OPEN DISCUSSION
    • Pretty quiet. Not even any bad jokes. Back to crunch time!
  • ANNOUNCE NEXT MEETING DATE AND TIME
    • next meeting will be : Tue Oct 7 16:00:00 UTC 2014 chair will be lutostag
  • MEETING ACTIONS
    • ACTION: all to review blueprint work items before next weeks meeting.

People present (lines said)

  • beisner (54)
  • smb (8)
  • meetingology (4)
  • smoser (3)
  • rbasak (3)
  • kickinz1 (3)
  • caribou (2)
  • gnuoy (1)
  • matsubara (1)
  • jamespage (1)
  • arges (1)
  • hallyn (1)

IRC Log

Read more
Joseph Salisbury

Meeting Minutes

IRC Log of the meeting.

Meeting minutes.

Agenda

20140930 Meeting Agenda


Release Metrics and Incoming Bugs

Release metrics and incoming bug data can be reviewed at the following link:

  • http://people.canonical.com/~kernel/reports/kt-meeting.txt


Status: Utopic Development Kernel

The Utopic kernel remainds rebased on the v3.16.3 upstream stable
kernel. The latest uploaded to the archive is 3.16.0-19.26. Please
test and let us know your results.
Also, Utopic Kernel Freeze is next week on Thurs Oct 9. Any patches
submitted after kernel freeze are subject to our Ubuntu kernel SRU
policy.
—–
Important upcoming dates:
Thurs Oct 9 – Utopic Kernel Freeze (~1 week away)
Thurs Oct 16 – Utopic Final Freeze (~2 weeks away)
Thurs Oct 23 – Utopic 14.10 Release (~3 weeks away)


Status: CVE’s

The current CVE status can be reviewed at the following link:

http://people.canonical.com/~kernel/cve/pkg/ALL-linux.html


Status: Stable, Security, and Bugfix Kernel Updates – Trusty/Precise/Lucid

Status for the main kernels, until today (Sept. 30):

  • Lucid – Verification and Testing
  • Precise – Verification and Testing
  • Trusty – Verification and Testing

    Current opened tracking bugs details:

  • http://kernel.ubuntu.com/sru/kernel-sru-workflow.html

    For SRUs, SRU report is a good source of information:

  • http://kernel.ubuntu.com/sru/sru-report.html

    Schedule:

    cycle: 19-Sep through 11-Oct
    ====================================================================
    19-Sep Last day for kernel commits for this cycle
    21-Sep – 27-Sep Kernel prep week.
    28-Sep – 04-Oct Bug verification & Regression testing.
    05-Oct – 11-Oct Regression testing & Release to -updates.


Open Discussion or Questions? Raise your hand to be recognized

No open discussion.

Read more
Louis

I have seen this setup documented a few places, but not for Ubuntu so here it goes.

I have used this many time to verify or diagnose Device Mapper Multipath (DM-MPIO) since it is rather easy to fail a path by switching off one of the network interfaces. Nowaday, I use two KVM virtual machines with two NIC each.

Those steps have been tested on Ubuntu 12.04 (Precise) and Ubuntu 14.04 (Trusty). The DM-MPIO section is mostly a cut and paste of the Ubuntu Server Guide

The virtual machine that will act as the iSCSI target provider is called PreciseS-iscsitarget. The VM that will connect to the target is called PreciseS-iscsi. Each one is configured with two network interfaces (NIC) that get their IP addresses from DHCP. Here is an example of the network configuration file :

$ cat /etc/network/interfaces
# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp
#
auto eth1
iface eth1 inet dhcp

The second NIC resolves to the same hostname with a « 2 » appended (i.e. PreciseS-iscsitarget2 and PreciseS-iscsi2)

Setting up the iSCSI Target VM

This is done by installing the following packages :

$ sudo apt-get install iscsitarget iscsitarget-dkms

Edit /etc/default/iscsitarget and change the following line to enable the service :

ISCSITARGET_ENABLE=true

We now proceed to create an iSCSI target (aka disk). This is done by creating a 50 Gb sparse file that will act as our disk :

$ sudo dd if=/dev/zero of=/home/ubuntu/iscsi_disk.img count=0 obs=1 seek=50G

This container is used in the definition of the iSCSI target. Edit the file /etc/iet/ietd.conf. At the bottom, add :

Target iqn.2014-09.PreciseS-iscsitarget:storage.sys0
        Lun 0 Path=/home/ubuntu/iscsi_disk.img,Type=fileio,ScsiId=lun0,ScsiSN=lun0

The iSCSI target service must be restarted for the new target to be accessible

$ sudo service iscsitarget restart


Setting up the iSCSI initiator

To be able to access the iSCSI target, only one package is required :

$ sudo apt-get install open-iscsi

Edit /etc/iscsi/iscsid.conf changing the following:

node.startup = automatic

This will ensure that the iSCSI targets that we discover are enabled automatically upon reboot.

Now we will proceed to discover and connect to the device that we setup in the previous section

$ sudo iscsiadm -m discovery -t st -p PreciseS-iscsitarget
$ sudo iscsiadm -m node --login
$ dmesg | tail
[   68.461405] iscsid (1458): /proc/1458/oom_adj is deprecated, please use /proc/1458/oom_score_adj instead.
[  189.989399] scsi2 : iSCSI Initiator over TCP/IP
[  190.245529] scsi 2:0:0:0: Direct-Access     IET      VIRTUAL-DISK     0    PQ: 0 ANSI: 4
[  190.245785] sd 2:0:0:0: Attached scsi generic sg0 type 0
[  190.249413] sd 2:0:0:0: [sda] 104857600 512-byte logical blocks: (53.6 GB/50.0 GiB)
[  190.250487] sd 2:0:0:0: [sda] Write Protect is off
[  190.250495] sd 2:0:0:0: [sda] Mode Sense: 77 00 00 08
[  190.251998] sd 2:0:0:0: [sda] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[  190.257341]  sda: unknown partition table
[  190.258535] sd 2:0:0:0: [sda] Attached SCSI disk

We can see in the dmesg output that the new device /dev/sda has been discovered. Format the new disk & create a file system. Then verify that everything is correct by mounting and unmounting the new file system.

$ fdisk /dev/sda
n
p
1
<ret>
<ret>
w
$  mkfs -t ext4 /dev/sda1
$ mount /dev/sda1 /mnt
$ umount /mnt

 

Setting up DM-MPIO

Since each of our virtual machines have been configured with two network interfaces, it is possible to reach the iSCSI target through the second interface :

$ iscsiadm -m discovery -t st -p
192.168.1.193:3260,1 iqn.2014-09.PreciseS-iscsitarget:storage.sys0
192.168.1.43:3260,1 iqn.2014-09.PreciseS-iscsitarget:storage.sys0
$ iscsiadm -m node -T iqn.2014-09.PreciseS-iscsitarget:storage.sys0 --login

Now that we have two paths toward our iSCSI target, we can proceed to setup DM-MPIO.

First of all, a /etc/multipath.conf file must exist.  Then we install the needed package :

$ sudo -s
# cat << EOF > /etc/multipath.conf
defaults {
        user_friendly_names yes
}
EOF
# exit
$ sudo apt-get -y install multipath-tools

Two paths to the iSCSI device created previously need to exist for the multipath device to be seen.

# multipath -ll
mpath0 (149455400000000006c756e30000000000000000000000000) dm-2 IET,VIRTUAL-DISK
size=50G features='0' hwhandler='0' wp=rw
|-+- policy='round-robin 0' prio=1 status=active
| `- 4:0:0:0 sda 8:0   active ready  running
`-+- policy='round-robin 0' prio=1 status=enabled
  `- 5:0:0:0 sdb 8:16  active ready  running

The two paths are indeed visible. We can move forward and verify that the partition table created previously is accessible :

$ sudo fdisk -l /dev/mapper/mpath0

Disk /dev/mapper/mpath0: 53.7 GB, 53687091200 bytes
64 heads, 32 sectors/track, 51200 cylinders, total 104857600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x0e5e5db1

              Device Boot      Start         End      Blocks   Id  System
/dev/mapper/mpath0p1            2048   104857599    52427776   83  Linux

All that is remaining is to add an entry to the /etc/fstab file so the file system that we created is mounted automatically at boot.  Notice the _netdev entry : this is required otherwise the iSCSI device will not be mounted.

$ sudo -s
# cat << EOF >> /etc/fstab
/dev/mapper/mpath0-part1        /mnt    ext4    defaults,_netdev        0 0
EOF
exit
$ sudo mount -a
$ df /mnt
Filesystem               1K-blocks   Used Available Use% Mounted on
/dev/mapper/mpath0-part1  51605116 184136  48799592   1% /mnt

Read more
Dustin Kirkland

A StackExchange question, back in February of this year inspired a new feature in Byobu, that I had been thinking about for quite some time:

Wouldn't it be nice to have a hot key in Byobu that would send a command to multiple splits (or windows?
This feature was added and is available in Byobu 5.73 and newer (in Ubuntu 14.04 and newer, and available in the Byobu PPA for older Ubuntu releases).

I actually use this feature all the time, to update packages across multiple computers.  Of course, Landscape is a fantastic way to do this as well.  But if you don't have access to Landscape, you can always do this very simply with Byobu!

Create some splits, using Ctrl-F2 and Shift-F2, and in each split, ssh into a target Ubuntu (or Debian) machine.

Now, use Shift-F9 to open up the purple prompt at the bottom of your screen.  Here, you enter the command you want to run on each split.  First, you might want to run:

sudo true

This will prompt you for your password, if you don't already have root or sudo access.  You might need to use Shift-Up, Shift-Down, Shift-Left, Shift-Right to move around your splits, and enter passwords.

Now, update your package lists:

sudo apt-get update

And now, apply your updates:

sudo apt-get dist-upgrade

Here's a video to demonstrate!


In a related note, another user-requested feature has been added, to simultaneously synchronize this behavior among all splits.  You'll need the latest version of Byobu, 5.87, which will be in Ubuntu 14.10 (Utopic).  Here, you'll press Alt-F9 and just start typing!  Another demonstration video here...




Cheers,
Dustin

Read more
Ben Howard

Cloud Images and Bash Vulnerabilities

The Ubuntu Cloud Image team has been monitoring the bash vulnerabilities. Due to the scope, impact and high profile nature of these vulnerabilties, we have published new images. New cloud images to address the lastest bash USN-2364-1 [1, 8, 9] are being released with a build serials of 20140927. These images include code to address all prior CVEs, including CVE-2014-6271 [6] and CVE-2014-7169 [7], and supersede images published in the past week which addressed those CVEs.

Please note: Securing Ubuntu Cloud Images requires users to regularly apply updates[5]; using the latest Cloud Images are insufficient. 

Addressing the full scope of the Bash vulnerability has been an iterative process. The security team has worked with the upstream bash community to address multiple aspects of the bash issue. As these fixes have become available, the Cloud Image team has published daily[2]. New released images[3] have been made available at the request of the Ubuntu Security team.

Canonical has been in contact with our public Cloud Partners to make these new builds available as soon as possible.

Cloud image update timeline

Daily image builds are automatically triggered when new package versions become available in the public archives. New releases for Cloud Images are triggered automatically when a new kernel becomes available. The Cloud Image team will manually trigger new released images when either requested by the Ubuntu Security team or when a significant defect requires.

Please note:  Securing Ubuntu cloud images requires that security updates be applied regularly [5], using the latest available cloud image is not sufficient in itself.  Cloud Images are built only after updated packages are made available in the public archives. Since it takes time to build the  images, test/QA and finally promote the images, there is time (sometimes  considerable) between public availablity of the package and updated Cloud Images. Users should consider this timing in their update strategy.

[1] http://www.ubuntu.com/usn/usn-2364-1/
[2] http://cloud-images.ubuntu.com/daily/server/
[3] http://cloud-images.ubuntu.com/releases/
[4] https://help.ubuntu.com/community/Repositories/Ubuntu/
[5] https://wiki.ubuntu.com/Security/Upgrades/
[6] http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-6271.html
[7] http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-7169.html
[8] http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-7187.html
[9] http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-7186.html

Read more
Stéphane Graber

I often have to deal with VPNs, either to connect to the company network, my own network when I’m abroad or to various other places where I’ve got servers I manage.

All of those VPNs use OpenVPN, all with a similar configuration and unfortunately quite a lot of them with overlapping networks. That means that when I connect to them, parts of my own network are no longer reachable or it means that I can’t connect to more than one of them at once.

Those I suspect are all pretty common issues with VPN users, especially those working with or for companies who over the years ended up using most of the rfc1918 subnets.

So I thought, I’m working with containers every day, nowadays we have those cool namespaces in the kernel which let you run crazy things as a a regular user, including getting your own, empty network stack, so why not use that?

Well, that’s what I ended up doing and so far, that’s all done in less than 100 lines of good old POSIX shell script :)

That gives me, fully unprivileged non-overlapping VPNs! OpenVPN and everything else run as my own user and nobody other than the user spawning the container can possibly get access to the resources behind the VPN.

The code is available at: git clone git://github.com/stgraber/vpn-container

Then it’s as simple as: ./start-vpn VPN-NAME CONFIG

What happens next is the script will call socat to proxy the VPN TCP socket to a UNIX socket, then a user namespace, network namespace, mount namespace and uts namespace are all created for the container. Your user is root in that namespace and so can start openvpn and create network interfaces and routes. With careful use of some bind-mounts, resolvconf and byobu are also made to work so DNS resolution is functional and we can start byobu to easily allow as many shell as you want in there.

In the end it looks like this:

stgraber@dakara:~/vpn$ ./start-vpn stgraber.net ../stgraber-vpn/stgraber.conf 
WARN: could not reopen tty: No such file or directory
lxc: call to cgmanager_move_pid_abs_sync(name=systemd) failed: invalid request
Fri Sep 26 17:48:07 2014 OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Feb  4 2014
Fri Sep 26 17:48:07 2014 WARNING: No server certificate verification method has been enabled.  See http://openvpn.net/howto.html#mitm for more info.
Fri Sep 26 17:48:07 2014 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
Fri Sep 26 17:48:07 2014 Attempting to establish TCP connection with [AF_INET]127.0.0.1:1194 [nonblock]
Fri Sep 26 17:48:07 2014 TCP connection established with [AF_INET]127.0.0.1:1194
Fri Sep 26 17:48:07 2014 TCPv4_CLIENT link local: [undef]
Fri Sep 26 17:48:07 2014 TCPv4_CLIENT link remote: [AF_INET]127.0.0.1:1194
Fri Sep 26 17:48:09 2014 [vorash.stgraber.org] Peer Connection Initiated with [AF_INET]127.0.0.1:1194
Fri Sep 26 17:48:12 2014 TUN/TAP device tun0 opened
Fri Sep 26 17:48:12 2014 Note: Cannot set tx queue length on tun0: Operation not permitted (errno=1)
Fri Sep 26 17:48:12 2014 do_ifconfig, tt->ipv6=1, tt->did_ifconfig_ipv6_setup=1
Fri Sep 26 17:48:12 2014 /sbin/ip link set dev tun0 up mtu 1500
Fri Sep 26 17:48:12 2014 /sbin/ip addr add dev tun0 172.16.35.50/24 broadcast 172.16.35.255
Fri Sep 26 17:48:12 2014 /sbin/ip -6 addr add 2001:470:b368:1035::50/64 dev tun0
Fri Sep 26 17:48:12 2014 /etc/openvpn/update-resolv-conf tun0 1500 1544 172.16.35.50 255.255.255.0 init
dhcp-option DNS 172.16.20.30
dhcp-option DNS 172.16.20.31
dhcp-option DNS 2001:470:b368:1020:216:3eff:fe24:5827
dhcp-option DNS nameserver
dhcp-option DOMAIN stgraber.net
Fri Sep 26 17:48:12 2014 add_route_ipv6(2607:f2c0:f00f:2700::/56 -> 2001:470:b368:1035::1 metric -1) dev tun0
Fri Sep 26 17:48:12 2014 add_route_ipv6(2001:470:714b::/48 -> 2001:470:b368:1035::1 metric -1) dev tun0
Fri Sep 26 17:48:12 2014 add_route_ipv6(2001:470:b368::/48 -> 2001:470:b368:1035::1 metric -1) dev tun0
Fri Sep 26 17:48:12 2014 add_route_ipv6(2001:470:b511::/48 -> 2001:470:b368:1035::1 metric -1) dev tun0
Fri Sep 26 17:48:12 2014 add_route_ipv6(2001:470:b512::/48 -> 2001:470:b368:1035::1 metric -1) dev tun0
Fri Sep 26 17:48:12 2014 Initialization Sequence Completed


To attach to this VPN, use: byobu -S /home/stgraber/vpn/stgraber.net.byobu
To kill this VPN, do: byobu -S /home/stgraber/vpn/stgraber.net.byobu kill-server
or from inside byobu: byobu kill-server

After that, just copy/paste the byobu command and you’ll get a shell inside the container. Don’t be alarmed by the fact that you’re root in there. root is mapped to your user’s uid and gid outside the container so it’s actually just your usual user but with a different name and with privileges against the resources owned by the container.

You can now use the VPN as you want without any possible overlap or conflict with any route or VPN you may be running on that system and with absolutely no possibility that a user sharing your machine may access your running VPN.

This has so far been tested with 5 different VPNs, on a regular Ubuntu 14.04 LTS system with all VPNs being TCP based. UDP based VPNs would probably just need a couple of tweaks to the socat unix-socket proxy.

Enjoy!

Read more
niemeyer

The qml package is right now one of the best choices for creating graphic applications under the Go language. Part of the reason why this is true comes from the convenience of QML, a high-level domain-specific language that allows describing visual components, events, animations, and content in general in a succinct and pleasing way. The integration of such a language with Go means having both a good mechanism for describing visual content, and a good platform for doing general development under, which can range from simple data manipulation to involved OpenGL content rendering.

On the practical side, one of the implications of using such a language partnership is that every Go qml application will have some sort of resource content to deal with, carrying the QML logic. Such content may be loaded either from files on disk, or from strings in memory. Loading from a file means the content may be organized in multiple files that directly reference each other without changing the Go application, and may be updated and tested without rebuilding. Loading from a string in memory means the content needs to be self-contained, but results in a standalone binary (linking aside – still depends on Qt libraries).

There’s a well known trick to have both benefits at once, though, and the basic solution has already been made available in multiple packages: have the content on disk, and use an external tool to pack it up into a Go file that is built into the binary when the content is updated. Unfortunately, this trick alone is not enough with the qml package, because the QML engine needs to know what resources are available and where so that the right thing happens when it sees a directory being imported or an image path being referenced.

To solve that problem, the qml package has been enhanced with functionality that leverages the existing Qt resource system to pack content into the binary itself. Rather than using the upstream C++ and XML-based resource compiler, though, a new resource packer was implemented inside the qml package and made available both under a friendly Go API, and as a tool that follows common Go idioms.

The help text for the genqrc tool describes it in detail:

Usage: genqrc [options] <subdir1> [<subdir2> ...]

The genqrc tool packs all resource files under the provided subdirectories into
a single qrc.go file that may be built into the generated binary. Bundled files
may then be loaded by Go or QML code under the URL "qrc:///some/path", where
"some/path" matches the original path for the resource file locally.

Starting with Go 1.4, this tool may be conveniently run by the "go generate"
subcommand by adding a line similar to the following one to any existent .go
file in the project (assuming the subdirectories ./code/ and ./images/ exist):

    //go:generate genqrc code images

Then, just run "go generate" to update the qrc.go file.

During development, the generated qrc.go can repack the filesystem content at
runtime to avoid the process of regenerating the qrc.go file and rebuilding the
application to test every minor change made. Runtime repacking is enabled by
setting the QRC_REPACK environment variable to 1:

    export QRC_REPACK=1

This does not update the static content in the qrc.go file, though, so after
the changes are performed, genqrc must be run again to update the content that
will ship with built binaries.

The tool may be installed via go get as usual:

go get gopkg.in/qml.v1/cmd/genqrc

and once the qrc.go file has been generated, the main qml file may be
loaded with logic equivalent to:

component, err := engine.LoadFile("qrc:///path/to/file.qml")

The loaded file can in turn reference any other content that was bundled
into the Go binary.

For a better picture, this example demonstrates the use of the tool.

Read more
David Callé

The SDK and Unity teams are constantly looking for ways to improve the overall performance and battery life of Ubuntu. Your app should be written in the same spirit: lightweight and fast.

The Ubuntu SDK performance overlay.

This article will show you how to measure performance in your QML app and give you some tips on avoiding common pitfalls and resource hogs. Read…

Read more
Nicholas Skaggs

Final Beta testing for Utopic

Can you believe final beta is here for utopic already? Where has the summer gone? The milestone and images are already prepared for the final beta testing. This is the first round of image testing for ubuntu this cycle. A final image will also be tested next month, but now is the time to try out the image on your system. Be sure to report any bugs you may find. This will help ensure there is time to fix them before the release images.

To help make sure the final utopic image is in good shape, we need your help and test results! Please, head over to the milestone on the isotracker, select your favorite flavor and perform the needed tests against the images.

If you've never submitted test results for the iso tracker, check out the handy links on top of the isotracker page detailing how to perform an image test, as well as a little about how the qatracker itself works. If you still aren't sure or get stuck, feel free to contact the qa community or myself for help. Happy Testing!

Read more
Joseph Salisbury

Meeting Minutes

IRC Log of the meeting.

Meeting minutes.

Agenda

20140923 Meeting Agenda


Release Metrics and Incoming Bugs

Release metrics and incoming bug data can be reviewed at the following link:

  • http://people.canonical.com/~kernel/reports/kt-meeting.txt


Status: Utopic Development Kernel

The Utopic kernel has been rebased to the v3.16.3 upstream stable kernel
and uploaded to the archive, ie. 3.16.0-17.23. Please test and let us
know your results.
Also, we’re approximately 2 weeks away from Utopic Kernel Freeze on
Thurs Oct 9. Any patches submitted after kernel freeze are subject to
our Ubuntu kernel SRU policy.
—–
Important upcoming dates:
Thurs Sep 25 – Utopic Final Beta (~2 days away)
Thurs Oct 9 – Utopic Kernel Freeze (~2 weeks away)
Thurs Oct 16 – Utopic Final Freeze (~3 weeks away)
Thurs Oct 23 – Utopic 14.10 Release (~4 weeks away)


Status: CVE’s

The current CVE status can be reviewed at the following link:

http://people.canonical.com/~kernel/cve/pkg/ALL-linux.html


Status: Stable, Security, and Bugfix Kernel Updates – Trusty/Precise/Lucid

Status for the main kernels, until today (Sept. 23):

  • Lucid – Kernel prep
  • Precise – Kernel prep
  • Trusty – Kernel prep

    Current opened tracking bugs details:

  • http://kernel.ubuntu.com/sru/kernel-sru-workflow.html

    For SRUs, SRU report is a good source of information:

  • http://kernel.ubuntu.com/sru/sru-report.html

    Schedule:

    cycle: 19-Sep through 11-Oct
    ====================================================================
    19-Sep Last day for kernel commits for this cycle
    21-Sep – 27-Sep Kernel prep week.
    28-Sep – 04-Oct Bug verification & Regression testing.
    05-Oct – 11-Oct Regression testing & Release to -updates.


Open Discussion or Questions? Raise your hand to be recognized

No open discussions.

Read more
niemeyer

There were a few long standing issues in the yaml.v1 package which were being postponed so that they could be done at once in a single incompatible change, and the time has come: yaml.v2 is now available.

Besides these incompatible changes, other compatible fixes and improvements were performed in that push, and those were also applied to the existing yaml.v1 package so that dependent applications benefit immediately and without modifications.

The subtopics below outline exactly what changed, and how to adapt existent code when necessary.

Type errors

With yaml.v1, decoding a YAML value that is not compatible with the Go value being unmarshaled into will silently drop the offending value without notice. In many cases continuing with degraded behavior by ignoring the problem is intended, but this was the one and only option.

In yaml.v2, these problems will cause a *yaml.TypeError to be returned, containing helpful information about what happened. For example:

yaml: unmarshal errors:
  line 3: cannot unmarshal !!str `foo` into int

On such errors the decoding process still continues until the end of the YAML document, so ignoring the TypeError will produce logic equivalent to the old yaml.v1 behavior.

New Marshaler and Unmarshaler interfaces

The way that yaml.v1 allowed custom types to implement marshaling and unmarshaling of YAML content was slightly confusing and troublesome. For example, considering a CustomType with a keys field:

type CustomType struct {
        keys map[string]int
}

and supposing the goal is to unmarshal this YAML map into it:

custom:
    a: 1
    b: 2
    c: 3

With yaml.v1, one would need to implement logic similar to the following for that:

func (v *CustomType) SetYAML(tag string, value interface{}) bool {
        if tag == "!!map" {
                m := value.(map[interface{}]interface{})
                // ... iterate/validate/convert key/value pairs 
        }
        return goodValue
}

This is too much trouble when the package can easily do those conversions internally already. To fix that, in yaml.v2 the Getter and Setter interfaces are both gone and were replaced by the Marshaler and Unmarshaler interfaces.

Using the new mechanism, the example above would be implemented as follows:

func (v *CustomType) UnmarshalYAML(unmarshal func(interface{}) error) error {
        return unmarshal(&v.keys)
}

Custom-ordered maps

By default both yaml.v1 and yaml.v2 will marshal keys in a stable order which is increasing within the same type and arbitrarily defined across types. So marshaling is already performed in a sensible order, but it cannot be customized in yaml.v1, and there’s also no way to tell which order the map was originally in, as some applications require.

To fix that, there is a new pair of types that support preserving the order of map keys both when marshaling and unmarshaling: MapSlice and MapItem.

Such an ordered map literal would look like:

m := yaml.MapSlice{{"c", 3}, {"b", 2}, {"a", 1}}

The MapSlice type may be used for variables going in and out of the yaml package, or in struct fields, map values, or anywhere else sensible.

Binary values

Strings in YAML must be valid UTF-8 or UTF-16 (with a byte order mark for the latter), and for binary data the specification defines a standard !!binary tag which represents the raw data encoded (encrypted?) as base64. This is now supported both in yaml.v1 and yaml.v2, transparently. That is, any string value that is not valid UTF-8 will be base64-encoded and appropriately tagged so that it roundtrips as the same string. Short strings are inlined, while long ones are automatically broken into several lines and represented in a proper style. For example:

one: !!binary gICA
two: !!binary |
  gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
  CAgICAgICAgICAgICAgICAgICAgICAgICAgICA

Multi-line strings

Any string that contains new-line characters (‘\n’) will now be encoded using the literal style. For example, a value that would before be encoded as:

key: "line 1\nline 2\nline 3\n"

is now encoded by both yaml.v1 and yaml.v2 as:

key: |
  line 1
  line 2
  line 3

Other improvements

Besides these major changes, some assorted minor improvements were also performed:

  • Better handling of top-level “null”s (issue #35)
  • Marshal base 60 floats quoted for YAML 1.1 compatibility (issue #34)
  • Better error on invalid map keys (issue #25)
  • Allow non-ASCII characters in plain strings (issue #11).
  • Do not catch unrelated panics by mistake (commit a6dc653f)

For obtaining the yaml.v1 improvements:

go get -u gopkg.in/yaml.v1

For updating to yaml.v2, adapt the code as necessary given the points above, replace the import path, and run:

go get -u gopkg.in/yaml.v2

Read more
David Owen

Working with wrap-around

Sometimes we need to work with numbers that wrap-around, such as angles or indexes into a circular array. There are some tricks for dealing with these cases.
Continue reading "Working with wrap-around"

Read more