Initial commit...
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Fixes ownershp
|
||||||
|
function main() {
|
||||||
|
sudo find . -type f -exec chmod 644 {} +
|
||||||
|
sudo find . -type d -exec chmod 755 {} +
|
||||||
|
|
||||||
|
# Set postrm permissions
|
||||||
|
for i in `find . -name postrm`; do
|
||||||
|
sudo chmod 755 "${i}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set pytop permissions
|
||||||
|
for i in `find . -name pytop`; do
|
||||||
|
sudo chmod 755 "${i}"
|
||||||
|
done
|
||||||
|
|
||||||
|
sudo chown -R root:root ./*/
|
||||||
|
|
||||||
|
builder;
|
||||||
|
bash ./chownAll.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
#builds debs
|
||||||
|
function builder() {
|
||||||
|
for i in `ls`; do
|
||||||
|
if [[ -d "${i}" ]]; then
|
||||||
|
dpkg --build "${i}"
|
||||||
|
else
|
||||||
|
echo "Not a dir."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
main;
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
sudo chown -R abaddon:abaddon .
|
||||||
|
}
|
||||||
|
main;
|
|
@ -0,0 +1,8 @@
|
||||||
|
Package: pytop64
|
||||||
|
Version: 0.0-1
|
||||||
|
Section: python
|
||||||
|
Priority: optional
|
||||||
|
Architecture: amd64
|
||||||
|
Depends: ffmpegthumbnailer (>= 2.0.10-0.1)
|
||||||
|
Maintainer: Maxim Stewart <1itdominator@gmail.com>
|
||||||
|
Description: Pytop is a custom desktop GUI.
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#postrm (script executed after uninstalling the package)
|
||||||
|
#set -e
|
||||||
|
|
||||||
|
if [ -f /bin/pytop ]; then
|
||||||
|
rm /bin/pytop
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /opt/Pytop ]; then
|
||||||
|
rm -rf /opt/Pytop
|
||||||
|
fi
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi, faulthandler
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('WebKit2', '4.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
from gi.repository import WebKit2 as webkit
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
from utils import Settings, Events
|
||||||
|
|
||||||
|
gdk.threads_init()
|
||||||
|
class Main:
|
||||||
|
def __init__(self):
|
||||||
|
faulthandler.enable()
|
||||||
|
webkit.WebView() # Needed for glade file to load...
|
||||||
|
|
||||||
|
self.builder = gtk.Builder()
|
||||||
|
self.settings = Settings()
|
||||||
|
self.settings.attachBuilder(self.builder)
|
||||||
|
self.builder.connect_signals(Events(self.settings))
|
||||||
|
|
||||||
|
window = self.settings.createWindow()
|
||||||
|
window.fullscreen()
|
||||||
|
window.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main = Main()
|
||||||
|
gtk.main()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# set -o xtrace ## To debug scripts
|
||||||
|
# set -o errexit ## To exit on error
|
||||||
|
# set -o errunset ## To exit if a variable is referenced but not set
|
||||||
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
# GTK_DEBUG=interactive python3 ./PyFM.py
|
||||||
|
python3 ./PyFM.py
|
||||||
|
}
|
||||||
|
main $@;
|
|
@ -0,0 +1,305 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.22.1 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<requires lib="webkit2gtk" version="2.12"/>
|
||||||
|
<object class="GtkFileFilter" id="Folders">
|
||||||
|
<mime-types>
|
||||||
|
<mime-type>inode/directory</mime-type>
|
||||||
|
</mime-types>
|
||||||
|
</object>
|
||||||
|
<object class="GtkImage" id="webDropDown">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="tooltip_text" translatable="yes">Show Mini Webbrowser</property>
|
||||||
|
<property name="stock">gtk-go-down</property>
|
||||||
|
<property name="icon_size">3</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkWindow" id="Window">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="default_width">800</property>
|
||||||
|
<property name="default_height">600</property>
|
||||||
|
<property name="type_hint">desktop</property>
|
||||||
|
<property name="decorated">False</property>
|
||||||
|
<property name="gravity">center</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="box1">
|
||||||
|
<property name="width_request">256</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="popOutBttn">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="image">webDropDown</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
<signal name="clicked" handler="showWebview" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFileChooserButton" id="selectedDirDialog">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="action">select-folder</property>
|
||||||
|
<property name="filter">Folders</property>
|
||||||
|
<property name="title" translatable="yes">Directory Chooser</property>
|
||||||
|
<signal name="file-set" handler="setIconViewDir" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSearchEntry" id="searDir">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||||
|
<property name="primary_icon_activatable">False</property>
|
||||||
|
<property name="primary_icon_sensitive">False</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkIconView" id="Desktop">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="margin">6</property>
|
||||||
|
<property name="selection_mode">multiple</property>
|
||||||
|
<property name="columns">6</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<object class="GtkPopover" id="iconControlsWindow">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="relative_to">popOutBttn</property>
|
||||||
|
<property name="position">bottom</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="iconRenameInput">
|
||||||
|
<property name="width_request">300</property>
|
||||||
|
<property name="height_request">26</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="primary_icon_stock">gtk-edit</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-copy</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-cut</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-paste</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-delete</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="margin_left">65</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="pack_type">end</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<object class="GtkPopover" id="webViewer">
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="relative_to">popOutBttn</property>
|
||||||
|
<property name="position">bottom</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-home</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
<signal name="clicked" handler="loadHome" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-refresh</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">True</property>
|
||||||
|
<property name="use_stock">True</property>
|
||||||
|
<property name="always_show_image">True</property>
|
||||||
|
<signal name="clicked" handler="refreshPage" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSearchEntry" id="webviewSearch">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||||
|
<property name="primary_icon_activatable">False</property>
|
||||||
|
<property name="primary_icon_sensitive">False</property>
|
||||||
|
<signal name="key-release-event" handler="runSearchWebview" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="WebKitWebView" id="webview">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<signal name="load-changed" handler="setUrlBar" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 858 B |
After Width: | Height: | Size: 850 B |
After Width: | Height: | Size: 702 B |
After Width: | Height: | Size: 925 B |
After Width: | Height: | Size: 882 B |
After Width: | Height: | Size: 707 B |
After Width: | Height: | Size: 798 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,88 @@
|
||||||
|
viewport,
|
||||||
|
treeview,
|
||||||
|
treeview > header,
|
||||||
|
notebook > stack,
|
||||||
|
notebook > header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
notebook > header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.24);
|
||||||
|
border-color: rgba(0, 232, 255, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
box,
|
||||||
|
iconview {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
treeview,
|
||||||
|
treeview.view {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
cell {
|
||||||
|
margin: 0em;
|
||||||
|
padding: 0em;
|
||||||
|
/* float: left; */
|
||||||
|
}
|
||||||
|
|
||||||
|
cell:focus {
|
||||||
|
outline-style: solid;
|
||||||
|
outline-color: rgba(0, 232, 255, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Ivonview and children default color */
|
||||||
|
.view {
|
||||||
|
background-color: rgba(0, 0, 0, 0.22);
|
||||||
|
color: #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Hover over color when not selected */
|
||||||
|
.view:hover {
|
||||||
|
box-shadow: inset 0 0 0 9999px alpha(rgba(0, 232, 255, 0.64), 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handles the icon selection hover and selected hover color. */
|
||||||
|
.view:selected,
|
||||||
|
.view:selected:hover {
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(15, 134, 13, 0.49);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rubberband coloring */
|
||||||
|
.rubberband,
|
||||||
|
rubberband,
|
||||||
|
flowbox rubberband,
|
||||||
|
treeview.view rubberband,
|
||||||
|
.content-view rubberband,
|
||||||
|
.content-view .rubberband,
|
||||||
|
XfdesktopIconView.view .rubberband {
|
||||||
|
border: 1px solid #6c6c6c;
|
||||||
|
background-color: rgba(21, 158, 167, 0.57);
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view:active {
|
||||||
|
background-color: rgba(172, 102, 21, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
XfdesktopIconView.view {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view:active {
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view .rubberband {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import os, gi
|
||||||
|
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import GObject
|
||||||
|
|
||||||
|
|
||||||
|
class Dragging:
|
||||||
|
def __init__(self):
|
||||||
|
# higher values make movement more performant
|
||||||
|
# lower values make movement smoother
|
||||||
|
self.SENSITIVITY = 1
|
||||||
|
self.desktop = None
|
||||||
|
self.EvMask = Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON1_MOTION_MASK
|
||||||
|
self.offsetx = 0
|
||||||
|
self.offsety = 0
|
||||||
|
self.px = 0
|
||||||
|
self.py = 0
|
||||||
|
self.maxx = 0
|
||||||
|
self.maxy = 0
|
||||||
|
|
||||||
|
def connectEvents(self, desktop, widget):
|
||||||
|
self.desktop = desktop
|
||||||
|
widget.set_events(self.EvMask)
|
||||||
|
widget.connect("button_press_event", self.press_event)
|
||||||
|
widget.connect("motion_notify_event", self.draggingEvent)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def press_event(self, w, event):
|
||||||
|
if event.button == 1:
|
||||||
|
p = w.get_parent()
|
||||||
|
# offset == distance of parent widget from edge of screen ...
|
||||||
|
self.offsetx, self.offsety = p.get_window().get_position()
|
||||||
|
# plus distance from pointer to edge of widget
|
||||||
|
self.offsetx += event.x
|
||||||
|
self.offsety += event.y
|
||||||
|
# self.maxx, self.maxy both relative to the parent
|
||||||
|
# note that we're rounding down now so that these max values don't get
|
||||||
|
# rounded upward later and push the widget off the edge of its parent.
|
||||||
|
self.maxx = self.RoundDownToMultiple(p.get_allocation().width - w.get_allocation().width, self.SENSITIVITY)
|
||||||
|
self.maxy = self.RoundDownToMultiple(p.get_allocation().height - w.get_allocation().height, self.SENSITIVITY)
|
||||||
|
|
||||||
|
|
||||||
|
def draggingEvent(self, widget, event):
|
||||||
|
# x_root,x_root relative to screen
|
||||||
|
# x,y relative to parent (fixed widget)
|
||||||
|
# self.px,self.py stores previous values of x,y
|
||||||
|
|
||||||
|
# get starting values for x,y
|
||||||
|
x = event.x_root - self.offsetx
|
||||||
|
y = event.y_root - self.offsety
|
||||||
|
# make sure the potential coordinates x,y:
|
||||||
|
# 1) will not push any part of the widget outside of its parent container
|
||||||
|
# 2) is a multiple of self.SENSITIVITY
|
||||||
|
x = self.RoundToNearestMultiple(self.Max(self.Min(x, self.maxx), 0), self.SENSITIVITY)
|
||||||
|
y = self.RoundToNearestMultiple(self.Max(self.Min(y, self.maxy), 0), self.SENSITIVITY)
|
||||||
|
if x != self.px or y != self.py:
|
||||||
|
self.px = x
|
||||||
|
self.py = y
|
||||||
|
self.desktop.move(widget, x, y)
|
||||||
|
|
||||||
|
def Min(self, a, b):
|
||||||
|
if b < a:
|
||||||
|
return b
|
||||||
|
return a
|
||||||
|
|
||||||
|
def Max(self, a, b):
|
||||||
|
if b > a:
|
||||||
|
return b
|
||||||
|
return a
|
||||||
|
|
||||||
|
def RoundDownToMultiple(self, i, m):
|
||||||
|
return i/m*m
|
||||||
|
|
||||||
|
def RoundToNearestMultiple(self, i, m):
|
||||||
|
if i % m > m / 2:
|
||||||
|
return (i/m+1)*m
|
||||||
|
return i/m*m
|
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
from .Grid import Grid
|
||||||
|
from .Dragging import Dragging
|
||||||
|
|
||||||
|
class Events:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.builder = self.settings.returnBuilder()
|
||||||
|
self.desktop = self.builder.get_object("Desktop")
|
||||||
|
self.webview = self.builder.get_object("webview")
|
||||||
|
self.desktopPath = self.settings.returnDesktopPath()
|
||||||
|
|
||||||
|
self.settings.setDefaultWebviewSettings(self.webview, self.webview.get_settings())
|
||||||
|
self.webview.load_uri(self.settings.returnWebHome())
|
||||||
|
|
||||||
|
# Add filter to allow only folders to be selected
|
||||||
|
selectedDirDialog = self.builder.get_object("selectedDirDialog")
|
||||||
|
filefilter = self.builder.get_object("Folders")
|
||||||
|
selectedDirDialog.add_filter(filefilter)
|
||||||
|
selectedDirDialog.set_filename(self.desktopPath)
|
||||||
|
|
||||||
|
self.grid = None
|
||||||
|
self.setIconViewDir(selectedDirDialog)
|
||||||
|
|
||||||
|
def setIconViewDir(self, widget, data=None):
|
||||||
|
newPath = widget.get_filename()
|
||||||
|
Grid(self.desktop, self.settings, newPath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# File control events
|
||||||
|
def createFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, widget, data=None):
|
||||||
|
newName = widget.get_text().strip()
|
||||||
|
if data and data.keyval == 65293: # Enter key event
|
||||||
|
self.grid.updateFile(newName)
|
||||||
|
elif data == None: # Save button 'event'
|
||||||
|
self.grid.updateFile(newName)
|
||||||
|
|
||||||
|
def deleteFile(self, widget, data=None):
|
||||||
|
self.grid.deleteFile()
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Webview events
|
||||||
|
def showWebview(self, widget):
|
||||||
|
self.builder.get_object("webViewer").popup()
|
||||||
|
|
||||||
|
def loadHome(self, widget):
|
||||||
|
self.webview.load_uri(self.settings.returnWebHome())
|
||||||
|
|
||||||
|
def runSearchWebview(self, widget, data=None):
|
||||||
|
if data.keyval == 65293:
|
||||||
|
self.webview.load_uri(widget.get_text().strip())
|
||||||
|
|
||||||
|
def refreshPage(self, widget, data=None):
|
||||||
|
self.webview.load_uri(self.webview.get_uri())
|
||||||
|
|
||||||
|
def setUrlBar(self, widget, data=None):
|
||||||
|
self.builder.get_object("webviewSearch").set_text(widget.get_uri())
|
|
@ -0,0 +1,93 @@
|
||||||
|
|
||||||
|
import os, shutil, subprocess, threading
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class FileHandler:
|
||||||
|
def __init__(self):
|
||||||
|
# 'Filters'
|
||||||
|
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx' '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||||
|
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
|
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||||
|
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
|
||||||
|
self.images = ('.png', '.jpg', '.jpeg', '.gif')
|
||||||
|
self.pdf = ('.pdf')
|
||||||
|
|
||||||
|
# Args
|
||||||
|
self.MEDIAPLAYER = "mpv";
|
||||||
|
self.IMGVIEWER = "mirage";
|
||||||
|
self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef";
|
||||||
|
self.OFFICEPROG = "libreoffice";
|
||||||
|
self.TEXTVIEWER = "leafpad";
|
||||||
|
self.PDFVIEWER = "evince";
|
||||||
|
self.FILEMANAGER = "spacefm";
|
||||||
|
self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% ";
|
||||||
|
self.MPV_WH = " -geometry 50%:50% ";
|
||||||
|
|
||||||
|
@threaded
|
||||||
|
def openFile(self, file):
|
||||||
|
print("Opening: " + file)
|
||||||
|
if file.lower().endswith(self.vids):
|
||||||
|
subprocess.Popen([self.MEDIAPLAYER, self.MPV_WH, file])
|
||||||
|
elif file.lower().endswith(self.music):
|
||||||
|
subprocess.Popen([self.MUSICPLAYER, file])
|
||||||
|
elif file.lower().endswith(self.images):
|
||||||
|
subprocess.Popen([self.IMGVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.txt):
|
||||||
|
subprocess.Popen([self.TEXTVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.pdf):
|
||||||
|
subprocess.Popen([self.PDFVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.office):
|
||||||
|
subprocess.Popen([self.OFFICEPROG, file])
|
||||||
|
else:
|
||||||
|
subprocess.Popen(['xdg-open', file])
|
||||||
|
|
||||||
|
|
||||||
|
def createFile(self, newFileName):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, oldFileName, newFileName):
|
||||||
|
try:
|
||||||
|
print("Renaming...")
|
||||||
|
print(oldFileName + " --> " + newFileName)
|
||||||
|
os.rename(oldFileName, newFileName)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured renaming the file:")
|
||||||
|
print(e)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def deleteFile(self, toDeleteFile):
|
||||||
|
try:
|
||||||
|
print("Deleting...")
|
||||||
|
print(toDeleteFile)
|
||||||
|
if os.path.exists(toDeleteFile):
|
||||||
|
if os.path.isfile(toDeleteFile):
|
||||||
|
os.remove(toDeleteFile)
|
||||||
|
elif os.path.isdir(toDeleteFile):
|
||||||
|
shutil.rmtree(toDeleteFile)
|
||||||
|
else:
|
||||||
|
print("An error occured deleting the file:")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print("The folder/file does not exist")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured deleting the file:")
|
||||||
|
print(e)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
|
@ -0,0 +1,214 @@
|
||||||
|
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
from gi.repository import GLib as glib
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import os, threading, time
|
||||||
|
from os.path import isdir, isfile, join
|
||||||
|
from os import listdir
|
||||||
|
from .Icon import Icon
|
||||||
|
from .FileHandler import FileHandler
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class Grid:
|
||||||
|
def __init__(self, desktop, settings, newPath):
|
||||||
|
self.desktop = desktop
|
||||||
|
self.settings = settings
|
||||||
|
self.filehandler = FileHandler()
|
||||||
|
|
||||||
|
self.store = gtk.ListStore(GdkPixbuf.Pixbuf, str)
|
||||||
|
self.usrHome = settings.returnUserHome()
|
||||||
|
self.builder = settings.returnBuilder()
|
||||||
|
self.ColumnSize = settings.returnColumnSize()
|
||||||
|
self.currentPath = ""
|
||||||
|
self.selectedFile = ""
|
||||||
|
|
||||||
|
self.desktop.set_model(self.store)
|
||||||
|
self.desktop.set_pixbuf_column(0)
|
||||||
|
self.desktop.set_text_column(1)
|
||||||
|
self.desktop.connect("item-activated", self.iconLeftClickEventManager)
|
||||||
|
self.desktop.connect("button_press_event", self.iconRightClickEventManager, (self.desktop,))
|
||||||
|
self.desktop.connect("selection-changed", self.setIconSelectionArray, (self.desktop,))
|
||||||
|
|
||||||
|
self.vidsList = settings.returnVidsExtensionList()
|
||||||
|
self.imagesList = settings.returnImagesExtensionList()
|
||||||
|
self.gtkLock = False # Thread checks for gtkLock
|
||||||
|
self.threadLock = False # Gtk checks for thread lock
|
||||||
|
self.helperThread = None # Helper thread object
|
||||||
|
self.toWorkPool = [] # Thread fills pool and gtk empties it
|
||||||
|
self.copyCutArry = []
|
||||||
|
|
||||||
|
self.setIconViewDir(newPath)
|
||||||
|
|
||||||
|
def setIconViewDir(self, path):
|
||||||
|
self.store.clear()
|
||||||
|
|
||||||
|
self.currentPath = path
|
||||||
|
dirPaths = ['.', '..']
|
||||||
|
vids = []
|
||||||
|
images = []
|
||||||
|
desktop = []
|
||||||
|
files = []
|
||||||
|
|
||||||
|
for f in listdir(path):
|
||||||
|
file = join(path, f)
|
||||||
|
if self.settings.isHideHiddenFiles():
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
if isfile(file):
|
||||||
|
if file.lower().endswith(self.vidsList):
|
||||||
|
vids.append(f)
|
||||||
|
elif file.lower().endswith(self.imagesList):
|
||||||
|
images.append(f)
|
||||||
|
elif file.lower().endswith((".desktop",)):
|
||||||
|
desktop.append(f)
|
||||||
|
else:
|
||||||
|
files.append(f)
|
||||||
|
else:
|
||||||
|
dirPaths.append(f)
|
||||||
|
|
||||||
|
dirPaths.sort()
|
||||||
|
vids.sort()
|
||||||
|
images.sort()
|
||||||
|
desktop.sort()
|
||||||
|
files.sort()
|
||||||
|
files = dirPaths + vids + images + desktop + files
|
||||||
|
|
||||||
|
if self.helperThread:
|
||||||
|
self.helperThread.terminate()
|
||||||
|
self.helperThread = None
|
||||||
|
|
||||||
|
# Run helper thread...
|
||||||
|
self.threadLock = True
|
||||||
|
self.helperThread = threading.Thread(target=self.generateDirectoryGridIcon, args=(path, files)).start()
|
||||||
|
glib.idle_add(self.addToGrid, (file,)) # This must stay in the main thread b/c
|
||||||
|
# gtk isn't thread safe/aware So, we
|
||||||
|
# make a sad lil thread hot potato 'game'
|
||||||
|
# out of this process.
|
||||||
|
|
||||||
|
|
||||||
|
# @threaded
|
||||||
|
def generateDirectoryGridIcon(self, dirPath, files):
|
||||||
|
# NOTE: We'll be passing pixbuf after retreval to keep Icon.py file more
|
||||||
|
# universaly usable. We can just remove get_pixbuf to get a gtk.Image type
|
||||||
|
for file in files:
|
||||||
|
image = Icon(self.settings).createIcon(dirPath, file)
|
||||||
|
self.toWorkPool.append([image.get_pixbuf(), file])
|
||||||
|
self.threadLock = False
|
||||||
|
self.gtkLock = True
|
||||||
|
|
||||||
|
|
||||||
|
def addToGrid(self, args):
|
||||||
|
# NOTE: Returning true tells gtk to check again in the future when idle.
|
||||||
|
# False ends checks and "continues normal flow"
|
||||||
|
files = args[0]
|
||||||
|
|
||||||
|
if len(self.toWorkPool) > 0:
|
||||||
|
for dataSet in self.toWorkPool:
|
||||||
|
self.store.append(dataSet)
|
||||||
|
|
||||||
|
if len(self.store) == len(files): # Confirm processed all files and cleanup
|
||||||
|
self.gtkLock = False
|
||||||
|
self.threadLock = False
|
||||||
|
self.toWorkPool.clear()
|
||||||
|
return False
|
||||||
|
# Check again when idle; If nothing else is updating, this function
|
||||||
|
# gets called immediatly. So, we play hot potato by passing lock to Thread
|
||||||
|
else:
|
||||||
|
self.toWorkPool.clear()
|
||||||
|
self.gtkLock = False
|
||||||
|
self.threadLock = True
|
||||||
|
time.sleep(.005) # Fixes refresh and up icon not being added.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setIconSelectionArray(self, widget, data=None):
|
||||||
|
pass
|
||||||
|
# os.system('cls||clear')
|
||||||
|
# print(data)
|
||||||
|
|
||||||
|
def iconLeftClickEventManager(self, widget, item):
|
||||||
|
try:
|
||||||
|
model = widget.get_model()
|
||||||
|
fileName = model[item][1]
|
||||||
|
dir = self.currentPath
|
||||||
|
file = dir + "/" + fileName
|
||||||
|
|
||||||
|
if fileName == ".":
|
||||||
|
self.setIconViewDir(dir)
|
||||||
|
elif fileName == "..":
|
||||||
|
parentDir = os.path.abspath(os.path.join(dir, os.pardir))
|
||||||
|
self.currentPath = parentDir
|
||||||
|
self.setIconViewDir(parentDir)
|
||||||
|
elif isdir(file):
|
||||||
|
self.currentPath = file
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
elif isfile(file):
|
||||||
|
self.filehandler.openFile(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def iconRightClickEventManager(self, widget, eve, params):
|
||||||
|
try:
|
||||||
|
if eve.type == gdk.EventType.BUTTON_PRESS and eve.button == 3:
|
||||||
|
popover = self.builder.get_object("iconControlsWindow")
|
||||||
|
popover.show_all()
|
||||||
|
popover.popup()
|
||||||
|
# # NOTE: Need to change name of listview box...
|
||||||
|
# children = widget.get_children()[0].get_children()
|
||||||
|
# fileName = children[1].get_text()
|
||||||
|
# dir = self.currentPath
|
||||||
|
# file = dir + "/" + fileName
|
||||||
|
#
|
||||||
|
# input = self.builder.get_object("iconRenameInput")
|
||||||
|
# popover = self.builder.get_object("iconControlsWindow")
|
||||||
|
# self.selectedFile = file # Used for return to caller
|
||||||
|
#
|
||||||
|
# input.set_text(fileName)
|
||||||
|
# popover.set_relative_to(widget)
|
||||||
|
# popover.set_position(gtk.PositionType.RIGHT)
|
||||||
|
# popover.show_all()
|
||||||
|
# popover.popup()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
# Passthrough file control events
|
||||||
|
def createFile(arg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, file):
|
||||||
|
newName = self.currentPath + "/" + file
|
||||||
|
status = self.filehandler.updateFile(self.selectedFile, newName)
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
self.selectedFile = newName
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
|
||||||
|
def deleteFile(self):
|
||||||
|
status = self.filehandler.deleteFile(self.selectedFile)
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
self.selectedFile = ""
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gio as gio
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
from xdg.DesktopEntry import DesktopEntry
|
||||||
|
|
||||||
|
# Python Imports
|
||||||
|
import os, subprocess, hashlib, threading
|
||||||
|
|
||||||
|
from os.path import isdir, isfile, join
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class Icon:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.thubnailGen = settings.getThumbnailGenerator()
|
||||||
|
self.vidsList = settings.returnVidsExtensionList()
|
||||||
|
self.imagesList = settings.returnImagesExtensionList()
|
||||||
|
self.GTK_ORIENTATION = settings.returnIconImagePos()
|
||||||
|
self.usrHome = settings.returnUserHome()
|
||||||
|
self.iconContainerWH = settings.returnContainerWH()
|
||||||
|
self.systemIconImageWH = settings.returnSystemIconImageWH()
|
||||||
|
self.viIconWH = settings.returnVIIconWH()
|
||||||
|
|
||||||
|
|
||||||
|
def createIcon(self, dir, file):
|
||||||
|
fullPath = dir + "/" + file
|
||||||
|
return self.getIconImage(file, fullPath)
|
||||||
|
|
||||||
|
|
||||||
|
def getIconImage(self, file, fullPath):
|
||||||
|
try:
|
||||||
|
thumbnl = None
|
||||||
|
|
||||||
|
# Video thumbnail
|
||||||
|
if file.lower().endswith(self.vidsList):
|
||||||
|
fileHash = hashlib.sha256(str.encode(fullPath)).hexdigest()
|
||||||
|
hashImgPth = self.usrHome + "/.thumbnails/normal/" + fileHash + ".png"
|
||||||
|
|
||||||
|
if isfile(hashImgPth) == False:
|
||||||
|
self.generateVideoThumbnail(fullPath, hashImgPth)
|
||||||
|
|
||||||
|
thumbnl = self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
# Image Icon
|
||||||
|
elif file.lower().endswith(self.imagesList):
|
||||||
|
thumbnl = self.createIconImageBuffer(fullPath, self.viIconWH)
|
||||||
|
# .desktop file parsing
|
||||||
|
elif fullPath.lower().endswith( ('.desktop',) ):
|
||||||
|
thumbnl = self.parseDesktopFiles(fullPath)
|
||||||
|
# System icons
|
||||||
|
else:
|
||||||
|
thumbnl = self.getSystemThumbnail(fullPath, self.systemIconImageWH[0])
|
||||||
|
|
||||||
|
if thumbnl == None: # If no icon, try stock file icon...
|
||||||
|
thumbnl = gtk.Image.new_from_icon_name("gtk-file", gtk.IconSize.LARGE_TOOLBAR)
|
||||||
|
|
||||||
|
if thumbnl == None: # If no icon whatsoever, return internal default
|
||||||
|
thumbnl = gtk.Image.new_from_file("resources/icons/bin.png")
|
||||||
|
|
||||||
|
return thumbnl
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return gtk.Image.new_from_file("resources/icons/bin.png")
|
||||||
|
|
||||||
|
|
||||||
|
def parseDesktopFiles(self, fullPath):
|
||||||
|
try:
|
||||||
|
xdgObj = DesktopEntry(fullPath)
|
||||||
|
icon = xdgObj.getIcon()
|
||||||
|
iconsDirs = "/usr/share/icons"
|
||||||
|
altIconPath = ""
|
||||||
|
|
||||||
|
if "steam" in icon:
|
||||||
|
steamIconsDir = self.usrHome + "/.thumbnails/steam_icons/"
|
||||||
|
name = xdgObj.getName()
|
||||||
|
fileHash = hashlib.sha256(str.encode(name)).hexdigest()
|
||||||
|
|
||||||
|
if isdir(steamIconsDir) == False:
|
||||||
|
os.mkdir(steamIconsDir)
|
||||||
|
|
||||||
|
hashImgPth = steamIconsDir + fileHash + ".jpg"
|
||||||
|
if isfile(hashImgPth) == True:
|
||||||
|
# Use video sizes since headers are bigger
|
||||||
|
return self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
|
||||||
|
execStr = xdgObj.getExec()
|
||||||
|
parts = execStr.split("steam://rungameid/")
|
||||||
|
id = parts[len(parts) - 1]
|
||||||
|
|
||||||
|
# NOTE: Can try this logic instead...
|
||||||
|
# if command exists use it instead of header image
|
||||||
|
# if "steamcmd app_info_print id":
|
||||||
|
# proc = subprocess.Popen(["steamcmd", "app_info_print", id])
|
||||||
|
# proc.wait()
|
||||||
|
# else:
|
||||||
|
# use the bottom logic
|
||||||
|
|
||||||
|
imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg"
|
||||||
|
proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink])
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
# Use video sizes since headers are bigger
|
||||||
|
return self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
elif os.path.exists(icon):
|
||||||
|
return self.createIconImageBuffer(icon, self.systemIconImageWH)
|
||||||
|
else:
|
||||||
|
for (dirpath, dirnames, filenames) in os.walk(iconsDirs):
|
||||||
|
for file in filenames:
|
||||||
|
appNM = "application-x-" + icon
|
||||||
|
if appNM in file:
|
||||||
|
altIconPath = dirpath + "/" + file
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.createIconImageBuffer(altIconPath, self.systemIconImageWH)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def getSystemThumbnail(self, filename, size):
|
||||||
|
try:
|
||||||
|
iconPath = None
|
||||||
|
if os.path.exists(filename):
|
||||||
|
file = gio.File.new_for_path(filename)
|
||||||
|
info = file.query_info('standard::icon' , 0 , gio.Cancellable())
|
||||||
|
icon = info.get_icon().get_names()[0]
|
||||||
|
iconTheme = gtk.IconTheme.get_default()
|
||||||
|
iconFile = iconTheme.lookup_icon(icon , size , 0)
|
||||||
|
|
||||||
|
if iconFile != None:
|
||||||
|
iconPath = iconFile.get_filename()
|
||||||
|
return self.createIconImageBuffer(iconPath, self.systemIconImageWH)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def createIconImageBuffer(self, path, wxh):
|
||||||
|
try:
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], False)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
|
|
||||||
|
def generateVideoThumbnail(self, fullPath, hashImgPth):
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen([self.thubnailGen, "-t", "65%", "-s", "300", "-c", "jpg", "-i", fullPath, "-o", hashImgPth])
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
|
@ -0,0 +1,139 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi, cairo, os
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self):
|
||||||
|
self.builder = None
|
||||||
|
self.hideHiddenFiles = True
|
||||||
|
|
||||||
|
|
||||||
|
self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1)
|
||||||
|
self.THUMB_GENERATOR = "ffmpegthumbnailer"
|
||||||
|
self.DEFAULTCOLOR = gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000
|
||||||
|
self.MOUSEOVERCOLOR = gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff
|
||||||
|
self.SELECTEDCOLOR = gdk.RGBA(0.4, 0.5, 0.1, 0.84)
|
||||||
|
|
||||||
|
self.ColumnSize = 8
|
||||||
|
self.usrHome = os.path.expanduser('~')
|
||||||
|
self.desktopPath = self.usrHome + "/Desktop"
|
||||||
|
self.webHome = 'http://webfm.com/'
|
||||||
|
self.iconContainerWxH = [128, 128]
|
||||||
|
self.systemIconImageWxH = [72, 72]
|
||||||
|
self.viIconWxH = [256, 128]
|
||||||
|
self.vidsExtensionList = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
|
self.imagesExtensionList = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga')
|
||||||
|
|
||||||
|
|
||||||
|
def attachBuilder(self, builder):
|
||||||
|
self.builder = builder
|
||||||
|
self.builder.add_from_file("resources/PyTop.glade")
|
||||||
|
|
||||||
|
def createWindow(self):
|
||||||
|
# Get window and connect signals
|
||||||
|
window = self.builder.get_object("Window")
|
||||||
|
window.connect("delete-event", gtk.main_quit)
|
||||||
|
self.setWindowData(window)
|
||||||
|
return window
|
||||||
|
|
||||||
|
def setWindowData(self, window):
|
||||||
|
screen = window.get_screen()
|
||||||
|
visual = screen.get_rgba_visual()
|
||||||
|
if visual != None and screen.is_composited():
|
||||||
|
window.set_visual(visual)
|
||||||
|
|
||||||
|
# bind css file
|
||||||
|
cssProvider = gtk.CssProvider()
|
||||||
|
cssProvider.load_from_path('resources/stylesheet.css')
|
||||||
|
screen = gdk.Screen.get_default()
|
||||||
|
styleContext = gtk.StyleContext()
|
||||||
|
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
|
window.set_app_paintable(True)
|
||||||
|
monitors = self.getMonitorData(screen)
|
||||||
|
window.resize(monitors[0].width, monitors[0].height)
|
||||||
|
|
||||||
|
def getMonitorData(self, screen):
|
||||||
|
monitors = []
|
||||||
|
for m in range(screen.get_n_monitors()):
|
||||||
|
monitors.append(screen.get_monitor_geometry(m))
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
|
||||||
|
|
||||||
|
return monitors
|
||||||
|
|
||||||
|
|
||||||
|
def returnBuilder(self): return self.builder
|
||||||
|
def returnUserHome(self): return self.usrHome
|
||||||
|
def returnDesktopPath(self): return self.usrHome + "/Desktop"
|
||||||
|
def returnIconImagePos(self): return self.GTK_ORIENTATION
|
||||||
|
def getThumbnailGenerator(self): return self.THUMB_GENERATOR
|
||||||
|
def returnColumnSize(self): return self.ColumnSize
|
||||||
|
def returnContainerWH(self): return self.iconContainerWxH
|
||||||
|
def returnSystemIconImageWH(self): return self.systemIconImageWxH
|
||||||
|
def returnVIIconWH(self): return self.viIconWxH
|
||||||
|
def returnWebHome(self): return self.webHome
|
||||||
|
def isHideHiddenFiles(self): return self.hideHiddenFiles
|
||||||
|
def returnVidsExtensionList(self): return self.vidsExtensionList
|
||||||
|
def returnImagesExtensionList(self): return self.imagesExtensionList
|
||||||
|
|
||||||
|
def setDefaultWebviewSettings(self, widget, settings=None):
|
||||||
|
# Usability
|
||||||
|
settings.set_property('enable-fullscreen', True)
|
||||||
|
settings.set_property('print-backgrounds', True)
|
||||||
|
settings.set_property('enable-frame-flattening', False)
|
||||||
|
settings.set_property('enable-plugins', True)
|
||||||
|
settings.set_property('enable-java', False)
|
||||||
|
settings.set_property('enable-resizable-text-areas', True)
|
||||||
|
settings.set_property('zoom-text-only', False)
|
||||||
|
settings.set_property('enable-smooth-scrolling', True)
|
||||||
|
settings.set_property('enable-back-forward-navigation-gestures', False)
|
||||||
|
settings.set_property('media-playback-requires-user-gesture', False)
|
||||||
|
settings.set_property('enable-tabs-to-links', True)
|
||||||
|
settings.set_property('enable-caret-browsing', False)
|
||||||
|
|
||||||
|
# Security
|
||||||
|
settings.set_property('user-agent','Mozilla/5.0 (X11; Generic; Linux x86-64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Safari/605.1.15')
|
||||||
|
settings.set_property('enable-private-browsing', False)
|
||||||
|
settings.set_property('enable-xss-auditor', True)
|
||||||
|
settings.set_property('enable-hyperlink-auditing', False)
|
||||||
|
settings.set_property('enable-site-specific-quirks', True)
|
||||||
|
settings.set_property('enable-offline-web-application-cache', True)
|
||||||
|
settings.set_property('enable-page-cache', True)
|
||||||
|
settings.set_property('allow-modal-dialogs', False)
|
||||||
|
settings.set_property('enable-html5-local-storage', True)
|
||||||
|
settings.set_property('enable-html5-database', True)
|
||||||
|
settings.set_property('allow-file-access-from-file-urls', False)
|
||||||
|
settings.set_property('allow-universal-access-from-file-urls', False)
|
||||||
|
settings.set_property('enable-dns-prefetching', False)
|
||||||
|
|
||||||
|
# Media stuff
|
||||||
|
# settings.set_property('hardware-acceleration-policy', 'on-demand')
|
||||||
|
settings.set_property('enable-webgl', False)
|
||||||
|
settings.set_property('enable-webaudio', True)
|
||||||
|
settings.set_property('enable-accelerated-2d-canvas', True)
|
||||||
|
settings.set_property('auto-load-images', True)
|
||||||
|
settings.set_property('enable-media-capabilities', True)
|
||||||
|
settings.set_property('enable-media-stream', True)
|
||||||
|
settings.set_property('enable-mediasource', True)
|
||||||
|
settings.set_property('enable-encrypted-media', True)
|
||||||
|
settings.set_property('media-playback-allows-inline', True)
|
||||||
|
|
||||||
|
# JS
|
||||||
|
settings.set_property('enable-javascript', True)
|
||||||
|
settings.set_property('enable-javascript-markup', True)
|
||||||
|
settings.set_property('javascript-can-access-clipboard', False)
|
||||||
|
settings.set_property('javascript-can-open-windows-automatically', False)
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
settings.set_property('enable-developer-extras', False)
|
||||||
|
settings.set_property('enable-write-console-messages-to-stdout', False)
|
||||||
|
settings.set_property('draw-compositing-indicators', False)
|
||||||
|
settings.set_property('enable-mock-capture-devices', False)
|
||||||
|
settings.set_property('enable-spatial-navigation', False)
|
|
@ -0,0 +1,6 @@
|
||||||
|
from utils.Dragging import Dragging
|
||||||
|
from utils.Settings import Settings
|
||||||
|
from utils.Events import Events
|
||||||
|
from utils.Grid import Grid
|
||||||
|
from utils.Icon import Icon
|
||||||
|
from utils.FileHandler import FileHandler
|
|
@ -0,0 +1,22 @@
|
||||||
|
Pytop is copyright 2019 Maxim Stewart.
|
||||||
|
Pytop is currently developed by ITDominator <1itdominator@gmail.com>.
|
||||||
|
|
||||||
|
License: GPLv2+
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
See /usr/share/common-licenses/GPL-2, or
|
||||||
|
<http://www.gnu.org/copyleft/gpl.txt> for the terms of the latest version
|
||||||
|
of the GNU General Public License.
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Python imports
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from utils import Settings
|
||||||
|
from signal_classes import Signals
|
||||||
|
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
def __init__(self, args):
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
# Gets the methods from the classes and sets to handler.
|
||||||
|
# Then, builder connects to any signals it needs.
|
||||||
|
classes = [Signals(settings)]
|
||||||
|
|
||||||
|
handlers = {}
|
||||||
|
for c in classes:
|
||||||
|
methods = None
|
||||||
|
try:
|
||||||
|
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||||
|
handlers.update(methods)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
settings.builder.connect_signals(handlers)
|
||||||
|
window = settings.createWindow()
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import argparse
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
import tracemalloc
|
||||||
|
tracemalloc.start()
|
||||||
|
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
import gi, faulthandler, traceback, signal
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from __init__ import Main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
setproctitle('PyFM')
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit)
|
||||||
|
faulthandler.enable() # For better debug info
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
# Add long and short arguments
|
||||||
|
parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.")
|
||||||
|
|
||||||
|
# Read arguments (If any...)
|
||||||
|
args = parser.parse_args()
|
||||||
|
main = Main(args)
|
||||||
|
gtk.main()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
|
@ -0,0 +1,568 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.38.2 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<object class="GtkImage" id="tggl_notebook_1_img">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="stock">gtk-apply</property>
|
||||||
|
<property name="icon_size">3</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkImage" id="tggl_notebook_2_img">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="stock">gtk-apply</property>
|
||||||
|
<property name="icon_size">3</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkImage" id="tggl_notebook_3_img">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="stock">gtk-apply</property>
|
||||||
|
<property name="icon_size">3</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkImage" id="tggl_notebook_4_img">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="stock">gtk-apply</property>
|
||||||
|
<property name="icon_size">3</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkApplicationWindow" id="Main_Window">
|
||||||
|
<property name="width-request">800</property>
|
||||||
|
<property name="height-request">600</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="title" translatable="yes">PyFM</property>
|
||||||
|
<property name="window-position">center</property>
|
||||||
|
<property name="default-width">1200</property>
|
||||||
|
<property name="default-height">720</property>
|
||||||
|
<property name="icon">pyfm.png</property>
|
||||||
|
<property name="gravity">center</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="baseline-position">top</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuBar">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_File</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-new</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-open</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-save</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-save-as</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkSeparatorMenuItem">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-quit</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_Edit</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-cut</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-copy</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-paste</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-delete</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_View</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">_Help</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<child type="submenu">
|
||||||
|
<object class="GtkMenu">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImageMenuItem">
|
||||||
|
<property name="label">gtk-about</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButtonBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="margin-right">45</property>
|
||||||
|
<property name="spacing">5</property>
|
||||||
|
<property name="layout-style">start</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="tggl_notebook_1">
|
||||||
|
<property name="name">tggl_notebook_1</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="image">tggl_notebook_1_img</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="toggle_notebook_pane" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="tggl_notebook_2">
|
||||||
|
<property name="name">tggl_notebook_2</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="image">tggl_notebook_2_img</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="toggle_notebook_pane" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="tggl_notebook_3">
|
||||||
|
<property name="name">tggl_notebook_3</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="image">tggl_notebook_3_img</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="toggle_notebook_pane" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="tggl_notebook_4">
|
||||||
|
<property name="name">tggl_notebook_4</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="image">tggl_notebook_4_img</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="toggle_notebook_pane" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-home</property>
|
||||||
|
<property name="name">go_home</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="do_action_from_bar_controls" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-refresh</property>
|
||||||
|
<property name="name">refresh_view</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="do_action_from_bar_controls" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-go-up</property>
|
||||||
|
<property name="name">go_up</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="do_action_from_bar_controls" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="path_entry">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="text" translatable="yes">Path...</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton">
|
||||||
|
<property name="label">gtk-add</property>
|
||||||
|
<property name="name">create_tab</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="receives-default">True</property>
|
||||||
|
<property name="use-stock">True</property>
|
||||||
|
<property name="always-show-image">True</property>
|
||||||
|
<signal name="released" handler="do_action_from_bar_controls" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">4</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkPaned" id="pane_master">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="wide-handle">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkPaned" id="pane_top">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="wide-handle">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkNotebook" id="window_1">
|
||||||
|
<property name="name">notebook1</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-left">5</property>
|
||||||
|
<property name="margin-right">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="scrollable">True</property>
|
||||||
|
<signal name="switch-page" handler="on_tab_switch_update" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkNotebook" id="window_2">
|
||||||
|
<property name="name">notebook2</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-left">5</property>
|
||||||
|
<property name="margin-right">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="scrollable">True</property>
|
||||||
|
<signal name="switch-page" handler="on_tab_switch_update" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkPaned" id="pane_bottom">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
|
<property name="wide-handle">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkNotebook" id="window_3">
|
||||||
|
<property name="name">notebook3</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-left">5</property>
|
||||||
|
<property name="margin-right">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="scrollable">True</property>
|
||||||
|
<signal name="switch-page" handler="on_tab_switch_update" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkNotebook" id="window_4">
|
||||||
|
<property name="name">notebook4</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="margin-right">5</property>
|
||||||
|
<property name="margin-top">5</property>
|
||||||
|
<property name="margin-bottom">5</property>
|
||||||
|
<property name="scrollable">True</property>
|
||||||
|
<signal name="switch-page" handler="on_tab_switch_update" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
<child type="tab">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">True</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
After Width: | Height: | Size: 40 KiB |
|
@ -0,0 +1,34 @@
|
||||||
|
/* Set fm to have transparent window */
|
||||||
|
|
||||||
|
* {
|
||||||
|
background: rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
* selection {
|
||||||
|
background-color: rgba(116, 0, 0, 0.65);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.view:selected,
|
||||||
|
.view:selected:hover {
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(15, 134, 13, 0.49);
|
||||||
|
color: rgba(255, 255, 255, 0.5);;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Rubberband coloring */
|
||||||
|
.rubberband,
|
||||||
|
rubberband,
|
||||||
|
flowbox rubberband,
|
||||||
|
treeview.view rubberband,
|
||||||
|
.content-view rubberband,
|
||||||
|
.content-view .rubberband,
|
||||||
|
XfdesktopIconView.view .rubberband {
|
||||||
|
border: 1px solid #6c6c6c;
|
||||||
|
background-color: rgba(21, 158, 167, 0.57);
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view:active {
|
||||||
|
background-color: rgba(172, 102, 21, 1);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
from .windows import WindowController
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Python imports
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .view import View
|
||||||
|
|
||||||
|
|
||||||
|
class Window:
|
||||||
|
def __init__(self):
|
||||||
|
self.id_length = 10
|
||||||
|
self.id = ""
|
||||||
|
self.name = ""
|
||||||
|
self.nickname = ""
|
||||||
|
self.isHidden = False
|
||||||
|
self.views = []
|
||||||
|
|
||||||
|
self.generate_id()
|
||||||
|
|
||||||
|
|
||||||
|
def random_with_N_digits(self, n):
|
||||||
|
range_start = 10**(n-1)
|
||||||
|
range_end = (10**n)-1
|
||||||
|
return randint(range_start, range_end)
|
||||||
|
|
||||||
|
def generate_id(self):
|
||||||
|
self.id = str(self.random_with_N_digits(self.id_length))
|
||||||
|
|
||||||
|
def get_window_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def create_view(self):
|
||||||
|
view = View()
|
||||||
|
self.views.append(view)
|
||||||
|
return view
|
||||||
|
|
||||||
|
def pop_view(self):
|
||||||
|
self.views.pop()
|
||||||
|
|
||||||
|
def delete_view_by_id(self, vid):
|
||||||
|
for view in self.views:
|
||||||
|
if view.id == vid:
|
||||||
|
self.views.remove(view)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def get_view_by_id(self, vid):
|
||||||
|
for view in self.views:
|
||||||
|
if view.id == vid:
|
||||||
|
return view
|
||||||
|
|
||||||
|
def get_view_by_index(self, index):
|
||||||
|
return self.views[index]
|
||||||
|
|
||||||
|
def get_views_count(self):
|
||||||
|
return len(self.views)
|
||||||
|
|
||||||
|
def get_all_views(self):
|
||||||
|
return self.views
|
||||||
|
|
||||||
|
def list_files_from_views(self):
|
||||||
|
for view in self.views:
|
||||||
|
print(view.files)
|
|
@ -0,0 +1,165 @@
|
||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from . import Window
|
||||||
|
|
||||||
|
|
||||||
|
class WindowController:
|
||||||
|
def __init__(self):
|
||||||
|
USER_HOME = path.expanduser('~')
|
||||||
|
CONFIG_PATH = USER_HOME + "/.config/pyfm"
|
||||||
|
self.session_file = CONFIG_PATH + "/session.json"
|
||||||
|
|
||||||
|
self.active_window_id = ""
|
||||||
|
self.active_tab_id = ""
|
||||||
|
self.windows = []
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_data(self, wid, tid):
|
||||||
|
self.active_window_id = str(wid)
|
||||||
|
self.active_tab_id = str(tid)
|
||||||
|
|
||||||
|
def get_active_data(self):
|
||||||
|
return self.active_window_id, self.active_tab_id
|
||||||
|
|
||||||
|
def create_window(self):
|
||||||
|
window = Window()
|
||||||
|
window.name = "window_" + window.id
|
||||||
|
window.nickname = "window_" + str(len(self.windows) + 1)
|
||||||
|
|
||||||
|
self.windows.append(window)
|
||||||
|
return window
|
||||||
|
|
||||||
|
|
||||||
|
def add_view_for_window(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
return window.create_view()
|
||||||
|
|
||||||
|
def add_view_for_window_by_name(self, name):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.name == name:
|
||||||
|
return window.create_view()
|
||||||
|
|
||||||
|
def add_view_for_window_by_nickname(self, nickname):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.nickname == nickname:
|
||||||
|
return window.create_view()
|
||||||
|
|
||||||
|
def pop_window(self):
|
||||||
|
self.windows.pop()
|
||||||
|
|
||||||
|
def delete_window_by_id(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
self.windows.remove(window)
|
||||||
|
break
|
||||||
|
|
||||||
|
def delete_window_by_name(self, name):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.name == name:
|
||||||
|
self.windows.remove(window)
|
||||||
|
break
|
||||||
|
|
||||||
|
def delete_window_by_nickname(self, nickname):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.nickname == nickname:
|
||||||
|
self.windows.remove(window)
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_window_by_id(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
return window
|
||||||
|
|
||||||
|
raise(f"No Window by ID {win_id} found!")
|
||||||
|
|
||||||
|
def get_window_by_name(self, name):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.name == name:
|
||||||
|
return window
|
||||||
|
|
||||||
|
raise(f"No Window by Name {name} found!")
|
||||||
|
|
||||||
|
def get_window_by_nickname(self, nickname):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.nickname == nickname:
|
||||||
|
return window
|
||||||
|
|
||||||
|
raise(f"No Window by Nickname {nickname} found!")
|
||||||
|
|
||||||
|
def get_window_by_index(self, index):
|
||||||
|
return self.windows[index]
|
||||||
|
|
||||||
|
def get_all_windows(self):
|
||||||
|
return self.windows
|
||||||
|
|
||||||
|
def set_window_nickname(self, win_id = None, nickname = ""):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
window.nickname = nickname
|
||||||
|
|
||||||
|
def list_windows(self):
|
||||||
|
print("\n[ ---- Windows ---- ]\n")
|
||||||
|
for window in self.windows:
|
||||||
|
print(f"\nID: {window.id}")
|
||||||
|
print(f"Name: {window.name}")
|
||||||
|
print(f"Nickname: {window.nickname}")
|
||||||
|
print(f"Is Hidden: {window.isHidden}")
|
||||||
|
print(f"View Count: {window.get_views_count()}")
|
||||||
|
print("\n-------------------------\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def list_files_from_views_of_window(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
window.list_files_from_views()
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_views_count(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
return window.get_views_count()
|
||||||
|
|
||||||
|
def get_views_from_window(self, win_id):
|
||||||
|
for window in self.windows:
|
||||||
|
if window.id == win_id:
|
||||||
|
return window.get_all_views()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
windows = []
|
||||||
|
for window in self.windows:
|
||||||
|
views = []
|
||||||
|
for view in window.views:
|
||||||
|
views.append(view.get_current_directory())
|
||||||
|
|
||||||
|
windows.append(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'window':{
|
||||||
|
"ID": window.id,
|
||||||
|
"Name": window.name,
|
||||||
|
"Nickname": window.nickname,
|
||||||
|
"isHidden": f"{window.isHidden}",
|
||||||
|
'views': views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(self.session_file, 'w') as outfile:
|
||||||
|
json.dump(windows, outfile, separators=(',', ':'), indent=4)
|
||||||
|
|
||||||
|
def load_state(self):
|
||||||
|
if path.isfile(self.session_file):
|
||||||
|
with open(self.session_file) as infile:
|
||||||
|
return json.load(infile)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .Window import Window
|
||||||
|
from .WindowController import WindowController
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class Path:
|
||||||
|
def get_home(self):
|
||||||
|
return os.path.expanduser("~") + self.subpath
|
||||||
|
|
||||||
|
def get_path(self):
|
||||||
|
return "/" + "/".join(self.path)
|
||||||
|
|
||||||
|
def get_path_list(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
def push_to_path(self, dir):
|
||||||
|
self.path.append(dir)
|
||||||
|
self.load_directory()
|
||||||
|
|
||||||
|
def pop_from_path(self):
|
||||||
|
self.path.pop()
|
||||||
|
|
||||||
|
if not self.go_past_home:
|
||||||
|
if self.get_home() not in self.get_path():
|
||||||
|
self.set_to_home()
|
||||||
|
|
||||||
|
self.load_directory()
|
||||||
|
|
||||||
|
|
||||||
|
def set_path(self, path):
|
||||||
|
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
|
||||||
|
self.load_directory()
|
||||||
|
|
||||||
|
def set_path_with_sub_path(self, sub_path):
|
||||||
|
path = os.path.join(self.get_home(), sub_path)
|
||||||
|
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
|
||||||
|
self.load_directory()
|
||||||
|
|
||||||
|
def set_to_home(self):
|
||||||
|
home = os.path.expanduser("~") + self.subpath
|
||||||
|
path = list( filter(None, home.replace("\\", "/").split('/')) )
|
||||||
|
self.path = path
|
||||||
|
self.load_directory()
|
|
@ -0,0 +1,196 @@
|
||||||
|
# Python imports
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isdir, isfile, join
|
||||||
|
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .utils import Settings, Launcher
|
||||||
|
from .icons import Icon
|
||||||
|
from . import Path
|
||||||
|
|
||||||
|
|
||||||
|
class View(Settings, Launcher, Icon, Path):
|
||||||
|
def __init__(self):
|
||||||
|
self. logger = None
|
||||||
|
self.id_length = 10
|
||||||
|
|
||||||
|
self.id = ""
|
||||||
|
self.files = []
|
||||||
|
self.dirs = []
|
||||||
|
self.vids = []
|
||||||
|
self.images = []
|
||||||
|
self.desktop = []
|
||||||
|
self.ungrouped = []
|
||||||
|
|
||||||
|
self.generate_id()
|
||||||
|
self.set_to_home()
|
||||||
|
|
||||||
|
|
||||||
|
def random_with_N_digits(self, n):
|
||||||
|
range_start = 10**(n-1)
|
||||||
|
range_end = (10**n)-1
|
||||||
|
return randint(range_start, range_end)
|
||||||
|
|
||||||
|
def generate_id(self):
|
||||||
|
self.id = str(self.random_with_N_digits(self.id_length))
|
||||||
|
|
||||||
|
def get_tab_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def load_directory(self):
|
||||||
|
path = self.get_path()
|
||||||
|
self.dirs = [".", ".."]
|
||||||
|
self.vids = []
|
||||||
|
self.images = []
|
||||||
|
self.desktop = []
|
||||||
|
self.ungrouped = []
|
||||||
|
self.files = []
|
||||||
|
|
||||||
|
if not isdir(path):
|
||||||
|
self.set_to_home()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
for f in listdir(path):
|
||||||
|
file = join(path, f)
|
||||||
|
if self.HIDE_HIDDEN_FILES:
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isfile(file):
|
||||||
|
lowerName = file.lower()
|
||||||
|
if lowerName.endswith(self.fvideos):
|
||||||
|
self.vids.append(f)
|
||||||
|
elif lowerName.endswith(self.fimages):
|
||||||
|
self.images.append(f)
|
||||||
|
elif lowerName.endswith((".desktop",)):
|
||||||
|
self.desktop.append(f)
|
||||||
|
else:
|
||||||
|
self.ungrouped.append(f)
|
||||||
|
else:
|
||||||
|
self.dirs.append(f)
|
||||||
|
|
||||||
|
self.dirs.sort()
|
||||||
|
self.vids.sort()
|
||||||
|
self.images.sort()
|
||||||
|
self.desktop.sort()
|
||||||
|
self.ungrouped.sort()
|
||||||
|
|
||||||
|
self.files = self.dirs + self.vids + self.images + self.desktop + self.ungrouped
|
||||||
|
|
||||||
|
def hash_text(self, text):
|
||||||
|
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
|
||||||
|
|
||||||
|
def hash_set(self, arry):
|
||||||
|
data = []
|
||||||
|
for arr in arry:
|
||||||
|
data.append([arr, self.hash_text(arr)])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def is_folder_locked(self, hash):
|
||||||
|
if self.lock_folder:
|
||||||
|
path_parts = self.get_path().split('/')
|
||||||
|
file = self.get_path_part_from_hash(hash)
|
||||||
|
|
||||||
|
# Insure chilren folders are locked too.
|
||||||
|
lockedFolderInPath = False
|
||||||
|
for folder in self.locked_folders:
|
||||||
|
if folder in path_parts:
|
||||||
|
lockedFolderInPath = True
|
||||||
|
break
|
||||||
|
|
||||||
|
return (file in self.locked_folders or lockedFolderInPath)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_path_part_from_hash(self, hash):
|
||||||
|
files = self.get_files()
|
||||||
|
file = None
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
if hash == f[1]:
|
||||||
|
file = f[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
return file
|
||||||
|
|
||||||
|
def get_files_formatted(self):
|
||||||
|
files = self.hash_set(self.files),
|
||||||
|
dirs = self.hash_set(self.dirs),
|
||||||
|
videos = self.get_videos(),
|
||||||
|
images = self.hash_set(self.images),
|
||||||
|
desktops = self.hash_set(self.desktop),
|
||||||
|
ungrouped = self.hash_set(self.ungrouped)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'path_head': self.get_path(),
|
||||||
|
'list': {
|
||||||
|
'files': files,
|
||||||
|
'dirs': dirs,
|
||||||
|
'videos': videos,
|
||||||
|
'images': images,
|
||||||
|
'desktops': desktops,
|
||||||
|
'ungrouped': ungrouped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pixbuf_icon_str_combo(self):
|
||||||
|
data = []
|
||||||
|
dir = self.get_current_directory()
|
||||||
|
for file in self.files:
|
||||||
|
icon = self.create_icon(dir, file).get_pixbuf()
|
||||||
|
data.append([icon, file])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_gtk_icon_str_combo(self):
|
||||||
|
data = []
|
||||||
|
dir = self.get_current_directory()
|
||||||
|
for file in self.files:
|
||||||
|
icon = self.create_icon(dir, file)
|
||||||
|
data.append([icon, file[0]])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_current_directory(self):
|
||||||
|
return self.get_path()
|
||||||
|
|
||||||
|
def get_current_sub_path(self):
|
||||||
|
path = self.get_path()
|
||||||
|
home = self.get_home() + "/"
|
||||||
|
return path.replace(home, "")
|
||||||
|
|
||||||
|
def get_end_of_path(self):
|
||||||
|
parts = self.get_current_directory().split("/")
|
||||||
|
size = len(parts)
|
||||||
|
return parts[size - 1]
|
||||||
|
|
||||||
|
def get_dot_dots(self):
|
||||||
|
return self.hash_set(['.', '..'])
|
||||||
|
|
||||||
|
def get_files(self):
|
||||||
|
return self.hash_set(self.files)
|
||||||
|
|
||||||
|
def get_dirs(self):
|
||||||
|
return self.hash_set(self.dirs)
|
||||||
|
|
||||||
|
def get_videos(self):
|
||||||
|
return self.hash_set(self.vids)
|
||||||
|
|
||||||
|
def get_images(self):
|
||||||
|
return self.hash_set(self.images)
|
||||||
|
|
||||||
|
def get_desktops(self):
|
||||||
|
return self.hash_set(self.desktop)
|
||||||
|
|
||||||
|
def get_ungrouped(self):
|
||||||
|
return self.hash_set(self.ungrouped)
|
|
@ -0,0 +1,5 @@
|
||||||
|
from .utils import *
|
||||||
|
from .icons import *
|
||||||
|
|
||||||
|
from .Path import Path
|
||||||
|
from .View import View
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Python Imports
|
||||||
|
import os, subprocess, threading, hashlib
|
||||||
|
from os.path import isfile
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .mixins import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Icon(DesktopIconMixin, VideoIconMixin):
|
||||||
|
def create_icon(self, dir, file):
|
||||||
|
full_path = dir + "/" + file
|
||||||
|
return self.get_icon_image(dir, file, full_path)
|
||||||
|
|
||||||
|
def get_icon_image(self, dir, file, full_path):
|
||||||
|
try:
|
||||||
|
thumbnl = None
|
||||||
|
|
||||||
|
if file.lower().endswith(self.fvideos): # Video icon
|
||||||
|
thumbnl = self.create_thumbnail(dir, file)
|
||||||
|
elif file.lower().endswith(self.fimages): # Image Icon
|
||||||
|
thumbnl = self.create_scaled_image(full_path, self.VIDEO_ICON_WH)
|
||||||
|
elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing
|
||||||
|
thumbnl = self.parse_desktop_files(full_path)
|
||||||
|
else: # System icons
|
||||||
|
thumbnl = self.get_system_thumbnail(full_path, self.SYS_ICON_WH[0])
|
||||||
|
|
||||||
|
if thumbnl == None: # If no icon whatsoever, return internal default
|
||||||
|
thumbnl = Gtk.Image.new_from_file(self.DEFAULT_ICON)
|
||||||
|
|
||||||
|
return thumbnl
|
||||||
|
except Exception as e:
|
||||||
|
print("Icon generation issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return Gtk.Image.new_from_file(self.DEFAULT_ICON)
|
||||||
|
|
||||||
|
def create_thumbnail(self, dir, file):
|
||||||
|
full_path = dir + "/" + file
|
||||||
|
try:
|
||||||
|
file_hash = hashlib.sha256(str.encode(full_path)).hexdigest()
|
||||||
|
hash_img_pth = self.ABS_THUMBS_PTH + "/" + file_hash + ".jpg"
|
||||||
|
if isfile(hash_img_pth) == False:
|
||||||
|
self.generate_video_thumbnail(full_path, hash_img_pth)
|
||||||
|
|
||||||
|
thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
|
||||||
|
if thumbnl == None: # If no icon whatsoever, return internal default
|
||||||
|
thumbnl = Gtk.Image.new_from_file(self.DEFAULT_ICONS + "/video.png")
|
||||||
|
|
||||||
|
return thumbnl
|
||||||
|
except Exception as e:
|
||||||
|
print("Thumbnail generation issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return Gtk.Image.new_from_file(self.DEFAULT_ICONS + "/video.png")
|
||||||
|
|
||||||
|
|
||||||
|
def create_scaled_image(self, path, wxh):
|
||||||
|
try:
|
||||||
|
pixbuf = Gtk.Image.new_from_file(path).get_pixbuf()
|
||||||
|
scaled_pixbuf = pixbuf.scale_simple(wxh[0], wxh[1], 2) # 2 = BILINEAR and is best by default
|
||||||
|
return Gtk.Image.new_from_pixbuf(scaled_pixbuf)
|
||||||
|
except Exception as e:
|
||||||
|
print("Image Scaling Issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_from_file(self, path):
|
||||||
|
try:
|
||||||
|
return Gtk.Image.new_from_file(path)
|
||||||
|
except Exception as e:
|
||||||
|
print("Image from file Issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return None
|
||||||
|
|
||||||
|
def return_generic_icon(self):
|
||||||
|
return Gtk.Image.new_from_file(self.DEFAULT_ICON)
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .mixins import DesktopIconMixin
|
||||||
|
from .mixins import VideoIconMixin
|
||||||
|
|
||||||
|
from .Icon import Icon
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Python Imports
|
||||||
|
import os, subprocess, hashlib
|
||||||
|
from os.path import isfile
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gio
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .xdg.DesktopEntry import DesktopEntry
|
||||||
|
|
||||||
|
|
||||||
|
class DesktopIconMixin:
|
||||||
|
def get_system_thumbnail(self, filename, size):
|
||||||
|
try:
|
||||||
|
if os.path.exists(filename):
|
||||||
|
gioFile = Gio.File.new_for_path(filename)
|
||||||
|
info = gioFile.query_info('standard::icon' , 0, Gio.Cancellable())
|
||||||
|
icon = info.get_icon().get_names()[0]
|
||||||
|
iconTheme = Gtk.IconTheme.get_default()
|
||||||
|
iconData = iconTheme.lookup_icon(icon , size , 0)
|
||||||
|
if iconData:
|
||||||
|
iconPath = iconData.get_filename()
|
||||||
|
return Gtk.Image.new_from_file(iconPath)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print("system icon generation issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_desktop_files(self, full_path):
|
||||||
|
try:
|
||||||
|
xdgObj = DesktopEntry(full_path)
|
||||||
|
icon = xdgObj.getIcon()
|
||||||
|
alt_icon_path = ""
|
||||||
|
|
||||||
|
if "steam" in icon:
|
||||||
|
name = xdgObj.getName()
|
||||||
|
file_hash = hashlib.sha256(str.encode(name)).hexdigest()
|
||||||
|
hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg"
|
||||||
|
|
||||||
|
if isfile(hash_img_pth) == True:
|
||||||
|
# Use video sizes since headers are bigger
|
||||||
|
return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
|
||||||
|
|
||||||
|
exec_str = xdgObj.getExec()
|
||||||
|
parts = exec_str.split("steam://rungameid/")
|
||||||
|
id = parts[len(parts) - 1]
|
||||||
|
imageLink = self.STEAM_BASE_URL + id + "/header.jpg"
|
||||||
|
proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink])
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
# Use video thumbnail sizes since headers are bigger
|
||||||
|
return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH)
|
||||||
|
elif os.path.exists(icon):
|
||||||
|
return self.create_scaled_image(icon, self.SYS_ICON_WH)
|
||||||
|
else:
|
||||||
|
alt_icon_path = ""
|
||||||
|
|
||||||
|
for dir in self.ICON_DIRS:
|
||||||
|
alt_icon_path = self.traverse_icons_folder(dir, icon)
|
||||||
|
if alt_icon_path != "":
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.create_scaled_image(alt_icon_path, self.SYS_ICON_WH)
|
||||||
|
except Exception as e:
|
||||||
|
print(".desktop icon generation issue:")
|
||||||
|
print( repr(e) )
|
||||||
|
return None
|
||||||
|
|
||||||
|
def traverse_icons_folder(self, path, icon):
|
||||||
|
alt_icon_path = ""
|
||||||
|
|
||||||
|
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||||
|
for file in filenames:
|
||||||
|
appNM = "application-x-" + icon
|
||||||
|
if icon in file or appNM in file:
|
||||||
|
alt_icon_path = dirpath + "/" + file
|
||||||
|
break
|
||||||
|
|
||||||
|
return alt_icon_path
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Python Imports
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class VideoIconMixin:
|
||||||
|
def generate_video_thumbnail(self, full_path, hash_img_pth):
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_pth])
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(repr(e))
|
||||||
|
self.ffprobe_generate_video_thumbnail(full_path, hash_img_pth)
|
||||||
|
|
||||||
|
|
||||||
|
def ffprobe_generate_video_thumbnail(self, full_path, hash_img_pth):
|
||||||
|
proc = None
|
||||||
|
try:
|
||||||
|
# Stream duration
|
||||||
|
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||||
|
data = subprocess.run(command, stdout=subprocess.PIPE)
|
||||||
|
duration = data.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
# Format (container) duration
|
||||||
|
if "N/A" in duration:
|
||||||
|
command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||||
|
data = subprocess.run(command , stdout=subprocess.PIPE)
|
||||||
|
duration = data.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
# Stream duration type: image2
|
||||||
|
if "N/A" in duration:
|
||||||
|
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||||
|
data = subprocess.run(command, stdout=subprocess.PIPE)
|
||||||
|
duration = data.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
# Format (container) duration type: image2
|
||||||
|
if "N/A" in duration:
|
||||||
|
command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
|
||||||
|
data = subprocess.run(command , stdout=subprocess.PIPE)
|
||||||
|
duration = data.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
# Get frame roughly 35% through video
|
||||||
|
grabTime = str( int( float( duration.split(".")[0] ) * 0.35) )
|
||||||
|
command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_pth]
|
||||||
|
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print("Video thumbnail generation issue in thread:")
|
||||||
|
print( repr(e) )
|
||||||
|
self.logger.debug(repr(e))
|
|
@ -0,0 +1,4 @@
|
||||||
|
from . import xdg
|
||||||
|
|
||||||
|
from .VideoIconMixin import VideoIconMixin
|
||||||
|
from .DesktopIconMixin import DesktopIconMixin
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
This module is based on a rox module (LGPL):
|
||||||
|
|
||||||
|
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log
|
||||||
|
|
||||||
|
The freedesktop.org Base Directory specification provides a way for
|
||||||
|
applications to locate shared data and configuration:
|
||||||
|
|
||||||
|
http://standards.freedesktop.org/basedir-spec/
|
||||||
|
|
||||||
|
(based on version 0.6)
|
||||||
|
|
||||||
|
This module can be used to load and save from and to these directories.
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
|
||||||
|
from rox import basedir
|
||||||
|
|
||||||
|
for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
|
||||||
|
print "Load settings from", dir
|
||||||
|
|
||||||
|
dir = basedir.save_config_path('mydomain.org', 'MyProg')
|
||||||
|
print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"
|
||||||
|
|
||||||
|
Note: see the rox.Options module for a higher-level API for managing options.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, stat
|
||||||
|
|
||||||
|
_home = os.path.expanduser('~')
|
||||||
|
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
|
||||||
|
os.path.join(_home, '.local', 'share')
|
||||||
|
|
||||||
|
xdg_data_dirs = [xdg_data_home] + \
|
||||||
|
(os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':')
|
||||||
|
|
||||||
|
xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
|
||||||
|
os.path.join(_home, '.config')
|
||||||
|
|
||||||
|
xdg_config_dirs = [xdg_config_home] + \
|
||||||
|
(os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':')
|
||||||
|
|
||||||
|
xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
|
||||||
|
os.path.join(_home, '.cache')
|
||||||
|
|
||||||
|
xdg_data_dirs = [x for x in xdg_data_dirs if x]
|
||||||
|
xdg_config_dirs = [x for x in xdg_config_dirs if x]
|
||||||
|
|
||||||
|
def save_config_path(*resource):
|
||||||
|
"""Ensure ``$XDG_CONFIG_HOME/<resource>/`` exists, and return its path.
|
||||||
|
'resource' should normally be the name of your application. Use this
|
||||||
|
when saving configuration settings.
|
||||||
|
"""
|
||||||
|
resource = os.path.join(*resource)
|
||||||
|
assert not resource.startswith('/')
|
||||||
|
path = os.path.join(xdg_config_home, resource)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.makedirs(path, 0o700)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save_data_path(*resource):
|
||||||
|
"""Ensure ``$XDG_DATA_HOME/<resource>/`` exists, and return its path.
|
||||||
|
'resource' should normally be the name of your application or a shared
|
||||||
|
resource. Use this when saving or updating application data.
|
||||||
|
"""
|
||||||
|
resource = os.path.join(*resource)
|
||||||
|
assert not resource.startswith('/')
|
||||||
|
path = os.path.join(xdg_data_home, resource)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.makedirs(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save_cache_path(*resource):
|
||||||
|
"""Ensure ``$XDG_CACHE_HOME/<resource>/`` exists, and return its path.
|
||||||
|
'resource' should normally be the name of your application or a shared
|
||||||
|
resource."""
|
||||||
|
resource = os.path.join(*resource)
|
||||||
|
assert not resource.startswith('/')
|
||||||
|
path = os.path.join(xdg_cache_home, resource)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.makedirs(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def load_config_paths(*resource):
|
||||||
|
"""Returns an iterator which gives each directory named 'resource' in the
|
||||||
|
configuration search path. Information provided by earlier directories should
|
||||||
|
take precedence over later ones, and the user-specific config dir comes
|
||||||
|
first."""
|
||||||
|
resource = os.path.join(*resource)
|
||||||
|
for config_dir in xdg_config_dirs:
|
||||||
|
path = os.path.join(config_dir, resource)
|
||||||
|
if os.path.exists(path): yield path
|
||||||
|
|
||||||
|
def load_first_config(*resource):
|
||||||
|
"""Returns the first result from load_config_paths, or None if there is nothing
|
||||||
|
to load."""
|
||||||
|
for x in load_config_paths(*resource):
|
||||||
|
return x
|
||||||
|
return None
|
||||||
|
|
||||||
|
def load_data_paths(*resource):
|
||||||
|
"""Returns an iterator which gives each directory named 'resource' in the
|
||||||
|
application data search path. Information provided by earlier directories
|
||||||
|
should take precedence over later ones."""
|
||||||
|
resource = os.path.join(*resource)
|
||||||
|
for data_dir in xdg_data_dirs:
|
||||||
|
path = os.path.join(data_dir, resource)
|
||||||
|
if os.path.exists(path): yield path
|
||||||
|
|
||||||
|
def get_runtime_dir(strict=True):
|
||||||
|
"""Returns the value of $XDG_RUNTIME_DIR, a directory path.
|
||||||
|
|
||||||
|
This directory is intended for 'user-specific non-essential runtime files
|
||||||
|
and other file objects (such as sockets, named pipes, ...)', and
|
||||||
|
'communication and synchronization purposes'.
|
||||||
|
|
||||||
|
As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not
|
||||||
|
set, with ``strict=True`` (the default), a KeyError is raised. With
|
||||||
|
``strict=False``, PyXDG will create a fallback under /tmp for the current
|
||||||
|
user. This fallback does *not* provide the same guarantees as the
|
||||||
|
specification requires for the runtime directory.
|
||||||
|
|
||||||
|
The strict default is deliberately conservative, so that application
|
||||||
|
developers can make a conscious decision to allow the fallback.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return os.environ['XDG_RUNTIME_DIR']
|
||||||
|
except KeyError:
|
||||||
|
if strict:
|
||||||
|
raise
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
|
||||||
|
create = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This must be a real directory, not a symlink, so attackers can't
|
||||||
|
# point it elsewhere. So we use lstat to check it.
|
||||||
|
st = os.lstat(fallback)
|
||||||
|
except OSError as e:
|
||||||
|
import errno
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
create = True
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# The fallback must be a directory
|
||||||
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
|
os.unlink(fallback)
|
||||||
|
create = True
|
||||||
|
# Must be owned by the user and not accessible by anyone else
|
||||||
|
elif (st.st_uid != os.getuid()) \
|
||||||
|
or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
|
||||||
|
os.rmdir(fallback)
|
||||||
|
create = True
|
||||||
|
|
||||||
|
if create:
|
||||||
|
os.mkdir(fallback, 0o700)
|
||||||
|
|
||||||
|
return fallback
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
Functions to configure Basic Settings
|
||||||
|
"""
|
||||||
|
|
||||||
|
language = "C"
|
||||||
|
windowmanager = None
|
||||||
|
icon_theme = "hicolor"
|
||||||
|
icon_size = 48
|
||||||
|
cache_time = 5
|
||||||
|
root_mode = False
|
||||||
|
|
||||||
|
def setWindowManager(wm):
|
||||||
|
global windowmanager
|
||||||
|
windowmanager = wm
|
||||||
|
|
||||||
|
def setIconTheme(theme):
|
||||||
|
global icon_theme
|
||||||
|
icon_theme = theme
|
||||||
|
import xdg.IconTheme
|
||||||
|
xdg.IconTheme.themes = []
|
||||||
|
|
||||||
|
def setIconSize(size):
|
||||||
|
global icon_size
|
||||||
|
icon_size = size
|
||||||
|
|
||||||
|
def setCacheTime(time):
|
||||||
|
global cache_time
|
||||||
|
cache_time = time
|
||||||
|
|
||||||
|
def setLocale(lang):
|
||||||
|
import locale
|
||||||
|
lang = locale.normalize(lang)
|
||||||
|
locale.setlocale(locale.LC_ALL, lang)
|
||||||
|
import xdg.Locale
|
||||||
|
xdg.Locale.update(lang)
|
||||||
|
|
||||||
|
def setRootMode(boolean):
|
||||||
|
global root_mode
|
||||||
|
root_mode = boolean
|
|
@ -0,0 +1,435 @@
|
||||||
|
"""
|
||||||
|
Complete implementation of the XDG Desktop Entry Specification
|
||||||
|
http://standards.freedesktop.org/desktop-entry-spec/
|
||||||
|
|
||||||
|
Not supported:
|
||||||
|
- Encoding: Legacy Mixed
|
||||||
|
- Does not check exec parameters
|
||||||
|
- Does not check URL's
|
||||||
|
- Does not completly validate deprecated/kde items
|
||||||
|
- Does not completly check categories
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .IniFile import IniFile
|
||||||
|
from . import Locale
|
||||||
|
|
||||||
|
from .IniFile import is_ascii
|
||||||
|
|
||||||
|
from .Exceptions import ParsingError
|
||||||
|
from .util import which
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
class DesktopEntry(IniFile):
|
||||||
|
"Class to parse and validate Desktop Entries"
|
||||||
|
|
||||||
|
defaultGroup = 'Desktop Entry'
|
||||||
|
|
||||||
|
def __init__(self, filename=None):
|
||||||
|
"""Create a new DesktopEntry.
|
||||||
|
|
||||||
|
If filename exists, it will be parsed as a desktop entry file. If not,
|
||||||
|
or if filename is None, a blank DesktopEntry is created.
|
||||||
|
"""
|
||||||
|
self.content = dict()
|
||||||
|
if filename and os.path.exists(filename):
|
||||||
|
self.parse(filename)
|
||||||
|
elif filename:
|
||||||
|
self.new(filename)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.getName()
|
||||||
|
|
||||||
|
def parse(self, file):
|
||||||
|
"""Parse a desktop entry file.
|
||||||
|
|
||||||
|
This can raise :class:`~xdg.Exceptions.ParsingError`,
|
||||||
|
:class:`~xdg.Exceptions.DuplicateGroupError` or
|
||||||
|
:class:`~xdg.Exceptions.DuplicateKeyError`.
|
||||||
|
"""
|
||||||
|
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
|
||||||
|
|
||||||
|
def findTryExec(self):
|
||||||
|
"""Looks in the PATH for the executable given in the TryExec field.
|
||||||
|
|
||||||
|
Returns the full path to the executable if it is found, None if not.
|
||||||
|
Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present.
|
||||||
|
"""
|
||||||
|
tryexec = self.get('TryExec', strict=True)
|
||||||
|
return which(tryexec)
|
||||||
|
|
||||||
|
# start standard keys
|
||||||
|
def getType(self):
|
||||||
|
return self.get('Type')
|
||||||
|
def getVersion(self):
|
||||||
|
"""deprecated, use getVersionString instead """
|
||||||
|
return self.get('Version', type="numeric")
|
||||||
|
def getVersionString(self):
|
||||||
|
return self.get('Version')
|
||||||
|
def getName(self):
|
||||||
|
return self.get('Name', locale=True)
|
||||||
|
def getGenericName(self):
|
||||||
|
return self.get('GenericName', locale=True)
|
||||||
|
def getNoDisplay(self):
|
||||||
|
return self.get('NoDisplay', type="boolean")
|
||||||
|
def getComment(self):
|
||||||
|
return self.get('Comment', locale=True)
|
||||||
|
def getIcon(self):
|
||||||
|
return self.get('Icon', locale=True)
|
||||||
|
def getHidden(self):
|
||||||
|
return self.get('Hidden', type="boolean")
|
||||||
|
def getOnlyShowIn(self):
|
||||||
|
return self.get('OnlyShowIn', list=True)
|
||||||
|
def getNotShowIn(self):
|
||||||
|
return self.get('NotShowIn', list=True)
|
||||||
|
def getTryExec(self):
|
||||||
|
return self.get('TryExec')
|
||||||
|
def getExec(self):
|
||||||
|
return self.get('Exec')
|
||||||
|
def getPath(self):
|
||||||
|
return self.get('Path')
|
||||||
|
def getTerminal(self):
|
||||||
|
return self.get('Terminal', type="boolean")
|
||||||
|
def getMimeType(self):
|
||||||
|
"""deprecated, use getMimeTypes instead """
|
||||||
|
return self.get('MimeType', list=True, type="regex")
|
||||||
|
def getMimeTypes(self):
|
||||||
|
return self.get('MimeType', list=True)
|
||||||
|
def getCategories(self):
|
||||||
|
return self.get('Categories', list=True)
|
||||||
|
def getStartupNotify(self):
|
||||||
|
return self.get('StartupNotify', type="boolean")
|
||||||
|
def getStartupWMClass(self):
|
||||||
|
return self.get('StartupWMClass')
|
||||||
|
def getURL(self):
|
||||||
|
return self.get('URL')
|
||||||
|
# end standard keys
|
||||||
|
|
||||||
|
# start kde keys
|
||||||
|
def getServiceTypes(self):
|
||||||
|
return self.get('ServiceTypes', list=True)
|
||||||
|
def getDocPath(self):
|
||||||
|
return self.get('DocPath')
|
||||||
|
def getKeywords(self):
|
||||||
|
return self.get('Keywords', list=True, locale=True)
|
||||||
|
def getInitialPreference(self):
|
||||||
|
return self.get('InitialPreference')
|
||||||
|
def getDev(self):
|
||||||
|
return self.get('Dev')
|
||||||
|
def getFSType(self):
|
||||||
|
return self.get('FSType')
|
||||||
|
def getMountPoint(self):
|
||||||
|
return self.get('MountPoint')
|
||||||
|
def getReadonly(self):
|
||||||
|
return self.get('ReadOnly', type="boolean")
|
||||||
|
def getUnmountIcon(self):
|
||||||
|
return self.get('UnmountIcon', locale=True)
|
||||||
|
# end kde keys
|
||||||
|
|
||||||
|
# start deprecated keys
|
||||||
|
def getMiniIcon(self):
|
||||||
|
return self.get('MiniIcon', locale=True)
|
||||||
|
def getTerminalOptions(self):
|
||||||
|
return self.get('TerminalOptions')
|
||||||
|
def getDefaultApp(self):
|
||||||
|
return self.get('DefaultApp')
|
||||||
|
def getProtocols(self):
|
||||||
|
return self.get('Protocols', list=True)
|
||||||
|
def getExtensions(self):
|
||||||
|
return self.get('Extensions', list=True)
|
||||||
|
def getBinaryPattern(self):
|
||||||
|
return self.get('BinaryPattern')
|
||||||
|
def getMapNotify(self):
|
||||||
|
return self.get('MapNotify')
|
||||||
|
def getEncoding(self):
|
||||||
|
return self.get('Encoding')
|
||||||
|
def getSwallowTitle(self):
|
||||||
|
return self.get('SwallowTitle', locale=True)
|
||||||
|
def getSwallowExec(self):
|
||||||
|
return self.get('SwallowExec')
|
||||||
|
def getSortOrder(self):
|
||||||
|
return self.get('SortOrder', list=True)
|
||||||
|
def getFilePattern(self):
|
||||||
|
return self.get('FilePattern', type="regex")
|
||||||
|
def getActions(self):
|
||||||
|
return self.get('Actions', list=True)
|
||||||
|
# end deprecated keys
|
||||||
|
|
||||||
|
# desktop entry edit stuff
|
||||||
|
def new(self, filename):
|
||||||
|
"""Make this instance into a new, blank desktop entry.
|
||||||
|
|
||||||
|
If filename has a .desktop extension, Type is set to Application. If it
|
||||||
|
has a .directory extension, Type is Directory. Other extensions will
|
||||||
|
cause :class:`~xdg.Exceptions.ParsingError` to be raised.
|
||||||
|
"""
|
||||||
|
if os.path.splitext(filename)[1] == ".desktop":
|
||||||
|
type = "Application"
|
||||||
|
elif os.path.splitext(filename)[1] == ".directory":
|
||||||
|
type = "Directory"
|
||||||
|
else:
|
||||||
|
raise ParsingError("Unknown extension", filename)
|
||||||
|
|
||||||
|
self.content = dict()
|
||||||
|
self.addGroup(self.defaultGroup)
|
||||||
|
self.set("Type", type)
|
||||||
|
self.filename = filename
|
||||||
|
# end desktop entry edit stuff
|
||||||
|
|
||||||
|
# validation stuff
|
||||||
|
def checkExtras(self):
|
||||||
|
# header
|
||||||
|
if self.defaultGroup == "KDE Desktop Entry":
|
||||||
|
self.warnings.append('[KDE Desktop Entry]-Header is deprecated')
|
||||||
|
|
||||||
|
# file extension
|
||||||
|
if self.fileExtension == ".kdelnk":
|
||||||
|
self.warnings.append("File extension .kdelnk is deprecated")
|
||||||
|
elif self.fileExtension != ".desktop" and self.fileExtension != ".directory":
|
||||||
|
self.warnings.append('Unknown File extension')
|
||||||
|
|
||||||
|
# Type
|
||||||
|
try:
|
||||||
|
self.type = self.content[self.defaultGroup]["Type"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Type' is missing")
|
||||||
|
|
||||||
|
# Name
|
||||||
|
try:
|
||||||
|
self.name = self.content[self.defaultGroup]["Name"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Name' is missing")
|
||||||
|
|
||||||
|
def checkGroup(self, group):
|
||||||
|
# check if group header is valid
|
||||||
|
if not (group == self.defaultGroup \
|
||||||
|
or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \
|
||||||
|
or (re.match("^X-", group) and is_ascii(group))):
|
||||||
|
self.errors.append("Invalid Group name: %s" % group)
|
||||||
|
else:
|
||||||
|
#OnlyShowIn and NotShowIn
|
||||||
|
if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]):
|
||||||
|
self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both")
|
||||||
|
|
||||||
|
def checkKey(self, key, value, group):
|
||||||
|
# standard keys
|
||||||
|
if key == "Type":
|
||||||
|
if value == "ServiceType" or value == "Service" or value == "FSDevice":
|
||||||
|
self.warnings.append("Type=%s is a KDE extension" % key)
|
||||||
|
elif value == "MimeType":
|
||||||
|
self.warnings.append("Type=MimeType is deprecated")
|
||||||
|
elif not (value == "Application" or value == "Link" or value == "Directory"):
|
||||||
|
self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value)
|
||||||
|
|
||||||
|
if self.fileExtension == ".directory" and not value == "Directory":
|
||||||
|
self.warnings.append("File extension is .directory, but Type is '%s'" % value)
|
||||||
|
elif self.fileExtension == ".desktop" and value == "Directory":
|
||||||
|
self.warnings.append("Files with Type=Directory should have the extension .directory")
|
||||||
|
|
||||||
|
if value == "Application":
|
||||||
|
if "Exec" not in self.content[group]:
|
||||||
|
self.warnings.append("Type=Application needs 'Exec' key")
|
||||||
|
if value == "Link":
|
||||||
|
if "URL" not in self.content[group]:
|
||||||
|
self.warnings.append("Type=Link needs 'URL' key")
|
||||||
|
|
||||||
|
elif key == "Version":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
|
||||||
|
elif re.match("^Name"+xdg.Locale.regex+"$", key):
|
||||||
|
pass # locale string
|
||||||
|
|
||||||
|
elif re.match("^GenericName"+xdg.Locale.regex+"$", key):
|
||||||
|
pass # locale string
|
||||||
|
|
||||||
|
elif key == "NoDisplay":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
|
||||||
|
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
|
||||||
|
pass # locale string
|
||||||
|
|
||||||
|
elif re.match("^Icon"+xdg.Locale.regex+"$", key):
|
||||||
|
self.checkValue(key, value)
|
||||||
|
|
||||||
|
elif key == "Hidden":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
|
||||||
|
elif key == "OnlyShowIn":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.checkOnlyShowIn(value)
|
||||||
|
|
||||||
|
elif key == "NotShowIn":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.checkOnlyShowIn(value)
|
||||||
|
|
||||||
|
elif key == "TryExec":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "Exec":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "Path":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "Terminal":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "Actions":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "MimeType":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "Categories":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
self.checkCategories(value)
|
||||||
|
|
||||||
|
elif re.match("^Keywords"+xdg.Locale.regex+"$", key):
|
||||||
|
self.checkValue(key, value, type="localestring", list=True)
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "StartupNotify":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "StartupWMClass":
|
||||||
|
self.checkType(key, "Application")
|
||||||
|
|
||||||
|
elif key == "URL":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "URL")
|
||||||
|
|
||||||
|
# kde extensions
|
||||||
|
elif key == "ServiceTypes":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "DocPath":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "InitialPreference":
|
||||||
|
self.checkValue(key, value, type="numeric")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "Dev":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "FSDevice")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "FSType":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "FSDevice")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "MountPoint":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "FSDevice")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif key == "ReadOnly":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
self.checkType(key, "FSDevice")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key):
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.checkType(key, "FSDevice")
|
||||||
|
self.warnings.append("Key '%s' is a KDE extension" % key)
|
||||||
|
|
||||||
|
# deprecated keys
|
||||||
|
elif key == "Encoding":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key):
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "TerminalOptions":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "DefaultApp":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "Protocols":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "Extensions":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "BinaryPattern":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "MapNotify":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key):
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "SwallowExec":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "FilePattern":
|
||||||
|
self.checkValue(key, value, type="regex", list=True)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
elif key == "SortOrder":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
self.warnings.append("Key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
# "X-" extensions
|
||||||
|
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.errors.append("Invalid key: %s" % key)
|
||||||
|
|
||||||
|
def checkType(self, key, type):
|
||||||
|
if not self.getType() == type:
|
||||||
|
self.errors.append("Key '%s' only allowed in Type=%s" % (key, type))
|
||||||
|
|
||||||
|
def checkOnlyShowIn(self, value):
|
||||||
|
values = self.getList(value)
|
||||||
|
valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity",
|
||||||
|
"XFCE", "Old"]
|
||||||
|
for item in values:
|
||||||
|
if item not in valid and item[0:2] != "X-":
|
||||||
|
self.errors.append("'%s' is not a registered OnlyShowIn value" % item);
|
||||||
|
|
||||||
|
def checkCategories(self, value):
|
||||||
|
values = self.getList(value)
|
||||||
|
|
||||||
|
main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"]
|
||||||
|
if not any(item in main for item in values):
|
||||||
|
self.errors.append("Missing main category")
|
||||||
|
|
||||||
|
additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly']
|
||||||
|
allcategories = additional + main
|
||||||
|
|
||||||
|
for item in values:
|
||||||
|
if item not in allcategories and not item.startswith("X-"):
|
||||||
|
self.errors.append("'%s' is not a registered Category" % item);
|
||||||
|
|
||||||
|
def checkCategorie(self, value):
|
||||||
|
"""Deprecated alias for checkCategories - only exists for backwards
|
||||||
|
compatibility.
|
||||||
|
"""
|
||||||
|
warnings.warn("checkCategorie is deprecated, use checkCategories",
|
||||||
|
DeprecationWarning)
|
||||||
|
return self.checkCategories(value)
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""
|
||||||
|
Exception Classes for the xdg package
|
||||||
|
"""
|
||||||
|
|
||||||
|
debug = False
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
"""Base class for exceptions defined here."""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
Exception.__init__(self, msg)
|
||||||
|
def __str__(self):
|
||||||
|
return self.msg
|
||||||
|
|
||||||
|
class ValidationError(Error):
|
||||||
|
"""Raised when a file fails to validate.
|
||||||
|
|
||||||
|
The filename is the .file attribute.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, file):
|
||||||
|
self.msg = msg
|
||||||
|
self.file = file
|
||||||
|
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
|
||||||
|
|
||||||
|
class ParsingError(Error):
|
||||||
|
"""Raised when a file cannot be parsed.
|
||||||
|
|
||||||
|
The filename is the .file attribute.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, file):
|
||||||
|
self.msg = msg
|
||||||
|
self.file = file
|
||||||
|
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
|
||||||
|
|
||||||
|
class NoKeyError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant key in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .key, .group and .file.
|
||||||
|
"""
|
||||||
|
def __init__(self, key, group, file):
|
||||||
|
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
|
||||||
|
self.key = key
|
||||||
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
class DuplicateKeyError(Error):
|
||||||
|
"""Raised when the same key occurs twice in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .key, .group and .file.
|
||||||
|
"""
|
||||||
|
def __init__(self, key, group, file):
|
||||||
|
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
|
||||||
|
self.key = key
|
||||||
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
class NoGroupError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant group in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .group and .file.
|
||||||
|
"""
|
||||||
|
def __init__(self, group, file):
|
||||||
|
Error.__init__(self, "No group: %s in file %s" % (group, file))
|
||||||
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
class DuplicateGroupError(Error):
|
||||||
|
"""Raised when the same key occurs twice in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .group and .file.
|
||||||
|
"""
|
||||||
|
def __init__(self, group, file):
|
||||||
|
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
|
||||||
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
class NoThemeError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant icon theme.
|
||||||
|
|
||||||
|
The name of the theme is the .theme attribute.
|
||||||
|
"""
|
||||||
|
def __init__(self, theme):
|
||||||
|
Error.__init__(self, "No such icon-theme: %s" % theme)
|
||||||
|
self.theme = theme
|
|
@ -0,0 +1,445 @@
|
||||||
|
"""
|
||||||
|
Complete implementation of the XDG Icon Spec
|
||||||
|
http://standards.freedesktop.org/icon-theme-spec/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, time
|
||||||
|
import re
|
||||||
|
|
||||||
|
from . import IniFile, Config
|
||||||
|
from .IniFile import is_ascii
|
||||||
|
from .BaseDirectory import xdg_data_dirs
|
||||||
|
from .Exceptions import NoThemeError, debug
|
||||||
|
|
||||||
|
|
||||||
|
class IconTheme(IniFile):
|
||||||
|
"Class to parse and validate IconThemes"
|
||||||
|
def __init__(self):
|
||||||
|
IniFile.__init__(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def parse(self, file):
|
||||||
|
IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
|
||||||
|
self.dir = os.path.dirname(file)
|
||||||
|
(nil, self.name) = os.path.split(self.dir)
|
||||||
|
|
||||||
|
def getDir(self):
|
||||||
|
return self.dir
|
||||||
|
|
||||||
|
# Standard Keys
|
||||||
|
def getName(self):
|
||||||
|
return self.get('Name', locale=True)
|
||||||
|
def getComment(self):
|
||||||
|
return self.get('Comment', locale=True)
|
||||||
|
def getInherits(self):
|
||||||
|
return self.get('Inherits', list=True)
|
||||||
|
def getDirectories(self):
|
||||||
|
return self.get('Directories', list=True)
|
||||||
|
def getScaledDirectories(self):
|
||||||
|
return self.get('ScaledDirectories', list=True)
|
||||||
|
def getHidden(self):
|
||||||
|
return self.get('Hidden', type="boolean")
|
||||||
|
def getExample(self):
|
||||||
|
return self.get('Example')
|
||||||
|
|
||||||
|
# Per Directory Keys
|
||||||
|
def getSize(self, directory):
|
||||||
|
return self.get('Size', type="integer", group=directory)
|
||||||
|
def getContext(self, directory):
|
||||||
|
return self.get('Context', group=directory)
|
||||||
|
def getType(self, directory):
|
||||||
|
value = self.get('Type', group=directory)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return "Threshold"
|
||||||
|
def getMaxSize(self, directory):
|
||||||
|
value = self.get('MaxSize', type="integer", group=directory)
|
||||||
|
if value or value == 0:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return self.getSize(directory)
|
||||||
|
def getMinSize(self, directory):
|
||||||
|
value = self.get('MinSize', type="integer", group=directory)
|
||||||
|
if value or value == 0:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return self.getSize(directory)
|
||||||
|
def getThreshold(self, directory):
|
||||||
|
value = self.get('Threshold', type="integer", group=directory)
|
||||||
|
if value or value == 0:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def getScale(self, directory):
|
||||||
|
value = self.get('Scale', type="integer", group=directory)
|
||||||
|
return value or 1
|
||||||
|
|
||||||
|
# validation stuff
|
||||||
|
def checkExtras(self):
|
||||||
|
# header
|
||||||
|
if self.defaultGroup == "KDE Icon Theme":
|
||||||
|
self.warnings.append('[KDE Icon Theme]-Header is deprecated')
|
||||||
|
|
||||||
|
# file extension
|
||||||
|
if self.fileExtension == ".theme":
|
||||||
|
pass
|
||||||
|
elif self.fileExtension == ".desktop":
|
||||||
|
self.warnings.append('.desktop fileExtension is deprecated')
|
||||||
|
else:
|
||||||
|
self.warnings.append('Unknown File extension')
|
||||||
|
|
||||||
|
# Check required keys
|
||||||
|
# Name
|
||||||
|
try:
|
||||||
|
self.name = self.content[self.defaultGroup]["Name"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Name' is missing")
|
||||||
|
|
||||||
|
# Comment
|
||||||
|
try:
|
||||||
|
self.comment = self.content[self.defaultGroup]["Comment"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Comment' is missing")
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
try:
|
||||||
|
self.directories = self.content[self.defaultGroup]["Directories"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Directories' is missing")
|
||||||
|
|
||||||
|
def checkGroup(self, group):
|
||||||
|
# check if group header is valid
|
||||||
|
if group == self.defaultGroup:
|
||||||
|
try:
|
||||||
|
self.name = self.content[group]["Name"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Name' in Group '%s' is missing" % group)
|
||||||
|
try:
|
||||||
|
self.name = self.content[group]["Comment"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Comment' in Group '%s' is missing" % group)
|
||||||
|
elif group in self.getDirectories():
|
||||||
|
try:
|
||||||
|
self.type = self.content[group]["Type"]
|
||||||
|
except KeyError:
|
||||||
|
self.type = "Threshold"
|
||||||
|
try:
|
||||||
|
self.name = self.content[group]["Size"]
|
||||||
|
except KeyError:
|
||||||
|
self.errors.append("Key 'Size' in Group '%s' is missing" % group)
|
||||||
|
elif not (re.match(r"^\[X-", group) and is_ascii(group)):
|
||||||
|
self.errors.append("Invalid Group name: %s" % group)
|
||||||
|
|
||||||
|
def checkKey(self, key, value, group):
|
||||||
|
# standard keys
|
||||||
|
if group == self.defaultGroup:
|
||||||
|
if re.match("^Name"+xdg.Locale.regex+"$", key):
|
||||||
|
pass
|
||||||
|
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
|
||||||
|
pass
|
||||||
|
elif key == "Inherits":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
elif key == "Directories":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
elif key == "ScaledDirectories":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
|
elif key == "Hidden":
|
||||||
|
self.checkValue(key, value, type="boolean")
|
||||||
|
elif key == "Example":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.errors.append("Invalid key: %s" % key)
|
||||||
|
elif group in self.getDirectories():
|
||||||
|
if key == "Size":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
|
elif key == "Context":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
elif key == "Type":
|
||||||
|
self.checkValue(key, value)
|
||||||
|
if value not in ["Fixed", "Scalable", "Threshold"]:
|
||||||
|
self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
|
||||||
|
elif key == "MaxSize":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
|
if self.type != "Scalable":
|
||||||
|
self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
|
||||||
|
elif key == "MinSize":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
|
if self.type != "Scalable":
|
||||||
|
self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
|
||||||
|
elif key == "Threshold":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
|
if self.type != "Threshold":
|
||||||
|
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
|
||||||
|
elif key == "Scale":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
|
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.errors.append("Invalid key: %s" % key)
|
||||||
|
|
||||||
|
|
||||||
|
class IconData(IniFile):
|
||||||
|
"Class to parse and validate IconData Files"
|
||||||
|
def __init__(self):
|
||||||
|
IniFile.__init__(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
displayname = self.getDisplayName()
|
||||||
|
if displayname:
|
||||||
|
return "<IconData: %s>" % displayname
|
||||||
|
else:
|
||||||
|
return "<IconData>"
|
||||||
|
|
||||||
|
def parse(self, file):
|
||||||
|
IniFile.parse(self, file, ["Icon Data"])
|
||||||
|
|
||||||
|
# Standard Keys
|
||||||
|
def getDisplayName(self):
|
||||||
|
"""Retrieve the display name from the icon data, if one is specified."""
|
||||||
|
return self.get('DisplayName', locale=True)
|
||||||
|
def getEmbeddedTextRectangle(self):
|
||||||
|
"""Retrieve the embedded text rectangle from the icon data as a list of
|
||||||
|
numbers (x0, y0, x1, y1), if it is specified."""
|
||||||
|
return self.get('EmbeddedTextRectangle', type="integer", list=True)
|
||||||
|
def getAttachPoints(self):
|
||||||
|
"""Retrieve the anchor points for overlays & emblems from the icon data,
|
||||||
|
as a list of co-ordinate pairs, if they are specified."""
|
||||||
|
return self.get('AttachPoints', type="point", list=True)
|
||||||
|
|
||||||
|
# validation stuff
|
||||||
|
def checkExtras(self):
|
||||||
|
# file extension
|
||||||
|
if self.fileExtension != ".icon":
|
||||||
|
self.warnings.append('Unknown File extension')
|
||||||
|
|
||||||
|
def checkGroup(self, group):
|
||||||
|
# check if group header is valid
|
||||||
|
if not (group == self.defaultGroup \
|
||||||
|
or (re.match(r"^\[X-", group) and is_ascii(group))):
|
||||||
|
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
|
||||||
|
|
||||||
|
def checkKey(self, key, value, group):
|
||||||
|
# standard keys
|
||||||
|
if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
|
||||||
|
pass
|
||||||
|
elif key == "EmbeddedTextRectangle":
|
||||||
|
self.checkValue(key, value, type="integer", list=True)
|
||||||
|
elif key == "AttachPoints":
|
||||||
|
self.checkValue(key, value, type="point", list=True)
|
||||||
|
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.errors.append("Invalid key: %s" % key)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
icondirs = []
|
||||||
|
for basedir in xdg_data_dirs:
|
||||||
|
icondirs.append(os.path.join(basedir, "icons"))
|
||||||
|
icondirs.append(os.path.join(basedir, "pixmaps"))
|
||||||
|
icondirs.append(os.path.expanduser("~/.icons"))
|
||||||
|
|
||||||
|
# just cache variables, they give a 10x speed improvement
|
||||||
|
themes = []
|
||||||
|
theme_cache = {}
|
||||||
|
dir_cache = {}
|
||||||
|
icon_cache = {}
|
||||||
|
|
||||||
|
def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
|
||||||
|
"""Get the path to a specified icon.
|
||||||
|
|
||||||
|
size :
|
||||||
|
Icon size in pixels. Defaults to ``xdg.Config.icon_size``.
|
||||||
|
theme :
|
||||||
|
Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't
|
||||||
|
found in the specified theme, it will be looked up in the basic 'hicolor'
|
||||||
|
theme.
|
||||||
|
extensions :
|
||||||
|
List of preferred file extensions.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> getIconPath("inkscape", 32)
|
||||||
|
'/usr/share/icons/hicolor/32x32/apps/inkscape.png'
|
||||||
|
"""
|
||||||
|
|
||||||
|
global themes
|
||||||
|
|
||||||
|
if size == None:
|
||||||
|
size = xdg.Config.icon_size
|
||||||
|
if theme == None:
|
||||||
|
theme = xdg.Config.icon_theme
|
||||||
|
|
||||||
|
# if we have an absolute path, just return it
|
||||||
|
if os.path.isabs(iconname):
|
||||||
|
return iconname
|
||||||
|
|
||||||
|
# check if it has an extension and strip it
|
||||||
|
if os.path.splitext(iconname)[1][1:] in extensions:
|
||||||
|
iconname = os.path.splitext(iconname)[0]
|
||||||
|
|
||||||
|
# parse theme files
|
||||||
|
if (themes == []) or (themes[0].name != theme):
|
||||||
|
themes = list(__get_themes(theme))
|
||||||
|
|
||||||
|
# more caching (icon looked up in the last 5 seconds?)
|
||||||
|
tmp = (iconname, size, theme, tuple(extensions))
|
||||||
|
try:
|
||||||
|
timestamp, icon = icon_cache[tmp]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if (time.time() - timestamp) >= xdg.Config.cache_time:
|
||||||
|
del icon_cache[tmp]
|
||||||
|
else:
|
||||||
|
return icon
|
||||||
|
|
||||||
|
for thme in themes:
|
||||||
|
icon = LookupIcon(iconname, size, thme, extensions)
|
||||||
|
if icon:
|
||||||
|
icon_cache[tmp] = (time.time(), icon)
|
||||||
|
return icon
|
||||||
|
|
||||||
|
# cache stuff again (directories looked up in the last 5 seconds?)
|
||||||
|
for directory in icondirs:
|
||||||
|
if (directory not in dir_cache \
|
||||||
|
or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \
|
||||||
|
and dir_cache[directory][2] < os.path.getmtime(directory))) \
|
||||||
|
and os.path.isdir(directory):
|
||||||
|
dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory))
|
||||||
|
|
||||||
|
for dir, values in dir_cache.items():
|
||||||
|
for extension in extensions:
|
||||||
|
try:
|
||||||
|
if iconname + "." + extension in values[0]:
|
||||||
|
icon = os.path.join(dir, iconname + "." + extension)
|
||||||
|
icon_cache[tmp] = [time.time(), icon]
|
||||||
|
return icon
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
if debug:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# we haven't found anything? "hicolor" is our fallback
|
||||||
|
if theme != "hicolor":
|
||||||
|
icon = getIconPath(iconname, size, "hicolor")
|
||||||
|
icon_cache[tmp] = [time.time(), icon]
|
||||||
|
return icon
|
||||||
|
|
||||||
|
def getIconData(path):
|
||||||
|
"""Retrieve the data from the .icon file corresponding to the given file. If
|
||||||
|
there is no .icon file, it returns None.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
getIconData("/usr/share/icons/Tango/scalable/places/folder.svg")
|
||||||
|
"""
|
||||||
|
if os.path.isfile(path):
|
||||||
|
icon_file = os.path.splitext(path)[0] + ".icon"
|
||||||
|
if os.path.isfile(icon_file):
|
||||||
|
data = IconData()
|
||||||
|
data.parse(icon_file)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __get_themes(themename):
|
||||||
|
"""Generator yielding IconTheme objects for a specified theme and any themes
|
||||||
|
from which it inherits.
|
||||||
|
"""
|
||||||
|
for dir in icondirs:
|
||||||
|
theme_file = os.path.join(dir, themename, "index.theme")
|
||||||
|
if os.path.isfile(theme_file):
|
||||||
|
break
|
||||||
|
theme_file = os.path.join(dir, themename, "index.desktop")
|
||||||
|
if os.path.isfile(theme_file):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if debug:
|
||||||
|
raise NoThemeError(themename)
|
||||||
|
return
|
||||||
|
|
||||||
|
theme = IconTheme()
|
||||||
|
theme.parse(theme_file)
|
||||||
|
yield theme
|
||||||
|
for subtheme in theme.getInherits():
|
||||||
|
for t in __get_themes(subtheme):
|
||||||
|
yield t
|
||||||
|
|
||||||
|
def LookupIcon(iconname, size, theme, extensions):
|
||||||
|
# look for the cache
|
||||||
|
if theme.name not in theme_cache:
|
||||||
|
theme_cache[theme.name] = []
|
||||||
|
theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
|
||||||
|
theme_cache[theme.name].append(0) # [1] mtime
|
||||||
|
theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]]
|
||||||
|
|
||||||
|
# cache stuff (directory lookuped up the in the last 5 seconds?)
|
||||||
|
if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time:
|
||||||
|
theme_cache[theme.name][0] = time.time()
|
||||||
|
for subdir in theme.getDirectories():
|
||||||
|
for directory in icondirs:
|
||||||
|
dir = os.path.join(directory,theme.name,subdir)
|
||||||
|
if (dir not in theme_cache[theme.name][2] \
|
||||||
|
or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
|
||||||
|
and subdir != "" \
|
||||||
|
and os.path.isdir(dir):
|
||||||
|
theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
|
||||||
|
theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
|
||||||
|
|
||||||
|
for dir, values in theme_cache[theme.name][2].items():
|
||||||
|
if DirectoryMatchesSize(values[0], size, theme):
|
||||||
|
for extension in extensions:
|
||||||
|
if iconname + "." + extension in values[1]:
|
||||||
|
return os.path.join(dir, iconname + "." + extension)
|
||||||
|
|
||||||
|
minimal_size = 2**31
|
||||||
|
closest_filename = ""
|
||||||
|
for dir, values in theme_cache[theme.name][2].items():
|
||||||
|
distance = DirectorySizeDistance(values[0], size, theme)
|
||||||
|
if distance < minimal_size:
|
||||||
|
for extension in extensions:
|
||||||
|
if iconname + "." + extension in values[1]:
|
||||||
|
closest_filename = os.path.join(dir, iconname + "." + extension)
|
||||||
|
minimal_size = distance
|
||||||
|
|
||||||
|
return closest_filename
|
||||||
|
|
||||||
|
def DirectoryMatchesSize(subdir, iconsize, theme):
|
||||||
|
Type = theme.getType(subdir)
|
||||||
|
Size = theme.getSize(subdir)
|
||||||
|
Threshold = theme.getThreshold(subdir)
|
||||||
|
MinSize = theme.getMinSize(subdir)
|
||||||
|
MaxSize = theme.getMaxSize(subdir)
|
||||||
|
if Type == "Fixed":
|
||||||
|
return Size == iconsize
|
||||||
|
elif Type == "Scaleable":
|
||||||
|
return MinSize <= iconsize <= MaxSize
|
||||||
|
elif Type == "Threshold":
|
||||||
|
return Size - Threshold <= iconsize <= Size + Threshold
|
||||||
|
|
||||||
|
def DirectorySizeDistance(subdir, iconsize, theme):
|
||||||
|
Type = theme.getType(subdir)
|
||||||
|
Size = theme.getSize(subdir)
|
||||||
|
Threshold = theme.getThreshold(subdir)
|
||||||
|
MinSize = theme.getMinSize(subdir)
|
||||||
|
MaxSize = theme.getMaxSize(subdir)
|
||||||
|
if Type == "Fixed":
|
||||||
|
return abs(Size - iconsize)
|
||||||
|
elif Type == "Scalable":
|
||||||
|
if iconsize < MinSize:
|
||||||
|
return MinSize - iconsize
|
||||||
|
elif iconsize > MaxSize:
|
||||||
|
return MaxSize - iconsize
|
||||||
|
return 0
|
||||||
|
elif Type == "Threshold":
|
||||||
|
if iconsize < Size - Threshold:
|
||||||
|
return MinSize - iconsize
|
||||||
|
elif iconsize > Size + Threshold:
|
||||||
|
return iconsize - MaxSize
|
||||||
|
return 0
|
|
@ -0,0 +1,419 @@
|
||||||
|
"""
|
||||||
|
Base Class for DesktopEntry, IconTheme and IconData
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re, os, stat, io
|
||||||
|
from .Exceptions import (ParsingError, DuplicateGroupError, NoGroupError,
|
||||||
|
NoKeyError, DuplicateKeyError, ValidationError,
|
||||||
|
debug)
|
||||||
|
# import xdg.Locale
|
||||||
|
from . import Locale
|
||||||
|
from .util import u
|
||||||
|
|
||||||
|
def is_ascii(s):
|
||||||
|
"""Return True if a string consists entirely of ASCII characters."""
|
||||||
|
try:
|
||||||
|
s.encode('ascii', 'strict')
|
||||||
|
return True
|
||||||
|
except UnicodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class IniFile:
|
||||||
|
defaultGroup = ''
|
||||||
|
fileExtension = ''
|
||||||
|
|
||||||
|
filename = ''
|
||||||
|
|
||||||
|
tainted = False
|
||||||
|
|
||||||
|
def __init__(self, filename=None):
|
||||||
|
self.content = dict()
|
||||||
|
if filename:
|
||||||
|
self.parse(filename)
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
return cmp(self.content, other.content)
|
||||||
|
|
||||||
|
def parse(self, filename, headers=None):
|
||||||
|
'''Parse an INI file.
|
||||||
|
|
||||||
|
headers -- list of headers the parser will try to select as a default header
|
||||||
|
'''
|
||||||
|
# for performance reasons
|
||||||
|
content = self.content
|
||||||
|
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
raise ParsingError("File not found", filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# The content should be UTF-8, but legacy files can have other
|
||||||
|
# encodings, including mixed encodings in one file. We don't attempt
|
||||||
|
# to decode them, but we silence the errors.
|
||||||
|
fd = io.open(filename, 'r', encoding='utf-8', errors='replace')
|
||||||
|
except IOError as e:
|
||||||
|
if debug:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# parse file
|
||||||
|
for line in fd:
|
||||||
|
line = line.strip()
|
||||||
|
# empty line
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
# comment
|
||||||
|
elif line[0] == '#':
|
||||||
|
continue
|
||||||
|
# new group
|
||||||
|
elif line[0] == '[':
|
||||||
|
currentGroup = line.lstrip("[").rstrip("]")
|
||||||
|
if debug and self.hasGroup(currentGroup):
|
||||||
|
raise DuplicateGroupError(currentGroup, filename)
|
||||||
|
else:
|
||||||
|
content[currentGroup] = {}
|
||||||
|
# key
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
except ValueError:
|
||||||
|
raise ParsingError("Invalid line: " + line, filename)
|
||||||
|
|
||||||
|
key = key.strip() # Spaces before/after '=' should be ignored
|
||||||
|
try:
|
||||||
|
if debug and self.hasKey(key, currentGroup):
|
||||||
|
raise DuplicateKeyError(key, currentGroup, filename)
|
||||||
|
else:
|
||||||
|
content[currentGroup][key] = value.strip()
|
||||||
|
except (IndexError, UnboundLocalError):
|
||||||
|
raise ParsingError("Parsing error on key, group missing", filename)
|
||||||
|
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
self.filename = filename
|
||||||
|
self.tainted = False
|
||||||
|
|
||||||
|
# check header
|
||||||
|
if headers:
|
||||||
|
for header in headers:
|
||||||
|
if header in content:
|
||||||
|
self.defaultGroup = header
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ParsingError("[%s]-Header missing" % headers[0], filename)
|
||||||
|
|
||||||
|
# start stuff to access the keys
|
||||||
|
def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
|
||||||
|
# set default group
|
||||||
|
if not group:
|
||||||
|
group = self.defaultGroup
|
||||||
|
|
||||||
|
# return key (with locale)
|
||||||
|
if (group in self.content) and (key in self.content[group]):
|
||||||
|
if locale:
|
||||||
|
value = self.content[group][self.__addLocale(key, group)]
|
||||||
|
else:
|
||||||
|
value = self.content[group][key]
|
||||||
|
else:
|
||||||
|
if strict or debug:
|
||||||
|
if group not in self.content:
|
||||||
|
raise NoGroupError(group, self.filename)
|
||||||
|
elif key not in self.content[group]:
|
||||||
|
raise NoKeyError(key, group, self.filename)
|
||||||
|
else:
|
||||||
|
value = ""
|
||||||
|
|
||||||
|
if list == True:
|
||||||
|
values = self.getList(value)
|
||||||
|
result = []
|
||||||
|
else:
|
||||||
|
values = [value]
|
||||||
|
|
||||||
|
for value in values:
|
||||||
|
if type == "boolean":
|
||||||
|
value = self.__getBoolean(value)
|
||||||
|
elif type == "integer":
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
value = 0
|
||||||
|
elif type == "numeric":
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
value = 0.0
|
||||||
|
elif type == "regex":
|
||||||
|
value = re.compile(value)
|
||||||
|
elif type == "point":
|
||||||
|
x, y = value.split(",")
|
||||||
|
value = int(x), int(y)
|
||||||
|
|
||||||
|
if list == True:
|
||||||
|
result.append(value)
|
||||||
|
else:
|
||||||
|
result = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
# end stuff to access the keys
|
||||||
|
|
||||||
|
# start subget
|
||||||
|
def getList(self, string):
|
||||||
|
if re.search(r"(?<!\\)\;", string):
|
||||||
|
list = re.split(r"(?<!\\);", string)
|
||||||
|
elif re.search(r"(?<!\\)\|", string):
|
||||||
|
list = re.split(r"(?<!\\)\|", string)
|
||||||
|
elif re.search(r"(?<!\\),", string):
|
||||||
|
list = re.split(r"(?<!\\),", string)
|
||||||
|
else:
|
||||||
|
list = [string]
|
||||||
|
if list[-1] == "":
|
||||||
|
list.pop()
|
||||||
|
return list
|
||||||
|
|
||||||
|
def __getBoolean(self, boolean):
|
||||||
|
if boolean == 1 or boolean == "true" or boolean == "True":
|
||||||
|
return True
|
||||||
|
elif boolean == 0 or boolean == "false" or boolean == "False":
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
# end subget
|
||||||
|
|
||||||
|
def __addLocale(self, key, group=None):
|
||||||
|
"add locale to key according the current lc_messages"
|
||||||
|
# set default group
|
||||||
|
if not group:
|
||||||
|
group = self.defaultGroup
|
||||||
|
|
||||||
|
for lang in Locale.langs:
|
||||||
|
langkey = "%s[%s]" % (key, lang)
|
||||||
|
if langkey in self.content[group]:
|
||||||
|
return langkey
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
# start validation stuff
|
||||||
|
def validate(self, report="All"):
|
||||||
|
"""Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
|
||||||
|
if there is anything amiss.
|
||||||
|
|
||||||
|
report can be 'All' / 'Warnings' / 'Errors'
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.warnings = []
|
||||||
|
self.errors = []
|
||||||
|
|
||||||
|
# get file extension
|
||||||
|
self.fileExtension = os.path.splitext(self.filename)[1]
|
||||||
|
|
||||||
|
# overwrite this for own checkings
|
||||||
|
self.checkExtras()
|
||||||
|
|
||||||
|
# check all keys
|
||||||
|
for group in self.content:
|
||||||
|
self.checkGroup(group)
|
||||||
|
for key in self.content[group]:
|
||||||
|
self.checkKey(key, self.content[group][key], group)
|
||||||
|
# check if value is empty
|
||||||
|
if self.content[group][key] == "":
|
||||||
|
self.warnings.append("Value of Key '%s' is empty" % key)
|
||||||
|
|
||||||
|
# raise Warnings / Errors
|
||||||
|
msg = ""
|
||||||
|
|
||||||
|
if report == "All" or report == "Warnings":
|
||||||
|
for line in self.warnings:
|
||||||
|
msg += "\n- " + line
|
||||||
|
|
||||||
|
if report == "All" or report == "Errors":
|
||||||
|
for line in self.errors:
|
||||||
|
msg += "\n- " + line
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
raise ValidationError(msg, self.filename)
|
||||||
|
|
||||||
|
# check if group header is valid
|
||||||
|
def checkGroup(self, group):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# check if key is valid
|
||||||
|
def checkKey(self, key, value, group):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# check random stuff
|
||||||
|
def checkValue(self, key, value, type="string", list=False):
|
||||||
|
if list == True:
|
||||||
|
values = self.getList(value)
|
||||||
|
else:
|
||||||
|
values = [value]
|
||||||
|
|
||||||
|
for value in values:
|
||||||
|
if type == "string":
|
||||||
|
code = self.checkString(value)
|
||||||
|
if type == "localestring":
|
||||||
|
continue
|
||||||
|
elif type == "boolean":
|
||||||
|
code = self.checkBoolean(value)
|
||||||
|
elif type == "numeric":
|
||||||
|
code = self.checkNumber(value)
|
||||||
|
elif type == "integer":
|
||||||
|
code = self.checkInteger(value)
|
||||||
|
elif type == "regex":
|
||||||
|
code = self.checkRegex(value)
|
||||||
|
elif type == "point":
|
||||||
|
code = self.checkPoint(value)
|
||||||
|
if code == 1:
|
||||||
|
self.errors.append("'%s' is not a valid %s" % (value, type))
|
||||||
|
elif code == 2:
|
||||||
|
self.warnings.append("Value of key '%s' is deprecated" % key)
|
||||||
|
|
||||||
|
def checkExtras(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def checkBoolean(self, value):
|
||||||
|
# 1 or 0 : deprecated
|
||||||
|
if (value == "1" or value == "0"):
|
||||||
|
return 2
|
||||||
|
# true or false: ok
|
||||||
|
elif not (value == "true" or value == "false"):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def checkNumber(self, value):
|
||||||
|
# float() ValueError
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
except:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def checkInteger(self, value):
|
||||||
|
# int() ValueError
|
||||||
|
try:
|
||||||
|
int(value)
|
||||||
|
except:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def checkPoint(self, value):
|
||||||
|
if not re.match("^[0-9]+,[0-9]+$", value):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def checkString(self, value):
|
||||||
|
return 0 if is_ascii(value) else 1
|
||||||
|
|
||||||
|
def checkRegex(self, value):
|
||||||
|
try:
|
||||||
|
re.compile(value)
|
||||||
|
except:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# write support
|
||||||
|
def write(self, filename=None, trusted=False):
|
||||||
|
if not filename and not self.filename:
|
||||||
|
raise ParsingError("File not found", "")
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
self.filename = filename
|
||||||
|
else:
|
||||||
|
filename = self.filename
|
||||||
|
|
||||||
|
if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
|
||||||
|
os.makedirs(os.path.dirname(filename))
|
||||||
|
|
||||||
|
with io.open(filename, 'w', encoding='utf-8') as fp:
|
||||||
|
|
||||||
|
# An executable bit signifies that the desktop file is
|
||||||
|
# trusted, but then the file can be executed. Add hashbang to
|
||||||
|
# make sure that the file is opened by something that
|
||||||
|
# understands desktop files.
|
||||||
|
if trusted:
|
||||||
|
fp.write(u("#!/usr/bin/env xdg-open\n"))
|
||||||
|
|
||||||
|
if self.defaultGroup:
|
||||||
|
fp.write(u("[%s]\n") % self.defaultGroup)
|
||||||
|
for (key, value) in self.content[self.defaultGroup].items():
|
||||||
|
fp.write(u("%s=%s\n") % (key, value))
|
||||||
|
fp.write(u("\n"))
|
||||||
|
for (name, group) in self.content.items():
|
||||||
|
if name != self.defaultGroup:
|
||||||
|
fp.write(u("[%s]\n") % name)
|
||||||
|
for (key, value) in group.items():
|
||||||
|
fp.write(u("%s=%s\n") % (key, value))
|
||||||
|
fp.write(u("\n"))
|
||||||
|
|
||||||
|
# Add executable bits to the file to show that it's trusted.
|
||||||
|
if trusted:
|
||||||
|
oldmode = os.stat(filename).st_mode
|
||||||
|
mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||||
|
os.chmod(filename, mode)
|
||||||
|
|
||||||
|
self.tainted = False
|
||||||
|
|
||||||
|
def set(self, key, value, group=None, locale=False):
|
||||||
|
# set default group
|
||||||
|
if not group:
|
||||||
|
group = self.defaultGroup
|
||||||
|
|
||||||
|
if locale == True and len(xdg.Locale.langs) > 0:
|
||||||
|
key = key + "[" + xdg.Locale.langs[0] + "]"
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.content[group][key] = value
|
||||||
|
except KeyError:
|
||||||
|
raise NoGroupError(group, self.filename)
|
||||||
|
|
||||||
|
self.tainted = (value == self.get(key, group))
|
||||||
|
|
||||||
|
def addGroup(self, group):
|
||||||
|
if self.hasGroup(group):
|
||||||
|
if debug:
|
||||||
|
raise DuplicateGroupError(group, self.filename)
|
||||||
|
else:
|
||||||
|
self.content[group] = {}
|
||||||
|
self.tainted = True
|
||||||
|
|
||||||
|
def removeGroup(self, group):
|
||||||
|
existed = group in self.content
|
||||||
|
if existed:
|
||||||
|
del self.content[group]
|
||||||
|
self.tainted = True
|
||||||
|
else:
|
||||||
|
if debug:
|
||||||
|
raise NoGroupError(group, self.filename)
|
||||||
|
return existed
|
||||||
|
|
||||||
|
def removeKey(self, key, group=None, locales=True):
|
||||||
|
# set default group
|
||||||
|
if not group:
|
||||||
|
group = self.defaultGroup
|
||||||
|
|
||||||
|
try:
|
||||||
|
if locales:
|
||||||
|
for name in list(self.content[group]):
|
||||||
|
if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key:
|
||||||
|
del self.content[group][name]
|
||||||
|
value = self.content[group].pop(key)
|
||||||
|
self.tainted = True
|
||||||
|
return value
|
||||||
|
except KeyError as e:
|
||||||
|
if debug:
|
||||||
|
if e == group:
|
||||||
|
raise NoGroupError(group, self.filename)
|
||||||
|
else:
|
||||||
|
raise NoKeyError(key, group, self.filename)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# misc
|
||||||
|
def groups(self):
|
||||||
|
return self.content.keys()
|
||||||
|
|
||||||
|
def hasGroup(self, group):
|
||||||
|
return group in self.content
|
||||||
|
|
||||||
|
def hasKey(self, key, group=None):
|
||||||
|
# set default group
|
||||||
|
if not group:
|
||||||
|
group = self.defaultGroup
|
||||||
|
|
||||||
|
return key in self.content[group]
|
||||||
|
|
||||||
|
def getFileName(self):
|
||||||
|
return self.filename
|
|
@ -0,0 +1,79 @@
|
||||||
|
"""
|
||||||
|
Helper Module for Locale settings
|
||||||
|
|
||||||
|
This module is based on a ROX module (LGPL):
|
||||||
|
|
||||||
|
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from locale import normalize
|
||||||
|
|
||||||
|
regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?"
|
||||||
|
|
||||||
|
def _expand_lang(locale):
|
||||||
|
locale = normalize(locale)
|
||||||
|
COMPONENT_CODESET = 1 << 0
|
||||||
|
COMPONENT_MODIFIER = 1 << 1
|
||||||
|
COMPONENT_TERRITORY = 1 << 2
|
||||||
|
# split up the locale into its base components
|
||||||
|
mask = 0
|
||||||
|
pos = locale.find('@')
|
||||||
|
if pos >= 0:
|
||||||
|
modifier = locale[pos:]
|
||||||
|
locale = locale[:pos]
|
||||||
|
mask |= COMPONENT_MODIFIER
|
||||||
|
else:
|
||||||
|
modifier = ''
|
||||||
|
pos = locale.find('.')
|
||||||
|
codeset = ''
|
||||||
|
if pos >= 0:
|
||||||
|
locale = locale[:pos]
|
||||||
|
pos = locale.find('_')
|
||||||
|
if pos >= 0:
|
||||||
|
territory = locale[pos:]
|
||||||
|
locale = locale[:pos]
|
||||||
|
mask |= COMPONENT_TERRITORY
|
||||||
|
else:
|
||||||
|
territory = ''
|
||||||
|
language = locale
|
||||||
|
ret = []
|
||||||
|
for i in range(mask+1):
|
||||||
|
if not (i & ~mask): # if all components for this combo exist ...
|
||||||
|
val = language
|
||||||
|
if i & COMPONENT_TERRITORY: val += territory
|
||||||
|
if i & COMPONENT_CODESET: val += codeset
|
||||||
|
if i & COMPONENT_MODIFIER: val += modifier
|
||||||
|
ret.append(val)
|
||||||
|
ret.reverse()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def expand_languages(languages=None):
|
||||||
|
# Get some reasonable defaults for arguments that were not supplied
|
||||||
|
if languages is None:
|
||||||
|
languages = []
|
||||||
|
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
|
||||||
|
val = os.environ.get(envar)
|
||||||
|
if val:
|
||||||
|
languages = val.split(':')
|
||||||
|
break
|
||||||
|
#if 'C' not in languages:
|
||||||
|
# languages.append('C')
|
||||||
|
|
||||||
|
# now normalize and expand the languages
|
||||||
|
nelangs = []
|
||||||
|
for lang in languages:
|
||||||
|
for nelang in _expand_lang(lang):
|
||||||
|
if nelang not in nelangs:
|
||||||
|
nelangs.append(nelang)
|
||||||
|
return nelangs
|
||||||
|
|
||||||
|
def update(language=None):
|
||||||
|
global langs
|
||||||
|
if language:
|
||||||
|
langs = expand_languages([language])
|
||||||
|
else:
|
||||||
|
langs = expand_languages()
|
||||||
|
|
||||||
|
langs = []
|
||||||
|
update()
|
|
@ -0,0 +1,541 @@
|
||||||
|
""" CLass to edit XDG Menus """
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
import xml.etree.cElementTree as etree
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from .Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder
|
||||||
|
from .BaseDirectory import xdg_config_dirs, xdg_data_dirs
|
||||||
|
from .Exceptions import ParsingError
|
||||||
|
from .Config import setRootMode
|
||||||
|
|
||||||
|
# XML-Cleanups: Move / Exclude
|
||||||
|
# FIXME: proper reverte/delete
|
||||||
|
# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
|
||||||
|
# FIXME: catch Exceptions
|
||||||
|
# FIXME: copy functions
|
||||||
|
# FIXME: More Layout stuff
|
||||||
|
# FIXME: unod/redo function / remove menu...
|
||||||
|
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
|
||||||
|
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
|
||||||
|
|
||||||
|
|
||||||
|
class MenuEditor(object):
|
||||||
|
|
||||||
|
def __init__(self, menu=None, filename=None, root=False):
|
||||||
|
self.menu = None
|
||||||
|
self.filename = None
|
||||||
|
self.tree = None
|
||||||
|
self.parser = XMLMenuBuilder()
|
||||||
|
self.parse(menu, filename, root)
|
||||||
|
|
||||||
|
# fix for creating two menus with the same name on the fly
|
||||||
|
self.filenames = []
|
||||||
|
|
||||||
|
def parse(self, menu=None, filename=None, root=False):
|
||||||
|
if root:
|
||||||
|
setRootMode(True)
|
||||||
|
|
||||||
|
if isinstance(menu, Menu):
|
||||||
|
self.menu = menu
|
||||||
|
elif menu:
|
||||||
|
self.menu = self.parser.parse(menu)
|
||||||
|
else:
|
||||||
|
self.menu = self.parser.parse()
|
||||||
|
|
||||||
|
if root:
|
||||||
|
self.filename = self.menu.Filename
|
||||||
|
elif filename:
|
||||||
|
self.filename = filename
|
||||||
|
else:
|
||||||
|
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.tree = etree.parse(self.filename)
|
||||||
|
except IOError:
|
||||||
|
root = etree.fromtring("""
|
||||||
|
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
|
||||||
|
<Menu>
|
||||||
|
<Name>Applications</Name>
|
||||||
|
<MergeFile type="parent">%s</MergeFile>
|
||||||
|
</Menu>
|
||||||
|
""" % self.menu.Filename)
|
||||||
|
self.tree = etree.ElementTree(root)
|
||||||
|
except ParsingError:
|
||||||
|
raise ParsingError('Not a valid .menu file', self.filename)
|
||||||
|
|
||||||
|
#FIXME: is this needed with etree ?
|
||||||
|
self.__remove_whitespace_nodes(self.tree)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.__saveEntries(self.menu)
|
||||||
|
self.__saveMenu()
|
||||||
|
|
||||||
|
def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
|
||||||
|
menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
|
||||||
|
menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
|
||||||
|
|
||||||
|
self.__addEntry(parent, menuentry, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
|
||||||
|
menu = Menu()
|
||||||
|
|
||||||
|
menu.Parent = parent
|
||||||
|
menu.Depth = parent.Depth + 1
|
||||||
|
menu.Layout = parent.DefaultLayout
|
||||||
|
menu.DefaultLayout = parent.DefaultLayout
|
||||||
|
|
||||||
|
menu = self.editMenu(menu, name, genericname, comment, icon)
|
||||||
|
|
||||||
|
self.__addEntry(parent, menu, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def createSeparator(self, parent, after=None, before=None):
|
||||||
|
separator = Separator(parent)
|
||||||
|
|
||||||
|
self.__addEntry(parent, separator, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return separator
|
||||||
|
|
||||||
|
def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
||||||
|
self.__deleteEntry(oldparent, menuentry, after, before)
|
||||||
|
self.__addEntry(newparent, menuentry, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
|
||||||
|
self.__deleteEntry(oldparent, menu, after, before)
|
||||||
|
self.__addEntry(newparent, menu, after, before)
|
||||||
|
|
||||||
|
root_menu = self.__getXmlMenu(self.menu.Name)
|
||||||
|
if oldparent.getPath(True) != newparent.getPath(True):
|
||||||
|
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def moveSeparator(self, separator, parent, after=None, before=None):
|
||||||
|
self.__deleteEntry(parent, separator, after, before)
|
||||||
|
self.__addEntry(parent, separator, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return separator
|
||||||
|
|
||||||
|
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
||||||
|
self.__addEntry(newparent, menuentry, after, before)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
|
||||||
|
deskentry = menuentry.DesktopEntry
|
||||||
|
|
||||||
|
if name:
|
||||||
|
if not deskentry.hasKey("Name"):
|
||||||
|
deskentry.set("Name", name)
|
||||||
|
deskentry.set("Name", name, locale=True)
|
||||||
|
if comment:
|
||||||
|
if not deskentry.hasKey("Comment"):
|
||||||
|
deskentry.set("Comment", comment)
|
||||||
|
deskentry.set("Comment", comment, locale=True)
|
||||||
|
if genericname:
|
||||||
|
if not deskentry.hasKey("GenericName"):
|
||||||
|
deskentry.set("GenericName", genericname)
|
||||||
|
deskentry.set("GenericName", genericname, locale=True)
|
||||||
|
if command:
|
||||||
|
deskentry.set("Exec", command)
|
||||||
|
if icon:
|
||||||
|
deskentry.set("Icon", icon)
|
||||||
|
|
||||||
|
if terminal:
|
||||||
|
deskentry.set("Terminal", "true")
|
||||||
|
elif not terminal:
|
||||||
|
deskentry.set("Terminal", "false")
|
||||||
|
|
||||||
|
if nodisplay is True:
|
||||||
|
deskentry.set("NoDisplay", "true")
|
||||||
|
elif nodisplay is False:
|
||||||
|
deskentry.set("NoDisplay", "false")
|
||||||
|
|
||||||
|
if hidden is True:
|
||||||
|
deskentry.set("Hidden", "true")
|
||||||
|
elif hidden is False:
|
||||||
|
deskentry.set("Hidden", "false")
|
||||||
|
|
||||||
|
menuentry.updateAttributes()
|
||||||
|
|
||||||
|
if len(menuentry.Parents) > 0:
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
|
||||||
|
# Hack for legacy dirs
|
||||||
|
if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
|
||||||
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||||
|
self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
|
||||||
|
menu.Directory.setAttributes(menu.Name + ".directory")
|
||||||
|
# Hack for New Entries
|
||||||
|
elif not isinstance(menu.Directory, MenuEntry):
|
||||||
|
if not name:
|
||||||
|
name = menu.Name
|
||||||
|
filename = self.__getFileName(name, ".directory").replace("/", "")
|
||||||
|
if not menu.Name:
|
||||||
|
menu.Name = filename.replace(".directory", "")
|
||||||
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||||
|
self.__addXmlTextElement(xml_menu, 'Directory', filename)
|
||||||
|
menu.Directory = MenuEntry(filename)
|
||||||
|
|
||||||
|
deskentry = menu.Directory.DesktopEntry
|
||||||
|
|
||||||
|
if name:
|
||||||
|
if not deskentry.hasKey("Name"):
|
||||||
|
deskentry.set("Name", name)
|
||||||
|
deskentry.set("Name", name, locale=True)
|
||||||
|
if genericname:
|
||||||
|
if not deskentry.hasKey("GenericName"):
|
||||||
|
deskentry.set("GenericName", genericname)
|
||||||
|
deskentry.set("GenericName", genericname, locale=True)
|
||||||
|
if comment:
|
||||||
|
if not deskentry.hasKey("Comment"):
|
||||||
|
deskentry.set("Comment", comment)
|
||||||
|
deskentry.set("Comment", comment, locale=True)
|
||||||
|
if icon:
|
||||||
|
deskentry.set("Icon", icon)
|
||||||
|
|
||||||
|
if nodisplay is True:
|
||||||
|
deskentry.set("NoDisplay", "true")
|
||||||
|
elif nodisplay is False:
|
||||||
|
deskentry.set("NoDisplay", "false")
|
||||||
|
|
||||||
|
if hidden is True:
|
||||||
|
deskentry.set("Hidden", "true")
|
||||||
|
elif hidden is False:
|
||||||
|
deskentry.set("Hidden", "false")
|
||||||
|
|
||||||
|
menu.Directory.updateAttributes()
|
||||||
|
|
||||||
|
if isinstance(menu.Parent, Menu):
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def hideMenuEntry(self, menuentry):
|
||||||
|
self.editMenuEntry(menuentry, nodisplay=True)
|
||||||
|
|
||||||
|
def unhideMenuEntry(self, menuentry):
|
||||||
|
self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
|
||||||
|
|
||||||
|
def hideMenu(self, menu):
|
||||||
|
self.editMenu(menu, nodisplay=True)
|
||||||
|
|
||||||
|
def unhideMenu(self, menu):
|
||||||
|
self.editMenu(menu, nodisplay=False, hidden=False)
|
||||||
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
|
||||||
|
deleted = xml_menu.findall('Deleted')
|
||||||
|
not_deleted = xml_menu.findall('NotDeleted')
|
||||||
|
for node in deleted + not_deleted:
|
||||||
|
xml_menu.remove(node)
|
||||||
|
|
||||||
|
def deleteMenuEntry(self, menuentry):
|
||||||
|
if self.getAction(menuentry) == "delete":
|
||||||
|
self.__deleteFile(menuentry.DesktopEntry.filename)
|
||||||
|
for parent in menuentry.Parents:
|
||||||
|
self.__deleteEntry(parent, menuentry)
|
||||||
|
self.menu.sort()
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def revertMenuEntry(self, menuentry):
|
||||||
|
if self.getAction(menuentry) == "revert":
|
||||||
|
self.__deleteFile(menuentry.DesktopEntry.filename)
|
||||||
|
menuentry.Original.Parents = []
|
||||||
|
for parent in menuentry.Parents:
|
||||||
|
index = parent.Entries.index(menuentry)
|
||||||
|
parent.Entries[index] = menuentry.Original
|
||||||
|
index = parent.MenuEntries.index(menuentry)
|
||||||
|
parent.MenuEntries[index] = menuentry.Original
|
||||||
|
menuentry.Original.Parents.append(parent)
|
||||||
|
self.menu.sort()
|
||||||
|
return menuentry
|
||||||
|
|
||||||
|
def deleteMenu(self, menu):
|
||||||
|
if self.getAction(menu) == "delete":
|
||||||
|
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||||
|
self.__deleteEntry(menu.Parent, menu)
|
||||||
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||||
|
parent = self.__get_parent_node(xml_menu)
|
||||||
|
parent.remove(xml_menu)
|
||||||
|
self.menu.sort()
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def revertMenu(self, menu):
|
||||||
|
if self.getAction(menu) == "revert":
|
||||||
|
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||||
|
menu.Directory = menu.Directory.Original
|
||||||
|
self.menu.sort()
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def deleteSeparator(self, separator):
|
||||||
|
self.__deleteEntry(separator.Parent, separator, after=True)
|
||||||
|
|
||||||
|
self.menu.sort()
|
||||||
|
|
||||||
|
return separator
|
||||||
|
|
||||||
|
""" Private Stuff """
|
||||||
|
def getAction(self, entry):
|
||||||
|
if isinstance(entry, Menu):
|
||||||
|
if not isinstance(entry.Directory, MenuEntry):
|
||||||
|
return "none"
|
||||||
|
elif entry.Directory.getType() == "Both":
|
||||||
|
return "revert"
|
||||||
|
elif entry.Directory.getType() == "User" and (
|
||||||
|
len(entry.Submenus) + len(entry.MenuEntries)
|
||||||
|
) == 0:
|
||||||
|
return "delete"
|
||||||
|
|
||||||
|
elif isinstance(entry, MenuEntry):
|
||||||
|
if entry.getType() == "Both":
|
||||||
|
return "revert"
|
||||||
|
elif entry.getType() == "User":
|
||||||
|
return "delete"
|
||||||
|
else:
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
def __saveEntries(self, menu):
|
||||||
|
if not menu:
|
||||||
|
menu = self.menu
|
||||||
|
if isinstance(menu.Directory, MenuEntry):
|
||||||
|
menu.Directory.save()
|
||||||
|
for entry in menu.getEntries(hidden=True):
|
||||||
|
if isinstance(entry, MenuEntry):
|
||||||
|
entry.save()
|
||||||
|
elif isinstance(entry, Menu):
|
||||||
|
self.__saveEntries(entry)
|
||||||
|
|
||||||
|
def __saveMenu(self):
|
||||||
|
if not os.path.isdir(os.path.dirname(self.filename)):
|
||||||
|
os.makedirs(os.path.dirname(self.filename))
|
||||||
|
self.tree.write(self.filename, encoding='utf-8')
|
||||||
|
|
||||||
|
def __getFileName(self, name, extension):
|
||||||
|
postfix = 0
|
||||||
|
while 1:
|
||||||
|
if postfix == 0:
|
||||||
|
filename = name + extension
|
||||||
|
else:
|
||||||
|
filename = name + "-" + str(postfix) + extension
|
||||||
|
if extension == ".desktop":
|
||||||
|
dir = "applications"
|
||||||
|
elif extension == ".directory":
|
||||||
|
dir = "desktop-directories"
|
||||||
|
if not filename in self.filenames and not os.path.isfile(
|
||||||
|
os.path.join(xdg_data_dirs[0], dir, filename)
|
||||||
|
):
|
||||||
|
self.filenames.append(filename)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
postfix += 1
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def __getXmlMenu(self, path, create=True, element=None):
|
||||||
|
# FIXME: we should also return the menu's parent,
|
||||||
|
# to avoid looking for it later on
|
||||||
|
# @see Element.getiterator()
|
||||||
|
if not element:
|
||||||
|
element = self.tree
|
||||||
|
|
||||||
|
if "/" in path:
|
||||||
|
(name, path) = path.split("/", 1)
|
||||||
|
else:
|
||||||
|
name = path
|
||||||
|
path = ""
|
||||||
|
|
||||||
|
found = None
|
||||||
|
for node in element.findall("Menu"):
|
||||||
|
name_node = node.find('Name')
|
||||||
|
if name_node.text == name:
|
||||||
|
if path:
|
||||||
|
found = self.__getXmlMenu(path, create, node)
|
||||||
|
else:
|
||||||
|
found = node
|
||||||
|
if found:
|
||||||
|
break
|
||||||
|
if not found and create:
|
||||||
|
node = self.__addXmlMenuElement(element, name)
|
||||||
|
if path:
|
||||||
|
found = self.__getXmlMenu(path, create, node)
|
||||||
|
else:
|
||||||
|
found = node
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
def __addXmlMenuElement(self, element, name):
|
||||||
|
menu_node = etree.SubElement('Menu', element)
|
||||||
|
name_node = etree.SubElement('Name', menu_node)
|
||||||
|
name_node.text = name
|
||||||
|
return menu_node
|
||||||
|
|
||||||
|
def __addXmlTextElement(self, element, name, text):
|
||||||
|
node = etree.SubElement(name, element)
|
||||||
|
node.text = text
|
||||||
|
return node
|
||||||
|
|
||||||
|
def __addXmlFilename(self, element, filename, type_="Include"):
|
||||||
|
# remove old filenames
|
||||||
|
includes = element.findall('Include')
|
||||||
|
excludes = element.findall('Exclude')
|
||||||
|
rules = includes + excludes
|
||||||
|
for rule in rules:
|
||||||
|
#FIXME: this finds only Rules whose FIRST child is a Filename element
|
||||||
|
if rule[0].tag == "Filename" and rule[0].text == filename:
|
||||||
|
element.remove(rule)
|
||||||
|
# shouldn't it remove all occurences, like the following:
|
||||||
|
#filename_nodes = rule.findall('.//Filename'):
|
||||||
|
#for fn in filename_nodes:
|
||||||
|
#if fn.text == filename:
|
||||||
|
##element.remove(rule)
|
||||||
|
#parent = self.__get_parent_node(fn)
|
||||||
|
#parent.remove(fn)
|
||||||
|
|
||||||
|
# add new filename
|
||||||
|
node = etree.SubElement(type_, element)
|
||||||
|
self.__addXmlTextElement(node, 'Filename', filename)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def __addXmlMove(self, element, old, new):
|
||||||
|
node = etree.SubElement("Move", element)
|
||||||
|
self.__addXmlTextElement(node, 'Old', old)
|
||||||
|
self.__addXmlTextElement(node, 'New', new)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def __addXmlLayout(self, element, layout):
|
||||||
|
# remove old layout
|
||||||
|
for node in element.findall("Layout"):
|
||||||
|
element.remove(node)
|
||||||
|
|
||||||
|
# add new layout
|
||||||
|
node = etree.SubElement("Layout", element)
|
||||||
|
for order in layout.order:
|
||||||
|
if order[0] == "Separator":
|
||||||
|
child = etree.SubElement("Separator", node)
|
||||||
|
elif order[0] == "Filename":
|
||||||
|
child = self.__addXmlTextElement(node, "Filename", order[1])
|
||||||
|
elif order[0] == "Menuname":
|
||||||
|
child = self.__addXmlTextElement(node, "Menuname", order[1])
|
||||||
|
elif order[0] == "Merge":
|
||||||
|
child = etree.SubElement("Merge", node)
|
||||||
|
child.attrib["type"] = order[1]
|
||||||
|
return node
|
||||||
|
|
||||||
|
def __addLayout(self, parent):
|
||||||
|
layout = Layout()
|
||||||
|
layout.order = []
|
||||||
|
layout.show_empty = parent.Layout.show_empty
|
||||||
|
layout.inline = parent.Layout.inline
|
||||||
|
layout.inline_header = parent.Layout.inline_header
|
||||||
|
layout.inline_alias = parent.Layout.inline_alias
|
||||||
|
layout.inline_limit = parent.Layout.inline_limit
|
||||||
|
|
||||||
|
layout.order.append(["Merge", "menus"])
|
||||||
|
for entry in parent.Entries:
|
||||||
|
if isinstance(entry, Menu):
|
||||||
|
layout.parseMenuname(entry.Name)
|
||||||
|
elif isinstance(entry, MenuEntry):
|
||||||
|
layout.parseFilename(entry.DesktopFileID)
|
||||||
|
elif isinstance(entry, Separator):
|
||||||
|
layout.parseSeparator()
|
||||||
|
layout.order.append(["Merge", "files"])
|
||||||
|
|
||||||
|
parent.Layout = layout
|
||||||
|
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def __addEntry(self, parent, entry, after=None, before=None):
|
||||||
|
if after or before:
|
||||||
|
if after:
|
||||||
|
index = parent.Entries.index(after) + 1
|
||||||
|
elif before:
|
||||||
|
index = parent.Entries.index(before)
|
||||||
|
parent.Entries.insert(index, entry)
|
||||||
|
else:
|
||||||
|
parent.Entries.append(entry)
|
||||||
|
|
||||||
|
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
|
||||||
|
|
||||||
|
if isinstance(entry, MenuEntry):
|
||||||
|
parent.MenuEntries.append(entry)
|
||||||
|
entry.Parents.append(parent)
|
||||||
|
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
|
||||||
|
elif isinstance(entry, Menu):
|
||||||
|
parent.addSubmenu(entry)
|
||||||
|
|
||||||
|
if after or before:
|
||||||
|
self.__addLayout(parent)
|
||||||
|
self.__addXmlLayout(xml_parent, parent.Layout)
|
||||||
|
|
||||||
|
def __deleteEntry(self, parent, entry, after=None, before=None):
|
||||||
|
parent.Entries.remove(entry)
|
||||||
|
|
||||||
|
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
|
||||||
|
|
||||||
|
if isinstance(entry, MenuEntry):
|
||||||
|
entry.Parents.remove(parent)
|
||||||
|
parent.MenuEntries.remove(entry)
|
||||||
|
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
|
||||||
|
elif isinstance(entry, Menu):
|
||||||
|
parent.Submenus.remove(entry)
|
||||||
|
|
||||||
|
if after or before:
|
||||||
|
self.__addLayout(parent)
|
||||||
|
self.__addXmlLayout(xml_parent, parent.Layout)
|
||||||
|
|
||||||
|
def __deleteFile(self, filename):
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.filenames.remove(filename)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __remove_whitespace_nodes(self, node):
|
||||||
|
for child in node:
|
||||||
|
text = child.text.strip()
|
||||||
|
if not text:
|
||||||
|
child.text = ''
|
||||||
|
tail = child.tail.strip()
|
||||||
|
if not tail:
|
||||||
|
child.tail = ''
|
||||||
|
if len(child):
|
||||||
|
self.__remove_whilespace_nodes(child)
|
||||||
|
|
||||||
|
def __get_parent_node(self, node):
|
||||||
|
# elements in ElementTree doesn't hold a reference to their parent
|
||||||
|
for parent, child in self.__iter_parent():
|
||||||
|
if child is node:
|
||||||
|
return child
|
||||||
|
|
||||||
|
def __iter_parent(self):
|
||||||
|
for parent in self.tree.getiterator():
|
||||||
|
for child in parent:
|
||||||
|
yield parent, child
|
|
@ -0,0 +1,780 @@
|
||||||
|
"""
|
||||||
|
This module is based on a rox module (LGPL):
|
||||||
|
|
||||||
|
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
|
||||||
|
|
||||||
|
This module provides access to the shared MIME database.
|
||||||
|
|
||||||
|
types is a dictionary of all known MIME types, indexed by the type name, e.g.
|
||||||
|
types['application/x-python']
|
||||||
|
|
||||||
|
Applications can install information about MIME types by storing an
|
||||||
|
XML file as <MIME>/packages/<application>.xml and running the
|
||||||
|
update-mime-database command, which is provided by the freedesktop.org
|
||||||
|
shared mime database package.
|
||||||
|
|
||||||
|
See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
|
||||||
|
information about the format of these files.
|
||||||
|
|
||||||
|
(based on version 0.13)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
|
from . import BaseDirectory, Locale
|
||||||
|
|
||||||
|
from .dom import minidom, XML_NAMESPACE
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
|
||||||
|
|
||||||
|
types = {} # Maps MIME names to type objects
|
||||||
|
|
||||||
|
exts = None # Maps extensions to types
|
||||||
|
globs = None # List of (glob, type) pairs
|
||||||
|
literals = None # Maps liternal names to types
|
||||||
|
magic = None
|
||||||
|
|
||||||
|
PY3 = (sys.version_info[0] >= 3)
|
||||||
|
|
||||||
|
def _get_node_data(node):
|
||||||
|
"""Get text of XML node"""
|
||||||
|
return ''.join([n.nodeValue for n in node.childNodes]).strip()
|
||||||
|
|
||||||
|
def lookup(media, subtype = None):
|
||||||
|
"""Get the MIMEtype object for the given type.
|
||||||
|
|
||||||
|
This remains for backwards compatibility; calling MIMEtype now does
|
||||||
|
the same thing.
|
||||||
|
|
||||||
|
The name can either be passed as one part ('text/plain'), or as two
|
||||||
|
('text', 'plain').
|
||||||
|
"""
|
||||||
|
return MIMEtype(media, subtype)
|
||||||
|
|
||||||
|
class MIMEtype(object):
|
||||||
|
"""Class holding data about a MIME type.
|
||||||
|
|
||||||
|
Calling the class will return a cached instance, so there is only one
|
||||||
|
instance for each MIME type. The name can either be passed as one part
|
||||||
|
('text/plain'), or as two ('text', 'plain').
|
||||||
|
"""
|
||||||
|
def __new__(cls, media, subtype=None):
|
||||||
|
if subtype is None and '/' in media:
|
||||||
|
media, subtype = media.split('/', 1)
|
||||||
|
assert '/' not in subtype
|
||||||
|
media = media.lower()
|
||||||
|
subtype = subtype.lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return types[(media, subtype)]
|
||||||
|
except KeyError:
|
||||||
|
mtype = super(MIMEtype, cls).__new__(cls)
|
||||||
|
mtype._init(media, subtype)
|
||||||
|
types[(media, subtype)] = mtype
|
||||||
|
return mtype
|
||||||
|
|
||||||
|
# If this is done in __init__, it is automatically called again each time
|
||||||
|
# the MIMEtype is returned by __new__, which we don't want. So we call it
|
||||||
|
# explicitly only when we construct a new instance.
|
||||||
|
def _init(self, media, subtype):
|
||||||
|
self.media = media
|
||||||
|
self.subtype = subtype
|
||||||
|
self._comment = None
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"Loads comment for current language. Use get_comment() instead."
|
||||||
|
resource = os.path.join('mime', self.media, self.subtype + '.xml')
|
||||||
|
for path in BaseDirectory.load_data_paths(resource):
|
||||||
|
doc = minidom.parse(path)
|
||||||
|
if doc is None:
|
||||||
|
continue
|
||||||
|
for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
|
||||||
|
lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
|
||||||
|
goodness = 1 + (lang in xdg.Locale.langs)
|
||||||
|
if goodness > self._comment[0]:
|
||||||
|
self._comment = (goodness, _get_node_data(comment))
|
||||||
|
if goodness == 2: return
|
||||||
|
|
||||||
|
# FIXME: add get_icon method
|
||||||
|
def get_comment(self):
|
||||||
|
"""Returns comment for current language, loading it if needed."""
|
||||||
|
# Should we ever reload?
|
||||||
|
if self._comment is None:
|
||||||
|
self._comment = (0, str(self))
|
||||||
|
self._load()
|
||||||
|
return self._comment[1]
|
||||||
|
|
||||||
|
def canonical(self):
|
||||||
|
"""Returns the canonical MimeType object if this is an alias."""
|
||||||
|
update_cache()
|
||||||
|
s = str(self)
|
||||||
|
if s in aliases:
|
||||||
|
return lookup(aliases[s])
|
||||||
|
return self
|
||||||
|
|
||||||
|
def inherits_from(self):
|
||||||
|
"""Returns a set of Mime types which this inherits from."""
|
||||||
|
update_cache()
|
||||||
|
return set(lookup(t) for t in inheritance[str(self)])
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.media + '/' + self.subtype
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'MIMEtype(%r, %r)' % (self.media, self.subtype)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.media) ^ hash(self.subtype)
|
||||||
|
|
||||||
|
class UnknownMagicRuleFormat(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DiscardMagicRules(Exception):
|
||||||
|
"Raised when __NOMAGIC__ is found, and caught to discard previous rules."
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MagicRule:
|
||||||
|
also = None
|
||||||
|
|
||||||
|
def __init__(self, start, value, mask, word, range):
|
||||||
|
self.start = start
|
||||||
|
self.value = value
|
||||||
|
self.mask = mask
|
||||||
|
self.word = word
|
||||||
|
self.range = range
|
||||||
|
|
||||||
|
rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, f):
|
||||||
|
"""Read a rule from the binary magics file. Returns a 2-tuple of
|
||||||
|
the nesting depth and the MagicRule."""
|
||||||
|
line = f.readline()
|
||||||
|
#print line
|
||||||
|
|
||||||
|
# [indent] '>'
|
||||||
|
nest_depth, line = line.split(b'>', 1)
|
||||||
|
nest_depth = int(nest_depth) if nest_depth else 0
|
||||||
|
|
||||||
|
# start-offset '='
|
||||||
|
start, line = line.split(b'=', 1)
|
||||||
|
start = int(start)
|
||||||
|
|
||||||
|
if line == b'__NOMAGIC__\n':
|
||||||
|
raise DiscardMagicRules
|
||||||
|
|
||||||
|
# value length (2 bytes, big endian)
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
lenvalue = int.from_bytes(line[:2], byteorder='big')
|
||||||
|
else:
|
||||||
|
lenvalue = (ord(line[0])<<8)+ord(line[1])
|
||||||
|
line = line[2:]
|
||||||
|
|
||||||
|
# value
|
||||||
|
# This can contain newlines, so we may need to read more lines
|
||||||
|
while len(line) <= lenvalue:
|
||||||
|
line += f.readline()
|
||||||
|
value, line = line[:lenvalue], line[lenvalue:]
|
||||||
|
|
||||||
|
# ['&' mask]
|
||||||
|
if line.startswith(b'&'):
|
||||||
|
# This can contain newlines, so we may need to read more lines
|
||||||
|
while len(line) <= lenvalue:
|
||||||
|
line += f.readline()
|
||||||
|
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
|
||||||
|
else:
|
||||||
|
mask = None
|
||||||
|
|
||||||
|
# ['~' word-size] ['+' range-length]
|
||||||
|
ending = cls.rule_ending_re.match(line)
|
||||||
|
if not ending:
|
||||||
|
# Per the spec, this will be caught and ignored, to allow
|
||||||
|
# for future extensions.
|
||||||
|
raise UnknownMagicRuleFormat(repr(line))
|
||||||
|
|
||||||
|
word, range = ending.groups()
|
||||||
|
word = int(word) if (word is not None) else 1
|
||||||
|
range = int(range) if (range is not None) else 1
|
||||||
|
|
||||||
|
return nest_depth, cls(start, value, mask, word, range)
|
||||||
|
|
||||||
|
def maxlen(self):
|
||||||
|
l = self.start + len(self.value) + self.range
|
||||||
|
if self.also:
|
||||||
|
return max(l, self.also.maxlen())
|
||||||
|
return l
|
||||||
|
|
||||||
|
def match(self, buffer):
|
||||||
|
if self.match0(buffer):
|
||||||
|
if self.also:
|
||||||
|
return self.also.match(buffer)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def match0(self, buffer):
|
||||||
|
l=len(buffer)
|
||||||
|
lenvalue = len(self.value)
|
||||||
|
for o in range(self.range):
|
||||||
|
s=self.start+o
|
||||||
|
e=s+lenvalue
|
||||||
|
if l<e:
|
||||||
|
return False
|
||||||
|
if self.mask:
|
||||||
|
test=''
|
||||||
|
for i in range(lenvalue):
|
||||||
|
if PY3:
|
||||||
|
c = buffer[s+i] & self.mask[i]
|
||||||
|
else:
|
||||||
|
c = ord(buffer[s+i]) & ord(self.mask[i])
|
||||||
|
test += chr(c)
|
||||||
|
else:
|
||||||
|
test = buffer[s:e]
|
||||||
|
|
||||||
|
if test==self.value:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'MagicRule(start=%r, value=%r, mask=%r, word=%r, range=%r)' %(
|
||||||
|
self.start,
|
||||||
|
self.value,
|
||||||
|
self.mask,
|
||||||
|
self.word,
|
||||||
|
self.range)
|
||||||
|
|
||||||
|
|
||||||
|
class MagicMatchAny(object):
|
||||||
|
"""Match any of a set of magic rules.
|
||||||
|
|
||||||
|
This has a similar interface to MagicRule objects (i.e. its match() and
|
||||||
|
maxlen() methods), to allow for duck typing.
|
||||||
|
"""
|
||||||
|
def __init__(self, rules):
|
||||||
|
self.rules = rules
|
||||||
|
|
||||||
|
def match(self, buffer):
|
||||||
|
return any(r.match(buffer) for r in self.rules)
|
||||||
|
|
||||||
|
def maxlen(self):
|
||||||
|
return max(r.maxlen() for r in self.rules)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, f):
|
||||||
|
"""Read a set of rules from the binary magic file."""
|
||||||
|
c=f.read(1)
|
||||||
|
f.seek(-1, 1)
|
||||||
|
depths_rules = []
|
||||||
|
while c and c != b'[':
|
||||||
|
try:
|
||||||
|
depths_rules.append(MagicRule.from_file(f))
|
||||||
|
except UnknownMagicRuleFormat:
|
||||||
|
# Ignored to allow for extensions to the rule format.
|
||||||
|
pass
|
||||||
|
c=f.read(1)
|
||||||
|
if c:
|
||||||
|
f.seek(-1, 1)
|
||||||
|
|
||||||
|
# Build the rule tree
|
||||||
|
tree = [] # (rule, [(subrule,[subsubrule,...]), ...])
|
||||||
|
insert_points = {0:tree}
|
||||||
|
for depth, rule in depths_rules:
|
||||||
|
subrules = []
|
||||||
|
insert_points[depth].append((rule, subrules))
|
||||||
|
insert_points[depth+1] = subrules
|
||||||
|
|
||||||
|
return cls.from_rule_tree(tree)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_rule_tree(cls, tree):
|
||||||
|
"""From a nested list of (rule, subrules) pairs, build a MagicMatchAny
|
||||||
|
instance, recursing down the tree.
|
||||||
|
|
||||||
|
Where there's only one top-level rule, this is returned directly,
|
||||||
|
to simplify the nested structure. Returns None if no rules were read.
|
||||||
|
"""
|
||||||
|
rules = []
|
||||||
|
for rule, subrules in tree:
|
||||||
|
if subrules:
|
||||||
|
rule.also = cls.from_rule_tree(subrules)
|
||||||
|
rules.append(rule)
|
||||||
|
|
||||||
|
if len(rules)==0:
|
||||||
|
return None
|
||||||
|
if len(rules)==1:
|
||||||
|
return rules[0]
|
||||||
|
return cls(rules)
|
||||||
|
|
||||||
|
class MagicDB:
|
||||||
|
def __init__(self):
|
||||||
|
self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
|
||||||
|
|
||||||
|
def merge_file(self, fname):
|
||||||
|
"""Read a magic binary file, and add its rules to this MagicDB."""
|
||||||
|
with open(fname, 'rb') as f:
|
||||||
|
line = f.readline()
|
||||||
|
if line != b'MIME-Magic\0\n':
|
||||||
|
raise IOError('Not a MIME magic file')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shead = f.readline().decode('ascii')
|
||||||
|
#print(shead)
|
||||||
|
if not shead:
|
||||||
|
break
|
||||||
|
if shead[0] != '[' or shead[-2:] != ']\n':
|
||||||
|
raise ValueError('Malformed section heading', shead)
|
||||||
|
pri, tname = shead[1:-2].split(':')
|
||||||
|
#print shead[1:-2]
|
||||||
|
pri = int(pri)
|
||||||
|
mtype = lookup(tname)
|
||||||
|
try:
|
||||||
|
rule = MagicMatchAny.from_file(f)
|
||||||
|
except DiscardMagicRules:
|
||||||
|
self.bytype.pop(mtype, None)
|
||||||
|
rule = MagicMatchAny.from_file(f)
|
||||||
|
if rule is None:
|
||||||
|
continue
|
||||||
|
#print rule
|
||||||
|
|
||||||
|
self.bytype[mtype].append((pri, rule))
|
||||||
|
|
||||||
|
def finalise(self):
|
||||||
|
"""Prepare the MagicDB for matching.
|
||||||
|
|
||||||
|
This should be called after all rules have been merged into it.
|
||||||
|
"""
|
||||||
|
maxlen = 0
|
||||||
|
self.alltypes = [] # (priority, mimetype, rule)
|
||||||
|
|
||||||
|
for mtype, rules in self.bytype.items():
|
||||||
|
for pri, rule in rules:
|
||||||
|
self.alltypes.append((pri, mtype, rule))
|
||||||
|
maxlen = max(maxlen, rule.maxlen())
|
||||||
|
|
||||||
|
self.maxlen = maxlen # Number of bytes to read from files
|
||||||
|
self.alltypes.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
|
||||||
|
def match_data(self, data, max_pri=100, min_pri=0, possible=None):
|
||||||
|
"""Do magic sniffing on some bytes.
|
||||||
|
|
||||||
|
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||||
|
rules to look for. possible can be a list of mimetypes to check, or None
|
||||||
|
(the default) to check all mimetypes until one matches.
|
||||||
|
|
||||||
|
Returns the MIMEtype found, or None if no entries match.
|
||||||
|
"""
|
||||||
|
if possible is not None:
|
||||||
|
types = []
|
||||||
|
for mt in possible:
|
||||||
|
for pri, rule in self.bytype[mt]:
|
||||||
|
types.append((pri, mt, rule))
|
||||||
|
types.sort(key=lambda x: x[0])
|
||||||
|
else:
|
||||||
|
types = self.alltypes
|
||||||
|
|
||||||
|
for priority, mimetype, rule in types:
|
||||||
|
#print priority, max_pri, min_pri
|
||||||
|
if priority > max_pri:
|
||||||
|
continue
|
||||||
|
if priority < min_pri:
|
||||||
|
break
|
||||||
|
|
||||||
|
if rule.match(data):
|
||||||
|
return mimetype
|
||||||
|
|
||||||
|
def match(self, path, max_pri=100, min_pri=0, possible=None):
|
||||||
|
"""Read data from the file and do magic sniffing on it.
|
||||||
|
|
||||||
|
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||||
|
rules to look for. possible can be a list of mimetypes to check, or None
|
||||||
|
(the default) to check all mimetypes until one matches.
|
||||||
|
|
||||||
|
Returns the MIMEtype found, or None if no entries match. Raises IOError
|
||||||
|
if the file can't be opened.
|
||||||
|
"""
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
buf = f.read(self.maxlen)
|
||||||
|
return self.match_data(buf, max_pri, min_pri, possible)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<MagicDB (%d types)>' % len(self.alltypes)
|
||||||
|
|
||||||
|
class GlobDB(object):
|
||||||
|
def __init__(self):
|
||||||
|
"""Prepare the GlobDB. It can't actually be used until .finalise() is
|
||||||
|
called, but merge_file() can be used to add data before that.
|
||||||
|
"""
|
||||||
|
# Maps mimetype to {(weight, glob, flags), ...}
|
||||||
|
self.allglobs = defaultdict(set)
|
||||||
|
|
||||||
|
def merge_file(self, path):
|
||||||
|
"""Loads name matching information from a globs2 file."""#
|
||||||
|
allglobs = self.allglobs
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('#'): continue # Comment
|
||||||
|
|
||||||
|
fields = line[:-1].split(':')
|
||||||
|
weight, type_name, pattern = fields[:3]
|
||||||
|
weight = int(weight)
|
||||||
|
mtype = lookup(type_name)
|
||||||
|
if len(fields) > 3:
|
||||||
|
flags = fields[3].split(',')
|
||||||
|
else:
|
||||||
|
flags = ()
|
||||||
|
|
||||||
|
if pattern == '__NOGLOBS__':
|
||||||
|
# This signals to discard any previous globs
|
||||||
|
allglobs.pop(mtype, None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
allglobs[mtype].add((weight, pattern, tuple(flags)))
|
||||||
|
|
||||||
|
def finalise(self):
|
||||||
|
"""Prepare the GlobDB for matching.
|
||||||
|
|
||||||
|
This should be called after all files have been merged into it.
|
||||||
|
"""
|
||||||
|
self.exts = defaultdict(list) # Maps extensions to [(type, weight),...]
|
||||||
|
self.cased_exts = defaultdict(list)
|
||||||
|
self.globs = [] # List of (regex, type, weight) triplets
|
||||||
|
self.literals = {} # Maps literal names to (type, weight)
|
||||||
|
self.cased_literals = {}
|
||||||
|
|
||||||
|
for mtype, globs in self.allglobs.items():
|
||||||
|
mtype = mtype.canonical()
|
||||||
|
for weight, pattern, flags in globs:
|
||||||
|
|
||||||
|
cased = 'cs' in flags
|
||||||
|
|
||||||
|
if pattern.startswith('*.'):
|
||||||
|
# *.foo -- extension pattern
|
||||||
|
rest = pattern[2:]
|
||||||
|
if not ('*' in rest or '[' in rest or '?' in rest):
|
||||||
|
if cased:
|
||||||
|
self.cased_exts[rest].append((mtype, weight))
|
||||||
|
else:
|
||||||
|
self.exts[rest.lower()].append((mtype, weight))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ('*' in pattern or '[' in pattern or '?' in pattern):
|
||||||
|
# Translate the glob pattern to a regex & compile it
|
||||||
|
re_flags = 0 if cased else re.I
|
||||||
|
pattern = re.compile(fnmatch.translate(pattern), flags=re_flags)
|
||||||
|
self.globs.append((pattern, mtype, weight))
|
||||||
|
else:
|
||||||
|
# No wildcards - literal pattern
|
||||||
|
if cased:
|
||||||
|
self.cased_literals[pattern] = (mtype, weight)
|
||||||
|
else:
|
||||||
|
self.literals[pattern.lower()] = (mtype, weight)
|
||||||
|
|
||||||
|
# Sort globs by weight & length
|
||||||
|
self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) )
|
||||||
|
|
||||||
|
def first_match(self, path):
|
||||||
|
"""Return the first match found for a given path, or None if no match
|
||||||
|
is found."""
|
||||||
|
try:
|
||||||
|
return next(self._match_path(path))[0]
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def all_matches(self, path):
|
||||||
|
"""Return a list of (MIMEtype, glob weight) pairs for the path."""
|
||||||
|
return list(self._match_path(path))
|
||||||
|
|
||||||
|
def _match_path(self, path):
|
||||||
|
"""Yields pairs of (mimetype, glob weight)."""
|
||||||
|
leaf = os.path.basename(path)
|
||||||
|
|
||||||
|
# Literals (no wildcards)
|
||||||
|
if leaf in self.cased_literals:
|
||||||
|
yield self.cased_literals[leaf]
|
||||||
|
|
||||||
|
lleaf = leaf.lower()
|
||||||
|
if lleaf in self.literals:
|
||||||
|
yield self.literals[lleaf]
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
ext = leaf
|
||||||
|
while 1:
|
||||||
|
p = ext.find('.')
|
||||||
|
if p < 0: break
|
||||||
|
ext = ext[p + 1:]
|
||||||
|
if ext in self.cased_exts:
|
||||||
|
for res in self.cased_exts[ext]:
|
||||||
|
yield res
|
||||||
|
ext = lleaf
|
||||||
|
while 1:
|
||||||
|
p = ext.find('.')
|
||||||
|
if p < 0: break
|
||||||
|
ext = ext[p+1:]
|
||||||
|
if ext in self.exts:
|
||||||
|
for res in self.exts[ext]:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
# Other globs
|
||||||
|
for (regex, mime_type, weight) in self.globs:
|
||||||
|
if regex.match(leaf):
|
||||||
|
yield (mime_type, weight)
|
||||||
|
|
||||||
|
# Some well-known types
|
||||||
|
text = lookup('text', 'plain')
|
||||||
|
octet_stream = lookup('application', 'octet-stream')
|
||||||
|
inode_block = lookup('inode', 'blockdevice')
|
||||||
|
inode_char = lookup('inode', 'chardevice')
|
||||||
|
inode_dir = lookup('inode', 'directory')
|
||||||
|
inode_fifo = lookup('inode', 'fifo')
|
||||||
|
inode_socket = lookup('inode', 'socket')
|
||||||
|
inode_symlink = lookup('inode', 'symlink')
|
||||||
|
inode_door = lookup('inode', 'door')
|
||||||
|
app_exe = lookup('application', 'executable')
|
||||||
|
|
||||||
|
_cache_uptodate = False
|
||||||
|
|
||||||
|
def _cache_database():
|
||||||
|
global globs, magic, aliases, inheritance, _cache_uptodate
|
||||||
|
|
||||||
|
_cache_uptodate = True
|
||||||
|
|
||||||
|
aliases = {} # Maps alias Mime types to canonical names
|
||||||
|
inheritance = defaultdict(set) # Maps to sets of parent mime types.
|
||||||
|
|
||||||
|
# Load aliases
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')):
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
alias, canonical = line.strip().split(None, 1)
|
||||||
|
aliases[alias] = canonical
|
||||||
|
|
||||||
|
# Load filename patterns (globs)
|
||||||
|
globs = GlobDB()
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')):
|
||||||
|
globs.merge_file(path)
|
||||||
|
globs.finalise()
|
||||||
|
|
||||||
|
# Load magic sniffing data
|
||||||
|
magic = MagicDB()
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
|
||||||
|
magic.merge_file(path)
|
||||||
|
magic.finalise()
|
||||||
|
|
||||||
|
# Load subclasses
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
sub, parent = line.strip().split(None, 1)
|
||||||
|
inheritance[sub].add(parent)
|
||||||
|
|
||||||
|
def update_cache():
|
||||||
|
if not _cache_uptodate:
|
||||||
|
_cache_database()
|
||||||
|
|
||||||
|
def get_type_by_name(path):
|
||||||
|
"""Returns type of file by its name, or None if not known"""
|
||||||
|
update_cache()
|
||||||
|
return globs.first_match(path)
|
||||||
|
|
||||||
|
def get_type_by_contents(path, max_pri=100, min_pri=0):
|
||||||
|
"""Returns type of file by its contents, or None if not known"""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
return magic.match(path, max_pri, min_pri)
|
||||||
|
|
||||||
|
def get_type_by_data(data, max_pri=100, min_pri=0):
|
||||||
|
"""Returns type of the data, which should be bytes."""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
return magic.match_data(data, max_pri, min_pri)
|
||||||
|
|
||||||
|
def _get_type_by_stat(st_mode):
|
||||||
|
"""Match special filesystem objects to Mimetypes."""
|
||||||
|
if stat.S_ISDIR(st_mode): return inode_dir
|
||||||
|
elif stat.S_ISCHR(st_mode): return inode_char
|
||||||
|
elif stat.S_ISBLK(st_mode): return inode_block
|
||||||
|
elif stat.S_ISFIFO(st_mode): return inode_fifo
|
||||||
|
elif stat.S_ISLNK(st_mode): return inode_symlink
|
||||||
|
elif stat.S_ISSOCK(st_mode): return inode_socket
|
||||||
|
return inode_door
|
||||||
|
|
||||||
|
def get_type(path, follow=True, name_pri=100):
|
||||||
|
"""Returns type of file indicated by path.
|
||||||
|
|
||||||
|
This function is *deprecated* - :func:`get_type2` is more accurate.
|
||||||
|
|
||||||
|
:param path: pathname to check (need not exist)
|
||||||
|
:param follow: when reading file, follow symbolic links
|
||||||
|
:param name_pri: Priority to do name matches. 100=override magic
|
||||||
|
|
||||||
|
This tries to use the contents of the file, and falls back to the name. It
|
||||||
|
can also handle special filesystem objects like directories and sockets.
|
||||||
|
"""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if follow:
|
||||||
|
st = os.stat(path)
|
||||||
|
else:
|
||||||
|
st = os.lstat(path)
|
||||||
|
except:
|
||||||
|
t = get_type_by_name(path)
|
||||||
|
return t or text
|
||||||
|
|
||||||
|
if stat.S_ISREG(st.st_mode):
|
||||||
|
# Regular file
|
||||||
|
t = get_type_by_contents(path, min_pri=name_pri)
|
||||||
|
if not t: t = get_type_by_name(path)
|
||||||
|
if not t: t = get_type_by_contents(path, max_pri=name_pri)
|
||||||
|
if t is None:
|
||||||
|
if stat.S_IMODE(st.st_mode) & 0o111:
|
||||||
|
return app_exe
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
return t
|
||||||
|
else:
|
||||||
|
return _get_type_by_stat(st.st_mode)
|
||||||
|
|
||||||
|
def get_type2(path, follow=True):
|
||||||
|
"""Find the MIMEtype of a file using the XDG recommended checking order.
|
||||||
|
|
||||||
|
This first checks the filename, then uses file contents if the name doesn't
|
||||||
|
give an unambiguous MIMEtype. It can also handle special filesystem objects
|
||||||
|
like directories and sockets.
|
||||||
|
|
||||||
|
:param path: file path to examine (need not exist)
|
||||||
|
:param follow: whether to follow symlinks
|
||||||
|
|
||||||
|
:rtype: :class:`MIMEtype`
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
try:
|
||||||
|
st = os.stat(path) if follow else os.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
return get_type_by_name(path) or octet_stream
|
||||||
|
|
||||||
|
if not stat.S_ISREG(st.st_mode):
|
||||||
|
# Special filesystem objects
|
||||||
|
return _get_type_by_stat(st.st_mode)
|
||||||
|
|
||||||
|
mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True)
|
||||||
|
if mtypes:
|
||||||
|
max_weight = mtypes[0][1]
|
||||||
|
i = 1
|
||||||
|
for mt, w in mtypes[1:]:
|
||||||
|
if w < max_weight:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
mtypes = mtypes[:i]
|
||||||
|
if len(mtypes) == 1:
|
||||||
|
return mtypes[0][0]
|
||||||
|
|
||||||
|
possible = [mt for mt,w in mtypes]
|
||||||
|
else:
|
||||||
|
possible = None # Try all magic matches
|
||||||
|
|
||||||
|
try:
|
||||||
|
t = magic.match(path, possible=possible)
|
||||||
|
except IOError:
|
||||||
|
t = None
|
||||||
|
|
||||||
|
if t:
|
||||||
|
return t
|
||||||
|
elif mtypes:
|
||||||
|
return mtypes[0][0]
|
||||||
|
elif stat.S_IMODE(st.st_mode) & 0o111:
|
||||||
|
return app_exe
|
||||||
|
else:
|
||||||
|
return text if is_text_file(path) else octet_stream
|
||||||
|
|
||||||
|
def is_text_file(path):
|
||||||
|
"""Guess whether a file contains text or binary data.
|
||||||
|
|
||||||
|
Heuristic: binary if the first 32 bytes include ASCII control characters.
|
||||||
|
This rule may change in future versions.
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
f = open(path, 'rb')
|
||||||
|
except IOError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with f:
|
||||||
|
return _is_text(f.read(32))
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
def _is_text(data):
|
||||||
|
return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data)
|
||||||
|
else:
|
||||||
|
def _is_text(data):
|
||||||
|
return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \
|
||||||
|
for b in data)
|
||||||
|
|
||||||
|
_mime2ext_cache = None
|
||||||
|
_mime2ext_cache_uptodate = False
|
||||||
|
|
||||||
|
def get_extensions(mimetype):
|
||||||
|
"""Retrieve the set of filename extensions matching a given MIMEtype.
|
||||||
|
|
||||||
|
Extensions are returned without a leading dot, e.g. 'py'. If no extensions
|
||||||
|
are registered for the MIMEtype, returns an empty set.
|
||||||
|
|
||||||
|
The extensions are stored in a cache the first time this is called.
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
global _mime2ext_cache, _mime2ext_cache_uptodate
|
||||||
|
update_cache()
|
||||||
|
if not _mime2ext_cache_uptodate:
|
||||||
|
_mime2ext_cache = defaultdict(set)
|
||||||
|
for ext, mtypes in globs.exts.items():
|
||||||
|
for mtype, prio in mtypes:
|
||||||
|
_mime2ext_cache[mtype].add(ext)
|
||||||
|
_mime2ext_cache_uptodate = True
|
||||||
|
|
||||||
|
return _mime2ext_cache[mimetype]
|
||||||
|
|
||||||
|
|
||||||
|
def install_mime_info(application, package_file):
|
||||||
|
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``
|
||||||
|
If package_file is None, install ``<app_dir>/<application>.xml``.
|
||||||
|
If already installed, does nothing. May overwrite an existing
|
||||||
|
file with the same name (if the contents are different)"""
|
||||||
|
application += '.xml'
|
||||||
|
|
||||||
|
new_data = open(package_file).read()
|
||||||
|
|
||||||
|
# See if the file is already installed
|
||||||
|
package_dir = os.path.join('mime', 'packages')
|
||||||
|
resource = os.path.join(package_dir, application)
|
||||||
|
for x in BaseDirectory.load_data_paths(resource):
|
||||||
|
try:
|
||||||
|
old_data = open(x).read()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if old_data == new_data:
|
||||||
|
return # Already installed
|
||||||
|
|
||||||
|
global _cache_uptodate
|
||||||
|
_cache_uptodate = False
|
||||||
|
|
||||||
|
# Not already installed; add a new copy
|
||||||
|
# Create the directory structure...
|
||||||
|
new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application)
|
||||||
|
|
||||||
|
# Write the file...
|
||||||
|
open(new_file, 'w').write(new_data)
|
||||||
|
|
||||||
|
# Update the database...
|
||||||
|
command = 'update-mime-database'
|
||||||
|
if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')):
|
||||||
|
os.unlink(new_file)
|
||||||
|
raise Exception("The '%s' command returned an error code!\n" \
|
||||||
|
"Make sure you have the freedesktop.org shared MIME package:\n" \
|
||||||
|
"http://standards.freedesktop.org/shared-mime-info/" % command)
|
|
@ -0,0 +1,181 @@
|
||||||
|
"""
|
||||||
|
Implementation of the XDG Recent File Storage Specification
|
||||||
|
http://standards.freedesktop.org/recent-file-spec
|
||||||
|
"""
|
||||||
|
|
||||||
|
import xml.dom.minidom, xml.sax.saxutils
|
||||||
|
import os, time, fcntl
|
||||||
|
from .Exceptions import ParsingError
|
||||||
|
|
||||||
|
class RecentFiles:
|
||||||
|
def __init__(self):
|
||||||
|
self.RecentFiles = []
|
||||||
|
self.filename = ""
|
||||||
|
|
||||||
|
def parse(self, filename=None):
|
||||||
|
"""Parse a list of recently used files.
|
||||||
|
|
||||||
|
filename defaults to ``~/.recently-used``.
|
||||||
|
"""
|
||||||
|
if not filename:
|
||||||
|
filename = os.path.join(os.getenv("HOME"), ".recently-used")
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = xml.dom.minidom.parse(filename)
|
||||||
|
except IOError:
|
||||||
|
raise ParsingError('File not found', filename)
|
||||||
|
except xml.parsers.expat.ExpatError:
|
||||||
|
raise ParsingError('Not a valid .menu file', filename)
|
||||||
|
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
for child in doc.childNodes:
|
||||||
|
if child.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||||
|
if child.tagName == "RecentFiles":
|
||||||
|
for recent in child.childNodes:
|
||||||
|
if recent.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||||
|
if recent.tagName == "RecentItem":
|
||||||
|
self.__parseRecentItem(recent)
|
||||||
|
|
||||||
|
self.sort()
|
||||||
|
|
||||||
|
def __parseRecentItem(self, item):
|
||||||
|
recent = RecentFile()
|
||||||
|
self.RecentFiles.append(recent)
|
||||||
|
|
||||||
|
for attribute in item.childNodes:
|
||||||
|
if attribute.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||||
|
if attribute.tagName == "URI":
|
||||||
|
recent.URI = attribute.childNodes[0].nodeValue
|
||||||
|
elif attribute.tagName == "Mime-Type":
|
||||||
|
recent.MimeType = attribute.childNodes[0].nodeValue
|
||||||
|
elif attribute.tagName == "Timestamp":
|
||||||
|
recent.Timestamp = int(attribute.childNodes[0].nodeValue)
|
||||||
|
elif attribute.tagName == "Private":
|
||||||
|
recent.Prviate = True
|
||||||
|
elif attribute.tagName == "Groups":
|
||||||
|
|
||||||
|
for group in attribute.childNodes:
|
||||||
|
if group.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||||
|
if group.tagName == "Group":
|
||||||
|
recent.Groups.append(group.childNodes[0].nodeValue)
|
||||||
|
|
||||||
|
def write(self, filename=None):
|
||||||
|
"""Write the list of recently used files to disk.
|
||||||
|
|
||||||
|
If the instance is already associated with a file, filename can be
|
||||||
|
omitted to save it there again.
|
||||||
|
"""
|
||||||
|
if not filename and not self.filename:
|
||||||
|
raise ParsingError('File not found', filename)
|
||||||
|
elif not filename:
|
||||||
|
filename = self.filename
|
||||||
|
|
||||||
|
f = open(filename, "w")
|
||||||
|
fcntl.lockf(f, fcntl.LOCK_EX)
|
||||||
|
f.write('<?xml version="1.0"?>\n')
|
||||||
|
f.write("<RecentFiles>\n")
|
||||||
|
|
||||||
|
for r in self.RecentFiles:
|
||||||
|
f.write(" <RecentItem>\n")
|
||||||
|
f.write(" <URI>%s</URI>\n" % xml.sax.saxutils.escape(r.URI))
|
||||||
|
f.write(" <Mime-Type>%s</Mime-Type>\n" % r.MimeType)
|
||||||
|
f.write(" <Timestamp>%s</Timestamp>\n" % r.Timestamp)
|
||||||
|
if r.Private == True:
|
||||||
|
f.write(" <Private/>\n")
|
||||||
|
if len(r.Groups) > 0:
|
||||||
|
f.write(" <Groups>\n")
|
||||||
|
for group in r.Groups:
|
||||||
|
f.write(" <Group>%s</Group>\n" % group)
|
||||||
|
f.write(" </Groups>\n")
|
||||||
|
f.write(" </RecentItem>\n")
|
||||||
|
|
||||||
|
f.write("</RecentFiles>\n")
|
||||||
|
fcntl.lockf(f, fcntl.LOCK_UN)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def getFiles(self, mimetypes=None, groups=None, limit=0):
|
||||||
|
"""Get a list of recently used files.
|
||||||
|
|
||||||
|
The parameters can be used to filter by mime types, by group, or to
|
||||||
|
limit the number of items returned. By default, the entire list is
|
||||||
|
returned, except for items marked private.
|
||||||
|
"""
|
||||||
|
tmp = []
|
||||||
|
i = 0
|
||||||
|
for item in self.RecentFiles:
|
||||||
|
if groups:
|
||||||
|
for group in groups:
|
||||||
|
if group in item.Groups:
|
||||||
|
tmp.append(item)
|
||||||
|
i += 1
|
||||||
|
elif mimetypes:
|
||||||
|
for mimetype in mimetypes:
|
||||||
|
if mimetype == item.MimeType:
|
||||||
|
tmp.append(item)
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
if item.Private == False:
|
||||||
|
tmp.append(item)
|
||||||
|
i += 1
|
||||||
|
if limit != 0 and i == limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return tmp
|
||||||
|
|
||||||
|
def addFile(self, item, mimetype, groups=None, private=False):
|
||||||
|
"""Add a recently used file.
|
||||||
|
|
||||||
|
item should be the URI of the file, typically starting with ``file:///``.
|
||||||
|
"""
|
||||||
|
# check if entry already there
|
||||||
|
if item in self.RecentFiles:
|
||||||
|
index = self.RecentFiles.index(item)
|
||||||
|
recent = self.RecentFiles[index]
|
||||||
|
else:
|
||||||
|
# delete if more then 500 files
|
||||||
|
if len(self.RecentFiles) == 500:
|
||||||
|
self.RecentFiles.pop()
|
||||||
|
# add entry
|
||||||
|
recent = RecentFile()
|
||||||
|
self.RecentFiles.append(recent)
|
||||||
|
|
||||||
|
recent.URI = item
|
||||||
|
recent.MimeType = mimetype
|
||||||
|
recent.Timestamp = int(time.time())
|
||||||
|
recent.Private = private
|
||||||
|
if groups:
|
||||||
|
recent.Groups = groups
|
||||||
|
|
||||||
|
self.sort()
|
||||||
|
|
||||||
|
def deleteFile(self, item):
|
||||||
|
"""Remove a recently used file, by URI, from the list.
|
||||||
|
"""
|
||||||
|
if item in self.RecentFiles:
|
||||||
|
self.RecentFiles.remove(item)
|
||||||
|
|
||||||
|
def sort(self):
|
||||||
|
self.RecentFiles.sort()
|
||||||
|
self.RecentFiles.reverse()
|
||||||
|
|
||||||
|
|
||||||
|
class RecentFile:
|
||||||
|
def __init__(self):
|
||||||
|
self.URI = ""
|
||||||
|
self.MimeType = ""
|
||||||
|
self.Timestamp = ""
|
||||||
|
self.Private = False
|
||||||
|
self.Groups = []
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
return cmp(self.Timestamp, other.Timestamp)
|
||||||
|
|
||||||
|
def __lt__ (self, other):
|
||||||
|
return self.Timestamp < other.Timestamp
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.URI == str(other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.URI
|
|
@ -0,0 +1,3 @@
|
||||||
|
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
|
||||||
|
|
||||||
|
__version__ = "0.26"
|
|
@ -0,0 +1,75 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] >= 3
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
def u(s):
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
# Unicode-like literals
|
||||||
|
def u(s):
|
||||||
|
return s.decode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# which() is available from Python 3.3
|
||||||
|
from shutil import which
|
||||||
|
except ImportError:
|
||||||
|
import os
|
||||||
|
# This is a copy of which() from Python 3.3
|
||||||
|
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||||
|
"""Given a command, mode, and a PATH string, return the path which
|
||||||
|
conforms to the given mode on the PATH, or None if there is no such
|
||||||
|
file.
|
||||||
|
|
||||||
|
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
|
||||||
|
of os.environ.get("PATH"), or can be overridden with a custom search
|
||||||
|
path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Check that a given file can be accessed with the correct mode.
|
||||||
|
# Additionally check that `file` is not a directory, as on Windows
|
||||||
|
# directories pass the os.access check.
|
||||||
|
def _access_check(fn, mode):
|
||||||
|
return (os.path.exists(fn) and os.access(fn, mode)
|
||||||
|
and not os.path.isdir(fn))
|
||||||
|
|
||||||
|
# If we're given a path with a directory part, look it up directly rather
|
||||||
|
# than referring to PATH directories. This includes checking relative to the
|
||||||
|
# current directory, e.g. ./script
|
||||||
|
if os.path.dirname(cmd):
|
||||||
|
if _access_check(cmd, mode):
|
||||||
|
return cmd
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# The current directory takes precedence on Windows.
|
||||||
|
if not os.curdir in path:
|
||||||
|
path.insert(0, os.curdir)
|
||||||
|
|
||||||
|
# PATHEXT is necessary to check on Windows.
|
||||||
|
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
|
||||||
|
# See if the given file matches any of the expected path extensions.
|
||||||
|
# This will allow us to short circuit when given "python.exe".
|
||||||
|
# If it does match, only test that one, otherwise we have to try
|
||||||
|
# others.
|
||||||
|
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
||||||
|
files = [cmd]
|
||||||
|
else:
|
||||||
|
files = [cmd + ext for ext in pathext]
|
||||||
|
else:
|
||||||
|
# On other platforms you don't have things like PATHEXT to tell you
|
||||||
|
# what file suffixes are executable, so just pass on cmd as-is.
|
||||||
|
files = [cmd]
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for dir in path:
|
||||||
|
normdir = os.path.normcase(dir)
|
||||||
|
if not normdir in seen:
|
||||||
|
seen.add(normdir)
|
||||||
|
for thefile in files:
|
||||||
|
name = os.path.join(dir, thefile)
|
||||||
|
if _access_check(name, mode):
|
||||||
|
return name
|
||||||
|
return None
|
|
@ -0,0 +1,93 @@
|
||||||
|
# System import
|
||||||
|
import os, subprocess, threading
|
||||||
|
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Apoplication imports
|
||||||
|
|
||||||
|
|
||||||
|
class Launcher:
|
||||||
|
def open_file_locally(self, file):
|
||||||
|
lowerName = file.lower()
|
||||||
|
command = []
|
||||||
|
|
||||||
|
if lowerName.endswith(self.fvideos):
|
||||||
|
command = [self.media_app]
|
||||||
|
|
||||||
|
if "mplayer" in self.media_app:
|
||||||
|
command += self.mplayer_options
|
||||||
|
|
||||||
|
command += [file]
|
||||||
|
elif lowerName.endswith(self.fimages):
|
||||||
|
command = [self.image_app, file]
|
||||||
|
elif lowerName.endswith(self.fmusic):
|
||||||
|
command = [self.music_app, file]
|
||||||
|
elif lowerName.endswith(self.foffice):
|
||||||
|
command = [self.office_app, file]
|
||||||
|
elif lowerName.endswith(self.ftext):
|
||||||
|
command = [self.text_app, file]
|
||||||
|
elif lowerName.endswith(self.fpdf):
|
||||||
|
command = [self.pdf_app, file]
|
||||||
|
else:
|
||||||
|
command = [self.file_manager_app, file]
|
||||||
|
|
||||||
|
self.logger.debug(command)
|
||||||
|
DEVNULL = open(os.devnull, 'w')
|
||||||
|
subprocess.Popen(command, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True)
|
||||||
|
|
||||||
|
|
||||||
|
def remux_video(self, hash, file):
|
||||||
|
remux_vid_pth = self.REMUX_FOLDER + "/" + hash + ".mp4"
|
||||||
|
self.logger.debug(remux_vid_pth)
|
||||||
|
|
||||||
|
if not os.path.isfile(remux_vid_pth):
|
||||||
|
self.check_remux_space()
|
||||||
|
|
||||||
|
command = ["ffmpeg", "-i", file, "-hide_banner", "-movflags", "+faststart"]
|
||||||
|
if file.endswith("mkv"):
|
||||||
|
command += ["-codec", "copy", "-strict", "-2"]
|
||||||
|
if file.endswith("avi"):
|
||||||
|
command += ["-c:v", "libx264", "-crf", "21", "-c:a", "aac", "-b:a", "192k", "-ac", "2"]
|
||||||
|
if file.endswith("wmv"):
|
||||||
|
command += ["-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-strict", "-2", "-q:a", "100"]
|
||||||
|
if file.endswith("f4v") or file.endswith("flv"):
|
||||||
|
command += ["-vcodec", "copy"]
|
||||||
|
|
||||||
|
command += [remux_vid_pth]
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(command)
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(message)
|
||||||
|
self.logger.debug(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_remux_space(self):
|
||||||
|
limit = self.remux_folder_max_disk_usage
|
||||||
|
try:
|
||||||
|
limit = int(limit)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
usage = self.get_remux_folder_usage(self.REMUX_FOLDER)
|
||||||
|
if usage > limit:
|
||||||
|
files = os.listdir(self.REMUX_FOLDER)
|
||||||
|
for file in files:
|
||||||
|
fp = os.path.join(self.REMUX_FOLDER, file)
|
||||||
|
os.unlink(fp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_remux_folder_usage(self, start_path = "."):
|
||||||
|
total_size = 0
|
||||||
|
for dirpath, dirnames, filenames in os.walk(start_path):
|
||||||
|
for f in filenames:
|
||||||
|
fp = os.path.join(dirpath, f)
|
||||||
|
if not os.path.islink(fp): # Skip if it is symbolic link
|
||||||
|
total_size += os.path.getsize(fp)
|
||||||
|
|
||||||
|
return total_size
|
|
@ -0,0 +1,93 @@
|
||||||
|
# System import
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Apoplication imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
logger = None
|
||||||
|
|
||||||
|
USER_HOME = path.expanduser('~')
|
||||||
|
CONFIG_PATH = USER_HOME + "/.config/pyfm"
|
||||||
|
CONFIG_FILE = CONFIG_PATH + "/settings.json"
|
||||||
|
HIDE_HIDDEN_FILES = True
|
||||||
|
|
||||||
|
GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1)
|
||||||
|
DEFAULT_ICONS = CONFIG_PATH + "/icons"
|
||||||
|
DEFAULT_ICON = DEFAULT_ICONS + "/text.png"
|
||||||
|
FFMPG_THUMBNLR = CONFIG_PATH + "/ffmpegthumbnailer" # Thumbnail generator binary
|
||||||
|
REMUX_FOLDER = USER_HOME + "/.remuxs" # Remuxed files folder
|
||||||
|
|
||||||
|
STEAM_BASE_URL = "https://steamcdn-a.akamaihd.net/steam/apps/"
|
||||||
|
ICON_DIRS = ["/usr/share/pixmaps", "/usr/share/icons", USER_HOME + "/.icons" ,]
|
||||||
|
BASE_THUMBS_PTH = USER_HOME + "/.thumbnails" # Used for thumbnail generation
|
||||||
|
ABS_THUMBS_PTH = BASE_THUMBS_PTH + "/normal" # Used for thumbnail generation
|
||||||
|
STEAM_ICONS_PTH = BASE_THUMBS_PTH + "/steam_icons"
|
||||||
|
CONTAINER_ICON_WH = [128, 128]
|
||||||
|
VIDEO_ICON_WH = [128, 64]
|
||||||
|
SYS_ICON_WH = [56, 56]
|
||||||
|
|
||||||
|
# CONTAINER_ICON_WH = [128, 128]
|
||||||
|
# VIDEO_ICON_WH = [96, 48]
|
||||||
|
# SYS_ICON_WH = [96, 96]
|
||||||
|
|
||||||
|
subpath = ""
|
||||||
|
go_past_home = None
|
||||||
|
lock_folder = None
|
||||||
|
locked_folders = None
|
||||||
|
mplayer_options = None
|
||||||
|
music_app = None
|
||||||
|
media_app = None
|
||||||
|
image_app = None
|
||||||
|
office_app = None
|
||||||
|
pdf_app = None
|
||||||
|
text_app = None
|
||||||
|
file_manager_app = None
|
||||||
|
remux_folder_max_disk_usage = None
|
||||||
|
|
||||||
|
if path.isfile(CONFIG_FILE):
|
||||||
|
with open(CONFIG_FILE) as infile:
|
||||||
|
settings = json.load(infile)["settings"]
|
||||||
|
|
||||||
|
subpath = settings["base_of_home"]
|
||||||
|
HIDE_HIDDEN_FILES = True if settings["hide_hidden_files"] == "true" else False
|
||||||
|
go_past_home = True if settings["go_past_home"] == "true" else False
|
||||||
|
lock_folder = True if settings["lock_folder"] == "true" else False
|
||||||
|
locked_folders = settings["locked_folders"].split("::::")
|
||||||
|
mplayer_options = settings["mplayer_options"].split()
|
||||||
|
music_app = settings["music_app"]
|
||||||
|
media_app = settings["media_app"]
|
||||||
|
image_app = settings["image_app"]
|
||||||
|
office_app = settings["office_app"]
|
||||||
|
pdf_app = settings["pdf_app"]
|
||||||
|
text_app = settings["text_app"]
|
||||||
|
file_manager_app = settings["file_manager_app"]
|
||||||
|
remux_folder_max_disk_usage = settings["remux_folder_max_disk_usage"]
|
||||||
|
|
||||||
|
# Filters
|
||||||
|
fvideos = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
|
foffice = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||||
|
fimages = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga')
|
||||||
|
ftext = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||||
|
fmusic = ('.psf', '.mp3', '.ogg', '.flac', '.m4a')
|
||||||
|
fpdf = ('.pdf')
|
||||||
|
|
||||||
|
|
||||||
|
# Dire structure check
|
||||||
|
if path.isdir(REMUX_FOLDER) == False:
|
||||||
|
os.mkdir(REMUX_FOLDER)
|
||||||
|
|
||||||
|
if path.isdir(BASE_THUMBS_PTH) == False:
|
||||||
|
os.mkdir(BASE_THUMBS_PTH)
|
||||||
|
|
||||||
|
if path.isdir(ABS_THUMBS_PTH) == False:
|
||||||
|
os.mkdir(ABS_THUMBS_PTH)
|
||||||
|
|
||||||
|
if path.isdir(STEAM_ICONS_PTH) == False:
|
||||||
|
os.mkdir(STEAM_ICONS_PTH)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .Settings import Settings
|
||||||
|
from .Launcher import Launcher
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Python imports
|
||||||
|
import threading, subprocess, os
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .mixins import *
|
||||||
|
# from pyfm.shellfm.windows import WindowController
|
||||||
|
from shellfm import WindowController
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Signals(WindowMixin, PaneMixin):
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.builder = self.settings.builder
|
||||||
|
self.logger = self.settings.logger
|
||||||
|
|
||||||
|
self.window_controller = WindowController()
|
||||||
|
self.state = self.window_controller.load_state()
|
||||||
|
|
||||||
|
self.window = self.builder.get_object("Main_Window")
|
||||||
|
self.window1 = self.builder.get_object("window1")
|
||||||
|
self.window2 = self.builder.get_object("window2")
|
||||||
|
self.window3 = self.builder.get_object("window3")
|
||||||
|
self.window4 = self.builder.get_object("window4")
|
||||||
|
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
|
||||||
|
|
||||||
|
self.single_click_open = False
|
||||||
|
self.is_pane1_hidden = False
|
||||||
|
self.is_pane2_hidden = False
|
||||||
|
self.is_pane3_hidden = False
|
||||||
|
self.is_pane4_hidden = False
|
||||||
|
|
||||||
|
self.window.show()
|
||||||
|
self.generate_windows(self.state)
|
||||||
|
|
||||||
|
def generate_windows(self, data = None):
|
||||||
|
if data:
|
||||||
|
for j, value in enumerate(data):
|
||||||
|
i = j + 1
|
||||||
|
isHidden = True if value[0]["window"]["isHidden"] == "True" else False
|
||||||
|
object = self.builder.get_object(f"tggl_notebook_{i}")
|
||||||
|
views = value[0]["window"]["views"]
|
||||||
|
self.window_controller.create_window()
|
||||||
|
|
||||||
|
for view in views:
|
||||||
|
self.create_new_view_notebook(None, view, i)
|
||||||
|
|
||||||
|
if isHidden:
|
||||||
|
self.toggle_notebook_pane(object)
|
||||||
|
else:
|
||||||
|
for j in range(0, 4):
|
||||||
|
i = j + 1
|
||||||
|
self.window_controller.create_window()
|
||||||
|
self.create_new_view_notebook(None, None, i)
|
||||||
|
|
||||||
|
|
||||||
|
def getClipboardData(self):
|
||||||
|
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
|
||||||
|
retcode = proc.wait()
|
||||||
|
data = proc.stdout.read()
|
||||||
|
return data.decode("utf-8").strip()
|
||||||
|
|
||||||
|
def setClipboardData(self, data):
|
||||||
|
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
|
||||||
|
proc.stdin.write(data)
|
||||||
|
proc.stdin.close()
|
||||||
|
retcode = proc.wait()
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""
|
||||||
|
Gtk Bound Signal Module
|
||||||
|
"""
|
||||||
|
from .mixins import *
|
||||||
|
from .Signals import Signals
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # TODO: Should rewrite to try and support more windows more naturally
|
||||||
|
class PaneMixin:
|
||||||
|
"""docstring for PaneMixin"""
|
||||||
|
|
||||||
|
def toggle_pane(self, child):
|
||||||
|
if child.is_visible():
|
||||||
|
child.hide()
|
||||||
|
else:
|
||||||
|
child.show()
|
||||||
|
|
||||||
|
def run_flag_toggle(self, pane_index):
|
||||||
|
if pane_index == 1:
|
||||||
|
self.is_pane1_hidden = not self.is_pane1_hidden
|
||||||
|
return self.is_pane1_hidden
|
||||||
|
elif pane_index == 2:
|
||||||
|
self.is_pane2_hidden = not self.is_pane2_hidden
|
||||||
|
return self.is_pane2_hidden
|
||||||
|
elif pane_index == 3:
|
||||||
|
self.is_pane3_hidden = not self.is_pane3_hidden
|
||||||
|
return self.is_pane3_hidden
|
||||||
|
elif pane_index == 4:
|
||||||
|
self.is_pane4_hidden = not self.is_pane4_hidden
|
||||||
|
return self.is_pane4_hidden
|
||||||
|
|
||||||
|
def toggle_notebook_pane(self, widget, eve=None):
|
||||||
|
name = widget.get_name()
|
||||||
|
pane_index = int(name[-1])
|
||||||
|
pane = None
|
||||||
|
|
||||||
|
master_pane = self.builder.get_object("pane_master")
|
||||||
|
pane = self.builder.get_object("pane_top") if pane_index in [1, 2] else self.builder.get_object("pane_bottom")
|
||||||
|
|
||||||
|
state = self.run_flag_toggle(pane_index)
|
||||||
|
if self.is_pane1_hidden and self.is_pane2_hidden and self.is_pane3_hidden and self.is_pane4_hidden:
|
||||||
|
state = self.run_flag_toggle(pane_index)
|
||||||
|
self._save_state(state, pane_index)
|
||||||
|
return
|
||||||
|
|
||||||
|
child = None
|
||||||
|
if pane_index in [1, 3]:
|
||||||
|
child = pane.get_child1()
|
||||||
|
elif pane_index in [2, 4]:
|
||||||
|
child = pane.get_child2()
|
||||||
|
|
||||||
|
self.toggle_pane(child)
|
||||||
|
self._save_state(state, pane_index)
|
||||||
|
|
||||||
|
def _save_state(self, state, pane_index):
|
||||||
|
window = self.window_controller.get_window_by_index(pane_index - 1)
|
||||||
|
window.isHidden = state
|
||||||
|
self.window_controller.save_state()
|
|
@ -0,0 +1,146 @@
|
||||||
|
# Python imports
|
||||||
|
import copy
|
||||||
|
from os.path import isdir, isfile
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from . import WidgetMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TabMixin(WidgetMixin):
|
||||||
|
"""docstring for TabMixin"""
|
||||||
|
|
||||||
|
def get_fm_window(self, wid):
|
||||||
|
return self.window_controller.get_window_by_nickname(f"window_{wid}")
|
||||||
|
|
||||||
|
def create_tab(self, wid, path=None, save_state=True):
|
||||||
|
notebook = self.builder.get_object(f"window_{wid}")
|
||||||
|
path_entry = self.builder.get_object(f"path_entry")
|
||||||
|
view = self.window_controller.add_view_for_window_by_nickname(f"window_{wid}")
|
||||||
|
view.logger = self.logger
|
||||||
|
|
||||||
|
if path: view.set_path(path)
|
||||||
|
|
||||||
|
tab = self.create_tab_widget(view)
|
||||||
|
scroll, store = self.create_grid_iconview_widget(view, wid)
|
||||||
|
index = notebook.append_page(scroll, tab)
|
||||||
|
|
||||||
|
# scroll, store = self.create_grid_treeview_widget(view, wid)
|
||||||
|
self.load_store(view, store, save_state)
|
||||||
|
|
||||||
|
self.window_controller.set_active_data(wid, view.get_tab_id())
|
||||||
|
path_entry.set_text(view.get_current_directory())
|
||||||
|
notebook.show_all()
|
||||||
|
notebook.set_current_page(index)
|
||||||
|
|
||||||
|
def close_tab(self, widget, eve):
|
||||||
|
notebook = widget.get_parent().get_parent()
|
||||||
|
page = notebook.get_current_page()
|
||||||
|
|
||||||
|
tid = self.get_tab_id_from_widget(widget.get_parent())
|
||||||
|
wid = int(notebook.get_name()[-1])
|
||||||
|
|
||||||
|
self.get_fm_window(wid).delete_view_by_id(tid)
|
||||||
|
notebook.remove_page(page)
|
||||||
|
self.window_controller.save_state()
|
||||||
|
|
||||||
|
def icon_double_left_click(self, widget, item):
|
||||||
|
try:
|
||||||
|
wid, tid = self.window_controller.get_active_data()
|
||||||
|
notebook = self.builder.get_object(f"window_{wid}")
|
||||||
|
path_entry = self.builder.get_object(f"path_entry")
|
||||||
|
tab_label = self.get_tab_label_widget_from_widget(notebook, widget)
|
||||||
|
|
||||||
|
view = self.get_fm_window(wid).get_view_by_id(tid)
|
||||||
|
model = widget.get_model()
|
||||||
|
|
||||||
|
fileName = model[item][1]
|
||||||
|
dir = view.get_current_directory()
|
||||||
|
file = dir + "/" + fileName
|
||||||
|
refresh = True
|
||||||
|
|
||||||
|
if fileName == ".":
|
||||||
|
view.load_directory()
|
||||||
|
elif fileName == "..":
|
||||||
|
view.pop_from_path()
|
||||||
|
elif isdir(file):
|
||||||
|
view.set_path(file)
|
||||||
|
elif isfile(file):
|
||||||
|
refresh = False
|
||||||
|
view.open_file_locally(file)
|
||||||
|
|
||||||
|
if refresh == True:
|
||||||
|
self.load_store(view, model)
|
||||||
|
tab_label.set_label(view.get_end_of_path())
|
||||||
|
path_entry.set_text(view.get_current_directory())
|
||||||
|
except Exception as e:
|
||||||
|
print(repr(e))
|
||||||
|
|
||||||
|
def icon_single_click(self, widget, eve):
|
||||||
|
try:
|
||||||
|
wid, tid = widget.get_name().split("|")
|
||||||
|
self.window_controller.set_active_data(wid, tid)
|
||||||
|
|
||||||
|
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
|
||||||
|
self.set_path_text(wid, tid)
|
||||||
|
|
||||||
|
if self.single_click_open: # FIXME: need to find a way to pass the model index
|
||||||
|
self.icon_double_left_click(widget)
|
||||||
|
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
|
||||||
|
pass
|
||||||
|
# input = self.builder.get_object("filenameInput")
|
||||||
|
# controls = self.builder.get_object("iconControlsWindow")
|
||||||
|
# iconsButtonBox = self.builder.get_object("iconsButtonBox")
|
||||||
|
# menuButtonBox = self.builder.get_object("menuButtonBox")
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if len(self.selectedFiles) == 1:
|
||||||
|
# parts = self.selectedFiles[0].split("/")
|
||||||
|
# input.set_text(parts[len(parts) - 1])
|
||||||
|
# input.show()
|
||||||
|
# iconsButtonBox.show()
|
||||||
|
# menuButtonBox.hide()
|
||||||
|
# controls.show()
|
||||||
|
# elif len(self.selectedFiles) > 1:
|
||||||
|
# input.set_text("")
|
||||||
|
# input.hide()
|
||||||
|
# menuButtonBox.hide()
|
||||||
|
# iconsButtonBox.show()
|
||||||
|
# controls.show()
|
||||||
|
# else:
|
||||||
|
# input.set_text("")
|
||||||
|
# input.show()
|
||||||
|
# menuButtonBox.show()
|
||||||
|
# iconsButtonBox.hide()
|
||||||
|
# controls.show()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(repr(e))
|
||||||
|
|
||||||
|
def do_action_from_bar_controls(self, widget, eve=None):
|
||||||
|
action = widget.get_name()
|
||||||
|
wid, tid = self.window_controller.get_active_data()
|
||||||
|
notebook = self.builder.get_object(f"window_{wid}")
|
||||||
|
icon_view, tab_label = self.get_icon_view_and_label_from_notebook(notebook, f"{wid}|{tid}")
|
||||||
|
|
||||||
|
view = self.get_fm_window(wid).get_view_by_id(tid)
|
||||||
|
store = icon_view.get_model()
|
||||||
|
|
||||||
|
if action == "go_up":
|
||||||
|
view.pop_from_path()
|
||||||
|
if action == "go_home":
|
||||||
|
view.set_to_home()
|
||||||
|
if action == "refresh_view":
|
||||||
|
view.load_directory()
|
||||||
|
if action == "create_tab" :
|
||||||
|
dir = view.get_current_directory()
|
||||||
|
self.create_tab(wid, dir)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.load_store(view, store, True)
|
||||||
|
self.set_path_text(wid, tid)
|
||||||
|
tab_label.set_label(view.get_end_of_path())
|
|
@ -0,0 +1,136 @@
|
||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetMixin:
|
||||||
|
# This feels ugly but I don't see a better option than itterating over the list.
|
||||||
|
def load_store(self, view, store, save_state=True):
|
||||||
|
store.clear()
|
||||||
|
files = view.get_pixbuf_icon_str_combo()
|
||||||
|
|
||||||
|
for data in files:
|
||||||
|
store.append(data)
|
||||||
|
|
||||||
|
if save_state:
|
||||||
|
self.window_controller.save_state()
|
||||||
|
|
||||||
|
def create_tab_widget(self, view):
|
||||||
|
tab = Gtk.Box()
|
||||||
|
label = Gtk.Label()
|
||||||
|
tid = Gtk.Label()
|
||||||
|
close = Gtk.EventBox()
|
||||||
|
icon = Gtk.Image(stock=Gtk.STOCK_CLOSE)
|
||||||
|
|
||||||
|
label.set_label(f"{view.get_end_of_path()}")
|
||||||
|
tid.set_label(f"{view.id}")
|
||||||
|
|
||||||
|
close.add(icon)
|
||||||
|
tab.add(label)
|
||||||
|
tab.add(close)
|
||||||
|
tab.add(tid)
|
||||||
|
|
||||||
|
close.connect("button_release_event", self.close_tab)
|
||||||
|
tab.show_all()
|
||||||
|
tid.hide()
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def create_grid_iconview_widget(self, view, wid):
|
||||||
|
scroll = Gtk.ScrolledWindow()
|
||||||
|
grid = Gtk.IconView()
|
||||||
|
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
|
||||||
|
|
||||||
|
grid.set_model(store)
|
||||||
|
grid.set_pixbuf_column(0)
|
||||||
|
grid.set_text_column(1)
|
||||||
|
|
||||||
|
grid.set_item_orientation(0)
|
||||||
|
grid.set_selection_mode(3)
|
||||||
|
grid.set_item_width(152)
|
||||||
|
grid.set_item_padding(2)
|
||||||
|
grid.set_margin(2)
|
||||||
|
grid.set_row_spacing(2)
|
||||||
|
grid.set_columns(-1)
|
||||||
|
grid.set_spacing(1)
|
||||||
|
grid.set_column_spacing(2)
|
||||||
|
|
||||||
|
grid.connect("button_release_event", self.icon_single_click)
|
||||||
|
grid.connect("item-activated", self.icon_double_left_click)
|
||||||
|
|
||||||
|
grid.show_all()
|
||||||
|
scroll.add(grid)
|
||||||
|
grid.set_name(f"{wid}|{view.id}")
|
||||||
|
return scroll, store
|
||||||
|
|
||||||
|
def create_grid_treeview_widget(self, view, wid):
|
||||||
|
scroll = Gtk.ScrolledWindow()
|
||||||
|
grid = Gtk.TreeView()
|
||||||
|
store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
|
||||||
|
column = Gtk.TreeViewColumn("Icons")
|
||||||
|
icon = Gtk.CellRendererPixbuf()
|
||||||
|
name = Gtk.CellRendererText()
|
||||||
|
|
||||||
|
grid.set_model(store)
|
||||||
|
column.pack_start(icon, False)
|
||||||
|
column.pack_start(name, True)
|
||||||
|
column.add_attribute(icon, "pixbuf", 0)
|
||||||
|
column.add_attribute(name, "text", 1)
|
||||||
|
column.set_expand(True)
|
||||||
|
|
||||||
|
grid.append_column(column)
|
||||||
|
grid.set_search_column(1)
|
||||||
|
grid.set_rubber_banding(True)
|
||||||
|
grid.set_headers_visible(False)
|
||||||
|
grid.set_enable_tree_lines(False)
|
||||||
|
grid.set_visible(True)
|
||||||
|
|
||||||
|
# grid.connect("button_release_event", self.icon_single_click)
|
||||||
|
# grid.connect("item-activated", self.icon_double_left_click)
|
||||||
|
|
||||||
|
column.set_visible(True)
|
||||||
|
icon.set_visible(True)
|
||||||
|
name.set_visible(True)
|
||||||
|
|
||||||
|
grid.show_all()
|
||||||
|
scroll.add(grid)
|
||||||
|
grid.set_name(f"{wid}|{view.id}")
|
||||||
|
return scroll, store
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def on_tab_switch_update(self, notebook, content=None, index=None):
|
||||||
|
wid, tid = content.get_children()[0].get_name().split("|")
|
||||||
|
self.window_controller.set_active_data(wid, tid)
|
||||||
|
self.set_path_text(wid, tid)
|
||||||
|
|
||||||
|
def set_path_text(self, wid, tid):
|
||||||
|
path_entry = self.builder.get_object("path_entry")
|
||||||
|
view = self.get_fm_window(wid).get_view_by_id(tid)
|
||||||
|
path_entry.set_text(view.get_current_directory())
|
||||||
|
|
||||||
|
def get_tab_id_from_widget(self, tab_box):
|
||||||
|
tid = tab_box.get_children()[2]
|
||||||
|
return tid.get_text()
|
||||||
|
|
||||||
|
def get_tab_label_widget_from_widget(self, notebook, widget):
|
||||||
|
return notebook.get_tab_label(widget.get_parent()).get_children()[0]
|
||||||
|
|
||||||
|
def get_icon_view_and_label_from_notebook(self, notebook, _name):
|
||||||
|
icon_view = None
|
||||||
|
tab_label = None
|
||||||
|
|
||||||
|
for obj in notebook.get_children():
|
||||||
|
icon_view = obj.get_children()[0]
|
||||||
|
name = icon_view.get_name()
|
||||||
|
if name == _name:
|
||||||
|
tab_label = notebook.get_tab_label(obj).get_children()[0]
|
||||||
|
|
||||||
|
return icon_view, tab_label
|
|
@ -0,0 +1,8 @@
|
||||||
|
from . import TabMixin
|
||||||
|
|
||||||
|
|
||||||
|
class WindowMixin(TabMixin):
|
||||||
|
"""docstring for WindowMixin"""
|
||||||
|
|
||||||
|
def create_new_view_notebook(self, widget=None, path=None, wid=None):
|
||||||
|
self.create_tab(wid, path, save_state=False)
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .PaneMixin import PaneMixin
|
||||||
|
from .WidgetMixin import WidgetMixin
|
||||||
|
from .TabMixin import TabMixin
|
||||||
|
from .WindowMixin import WindowMixin
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Python imports
|
||||||
|
import os, logging
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True):
|
||||||
|
"""
|
||||||
|
Create a new logging object and return it.
|
||||||
|
:note:
|
||||||
|
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
|
||||||
|
Log Levels (From least to most)
|
||||||
|
Type Value
|
||||||
|
CRITICAL 50
|
||||||
|
ERROR 40
|
||||||
|
WARNING 30
|
||||||
|
INFO 20
|
||||||
|
DEBUG 10
|
||||||
|
:param loggerName: Sets the name of the logger object. (Used in log lines)
|
||||||
|
:param createFile: Whether we create a log file or just pump to terminal
|
||||||
|
|
||||||
|
:return: the logging object we created
|
||||||
|
"""
|
||||||
|
|
||||||
|
globalLogLvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
|
||||||
|
chLogLevel = logging.CRITICAL # Prety musch the only one we change ever
|
||||||
|
fhLogLevel = logging.DEBUG
|
||||||
|
log = logging.getLogger(loggerName)
|
||||||
|
log.setLevel(globalLogLvl)
|
||||||
|
|
||||||
|
# Set our log output styles
|
||||||
|
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
|
||||||
|
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(level=chLogLevel)
|
||||||
|
ch.setFormatter(cFormatter)
|
||||||
|
log.addHandler(ch)
|
||||||
|
|
||||||
|
if createFile:
|
||||||
|
folder = "logs"
|
||||||
|
file = folder + "/application.log"
|
||||||
|
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.mkdir(folder)
|
||||||
|
|
||||||
|
fh = logging.FileHandler(file)
|
||||||
|
fh.setLevel(level=fhLogLevel)
|
||||||
|
fh.setFormatter(fFormatter)
|
||||||
|
log.addHandler(fh)
|
||||||
|
|
||||||
|
return log
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Gtk imports
|
||||||
|
import gi, cairo
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from . import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self):
|
||||||
|
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
self.builder = gtk.Builder()
|
||||||
|
self.logger = Logger().get_logger()
|
||||||
|
self.builder.add_from_file(self.SCRIPT_PTH + "/../resources/Main_Window.glade")
|
||||||
|
|
||||||
|
|
||||||
|
def createWindow(self):
|
||||||
|
# Get window and connect signals
|
||||||
|
window = self.builder.get_object("Main_Window")
|
||||||
|
window.connect("delete-event", gtk.main_quit)
|
||||||
|
self.setWindowData(window, False)
|
||||||
|
return window
|
||||||
|
|
||||||
|
def setWindowData(self, window, paintable):
|
||||||
|
screen = window.get_screen()
|
||||||
|
visual = screen.get_rgba_visual()
|
||||||
|
|
||||||
|
if visual != None and screen.is_composited():
|
||||||
|
window.set_visual(visual)
|
||||||
|
|
||||||
|
# bind css file
|
||||||
|
cssProvider = gtk.CssProvider()
|
||||||
|
cssProvider.load_from_path(self.SCRIPT_PTH + '/../resources/stylesheet.css')
|
||||||
|
screen = gdk.Screen.get_default()
|
||||||
|
styleContext = gtk.StyleContext()
|
||||||
|
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
|
window.set_app_paintable(paintable)
|
||||||
|
if paintable:
|
||||||
|
window.connect("draw", self.area_draw)
|
||||||
|
|
||||||
|
def getMonitorData(self):
|
||||||
|
screen = self.builder.get_object("Main_Window").get_screen()
|
||||||
|
monitors = []
|
||||||
|
for m in range(screen.get_n_monitors()):
|
||||||
|
monitors.append(screen.get_monitor_geometry(m))
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
|
||||||
|
|
||||||
|
return monitors
|
||||||
|
|
||||||
|
def area_draw(self, widget, cr):
|
||||||
|
cr.set_source_rgba(0, 0, 0, 0.54)
|
||||||
|
cr.set_operator(cairo.OPERATOR_SOURCE)
|
||||||
|
cr.paint()
|
||||||
|
cr.set_operator(cairo.OPERATOR_OVER)
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
Utils module
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .Logger import Logger
|
||||||
|
from .Settings import Settings
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi, faulthandler, signal
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('WebKit2', '4.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
from gi.repository import WebKit2 as webkit
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
from utils import Settings, Events
|
||||||
|
|
||||||
|
gdk.threads_init()
|
||||||
|
class Main:
|
||||||
|
def __init__(self):
|
||||||
|
faulthandler.enable()
|
||||||
|
webkit.WebView() # Needed for glade file to load...
|
||||||
|
|
||||||
|
self.builder = gtk.Builder()
|
||||||
|
self.settings = Settings()
|
||||||
|
self.settings.attachBuilder(self.builder)
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, gtk.main_quit)
|
||||||
|
self.builder.connect_signals(Events(self.settings))
|
||||||
|
|
||||||
|
window = self.settings.createWindow()
|
||||||
|
window.fullscreen()
|
||||||
|
window.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main = Main()
|
||||||
|
gtk.main()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# set -o xtrace ## To debug scripts
|
||||||
|
# set -o errexit ## To exit on error
|
||||||
|
# set -o errunset ## To exit if a variable is referenced but not set
|
||||||
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
# GTK_DEBUG=interactive python3 ./PyFM.py
|
||||||
|
python3 ./PyFM.py
|
||||||
|
}
|
||||||
|
main $@;
|
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 858 B |
After Width: | Height: | Size: 850 B |
After Width: | Height: | Size: 702 B |
After Width: | Height: | Size: 925 B |
After Width: | Height: | Size: 882 B |
After Width: | Height: | Size: 707 B |
After Width: | Height: | Size: 798 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,88 @@
|
||||||
|
viewport,
|
||||||
|
treeview,
|
||||||
|
treeview > header,
|
||||||
|
notebook > stack,
|
||||||
|
notebook > header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
notebook > header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.24);
|
||||||
|
border-color: rgba(0, 232, 255, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
box,
|
||||||
|
iconview {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
treeview,
|
||||||
|
treeview.view {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
cell {
|
||||||
|
margin: 0em;
|
||||||
|
padding: 0em;
|
||||||
|
/* float: left; */
|
||||||
|
}
|
||||||
|
|
||||||
|
cell:focus {
|
||||||
|
outline-style: solid;
|
||||||
|
outline-color: rgba(0, 232, 255, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Ivonview and children default color */
|
||||||
|
.view {
|
||||||
|
background-color: rgba(0, 0, 0, 0.22);
|
||||||
|
color: #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Hover over color when not selected */
|
||||||
|
.view:hover {
|
||||||
|
box-shadow: inset 0 0 0 9999px alpha(rgba(0, 232, 255, 0.64), 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handles the icon selection hover and selected hover color. */
|
||||||
|
.view:selected,
|
||||||
|
.view:selected:hover {
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(15, 134, 13, 0.49);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rubberband coloring */
|
||||||
|
.rubberband,
|
||||||
|
rubberband,
|
||||||
|
flowbox rubberband,
|
||||||
|
treeview.view rubberband,
|
||||||
|
.content-view rubberband,
|
||||||
|
.content-view .rubberband,
|
||||||
|
XfdesktopIconView.view .rubberband {
|
||||||
|
border: 1px solid #6c6c6c;
|
||||||
|
background-color: rgba(21, 158, 167, 0.57);
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view:active {
|
||||||
|
background-color: rgba(172, 102, 21, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
XfdesktopIconView.view {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view:active {
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
XfdesktopIconView.view .rubberband {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import os, gi
|
||||||
|
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import GObject
|
||||||
|
|
||||||
|
|
||||||
|
class Dragging:
|
||||||
|
def __init__(self):
|
||||||
|
# higher values make movement more performant
|
||||||
|
# lower values make movement smoother
|
||||||
|
self.SENSITIVITY = 1
|
||||||
|
self.desktop = None
|
||||||
|
self.EvMask = Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON1_MOTION_MASK
|
||||||
|
self.offsetx = 0
|
||||||
|
self.offsety = 0
|
||||||
|
self.px = 0
|
||||||
|
self.py = 0
|
||||||
|
self.maxx = 0
|
||||||
|
self.maxy = 0
|
||||||
|
|
||||||
|
def connectEvents(self, desktop, widget):
|
||||||
|
self.desktop = desktop
|
||||||
|
widget.set_events(self.EvMask)
|
||||||
|
widget.connect("button_press_event", self.press_event)
|
||||||
|
widget.connect("motion_notify_event", self.draggingEvent)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def press_event(self, w, event):
|
||||||
|
if event.button == 1:
|
||||||
|
p = w.get_parent()
|
||||||
|
# offset == distance of parent widget from edge of screen ...
|
||||||
|
self.offsetx, self.offsety = p.get_window().get_position()
|
||||||
|
# plus distance from pointer to edge of widget
|
||||||
|
self.offsetx += event.x
|
||||||
|
self.offsety += event.y
|
||||||
|
# self.maxx, self.maxy both relative to the parent
|
||||||
|
# note that we're rounding down now so that these max values don't get
|
||||||
|
# rounded upward later and push the widget off the edge of its parent.
|
||||||
|
self.maxx = self.RoundDownToMultiple(p.get_allocation().width - w.get_allocation().width, self.SENSITIVITY)
|
||||||
|
self.maxy = self.RoundDownToMultiple(p.get_allocation().height - w.get_allocation().height, self.SENSITIVITY)
|
||||||
|
|
||||||
|
|
||||||
|
def draggingEvent(self, widget, event):
|
||||||
|
# x_root,x_root relative to screen
|
||||||
|
# x,y relative to parent (fixed widget)
|
||||||
|
# self.px,self.py stores previous values of x,y
|
||||||
|
|
||||||
|
# get starting values for x,y
|
||||||
|
x = event.x_root - self.offsetx
|
||||||
|
y = event.y_root - self.offsety
|
||||||
|
# make sure the potential coordinates x,y:
|
||||||
|
# 1) will not push any part of the widget outside of its parent container
|
||||||
|
# 2) is a multiple of self.SENSITIVITY
|
||||||
|
x = self.RoundToNearestMultiple(self.Max(self.Min(x, self.maxx), 0), self.SENSITIVITY)
|
||||||
|
y = self.RoundToNearestMultiple(self.Max(self.Min(y, self.maxy), 0), self.SENSITIVITY)
|
||||||
|
if x != self.px or y != self.py:
|
||||||
|
self.px = x
|
||||||
|
self.py = y
|
||||||
|
self.desktop.move(widget, x, y)
|
||||||
|
|
||||||
|
def Min(self, a, b):
|
||||||
|
if b < a:
|
||||||
|
return b
|
||||||
|
return a
|
||||||
|
|
||||||
|
def Max(self, a, b):
|
||||||
|
if b > a:
|
||||||
|
return b
|
||||||
|
return a
|
||||||
|
|
||||||
|
def RoundDownToMultiple(self, i, m):
|
||||||
|
return i/m*m
|
||||||
|
|
||||||
|
def RoundToNearestMultiple(self, i, m):
|
||||||
|
if i % m > m / 2:
|
||||||
|
return (i/m+1)*m
|
||||||
|
return i/m*m
|
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
from .Grid import Grid
|
||||||
|
from .Dragging import Dragging
|
||||||
|
|
||||||
|
class Events:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.builder = self.settings.returnBuilder()
|
||||||
|
self.desktop = self.builder.get_object("Desktop")
|
||||||
|
self.webview = self.builder.get_object("webview")
|
||||||
|
self.desktopPath = self.settings.returnDesktopPath()
|
||||||
|
|
||||||
|
self.settings.setDefaultWebviewSettings(self.webview, self.webview.get_settings())
|
||||||
|
self.webview.load_uri(self.settings.returnWebHome())
|
||||||
|
|
||||||
|
# Add filter to allow only folders to be selected
|
||||||
|
selectedDirDialog = self.builder.get_object("selectedDirDialog")
|
||||||
|
filefilter = self.builder.get_object("Folders")
|
||||||
|
selectedDirDialog.add_filter(filefilter)
|
||||||
|
selectedDirDialog.set_filename(self.desktopPath)
|
||||||
|
|
||||||
|
self.grid = None
|
||||||
|
self.setIconViewDir(selectedDirDialog)
|
||||||
|
|
||||||
|
def setIconViewDir(self, widget, data=None):
|
||||||
|
newPath = widget.get_filename()
|
||||||
|
Grid(self.desktop, self.settings, newPath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# File control events
|
||||||
|
def createFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, widget, data=None):
|
||||||
|
newName = widget.get_text().strip()
|
||||||
|
if data and data.keyval == 65293: # Enter key event
|
||||||
|
self.grid.updateFile(newName)
|
||||||
|
elif data == None: # Save button 'event'
|
||||||
|
self.grid.updateFile(newName)
|
||||||
|
|
||||||
|
def deleteFile(self, widget, data=None):
|
||||||
|
self.grid.deleteFile()
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Webview events
|
||||||
|
def showWebview(self, widget):
|
||||||
|
self.builder.get_object("webViewer").popup()
|
||||||
|
|
||||||
|
def loadHome(self, widget):
|
||||||
|
self.webview.load_uri(self.settings.returnWebHome())
|
||||||
|
|
||||||
|
def runSearchWebview(self, widget, data=None):
|
||||||
|
if data.keyval == 65293:
|
||||||
|
self.webview.load_uri(widget.get_text().strip())
|
||||||
|
|
||||||
|
def refreshPage(self, widget, data=None):
|
||||||
|
self.webview.load_uri(self.webview.get_uri())
|
||||||
|
|
||||||
|
def setUrlBar(self, widget, data=None):
|
||||||
|
self.builder.get_object("webviewSearch").set_text(widget.get_uri())
|
|
@ -0,0 +1,93 @@
|
||||||
|
|
||||||
|
import os, shutil, subprocess, threading
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class FileHandler:
|
||||||
|
def __init__(self):
|
||||||
|
# 'Filters'
|
||||||
|
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx' '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||||
|
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
|
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||||
|
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
|
||||||
|
self.images = ('.png', '.jpg', '.jpeg', '.gif')
|
||||||
|
self.pdf = ('.pdf')
|
||||||
|
|
||||||
|
# Args
|
||||||
|
self.MEDIAPLAYER = "mpv";
|
||||||
|
self.IMGVIEWER = "mirage";
|
||||||
|
self.MUSICPLAYER = "/opt/deadbeef/bin/deadbeef";
|
||||||
|
self.OFFICEPROG = "libreoffice";
|
||||||
|
self.TEXTVIEWER = "leafpad";
|
||||||
|
self.PDFVIEWER = "evince";
|
||||||
|
self.FILEMANAGER = "spacefm";
|
||||||
|
self.MPLAYER_WH = " -xy 1600 -geometry 50%:50% ";
|
||||||
|
self.MPV_WH = " -geometry 50%:50% ";
|
||||||
|
|
||||||
|
@threaded
|
||||||
|
def openFile(self, file):
|
||||||
|
print("Opening: " + file)
|
||||||
|
if file.lower().endswith(self.vids):
|
||||||
|
subprocess.Popen([self.MEDIAPLAYER, self.MPV_WH, file])
|
||||||
|
elif file.lower().endswith(self.music):
|
||||||
|
subprocess.Popen([self.MUSICPLAYER, file])
|
||||||
|
elif file.lower().endswith(self.images):
|
||||||
|
subprocess.Popen([self.IMGVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.txt):
|
||||||
|
subprocess.Popen([self.TEXTVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.pdf):
|
||||||
|
subprocess.Popen([self.PDFVIEWER, file])
|
||||||
|
elif file.lower().endswith(self.office):
|
||||||
|
subprocess.Popen([self.OFFICEPROG, file])
|
||||||
|
else:
|
||||||
|
subprocess.Popen(['xdg-open', file])
|
||||||
|
|
||||||
|
|
||||||
|
def createFile(self, newFileName):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, oldFileName, newFileName):
|
||||||
|
try:
|
||||||
|
print("Renaming...")
|
||||||
|
print(oldFileName + " --> " + newFileName)
|
||||||
|
os.rename(oldFileName, newFileName)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured renaming the file:")
|
||||||
|
print(e)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def deleteFile(self, toDeleteFile):
|
||||||
|
try:
|
||||||
|
print("Deleting...")
|
||||||
|
print(toDeleteFile)
|
||||||
|
if os.path.exists(toDeleteFile):
|
||||||
|
if os.path.isfile(toDeleteFile):
|
||||||
|
os.remove(toDeleteFile)
|
||||||
|
elif os.path.isdir(toDeleteFile):
|
||||||
|
shutil.rmtree(toDeleteFile)
|
||||||
|
else:
|
||||||
|
print("An error occured deleting the file:")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print("The folder/file does not exist")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured deleting the file:")
|
||||||
|
print(e)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
|
@ -0,0 +1,214 @@
|
||||||
|
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
from gi.repository import GLib as glib
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import os, threading, time
|
||||||
|
from os.path import isdir, isfile, join
|
||||||
|
from os import listdir
|
||||||
|
from .Icon import Icon
|
||||||
|
from .FileHandler import FileHandler
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class Grid:
|
||||||
|
def __init__(self, desktop, settings, newPath):
|
||||||
|
self.desktop = desktop
|
||||||
|
self.settings = settings
|
||||||
|
self.filehandler = FileHandler()
|
||||||
|
|
||||||
|
self.store = gtk.ListStore(GdkPixbuf.Pixbuf, str)
|
||||||
|
self.usrHome = settings.returnUserHome()
|
||||||
|
self.builder = settings.returnBuilder()
|
||||||
|
self.ColumnSize = settings.returnColumnSize()
|
||||||
|
self.currentPath = ""
|
||||||
|
self.selectedFile = ""
|
||||||
|
|
||||||
|
self.desktop.set_model(self.store)
|
||||||
|
self.desktop.set_pixbuf_column(0)
|
||||||
|
self.desktop.set_text_column(1)
|
||||||
|
self.desktop.connect("item-activated", self.iconLeftClickEventManager)
|
||||||
|
self.desktop.connect("button_press_event", self.iconRightClickEventManager, (self.desktop,))
|
||||||
|
self.desktop.connect("selection-changed", self.setIconSelectionArray, (self.desktop,))
|
||||||
|
|
||||||
|
self.vidsList = settings.returnVidsExtensionList()
|
||||||
|
self.imagesList = settings.returnImagesExtensionList()
|
||||||
|
self.gtkLock = False # Thread checks for gtkLock
|
||||||
|
self.threadLock = False # Gtk checks for thread lock
|
||||||
|
self.helperThread = None # Helper thread object
|
||||||
|
self.toWorkPool = [] # Thread fills pool and gtk empties it
|
||||||
|
self.copyCutArry = []
|
||||||
|
|
||||||
|
self.setIconViewDir(newPath)
|
||||||
|
|
||||||
|
def setIconViewDir(self, path):
|
||||||
|
self.store.clear()
|
||||||
|
|
||||||
|
self.currentPath = path
|
||||||
|
dirPaths = ['.', '..']
|
||||||
|
vids = []
|
||||||
|
images = []
|
||||||
|
desktop = []
|
||||||
|
files = []
|
||||||
|
|
||||||
|
for f in listdir(path):
|
||||||
|
file = join(path, f)
|
||||||
|
if self.settings.isHideHiddenFiles():
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
if isfile(file):
|
||||||
|
if file.lower().endswith(self.vidsList):
|
||||||
|
vids.append(f)
|
||||||
|
elif file.lower().endswith(self.imagesList):
|
||||||
|
images.append(f)
|
||||||
|
elif file.lower().endswith((".desktop",)):
|
||||||
|
desktop.append(f)
|
||||||
|
else:
|
||||||
|
files.append(f)
|
||||||
|
else:
|
||||||
|
dirPaths.append(f)
|
||||||
|
|
||||||
|
dirPaths.sort()
|
||||||
|
vids.sort()
|
||||||
|
images.sort()
|
||||||
|
desktop.sort()
|
||||||
|
files.sort()
|
||||||
|
files = dirPaths + vids + images + desktop + files
|
||||||
|
|
||||||
|
if self.helperThread:
|
||||||
|
self.helperThread.terminate()
|
||||||
|
self.helperThread = None
|
||||||
|
|
||||||
|
# Run helper thread...
|
||||||
|
self.threadLock = True
|
||||||
|
self.helperThread = threading.Thread(target=self.generateDirectoryGridIcon, args=(path, files)).start()
|
||||||
|
glib.idle_add(self.addToGrid, (file,)) # This must stay in the main thread b/c
|
||||||
|
# gtk isn't thread safe/aware So, we
|
||||||
|
# make a sad lil thread hot potato 'game'
|
||||||
|
# out of this process.
|
||||||
|
|
||||||
|
|
||||||
|
# @threaded
|
||||||
|
def generateDirectoryGridIcon(self, dirPath, files):
|
||||||
|
# NOTE: We'll be passing pixbuf after retreval to keep Icon.py file more
|
||||||
|
# universaly usable. We can just remove get_pixbuf to get a gtk.Image type
|
||||||
|
for file in files:
|
||||||
|
image = Icon(self.settings).createIcon(dirPath, file)
|
||||||
|
self.toWorkPool.append([image.get_pixbuf(), file])
|
||||||
|
self.threadLock = False
|
||||||
|
self.gtkLock = True
|
||||||
|
|
||||||
|
|
||||||
|
def addToGrid(self, args):
|
||||||
|
# NOTE: Returning true tells gtk to check again in the future when idle.
|
||||||
|
# False ends checks and "continues normal flow"
|
||||||
|
files = args[0]
|
||||||
|
|
||||||
|
if len(self.toWorkPool) > 0:
|
||||||
|
for dataSet in self.toWorkPool:
|
||||||
|
self.store.append(dataSet)
|
||||||
|
|
||||||
|
if len(self.store) == len(files): # Confirm processed all files and cleanup
|
||||||
|
self.gtkLock = False
|
||||||
|
self.threadLock = False
|
||||||
|
self.toWorkPool.clear()
|
||||||
|
return False
|
||||||
|
# Check again when idle; If nothing else is updating, this function
|
||||||
|
# gets called immediatly. So, we play hot potato by passing lock to Thread
|
||||||
|
else:
|
||||||
|
self.toWorkPool.clear()
|
||||||
|
self.gtkLock = False
|
||||||
|
self.threadLock = True
|
||||||
|
time.sleep(.005) # Fixes refresh and up icon not being added.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setIconSelectionArray(self, widget, data=None):
|
||||||
|
pass
|
||||||
|
# os.system('cls||clear')
|
||||||
|
# print(data)
|
||||||
|
|
||||||
|
def iconLeftClickEventManager(self, widget, item):
|
||||||
|
try:
|
||||||
|
model = widget.get_model()
|
||||||
|
fileName = model[item][1]
|
||||||
|
dir = self.currentPath
|
||||||
|
file = dir + "/" + fileName
|
||||||
|
|
||||||
|
if fileName == ".":
|
||||||
|
self.setIconViewDir(dir)
|
||||||
|
elif fileName == "..":
|
||||||
|
parentDir = os.path.abspath(os.path.join(dir, os.pardir))
|
||||||
|
self.currentPath = parentDir
|
||||||
|
self.setIconViewDir(parentDir)
|
||||||
|
elif isdir(file):
|
||||||
|
self.currentPath = file
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
elif isfile(file):
|
||||||
|
self.filehandler.openFile(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def iconRightClickEventManager(self, widget, eve, params):
|
||||||
|
try:
|
||||||
|
if eve.type == gdk.EventType.BUTTON_PRESS and eve.button == 3:
|
||||||
|
popover = self.builder.get_object("iconControlsWindow")
|
||||||
|
popover.show_all()
|
||||||
|
popover.popup()
|
||||||
|
# # NOTE: Need to change name of listview box...
|
||||||
|
# children = widget.get_children()[0].get_children()
|
||||||
|
# fileName = children[1].get_text()
|
||||||
|
# dir = self.currentPath
|
||||||
|
# file = dir + "/" + fileName
|
||||||
|
#
|
||||||
|
# input = self.builder.get_object("iconRenameInput")
|
||||||
|
# popover = self.builder.get_object("iconControlsWindow")
|
||||||
|
# self.selectedFile = file # Used for return to caller
|
||||||
|
#
|
||||||
|
# input.set_text(fileName)
|
||||||
|
# popover.set_relative_to(widget)
|
||||||
|
# popover.set_position(gtk.PositionType.RIGHT)
|
||||||
|
# popover.show_all()
|
||||||
|
# popover.popup()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
# Passthrough file control events
|
||||||
|
def createFile(arg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFile(self, file):
|
||||||
|
newName = self.currentPath + "/" + file
|
||||||
|
status = self.filehandler.updateFile(self.selectedFile, newName)
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
self.selectedFile = newName
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
|
||||||
|
def deleteFile(self):
|
||||||
|
status = self.filehandler.deleteFile(self.selectedFile)
|
||||||
|
|
||||||
|
if status == 0:
|
||||||
|
self.selectedFile = ""
|
||||||
|
self.setIconViewDir(self.currentPath)
|
||||||
|
|
||||||
|
def copyFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cutFile(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pasteFile(self):
|
||||||
|
pass
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gio as gio
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
from xdg.DesktopEntry import DesktopEntry
|
||||||
|
|
||||||
|
# Python Imports
|
||||||
|
import os, subprocess, hashlib, threading
|
||||||
|
|
||||||
|
from os.path import isdir, isfile, join
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class Icon:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.thubnailGen = settings.getThumbnailGenerator()
|
||||||
|
self.vidsList = settings.returnVidsExtensionList()
|
||||||
|
self.imagesList = settings.returnImagesExtensionList()
|
||||||
|
self.GTK_ORIENTATION = settings.returnIconImagePos()
|
||||||
|
self.usrHome = settings.returnUserHome()
|
||||||
|
self.iconContainerWH = settings.returnContainerWH()
|
||||||
|
self.systemIconImageWH = settings.returnSystemIconImageWH()
|
||||||
|
self.viIconWH = settings.returnVIIconWH()
|
||||||
|
|
||||||
|
|
||||||
|
def createIcon(self, dir, file):
|
||||||
|
fullPath = dir + "/" + file
|
||||||
|
return self.getIconImage(file, fullPath)
|
||||||
|
|
||||||
|
|
||||||
|
def getIconImage(self, file, fullPath):
|
||||||
|
try:
|
||||||
|
thumbnl = None
|
||||||
|
|
||||||
|
# Video thumbnail
|
||||||
|
if file.lower().endswith(self.vidsList):
|
||||||
|
fileHash = hashlib.sha256(str.encode(fullPath)).hexdigest()
|
||||||
|
hashImgPth = self.usrHome + "/.thumbnails/normal/" + fileHash + ".png"
|
||||||
|
|
||||||
|
if isfile(hashImgPth) == False:
|
||||||
|
self.generateVideoThumbnail(fullPath, hashImgPth)
|
||||||
|
|
||||||
|
thumbnl = self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
# Image Icon
|
||||||
|
elif file.lower().endswith(self.imagesList):
|
||||||
|
thumbnl = self.createIconImageBuffer(fullPath, self.viIconWH)
|
||||||
|
# .desktop file parsing
|
||||||
|
elif fullPath.lower().endswith( ('.desktop',) ):
|
||||||
|
thumbnl = self.parseDesktopFiles(fullPath)
|
||||||
|
# System icons
|
||||||
|
else:
|
||||||
|
thumbnl = self.getSystemThumbnail(fullPath, self.systemIconImageWH[0])
|
||||||
|
|
||||||
|
if thumbnl == None: # If no icon, try stock file icon...
|
||||||
|
thumbnl = gtk.Image.new_from_icon_name("gtk-file", gtk.IconSize.LARGE_TOOLBAR)
|
||||||
|
|
||||||
|
if thumbnl == None: # If no icon whatsoever, return internal default
|
||||||
|
thumbnl = gtk.Image.new_from_file("resources/icons/bin.png")
|
||||||
|
|
||||||
|
return thumbnl
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return gtk.Image.new_from_file("resources/icons/bin.png")
|
||||||
|
|
||||||
|
|
||||||
|
def parseDesktopFiles(self, fullPath):
|
||||||
|
try:
|
||||||
|
xdgObj = DesktopEntry(fullPath)
|
||||||
|
icon = xdgObj.getIcon()
|
||||||
|
iconsDirs = "/usr/share/icons"
|
||||||
|
altIconPath = ""
|
||||||
|
|
||||||
|
if "steam" in icon:
|
||||||
|
steamIconsDir = self.usrHome + "/.thumbnails/steam_icons/"
|
||||||
|
name = xdgObj.getName()
|
||||||
|
fileHash = hashlib.sha256(str.encode(name)).hexdigest()
|
||||||
|
|
||||||
|
if isdir(steamIconsDir) == False:
|
||||||
|
os.mkdir(steamIconsDir)
|
||||||
|
|
||||||
|
hashImgPth = steamIconsDir + fileHash + ".jpg"
|
||||||
|
if isfile(hashImgPth) == True:
|
||||||
|
# Use video sizes since headers are bigger
|
||||||
|
return self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
|
||||||
|
execStr = xdgObj.getExec()
|
||||||
|
parts = execStr.split("steam://rungameid/")
|
||||||
|
id = parts[len(parts) - 1]
|
||||||
|
|
||||||
|
# NOTE: Can try this logic instead...
|
||||||
|
# if command exists use it instead of header image
|
||||||
|
# if "steamcmd app_info_print id":
|
||||||
|
# proc = subprocess.Popen(["steamcmd", "app_info_print", id])
|
||||||
|
# proc.wait()
|
||||||
|
# else:
|
||||||
|
# use the bottom logic
|
||||||
|
|
||||||
|
imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg"
|
||||||
|
proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink])
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
# Use video sizes since headers are bigger
|
||||||
|
return self.createIconImageBuffer(hashImgPth, self.viIconWH)
|
||||||
|
elif os.path.exists(icon):
|
||||||
|
return self.createIconImageBuffer(icon, self.systemIconImageWH)
|
||||||
|
else:
|
||||||
|
for (dirpath, dirnames, filenames) in os.walk(iconsDirs):
|
||||||
|
for file in filenames:
|
||||||
|
appNM = "application-x-" + icon
|
||||||
|
if appNM in file:
|
||||||
|
altIconPath = dirpath + "/" + file
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.createIconImageBuffer(altIconPath, self.systemIconImageWH)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def getSystemThumbnail(self, filename, size):
|
||||||
|
try:
|
||||||
|
iconPath = None
|
||||||
|
if os.path.exists(filename):
|
||||||
|
file = gio.File.new_for_path(filename)
|
||||||
|
info = file.query_info('standard::icon' , 0 , gio.Cancellable())
|
||||||
|
icon = info.get_icon().get_names()[0]
|
||||||
|
iconTheme = gtk.IconTheme.get_default()
|
||||||
|
iconFile = iconTheme.lookup_icon(icon , size , 0)
|
||||||
|
|
||||||
|
if iconFile != None:
|
||||||
|
iconPath = iconFile.get_filename()
|
||||||
|
return self.createIconImageBuffer(iconPath, self.systemIconImageWH)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def createIconImageBuffer(self, path, wxh):
|
||||||
|
try:
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], False)
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
|
|
||||||
|
def generateVideoThumbnail(self, fullPath, hashImgPth):
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen([self.thubnailGen, "-t", "65%", "-s", "300", "-c", "jpg", "-i", fullPath, "-o", hashImgPth])
|
||||||
|
proc.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
|
@ -0,0 +1,139 @@
|
||||||
|
|
||||||
|
# Gtk Imports
|
||||||
|
import gi, cairo, os
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk as gtk
|
||||||
|
from gi.repository import Gdk as gdk
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self):
|
||||||
|
self.builder = None
|
||||||
|
self.hideHiddenFiles = True
|
||||||
|
|
||||||
|
|
||||||
|
self.GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1)
|
||||||
|
self.THUMB_GENERATOR = "ffmpegthumbnailer"
|
||||||
|
self.DEFAULTCOLOR = gdk.RGBA(0.0, 0.0, 0.0, 0.0) # ~#00000000
|
||||||
|
self.MOUSEOVERCOLOR = gdk.RGBA(0.0, 0.9, 1.0, 0.64) # ~#00e8ff
|
||||||
|
self.SELECTEDCOLOR = gdk.RGBA(0.4, 0.5, 0.1, 0.84)
|
||||||
|
|
||||||
|
self.ColumnSize = 8
|
||||||
|
self.usrHome = os.path.expanduser('~')
|
||||||
|
self.desktopPath = self.usrHome + "/Desktop"
|
||||||
|
self.webHome = 'http://webfm.com/'
|
||||||
|
self.iconContainerWxH = [128, 128]
|
||||||
|
self.systemIconImageWxH = [72, 72]
|
||||||
|
self.viIconWxH = [256, 128]
|
||||||
|
self.vidsExtensionList = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
|
self.imagesExtensionList = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga')
|
||||||
|
|
||||||
|
|
||||||
|
def attachBuilder(self, builder):
|
||||||
|
self.builder = builder
|
||||||
|
self.builder.add_from_file("resources/PyFM.glade")
|
||||||
|
|
||||||
|
def createWindow(self):
|
||||||
|
# Get window and connect signals
|
||||||
|
window = self.builder.get_object("Window")
|
||||||
|
window.connect("delete-event", gtk.main_quit)
|
||||||
|
self.setWindowData(window)
|
||||||
|
return window
|
||||||
|
|
||||||
|
def setWindowData(self, window):
|
||||||
|
screen = window.get_screen()
|
||||||
|
visual = screen.get_rgba_visual()
|
||||||
|
if visual != None and screen.is_composited():
|
||||||
|
window.set_visual(visual)
|
||||||
|
|
||||||
|
# bind css file
|
||||||
|
cssProvider = gtk.CssProvider()
|
||||||
|
cssProvider.load_from_path('resources/stylesheet.css')
|
||||||
|
screen = gdk.Screen.get_default()
|
||||||
|
styleContext = gtk.StyleContext()
|
||||||
|
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
|
window.set_app_paintable(True)
|
||||||
|
monitors = self.getMonitorData(screen)
|
||||||
|
window.resize(monitors[0].width, monitors[0].height)
|
||||||
|
|
||||||
|
def getMonitorData(self, screen):
|
||||||
|
monitors = []
|
||||||
|
for m in range(screen.get_n_monitors()):
|
||||||
|
monitors.append(screen.get_monitor_geometry(m))
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
|
||||||
|
|
||||||
|
return monitors
|
||||||
|
|
||||||
|
|
||||||
|
def returnBuilder(self): return self.builder
|
||||||
|
def returnUserHome(self): return self.usrHome
|
||||||
|
def returnDesktopPath(self): return self.usrHome + "/Desktop"
|
||||||
|
def returnIconImagePos(self): return self.GTK_ORIENTATION
|
||||||
|
def getThumbnailGenerator(self): return self.THUMB_GENERATOR
|
||||||
|
def returnColumnSize(self): return self.ColumnSize
|
||||||
|
def returnContainerWH(self): return self.iconContainerWxH
|
||||||
|
def returnSystemIconImageWH(self): return self.systemIconImageWxH
|
||||||
|
def returnVIIconWH(self): return self.viIconWxH
|
||||||
|
def returnWebHome(self): return self.webHome
|
||||||
|
def isHideHiddenFiles(self): return self.hideHiddenFiles
|
||||||
|
def returnVidsExtensionList(self): return self.vidsExtensionList
|
||||||
|
def returnImagesExtensionList(self): return self.imagesExtensionList
|
||||||
|
|
||||||
|
def setDefaultWebviewSettings(self, widget, settings=None):
|
||||||
|
# Usability
|
||||||
|
settings.set_property('enable-fullscreen', True)
|
||||||
|
settings.set_property('print-backgrounds', True)
|
||||||
|
settings.set_property('enable-frame-flattening', False)
|
||||||
|
settings.set_property('enable-plugins', True)
|
||||||
|
settings.set_property('enable-java', False)
|
||||||
|
settings.set_property('enable-resizable-text-areas', True)
|
||||||
|
settings.set_property('zoom-text-only', False)
|
||||||
|
settings.set_property('enable-smooth-scrolling', True)
|
||||||
|
settings.set_property('enable-back-forward-navigation-gestures', False)
|
||||||
|
settings.set_property('media-playback-requires-user-gesture', False)
|
||||||
|
settings.set_property('enable-tabs-to-links', True)
|
||||||
|
settings.set_property('enable-caret-browsing', False)
|
||||||
|
|
||||||
|
# Security
|
||||||
|
settings.set_property('user-agent','Mozilla/5.0 (X11; Generic; Linux x86-64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Safari/605.1.15')
|
||||||
|
settings.set_property('enable-private-browsing', False)
|
||||||
|
settings.set_property('enable-xss-auditor', True)
|
||||||
|
settings.set_property('enable-hyperlink-auditing', False)
|
||||||
|
settings.set_property('enable-site-specific-quirks', True)
|
||||||
|
settings.set_property('enable-offline-web-application-cache', True)
|
||||||
|
settings.set_property('enable-page-cache', True)
|
||||||
|
settings.set_property('allow-modal-dialogs', False)
|
||||||
|
settings.set_property('enable-html5-local-storage', True)
|
||||||
|
settings.set_property('enable-html5-database', True)
|
||||||
|
settings.set_property('allow-file-access-from-file-urls', False)
|
||||||
|
settings.set_property('allow-universal-access-from-file-urls', False)
|
||||||
|
settings.set_property('enable-dns-prefetching', False)
|
||||||
|
|
||||||
|
# Media stuff
|
||||||
|
# settings.set_property('hardware-acceleration-policy', 'on-demand')
|
||||||
|
settings.set_property('enable-webgl', False)
|
||||||
|
settings.set_property('enable-webaudio', True)
|
||||||
|
settings.set_property('enable-accelerated-2d-canvas', True)
|
||||||
|
settings.set_property('auto-load-images', True)
|
||||||
|
settings.set_property('enable-media-capabilities', True)
|
||||||
|
settings.set_property('enable-media-stream', True)
|
||||||
|
settings.set_property('enable-mediasource', True)
|
||||||
|
settings.set_property('enable-encrypted-media', True)
|
||||||
|
settings.set_property('media-playback-allows-inline', True)
|
||||||
|
|
||||||
|
# JS
|
||||||
|
settings.set_property('enable-javascript', True)
|
||||||
|
settings.set_property('enable-javascript-markup', True)
|
||||||
|
settings.set_property('javascript-can-access-clipboard', False)
|
||||||
|
settings.set_property('javascript-can-open-windows-automatically', False)
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
settings.set_property('enable-developer-extras', False)
|
||||||
|
settings.set_property('enable-write-console-messages-to-stdout', False)
|
||||||
|
settings.set_property('draw-compositing-indicators', False)
|
||||||
|
settings.set_property('enable-mock-capture-devices', False)
|
||||||
|
settings.set_property('enable-spatial-navigation', False)
|
|
@ -0,0 +1,6 @@
|
||||||
|
from utils.Dragging import Dragging
|
||||||
|
from utils.Settings import Settings
|
||||||
|
from utils.Events import Events
|
||||||
|
from utils.Grid import Grid
|
||||||
|
from utils.Icon import Icon
|
||||||
|
from utils.FileHandler import FileHandler
|
|
@ -0,0 +1,10 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
chdir("/opt/Pytop/");
|
||||||
|
system("python3 PyTop.py");
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# set -o xtrace ## To debug scripts
|
||||||
|
# set -o errexit ## To exit on error
|
||||||
|
# set -o errunset ## To exit if a variable is referenced but not set
|
||||||
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
find . -name "__pycache__" -exec rm -rf $1 {} \;
|
||||||
|
find . -name "*.pyc" -exec rm -rf $1 {} \;
|
||||||
|
}
|
||||||
|
main
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
gcc -no-pie -s Pytop_exec_bin.cpp -o pytop
|
||||||
|
}
|
||||||
|
main;
|