Skip to content

Commit

Permalink
Update default application lookup to latest spec
Browse files Browse the repository at this point in the history
Proper support of default applicaitons according to xdg spec
https://specifications.freedesktop.org/mime-apps-spec/latest/

Should give better inoperability on linux desktops

Fixes #657
  • Loading branch information
jaap-karssenberg committed Jan 30, 2021
1 parent 2597664 commit 9aa08f1
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 79 deletions.
10 changes: 6 additions & 4 deletions data/manual/Help/Default_Applications.txt
Expand Up @@ -9,14 +9,14 @@ Zim opens files and attachments with other applications. Usually clicking on a l
If you want to change the default application, or you want to add additional applications to the "//Open With...//" menu, there is a menu item "//Customize...//" at the bottom of the popup. This item brings up a dialog that allows changing the default application and adding new applications.

===== Configure Applications Dialog =====
The //Configure Applications //dialog has a drop down menu to choice a default application. Applications zim knows about are shown in this drop down. Also there is a special menu item "**System Default**" which means to use whatever application is the default used by the operating system.
The //Configure Applications //dialog has a drop down menu to choice a default application. Applications zim knows about are shown in this drop down. Also there is a special menu item "**System Default**" which means to use whatever application is the default used by the operating system.

Note that there may be applications installed that zim does not know about. Setting the default to "**System Default**" may result in the correct application being used, even if it is not shown in the list.

To add applications to the list (and to the "//Open With...//" menu) click the button "**Add Application**", this will bring up the //Add New Application// dialog.

===== Add Application Dialog =====
The //Add Application// dialog allows to add a new application for a specific file type.
The //Add Application// dialog allows to add a new application for a specific file type.

The **Name** is the application name and **Command** is the command to execute. Usually this is just the name of an executable or a script to execute. The command can also use the following special codes:

Expand All @@ -31,9 +31,11 @@ If "**Make default application**" is enabled the new application will become the


===== Technical Details =====
Zim uses the XDG Desktop Entry spec to store and retrieve application informations. The default application per mimetype is stored in a file in the XDG_DATA_HOME folder, typically ''~/.local/share/applications/defaults.list'' . This file with defaults is not part of the spec, but it seems to be in line with the implementation for the Gnome and KDE desktop environments.
Zim uses the XDG Desktop Entry spec to store and retrieve application informations. The default application per mimetype is stored in a file in the ''XDG_CONFIG_HOME'' folder, typically ''~/.config/mimeapps.list'' according to the "mime-apps-spec". In the same file you can also
add or remove (blacklist) applications to be shown in the "Open With" menu for a given
mimetype.

To populate the "//Open With...//" menu zim searches the ''XDG_DATA_HOME/share/applications/'' and ''XDG_DATA_DIRS/share/applications/'' folders for ''.desktop'' files that list the specific mimetype. As an optimization we assume a file "''mimeinfo.cache''" to be present that lists applications entries by mimetype
To populate the "//Open With...//" menu first the ''mimeapps.list'' is read. In addtion zim searches the ''XDG_DATA_HOME/share/applications/'' and ''XDG_DATA_DIRS/share/applications/'' folders for a "''mimeinfo.cache''" file which lists applications per mimetype. Applications are configured via ''.desktop'' files that should reside in the same folder as the cache file.

When the user adds a new application zim creates a new ''.desktop'' file in the XDG_DATA_HOME folder and updates the cache. Next time it lists applications for a specific type, this entry will show up. For a new default application we also update ''defaults.list'', but the desktop entry has "''NoDisplay''" set, so it is hidden from the menu.

Expand Down
178 changes: 175 additions & 3 deletions tests/applications.py
Expand Up @@ -13,6 +13,7 @@
from gi.repository import Gtk

from zim.gui.applications import *
from zim.gui.applications import _create_application
from zim.notebook import Path
from zim.fs import Dir, TmpFile

Expand Down Expand Up @@ -124,6 +125,20 @@ def testPythonCmd(self):
@tests.slowTest
class TestApplicationManager(tests.TestCase):

def tearDown(self):
ApplicationManager._defaults_app_cache.clear()

def remove_file(path):
#print("REMOVE", path)
assert path.startswith(tests.TMPDIR)
if os.path.exists(path):
os.unlink(path)

remove_file(XDG_CONFIG_HOME.file('mimeapps.list').path)
dir = XDG_DATA_HOME.subdir('applications')
for basename in dir.list():
remove_file(dir.file(basename).path)

def testGetMimeType(self):
for obj, mimetype in (
(File('file.txt'), 'text/plain'),
Expand All @@ -135,7 +150,7 @@ def testGetMimeType(self):
):
self.assertEqual(get_mimetype(obj), mimetype)

def testGetSetApplications(self):
def testGetSetListApplications(self):
# Typically a system will have multiple programs installed for
# text/plain and text/html, but do not rely on them for
# testing, so create our own first to test.
Expand All @@ -153,8 +168,23 @@ def testGetSetApplications(self):
self.assertFalse(entry['Desktop Entry']['NoDisplay'])

## Test Set & Get Default
defaults = XDG_DATA_HOME.file('applications/defaults.list')
self.assertFalse(defaults.exists())
defaults = XDG_CONFIG_HOME.file('mimeapps.list')
self.assertEqual(
defaults.read(),
'[Added Associations]\n'
'text/plain=test_entry_text-usercreated.desktop\n'
'text/html=test_entry_html-usercreated.desktop\n'
'x-scheme-handler/ssh=test_entry_ssh-usercreated.desktop\n'
)

cache = XDG_DATA_HOME.file('applications/mimeinfo.cache')
self.assertEqual(
cache.read(),
'[MIME Cache]\n'
'text/plain=test_entry_text-usercreated.desktop\n'
'text/html=test_entry_html-usercreated.desktop\n'
'x-scheme-handler/ssh=test_entry_ssh-usercreated.desktop\n'
)

default = manager.get_default_application('text/plain')
self.assertIsInstance(default, (None.__class__, DesktopEntryFile))
Expand All @@ -165,13 +195,23 @@ def testGetSetApplications(self):

self.assertTrue(defaults.exists())
self.assertEqual(defaults.read(),
'[Added Associations]\n'
'text/plain=test_entry_text-usercreated.desktop\n'
'text/html=test_entry_html-usercreated.desktop\n'
'x-scheme-handler/ssh=test_entry_ssh-usercreated.desktop\n'
'\n'
'[Default Applications]\n'
'text/plain=test_entry_text-usercreated.desktop\n'
)
self.assertEqual(manager.get_default_application('text/plain'), entry_text)

manager.set_default_application('text/plain', None)
self.assertEqual(defaults.read(),
'[Added Associations]\n'
'text/plain=test_entry_text-usercreated.desktop\n'
'text/html=test_entry_html-usercreated.desktop\n'
'x-scheme-handler/ssh=test_entry_ssh-usercreated.desktop\n'
'\n'
'[Default Applications]\n'
)
self.assertNotEqual(manager.get_default_application('text/plain'), entry_text)
Expand All @@ -198,6 +238,138 @@ def testGetSetApplications(self):
self.assertIsInstance(manager.get_application('startfile'), StartFile)
self.assertIsNone(manager.get_application('non_existing_application'))

def testSetGetWithoutCache(self):
manager = ApplicationManager()
entry_text = manager.create('text/plain', 'Test_Entry_Text', 'test_text 123', NoDisplay=False)
manager.set_default_application('text/plain', entry_text)
self.assertEqual(manager.get_default_application('text/plain'), entry_text)
manager._defaults_app_cache.clear()
self.assertEqual(manager.get_default_application('text/plain'), entry_text)

def testSetGetForMimeappsWithMultipleSections(self):
# Make sure we respect the section when writing
defaults_file = XDG_CONFIG_HOME.file('mimeapps.list')
defaults_file.write(
'[Default Applications]\n'
'text/html=foo.desktop\n'
'\n'
'[Added Associations]\n'
'text/plain=bar.desktop\n'
)
manager = ApplicationManager()
entry_text = manager.create('text/plain', 'Test_Entry_Text', 'test_text 123', NoDisplay=False)
manager.set_default_application('text/plain', entry_text)
self.assertEqual(
defaults_file.read(),
'[Default Applications]\n'
'text/html=foo.desktop\n'
'text/plain=test_entry_text-usercreated.desktop\n'
'\n'
'[Added Associations]\n'
'text/plain=test_entry_text-usercreated.desktop;bar.desktop\n'
)

def testSupportDesktopMimeappsList(self):
orig_desktop = os.environ.get('XDG_CURRENT_DESKTOP')
def restore_desktop():
os.environ['XDG_CURRENT_DESKTOP'] = orig_desktop
self.addCleanup(restore_desktop)

os.environ['XDG_CURRENT_DESKTOP'] = 'Test'
desktopfile = XDG_CONFIG_HOME.file('Test-mimeapps.list')
defaultfile = XDG_CONFIG_HOME.file('mimeapps.list')

dir = XDG_DATA_HOME.subdir('applications')
for basename in ('desktop-foo.desktop', 'normal-foo.desktop', 'ignore_this.desktop'):
_create_application(dir, basename, 'test', 'test')

desktopfile.write(
'[Default Applications]\n'
'text/plain=desktop-foo.desktop\n'
'\n'
'[Removed Associations]\n'
'text/html=ignore_this.desktop\n'
)
defaultfile.write(
'[Default Applications]\n'
'text/plain=normal-foo.desktop\n'
'text/html=ignore_this.desktop\n'
'\n'
)
manager = ApplicationManager()

# Test default picked up from desktop file
self.assertEqual(manager.get_default_application('text/plain').key, 'desktop-foo')

# Test blacklist in effect
self.assertNotIn('ignore_this', manager.list_applications('text/plain'))

def testSupportBackwardDefaultsList(self):
defaultfile = XDG_CONFIG_HOME.file('mimeapps.list')
backwardfile = XDG_DATA_HOME.file('applications/defaults.list')

dir = XDG_DATA_HOME.subdir('applications')
for basename in ('foo.desktop', 'bar.desktop', 'ignore_this.desktop'):
_create_application(dir, basename, 'test', 'test')

defaultfile.write(
'[Default Applications]\n'
'text/html=bar.desktop\n'
'\n'
)
backwardfile.write(
'[Default Applications]\n'
'text/plain=foo.desktop\n'
'text/html=ignore_this.desktop\n'
'\n'
)

manager = ApplicationManager()
self.assertEqual(manager.get_default_application('text/plain').key, 'foo')
self.assertEqual(manager.get_default_application('text/html').key, 'bar')

def testListApplications(self):
defaultfile = XDG_CONFIG_HOME.file('mimeapps.list')
cachefile = XDG_DATA_HOME.file('applications/mimeinfo.cache')

dir = XDG_DATA_HOME.subdir('applications')
for basename in ('aaa.desktop', 'bbb.desktop', 'ccc.desktop', 'ddd.desktop', 'ignore_this.desktop', 'browser.desktop'):
_create_application(dir, basename, 'test', 'test', NoDisplay=False)
_create_application(dir, 'do_not_list.desktop', 'test', 'test', NoDisplay=True)

defaultfile.write(
'[Default Applications]\n'
'text/plain=aaa.desktop\n'
'text/html=browser.desktop\n'
'\n'
'[Added Associations]\n'
'text/plain=bbb.desktop;ccc.desktop;do_not_list.desktop\n'
'\n'
'[Removed Associations]\n'
'text/plain=ignore_this.desktop\n'
)
cachefile.write(
'[MIME Cache]\n'
'text/plain=ddd.desktop;ignore_this.desktop\n'
)

manager = ApplicationManager()
self.assertEqual(
[e.key for e in manager.list_applications('text/plain')],
['aaa', 'bbb', 'ccc', 'ddd']
)

# Test url scheme also falls back to text/html
self.assertEqual(
[e.key for e in manager.list_applications('text/html')],
['browser']
)
self.assertEqual(
[e.key for e in manager.list_applications('x-scheme-handler/http')],
['browser']
)



#~ class TestOpenWithMenu(tests.TestCase):
class Foo(object): # FIXME - this test blocks on full test runs ??
Expand Down

0 comments on commit 9aa08f1

Please sign in to comment.