Author: fforw

Toggle Show WireFrame Addon for Blender

Recently watched a youtube video about the Blender boolean workflow.

Even though I got the Boxcutter and HardOps addons, they’re basically just a fancier and more user-friendly repackaging of the boolean operation workflow in Blender without those addons. So I thought there might be value in understand the normal one better.

But that’s not the topic here. While he was explaining things, he mentioned that he made a keyboard shortcut for an option that I always found more attractive than the comparable alternatives.

If you click “Wireframe” under “Viewport Display” in the Object properties tab, you get the same render mode as before, but with a wireframe of the object on top.

In find this option much more attractive than just enabling Wireframe for all objects. You can fine control which objects you want to see and don’t suddenly have a huge mess of lines from all the objects.

Most often, I just want to check two meshes against each other.

Image showing the location of the wireframe option within the Blender UI

However, that option there is no Blender operation, so you can’t define a keyboard shortcut for it :\

However, after some googling I found that it is easy to write your own addon to add such an operation with a default key binding and all.

Install Addon

To install the addon, download and save the python file below and then click “Install…” in the Blender preferences under “Add-ons” and select the file to install it.

Image showing the location of the "Install..." button within the Blender Preferences

Usage

With the addon installed, you can select one or multiple objects in the object mode and press Alt + Shift + J to toggle the wireframe display for all selected objects individually.

If you don’t like the default shortcut, you can change it in the Blender preferences under “Keymap”.

Links

InteractiveQuery in Automaton

For the second post in our “Cool shit in Automaton” series, I’d like to write about the InteractiveQuery mechanism. From the general conception to the final implementation with some nice Automaton features.

This post might be conceptually interesting for everyone involved with GraphQL, concrete it is most useful if you’re using Automaton.

Design

The InteractiveQuery mechanism was designed to drive the data needs of our widgets. It is a set of high-level abstract query definition types that offer interactive pagination, filtering, sorting and configuring of GraphQL queries.

It touches many Automaton subsystems which I will discuss in some detail here although it is not strictly necessary to understand all of it to use it. In many ways it’s pretty magical.

Defining an InteractiveQuery based GraphQL Query

One of design goals was of course user-friendliness but not at the expense of power and general applicability. A part of that being that we want to write the least amount of code possible, but still retain all the flexibility we need in our client’s domains.

InteractiveQuery turned out to be the driving force behind the Degenerification feature in DomainQL. The basic idea is that we want to be able to use Generics in Java and translate that into the GraphQL type world.

Let’s look at the default GraphQL query implementation for an example entity.

    /**
     * Default implementation of an InteractiveQuery based query for type [T].
     *
     * @param type
     * @param env
     * @param config    configuration for the Interactive query.
     * @param <T>
     * @return
     */
    @GraphQLQuery
    public <T> InteractiveQuery<T> iQuery(

        @GraphQLTypeParam(
            types = {
                Foo.class
            }
        )
        Class<T> type,
        DataFetchingEnvironment env,
        @NotNull QueryConfig config
    )
    {

        log.info("iQuery<{}>, config = {}", type, config);

        return interactiveQueryService.buildInteractiveQuery( type, env, config)
            .execute();

    }
Code language: Java (java)

This is the standard implementation for InteractiveQuery based GraphQL queries. The first method parameter “type” controls the generation of types for this generic method. For each concrete type listed, a degenerified variant of the iQuery method will be defined, by default appending the name of the concrete type to the method method name (here “iQueryFoo”). This is not the most pretty solution but it does keep things in order especially with huge domains. Our app schema already contains 42 variants of this and we’ve just started.

InteractiveQuery in GraphQL

Let’s take a look at the GraphQL types generated from that Java code snippet.

type QueryType {
    "Default implementation of an InteractiveQuery based query for type Foo."
    iQueryFoo(config: QueryConfigInput!): InteractiveQueryFoo
}

"Interactive Query with Foo payload."
type InteractiveQueryFoo {
    "Column states for the current result."
    columnStates: [ColumnState]
    "Query configuration the current result was produced with."
    queryConfig: QueryConfig
    "Total row count available."
    rowCount: Int
    "List with current rows of Foo."
    rows: [Foo]
    "Name of payload type (always 'Foo')"
    type: String
}

"The state of a column within an interactive query."
type ColumnState {
    "True if column is enabled. Server might disabled columns."
    enabled: Boolean
    "Column name"
    name: String
    "True if the column is sortable."
    sortable: Boolean
}

"Encapsulates all parameters of an interactive query."
type QueryConfig {
    "FilterDSL condition graph or null"
    condition: Condition
    "Current page within the paginated results"
    offset: Int
    "Optional unique query identifier. Useful for server-side query implementations."
    id: String
    "Maximum number of paginated results.,"
    pageSize: Int
    "Current sort order for the query."
    sortFields: [FieldExpression]
}

"Map graph representing JOOQ conditions"
scalar Condition

"Map graph representing a JOOQ field expression"
scalar FieldExpression
Code language: JavaScript (javascript)

The iQueryFoo query returns the InteractiveQueryFoo type which is a degenerification of InteractiveQuery<T> for the type Foo.

The InteractiveQuery types are all structurally the same and only differ in the payload type. The “rows” field contains a List of the payload type.

Filtering and Sorting

Here is where the real magic starts. So we want to filter a query, but we don’t want to write code for it. The widget provides a JSON description of a filter which we then apply in the context of the current SQL query.

FilterDSL

There is another complication in that while we / Automaton clearly prefers PostgreSQL as database, our clients may or may not, so we will support all databases that JOOQ supports.

Which also means that we wanted to avoid mucking around with SQL in that degree in any case, i.e. we needed an actual abstraction.

Luckily, JOOQ comes with it’s own Condition API which we can just adapt.

So we created the FilterDSL which is a pretty close copy of the JOOQ condition DSL with some necessary additions.

./src/main/js/apps/myapp/queries/Q_Foo.js
import { query } from "@quinscape/automaton-js"

export default query(
    // language=GraphQL
        `query iQueryFoo($config: QueryConfigInput!)
    {
        iQueryFoo(config: $config)
        {
            type
            columnStates{
                name
                enabled
                sortable
            }
            queryConfig{
                id
                condition
                currentPage
                pageSize
                sortFields
            }
            rows{
                id
                name
                description
                flag
                type
                owner{
                    id
                    login
                }
            }
            rowCount
        }
    }`,
    {
        "config": {
            "pageSize": 20
        }
    }
)
Code language: JavaScript (javascript)

Here we see the query definition to query our Foo type. It has fields of different scalar types and an embedded owner object with an id and a login name.

Now let’s define a filter for that query

import { FilterDSL } from "@quinscape/automaton-js";
import Q_Foo from "../../queries/Q_Foo";

// deconstruct FilterDSL methods
const { field, value, and, or, not } = FilterDSL;

Q_Foo.execute({
        config: {
            condition : and(
                field("name").eq(value("AAA")),
                not(
                    field(value("owner.login")).eq("admin")
                )
            )
        }
    })
    .then(
        ({iQueryFoo}) => {
            // ...
        }
    )Code language: JavaScript (javascript)

We query all Foo objects whose name is “AAA” and which are not owned by “admin”.

You can choose how you want to write your boolean conditions. Either like above or you could write the same condition as

field("name").eq(
    value("AAA")
).and(
    field(
        value("owner.login")
    ).eq("admin")
    .not()
)Code language: CSS (css)

or a mix in between. Personally I find the second style not so good and the dangling .not() is outright horrible.

Using value()

To define values for our comparisons, we need to wrap the Javascript values in the value() method.

value(val, type = getDefaultType(value))Code language: JavaScript (javascript)

Most of the time you can get away with simply wrapping your JavaScript values with value(). The scalar type is then chosen based on the JavaScript type. If you use special number types or if you need a “Date” you must define that type as second argument.

While the FilterDSL uses prototype chaining to provide the API, it returns plain Javascript objects and arrays.

For our example condition the JSON would be

{
    "type": "Condition",
    "name": "and",
    "operands": [
        {
            "type": "Condition",
            "name": "eq",
            "operands": [
                {
                    "type": "Field",
                    "name": "name"
                },
                {
                    "type": "Value",
                    "value": "AAA",
                    "scalarType" : "String",
                    "name": null
                }
            ]
        },
        {
            "type": "Condition",
            "name": "not",
            "operands": [
                {
                    "type": "Condition",
                    "name": "eq",
                    "operands": [
                        {
                            "type": "Field",
                            "name": "owner.login"
                        },
                        {
                            "type": "Value",
                            "value": "admin",
                            "scalarType" : "String",
                            "name": null
                        }
                    ]
                }
            ]
        }
    ]
}Code language: JSON / JSON with Comments (json)

We use a special Condition scalar to transport these condition graphs through GraphQL without having to select the fields, which we couldn’t anyway except by setting arbitrary complexity limits on our filters.

Sorting

Just like our Condition scalar is a object equivalent to the WHERE clause, we can define the ORDER BY clause by using the sortFields field / the FieldExpression scalar.

The FieldExpression scalar is a close cousin of the condition API with a special shortcut version.

Most commonly we want very simple sorting

sortFields: ["name"]Code language: CSS (css)

Sort by name

sortFields: ["owner.login", "!name"]Code language: CSS (css)

Sort first by owner name and then descending by name.

And because the implementation was nearly trivial, we can also do complex sorting.

sortFields: [ field("numA").add(field("numB")).desc() ]Code language: CSS (css)

Sort descending by the sum of the fields numA and numB.

Generalized Filtering

The FilterDSL started out and was designed to be transformed into a JOOQ condition and finally an SQL WHERE clause, but in the end it is a pretty abstract filter definition language.

Just like we did in OpenSAGA, we’re using this to filter different things in different contexts.

Automaton contains a transformer that transforms FilterDSL condition graphs into a hierarchy of Java filter classes that can be used to filter Java object instances.

So when we implemented Websocket pubsub for Automaton we could use that to implement a nice topic based pubsub that can execute user-defined filters to decide who wants to see what message on each topic.

So when we have an application of that like the useDomainMonitor hook, we can very succinctly tell the hook what we’re interested in.

    const monitor = useDomainMonitor(
        field("domainType")
            .eq(
                value("Foo")
            )
    )Code language: JavaScript (javascript)

Above code will create a monitor instance that receives all domain monitoring meta data for our Foo type. But we could just as well limit that to a single entity or a mix of entity types.

Working with results

In our example above, we receive the iQueryFoo object which is an InteractiveQueryFoo instance.

Now automaton not only does auto-conversion of GraphQL queries to bring the scalar values into the right formats, it also can instantiate MobX classes for GraphQL types and even based on the original type of the degenerified type, which means for InteractiveQueryFoo that we will automatically receive a JavaScript implementation for InteractiveQuery which contains the data fields as observable fields but also offers two methods to continue to work with the query.


// update the iQuery document
iQueryFoo.update(queryConfig)

// Update / merge query conditions (advanced usage)
iQueryFoo.updateCondition(
    componentCondition, 
    componentId = NO_COMPONENT,
    checkConditions = true
)Code language: JavaScript (javascript)

iq.update(config) let’s you update the iQuery document with a new partial query config. For example

iQueryFoo.update({ offset: 20})Code language: CSS (css)

will update iQueryFoo with the rows for the second page. Internally the InteractiveQuery.js implementation will just update the document observable with new values.

This allows for cooperative control of data-sources from e.g. a datagrid widget and an external complex filter form.

Injection Pitfalls

It is possible to provide a default filter for both directly and indirectly injected InteractiveQuery documents with one caveat.

babel-plugin-track-usage is not clever enough to understand our FilterDSL so we can’t use it 🙁

If you find yourself in that situation it’s mostly the easiest to write a little script where you can test a FilterDSL expression and then grab the JSON output of that.

InteractiveQuery Configuration

The columnStates field is used for an advanced feature where a user can customize the GraphQL query. The query definition defines the maximum set of fields and the user can disable fields.

This requires the server-side to store e.g. user-specifc configuration and merge that in query method to use instead of the default full selection.

Complex Query Performance

In spite of being very very complex and versatile, we actually managed to get the standard querying mechanism to perform about as well as possible.

We don’t do cascading fetcher fetching, but we also don’t batch like it is commonly done.

Instead, the InteractiveQueryService looks at the domain / GraphQL types and creates an optimized execution plan that fetches the results in very small number of queries.

For one, if we, like in the example about have our Foo with owner field, we don’t fetch the foo object and then the owner, we actually do a

SELECT "foo"."id", "foo"."name", "foo"."description", "foo"."flag", "foo"."type", "owner"."id", "owner"."login", "foo"."owner_id" 
    FROM "public"."foo" as "foo" 
        LEFT OUTER JOIN "public"."app_user" as "owner" 
        ON "owner"."id" = "foo"."owner_id"
    ORDER BY "foo"."name"Code language: JavaScript (javascript)

that is we join-in foreign key relations right within the initial query.

Only when we follow *-to-many we split off into different queries which are then batched in themselves. If I fetch ten Foo and go into e.g. a bars field containing a List of associated Bar entities, the InteractiveQuery service will fetch all bar instances with one query.

There’s one limitation in that such a *-to-many “join” will always be a left outer join and not a right join or so.

Outlook

The InteractiveQuery mechanism is something that is really lacking in DomainQL if you should happen to need it. It is however also very opinionated and kind of the one query to rule them all.

It would be one of the top candidates to be moved into its own sub-library.

Links

GraphQL-injection with Automaton/DomainQL

Symbol picture: Injection needle with GraphQL-Logo

In this blog post I will try to explain how we organize data loading in Automaton/DomainQL and walk you through the general idea of it and how to adapt it for your own project.

Conceptually it provides an alternative to asynchronous loading / suspense on application startup.

Background

We are working on the relaunch of a German state-level government application. We have been doing paid Open-Source development for clients for quite some time now and are now doing it again for this project as requested by our client.

On the most general level, we use DomainQL to drive our GraphQL.

Symbolic DomainQL Explanation: Database + JOOQ + DomainQL is combined to GraphQL

The current workflow is that we start with a PostgreSQL database from which JOOQ generates Java classes, so called POJOs (Plain Old Java Objects, basically normal/anemic Java classes with getters and setters according to the old Java Bean standard).

Basic injection in DomainQL

DomainQL already contains a comparatively simple GraphQL injection method based on exports with magic names. At point I’m close to considering it deprecated since the new system works so much better now.

At first I expected DomainQL to be our main thing, but along the way it became clear to me that we needed more and we added Automaton as an additional layer. Where DomainQL is still very general, Automaton has a clear and distinct vision and a very opinionated way of getting there.

GraphQL injection in Automaton

Let’s start with the basic idea of our kind of data injection. In a traditional Javascript application, the user loads the initial HTML document which contains very little, it downloads all the Javascript bundles and CSS bundles and other resources and then the JavaScript is executed and let’s say React starts rendering an application.

For which it might need a lot of data which it loads asynchronously which necessitates loading indicators, which prompted solutions like Suspense etc.

Often, this is preferable to having the user wait longer but it introduces a lot of additional latency, even if we can manage to tame the visual artifacts with Suspense.

But wouldn’t it be awesome if the components didn’t have to request the data they need because it is already there? What if the server could provide all the data the components need by knowing what they will request before they do?

Static Code Analysis

When I started wondering about how to do this, I was already using the solution, a small, maybe odd library I had written earlier called babel-plugin-track-usage.

Code is just data and as we do it, we never actually use JavaScript as-is, but always transpile it, compress it etc.

babel-plugin-track-usage uses the Babel AST to detect and track statically analyzable calls within your code base.

At first we used it for i18n.

    [
        "track-usage",
        {
            "sourceRoot" : "src/main/js/",
            "trackedFunctions": {
                "i18n": {
                    "module": "./service/i18n",
                    "fn": "",
                    "varArgs": true
                }
            },
            "debug": false
        }
    ]
Code language: JSON / JSON with Comments (json)

Here we see the plugin configuration for an i18n service. We define that all our sources are under “src/main/js” (we’re using a Maven project layout) and then we define that we want to track the default export of a module “./service/i18n” which accepts variable arguments.

This lets us write expressions like “i18n("Hello {0}", user)” in our code. The function just looks up the first argument in a translation map that contains all the necessary translations because the babel-plugin-track-usage provided the server with all the invocations of our i18n function as long as all arguments are statically analyzable. With the varArgs setting it only considers the first argument and allows non-static analyzable varargs to follow.

In combination with a mini-Webpack-Plugin, babel-plugin-track-usage then exports JSON data for the server to process.

{
    "usages": {
        "./apps/shipping/processes/crud-test/composites/CRUDList": {
            "requires": [
                "@quinscape/automaton-js", 
                "mobx-react-lite"
            ],
            "calls": {
                "i18n": [
                    ["Foo List"], 
                    ["Action"], 
                    ["owner"]
                ]
            }
        },
        ...
    }
}Code language: JavaScript (javascript)

“usages” contains a map of relative module names as keys. The “calls” array contains a map of symbolic call names (Here our “i18n”) with a nested array containing an array per function invocation found.

“requires” contains an array with all modules required by that module. This can be used to follow all import dependencies and find all calls starting with a module as starting point.

After I extended the library a few times to support template literals, ES6 imports and also identifiers, it was ready to drive our data injection too.

Direct GraphQL Query injection

Schematic diagram illustrating the injection process explained below

Our apps in automaton correspond to Webpack entrypoints. Each of them contains a number of processes that make up the application functionality.

For each process there is a MobX class that defines the process scope containing all the data for the process.

Here is how our first version of GraphQL injection looked and still does, although we rarely use it like this

import { observable } from "mobx";
import { query } from "@quinscape/automaton-js"

export default class ExampleScope {
    @observable
    injected = query(
        // language=GraphQL
            `query iQueryFoo($config: QueryConfigInput!)
        {
            iQueryFoo(config: $config)
            {
                # ...
            }
        }`,
        {
            "config": {
                "pageSize": 5,
                "sortFields": ["name"]
            }
        }
    )
}Code language: JavaScript (javascript)

We important the “query” function from our library which is registered tracked. The function takes a simple template literal string with the query and a static JSON block with the default config as second argument.

The naming conventions for our file tell the server which process the statically analyzable query belongs to. We follow all imports etc.

When the user starts the application with one of its processes, the server can look up the query() calls it found and use that to fetch the data from GraphQL service.

This method works very well, even when we inject many queries. We don’t even need to bother to craft GraphQL documents with multiple queries. All injected queries will be executed together using an internal shortcut without using HTTP. (Hurray for GraphQL’s transport neutrality).

At the end we can then embed all the queried data in our initial HTML object as script tag with fantasy type.

Our system then can provide the process with a new instance of the process scope that magically contains all the injected data blocks and the React components involved can start rendering right away.

The main reason we switched to a different method is that we in fact have a lot of queries to execute in some processes which makes the process scope very lengthy

Indirect GraphQL injection

At first I couldn’t quite figure out an elegant solution until I realized that like so often, the solution to the problem is another layer of indirection.

What we did was to reify the query arguments into its own named thing and add a second tracked function.

We split the injection into multiple files.

First we have the query definition which we put into its own file.

./src/queries/Q_Foo.js
import { query } from "@quinscape/automaton-js"

export default query(
    // language=GraphQL
        `query iQueryFoo($config: QueryConfigInput!)
    {
        iQueryFoo(config: $config)
        {
            # ...
        }
    }`,
    {
        "config": {
            "pageSize": 5,
            "sortFields": ["name"]
        }
    }
)
Code language: JavaScript (javascript)

We have a standard folder where all our named query definitions are located (here shortened to ./src/queries/). Each definition is named after the file it is in, here Q_Foo.

Now to inject the current value of Q_Foo we just need to write

./src/processes/example/example.js
import { observable } from "mobx";
import { query, injection } from "@quinscape/automaton-js"
import Q_Foo from "../../queries/Q_Foo"

export default class ExampleScope {
    @observable
    injected = injection(Q_Foo)
}Code language: JavaScript (javascript)

I just love the way it works out. The Javascript syntax is just like one would normally write it, I can use my IDE to ctrl+click on the the name and lookup the definitions (or do quick lookups etc). Everything is neatly organized and the scope definitions are much clearer.

The plugin just generates a special JSON block for our identifier

{
    "usages": {
        "./src/processes/example/example": {
            "requires": [
                "mobx",
                "@quinscape/automaton-js",
                "./src/queries/Q_Foo"
            ],
            "calls": {
                "injection": [
                    [
                        {
                            "__identifier": "Q_Foo"
                        }
                    ]
                ]
            }
        }
    }
}
Code language: JSON / JSON with Comments (json)

The JSON data just provides the name and the server has to know / define what that means, in our case “Hey there should be a ‘./src/queries/Q_Foo’ which in turn contains a query() invocation that defines what data we want here.”

Links

Inktober 2019

Inktober 2019 is finished and it’s the first time I participated from start to finish. It was hard to draw every day and some days even more than others, but overall I’m really happy with the results.

Inktober 2018

After having had a phase of acrylic painting for a while, I have joined Inktober 2018.  Started a bit late, had some weak days at the end, but all in all I had a real good time trying out ink and other techniques.

At first I really like the simplicity of simple ink pens, but over time the limitations in line width etc just became more important to me. In the end I started drawing with dipping pens which I had avoided for so long. The cleaning is a bit annoying but I really got into working with the ink nibs.

Vacation Art: Part 2

Hey.. what happened to Part 1? Well.. that happened on my last home vacation  and mostly on twitter.

This year I started again with doing art stuff and got right into it..

Reflection on my last name
Reflection on my last name

Doesn’t make much sense in both directions, but that’s names, I guess..

Agitprop

Somehow got into the idea of recreating agitprop like abstract thingies with SVG and modern, more hipsterish people instead of soviet working populace. Just something about the style, I guess..

Coffee!

First results are mixed, I’d say.. Result is so so, but I think I learned a lot doing it.

Drawing-Stuff on Youtube

I keep watching a lot of drawing and painting related tutorials on youtube and one of them is Alphonso Dunn’s channel on drawing things. I was trying to get into a more inky workflow and maaybe considering getting into actual coal drawings, but then I got stuck on my inability to draw and I watched a lot of stuff on Alphonso’s channel about cross-hatching but also other stuff. Like basic kind of lines, purpose of these lines etc. The cross-hatching went so so, I still have to learn a lot there I think, both on a physical level of actually doing it on my graphics tablet as well as reaching the consistency etc required for real good results. Slow progress..

Among all the concrete drawing stuff Alphonso talked about the communicative value of lines (and other elements) and somehow I never really had thought about it that way and at first I just found it interesting, but then later when I did the SVG work, which is not really that related at all, it kind of really clicked for me.

What is the purpose of the SVG shape I’m drawing here, what is it supposed to be, what does it communicate to the viewer?

Then I did another SVG piece, that really felt like a quantum leap for me in terms of what I feel I can accomplish with the medium of SVG vector art and more.


Except for that annoying piece of parseley, the whole thing us based on very simple, even imperfect shapes, but each element is something. The light part of this and that, the reflection of that and that, the anti-shadow there, the rim of that.

Each element was there in the initial reference material I looked at for the reflections etc, but it was there as a chaotic mishmash of oddly colored pixel noise. It takes me, the artist to reformulate them into shapes, into common areas that have a united purpose within the image.

And even though it is imperfect and wonky in places, and even if it looks very different from the references in the end, the brain will recognize the things communicated, will take the hints of color and value and recreate that ideal of the soup bowl in our heads.

I don’t know why I never thought of it that way before, and now that I do it seems so very obvious.


Links

How to change the Trash shortcut in Ubuntu 11.10 oneiric ocelot

Unity’s default shortcut to use <Super>+T to open the trash has been bugging me for some time, mostly because it conflicts with my open terminal shortcut. Judging from the questions I found by googling, I don’t seem to be the only person who would like to configure it.

Turns out, there is no easy way. Although there are two different places where I can configure keyboard shortcuts, none of them can be used to change the shortcut used to open the trash window. It’s kind of odd how this is a shortcut at all. I mean, how often do I actually look into the trashcan to see what’s in there? That’s not even remotely close to being a common operation.

The cumbersome way

This being free software, there is of course another way, which you might or might not consider to be worth doing, that is downloading the source-code for a package, change it and compile it into a .deb package again (If you don’t know how to do that, consult the link I provided below).

Turns out changing the source code was really easy. Simple searching for “trash, I soon found “plugins/unityshell/src/TrashLauncherIcon.cpp” where I found the following code:

TrashLauncherIcon::TrashLauncherIcon(Launcher* IconManager)
  : SimpleLauncherIcon(IconManager)
  , proxy_("org.gnome.Nautilus", "/org/gnome/Nautilus", "org.gnome.Nautilus.FileOperations")
{
  tooltip_text = _("Trash");
  SetIconName("user-trash");
  SetQuirk(QUIRK_VISIBLE, true);
  SetQuirk(QUIRK_RUNNING, false);
  SetIconType(TYPE_TRASH);
  SetShortcut('t');

I changed the last line to set ‘x’ as the shortcut and build the package, which turned out to be multiple .deb files of which I found the right one by unpacking them into a temp directory and looking for the TrashLauncherIcon symbol.

I’m not sure the amount of work required to do this is really worth it, especially considering that I have to redo it every time the unity package is updated again, but for now I’m really satisfied.

Links:

Update: Added bugtracker URL for this.

Update2: Build and uploaded new .deb for new version (12.11.2011)

The people are just disturbing governance

© 2024 fforw.de

Theme by Anders NorénUp ↑