├── AUTHORS ├── CHANGES ├── COPYING ├── INSTALL ├── LICENSE ├── README.md ├── Sunflower.desktop ├── images ├── splash.png ├── sunflower.png ├── sunflower.svg └── sunflower_64.png ├── setup.py ├── styles └── main.css ├── sunflower ├── __init__.py ├── __main__.py ├── accelerator_group.py ├── accelerator_manager.py ├── associations.py ├── clipboard.py ├── common.py ├── config.py ├── emblems.py ├── gui │ ├── __init__.py │ ├── about_window.py │ ├── error_list.py │ ├── history_list.py │ ├── input_dialog.py │ ├── keyring_manager_window.py │ ├── main_window.py │ ├── mounts_manager_window.py │ ├── operation_dialog.py │ ├── preferences │ │ ├── __init__.py │ │ ├── accelerators.py │ │ ├── associations.py │ │ ├── bookmarks.py │ │ ├── commands.py │ │ ├── display.py │ │ ├── item_list.py │ │ ├── operation.py │ │ ├── plugins.py │ │ ├── terminal.py │ │ ├── toolbar.py │ │ └── view_and_edit.py │ ├── preferences_window.py │ ├── properties_window.py │ └── shortcuts_window.py ├── history.py ├── icons.py ├── indicator.py ├── keyring.py ├── menus.py ├── mounts.py ├── notifications.py ├── operation.py ├── parameters.py ├── plugin_base │ ├── __init__.py │ ├── column_editor_extension.py │ ├── column_extension.py │ ├── find_extension.py │ ├── item_list.py │ ├── monitor.py │ ├── mount_manager_extension.py │ ├── plugin.py │ ├── provider.py │ ├── rename_extension.py │ ├── terminal.py │ ├── toolbar_factory.py │ └── viewer_extension.py ├── plugins │ ├── __init__.py │ ├── archive_support │ │ ├── __init__.py │ │ ├── plugin.conf │ │ ├── plugin.py │ │ └── zip_provider.py │ ├── default_toolbar │ │ ├── __init__.py │ │ ├── bookmark_button.py │ │ ├── bookmarks_button.py │ │ ├── home_directory_button.py │ │ ├── parent_directory_button.py │ │ ├── plugin.conf │ │ ├── plugin.py │ │ └── separator.py │ ├── file_list │ │ ├── __init__.py │ │ ├── column_editor.py │ │ ├── dialogs.py │ │ ├── file_list.py │ │ ├── gio_extension.py │ │ ├── gio_provider.py │ │ ├── gio_wrapper.py │ │ ├── local_monitor.py │ │ ├── local_provider.py │ │ ├── plugin.conf │ │ ├── plugin.py │ │ └── trash_list.py │ ├── find_file_extensions │ │ ├── __init__.py │ │ ├── contents.py │ │ ├── default.py │ │ ├── plugin.conf │ │ ├── plugin.py │ │ └── size.py │ ├── gvim_viewer │ │ ├── __init__.py │ │ ├── plugin.conf │ │ └── plugin.py │ ├── owner_column │ │ ├── __init__.py │ │ ├── plugin.conf │ │ └── plugin.py │ ├── rename_extensions │ │ ├── __init__.py │ │ ├── audio_metadata.py │ │ ├── default.py │ │ ├── letter_case.py │ │ ├── plugin.conf │ │ └── plugin.py │ ├── sessions │ │ ├── __init__.py │ │ ├── plugin.conf │ │ └── plugin.py │ └── system_terminal │ │ ├── __init__.py │ │ ├── plugin.conf │ │ └── plugin.py ├── queue.py ├── toolbar.py ├── tools │ ├── __init__.py │ ├── advanced_rename.py │ ├── disk_usage.py │ ├── find_files.py │ ├── version_check.py │ └── viewer.py └── widgets │ ├── __init__.py │ ├── breadcrumbs.py │ ├── command_row.py │ ├── completion_entry.py │ ├── context_menu.py │ ├── emblems_renderer.py │ ├── location_menu.py │ ├── popup_menu.py │ ├── settings_page.py │ ├── status_bar.py │ ├── tab_label.py │ ├── thumbnail_view.py │ └── title_bar.py └── translations ├── be └── LC_MESSAGES │ └── sunflower.mo ├── bg └── LC_MESSAGES │ └── sunflower.mo ├── ca └── LC_MESSAGES │ └── sunflower.mo ├── cs └── LC_MESSAGES │ └── sunflower.mo ├── cs_CZ └── LC_MESSAGES │ └── sunflower.mo ├── de └── LC_MESSAGES │ └── sunflower.mo ├── de_DE └── LC_MESSAGES │ └── sunflower.mo ├── el └── LC_MESSAGES │ └── sunflower.mo ├── en_AU └── LC_MESSAGES │ └── sunflower.mo ├── es └── LC_MESSAGES │ └── sunflower.mo ├── es_AR └── LC_MESSAGES │ └── sunflower.mo ├── fr └── LC_MESSAGES │ └── sunflower.mo ├── hu └── LC_MESSAGES │ └── sunflower.mo ├── it_IT └── LC_MESSAGES │ └── sunflower.mo ├── ja_JP └── LC_MESSAGES │ └── sunflower.mo ├── lt └── LC_MESSAGES │ └── sunflower.mo ├── lv └── LC_MESSAGES │ └── sunflower.mo ├── nl └── LC_MESSAGES │ └── sunflower.mo ├── nl_BE └── LC_MESSAGES │ └── sunflower.mo ├── pl └── LC_MESSAGES │ └── sunflower.mo ├── pl_PL └── LC_MESSAGES │ └── sunflower.mo ├── pt_BR └── LC_MESSAGES │ └── sunflower.mo ├── ru └── LC_MESSAGES │ └── sunflower.mo ├── ru_RU └── LC_MESSAGES │ └── sunflower.mo ├── sk └── LC_MESSAGES │ └── sunflower.mo ├── sr └── LC_MESSAGES │ └── sunflower.mo ├── sv └── LC_MESSAGES │ └── sunflower.mo ├── tr └── LC_MESSAGES │ └── sunflower.mo ├── uk └── LC_MESSAGES │ └── sunflower.mo ├── uk_UA └── LC_MESSAGES │ └── sunflower.mo ├── zh_CN.GB2312 └── LC_MESSAGES │ └── sunflower.mo └── zh_TW └── LC_MESSAGES └── sunflower.mo /AUTHORS: -------------------------------------------------------------------------------- 1 | Programming: 2 | Mladen Mijatov 3 | Wojciech Kluczka 4 | Grigory Petrov 5 | Sebastian Gaul 6 | Arseniy Krasnov 7 | Sevka Fedoroff 8 | multiSnow 9 | Christian Mallwitz 10 | Joshas 11 | niknah 12 | Thomas Jollans 13 | Sviatoslav Abakumov 14 | Torsten Funck 15 | Michael Vetter 16 | Unknown 17 | 18 | Artists: 19 | Andrea Pavlović 20 | Michael Kerch 21 | 22 | Translations: 23 | Radek Tříška 24 | Jakub Dyszkiewicz <144.kuba@gmail.com> 25 | Wojciech Kluczka 26 | Vladimir Kolev 27 | Keringer László 28 | Sergey Malkin 29 | Sebastian Gaul 30 | Damián Nohales 31 | Андрій Кондратьєв 32 | Халіманенко Тарас 33 | Táncos Tamás 34 | Radek Otáhal 35 | Kevin Pellet 36 | Helene Clozel 37 | Sevka Fedoroff 38 | Jack Chen 39 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | python3 2 | python3-gi 3 | python3-chardet 4 | gir1.2-gtk-3.0 >= 3.22 5 | gir1.2-notify >= 0.7 6 | gir1.2-gdkpixbuf >= 2.0 7 | gir1.2-vte-2.91 >= 0.46 8 | gir1.2-glib-2.0 >= 1.50 9 | gir1.2-pango-1.0 >= 1.0 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Note: Preferred repository hosting is [GitLab](https://gitlab.com/MeanEYE/Sunflower). If you don't have an account there and don't wish to make one interacting with one on GitHub is fine. 2 | 3 | Sunflower 4 | ========= 5 | 6 | Sunflower is a small and highly customizable twin-panel file manager for Linux with support for plugins. It is intended to be an easy-to-use and powerful file manager that seamlessly integrates into the GNOME desktop environment (but not limited to). Fully compatible and native to Wayland compositors. 7 | 8 | ![Screen shot](https://i.imgur.com/s2FRrmH.png) 9 | 10 | ### Packages 11 | 12 | Sunflower package can be downloaded from following locations: 13 | 14 | * [Arch Linux AUR](https://aur.archlinux.org/packages/sunflower/) 15 | * [Ubuntu PPA](https://launchpad.net/~atareao/+archive/sunflower) (usually late by a version or two) 16 | * [Gentoo](http://packages.gentoo.org/package/x11-misc/sunflower) 17 | Installation: `emerge --ask x11-misc/sunflower` 18 | 19 | [Official packages can be downloaded from here](http://sunflower-fm.org/download) or releases page above. 20 | 21 | ### Plugins 22 | 23 | Check some of the plugins made by community: 24 | * [Image manipulation](https://github.com/ArseniyK/image_manipulation) 25 | * [Extract here menu option](https://github.com/ArseniyK/archiver) 26 | * [SQLite viewer](https://github.com/ArseniyK/sqlite_viewer) 27 | 28 | Plugins can be installed locally to: `~/.config/sunflower/user_plugins/` 29 | 30 | ### How to contribute 31 | You can be a part of this project in many ways. We suggest posting on our mailing list or visiting our IRC channel #sunflower on Libera chat and we'll try to help you get started. 32 | 33 | And as usual testers are more than welcome. If you wish to help translate program to your language please join translation team(s) on [Transifex](http://transifex.com/projects/p/sunflower/). 34 | 35 | ### Staying in touch 36 | Preferred way of getting in touch with us is through [mailing list](https://groups.google.com/forum/#!forum/sunflower-fm). 37 | 38 | I like talking to people a lot so please feel free to contact me any time. You can always follow me on my [twitter account](http://twitter.com/MeanEYE_rcf) and [Reddit](https://www.reddit.com/user/MeanEYE). We also have IRC channel, `#sunflower` on `libera.chat`. 39 | 40 | ### Screenshots 41 | Check out [screenshots](http://sunflower-fm.org/screenshots). 42 | 43 | Please note that screen shots and videos are months old, but they will help you know a little bit more what you are getting with this program. 44 | -------------------------------------------------------------------------------- /Sunflower.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -m sunflower 2 | [Desktop Entry] 3 | Type=Application 4 | Icon=sunflower 5 | Name=Sunflower 6 | StartupWMClass=Sunflower 7 | GenericName=File Manager 8 | GenericName[be]=Файлавы менеджар 9 | GenericName[bg]=Файлов мениджър 10 | GenericName[cs]=Správce souborů 11 | GenericName[da]=Filhåndtering 12 | GenericName[de]=Dateimanager 13 | GenericName[el]=Διαχειριστής αρχείων 14 | GenericName[en_GB]=File Manager 15 | GenericName[es]=Gestor de archivos 16 | GenericName[fa]=مدیر فایل 17 | GenericName[fi]=Tiedoston hallinta 18 | GenericName[fr]=Ouvrir dans le gestionnaire de fichiers 19 | GenericName[gl]=Xestor de ficheiros 20 | GenericName[he]=מנהל קבצים 21 | GenericName[hr]=Upravitelj datotekama 22 | GenericName[hu]=Fájlkezelő 23 | GenericName[id]=Menejemen Berkas 24 | GenericName[it]=File Manager 25 | GenericName[ja]=ファイルマネージャ 26 | GenericName[lg]=Gulawo Ekiteekateekafayiro 27 | GenericName[lt]=Failų tvarkyklė 28 | GenericName[lv]=Failu pārvaldnieks 29 | GenericName[nl]=Bestandbeheerder 30 | GenericName[pa]=ਫਾਇਲ ਮੈਨੇਜਰ 31 | GenericName[pl]=Menedżer plików 32 | GenericName[pt]=Gestor de ficheiros 33 | GenericName[pt_BR]=Gerenciador de arquivos 34 | GenericName[ru]=Файловый менеджер 35 | GenericName[sl]=Upravljalnik datotek 36 | GenericName[sr]=Управник датотека 37 | GenericName[sr@latin]=Upravnik datoteka 38 | GenericName[sv]=Filhanterare 39 | GenericName[te]=ఫైల్ నిర్వాహకం 40 | GenericName[tr]=Dosya Yöneticisi 41 | GenericName[tt_RU]=Файл-менеджер 42 | GenericName[uk]=Менеджер файлів 43 | GenericName[vi]=Bộ quản lý Tập tin 44 | GenericName[zh_CN]=文件管理器 45 | GenericName[zh_TW]=檔案管理程式 46 | Comment=Browse the file system and manage the files 47 | Comment[be]=Прагляд файлавай сістэмы і кіраванне файламі 48 | Comment[bg]=Разглеждане на файловата система и управляване на файловете 49 | Comment[cs]=Procházet systém souborů správcem souborů 50 | Comment[da]=Gennemse filsystemet og håndter filerne 51 | Comment[de]=Das Dateisystem durchsuchen und Dateien verwalten 52 | Comment[el]=Περιήγηση στο σύστημα αρχείων και διαχείριση αρχείων 53 | Comment[en_GB]=Browse the file system and manage the files 54 | Comment[es]=Explorar el sistema de archivos y gestionar los archivos 55 | Comment[fa]=مرور فایل سیستم و مدیریت فایل ها 56 | Comment[fi]=Selaa tiedostojärjestelmää ja hallitse tiedostoja 57 | Comment[fr]=Parcourir le système de fichiers et gérer les fichiers 58 | Comment[gl]=Navegar polo sistema de ficheiros e xestionar os ficheiros 59 | Comment[he]=עיון במערכת הקבצים וניהול הקבצים 60 | Comment[hu]=Fájlrendszer tallózása és fájlok kezelése 61 | Comment[it]=Sfoglia il file system e gestisci i file 62 | Comment[ja]=ファイルシステムをブラウズし、ファイルの管理を行います 63 | Comment[lg]=Lambula n'\''okuteekateeka fayiro eziri ku sisitemu yonna 64 | Comment[lt]=Tvarkykite failus ir aplankus 65 | Comment[lv]=Pārlūkot failu sistēmu un pārvaldīt failus 66 | Comment[nl]=Blader door het bestandssysteem en beheer de bestanden 67 | Comment[pa]=ਫਾਇਲ ਸਿਸਟਮ ਵੇਖੋ ਤੇ ਫਾਇਲਾਂ ਦਾ ਪਰਬੰਧ ਕਰੋ 68 | Comment[pl]=Umożliwia przeglądanie systemu plików i zarządza jego zawartością 69 | Comment[pt]=Navegar no sistema e gerir ficheiros 70 | Comment[pt_BR]=Navegue pelo sistema de arquivos e gerencie arquivos e pastas 71 | Comment[ru]=Просмотр файловой системы и управление файлами 72 | Comment[sl]=Brskajte po datotečnem sistemu in upravljajte datoteke 73 | Comment[sr]=Управљајте системом датотека 74 | Comment[sr@latin]=Upravljajte sistemom datoteka 75 | Comment[sv]=Utforska filsystemet och hantera filerna 76 | Comment[te]=ఫైల్ వ్యవస్థను అన్వేషించు మరియు ఫైళ్ళను నిర్వహించు 77 | Comment[tr]=Dosya sistemine göz at ve dosyaları yönet 78 | Comment[tt_RU]=Файл системасын карау һәм файллар белән идарә итү 79 | Comment[uk]=Показує файлову систему і керує файлами 80 | Comment[vi]=Xem hệ thống tập tin và quản lý dữ liệu 81 | Comment[zh_CN]=浏览文件系统和管理文件 82 | Comment[zh_TW]=瀏覽檔案系統及管理檔案 83 | Categories=FileTools;FileManager;Utility;Core;GTK; 84 | Exec=sunflower %U 85 | StartupNotify=true 86 | Terminal=false 87 | MimeType=inode/directory; 88 | -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/images/splash.png -------------------------------------------------------------------------------- /images/sunflower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/images/sunflower.png -------------------------------------------------------------------------------- /images/sunflower_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/images/sunflower_64.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | from pathlib import Path 5 | 6 | def molist(): 7 | trdir = "translations/" 8 | langs = [x.name for x in Path(trdir).iterdir() if x.is_dir()] 9 | molist = [ (f'share/locale/{l}/LC_MESSAGES/', [f"translations/{l}/LC_MESSAGES/sunflower.mo"]) for l in langs] 10 | return molist 11 | 12 | def get_version(): 13 | """Get software version from the main window.""" 14 | import gi 15 | gi.require_version('Gtk', '3.0') 16 | gi.require_version('Notify', '0.7') 17 | from sunflower.gui.main_window import MainWindow 18 | return '{major}.{minor}.{build}'.format(**MainWindow.version) 19 | 20 | 21 | setup ( 22 | name='Sunflower', 23 | version=get_version(), 24 | description='Twin-panel file manager.', 25 | author='Mladen Mijatov', 26 | author_email='meaneye.rcf@gmail.com', 27 | url='https://sunflower-fm.org', 28 | license='GPLv3', 29 | install_requires=[ 30 | 'PyGObject', 31 | 'chardet' 32 | ], 33 | packages=find_packages(), 34 | include_package_data=True, 35 | data_files=[ 36 | ('share/sunflower/images/', list(str(i) for i in Path('images/').rglob('*') if i.is_file())), 37 | ('share/sunflower/styles', ['styles/main.css']), 38 | ('share/applications', ['Sunflower.desktop']), 39 | *molist() 40 | ], 41 | entry_points={'console_scripts': ['sunflower = sunflower.__main__:main']} 42 | ) 43 | -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Tab title bar 3 | */ 4 | box.horizontal.sunflower-title-bar { 5 | border: 5px solid @theme_unfocused_bg_color; 6 | background-color: @theme_unfocused_bg_color; 7 | } 8 | 9 | /** 10 | * Breadcrumbs 11 | */ 12 | scrolledwindow.sunflower-breadcrumbs * { 13 | min-height: 0; 14 | } 15 | 16 | scrolledwindow.sunflower-breadcrumbs button { 17 | margin: 0; 18 | padding: 0.3em; 19 | 20 | border: 0; 21 | outline: 0; 22 | background: none; 23 | box-shadow: none; 24 | } 25 | 26 | scrolledwindow.sunflower-breadcrumbs button:checked { 27 | color: @theme_selected_bg_color; 28 | } 29 | 30 | /** 31 | * Item list 32 | */ 33 | treeview.sunflower-main-object:selected { 34 | outline: none; 35 | background-color: alpha(@theme_selected_bg_color, 0.3); 36 | color: @theme_fg_color; 37 | } 38 | 39 | treeview.sunflower-main-object:selected:focus { 40 | background-color: @theme_selected_bg_color; 41 | color: @theme_selected_fg_color; 42 | } 43 | -------------------------------------------------------------------------------- /sunflower/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/__init__.py -------------------------------------------------------------------------------- /sunflower/accelerator_manager.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Gdk 2 | 3 | 4 | class GroupType: 5 | MAIN_MENU = 0 6 | PLUGIN_BASE = 1 7 | ALL_GROUPS = 2 8 | 9 | 10 | class AcceleratorManager: 11 | """This manager handles saving and loading of accelerators""" 12 | 13 | def __init__(self, application): 14 | self._application = application 15 | self._config = None 16 | 17 | self._groups = [] 18 | self._group_names = [] 19 | 20 | self._scheduled_groups = None 21 | self._scheduled_owner = None 22 | 23 | def _save_accelerator(self, section, name, accelerator=None, primary=True, can_overwrite=False): 24 | """Save accelerator to config file""" 25 | section = self._config.section(section) 26 | 27 | if not primary: 28 | name = '{0}_2'.format(name) 29 | 30 | label = '' 31 | if accelerator is not None: 32 | label = Gtk.accelerator_name(accelerator[0], Gdk.ModifierType(accelerator[1])) 33 | 34 | # don't allow overwriting user's configuration unless strictly specified 35 | if not section.has(name) or (section.has(name) and can_overwrite): 36 | section.set(name, label) 37 | 38 | def _load_accelerator(self, section, name, primary=True): 39 | """Load accelerator from config file""" 40 | result = None 41 | 42 | if not primary: 43 | name = '{0}_2'.format(name) 44 | 45 | # try to load only if config has accelerator specified 46 | if self._config.has_section(section) \ 47 | and self._config.section(section).has(name): 48 | result = Gtk.accelerator_parse(self._config.section(section).get(name)) 49 | 50 | return result 51 | 52 | def _get_group_by_name(self, name): 53 | """Get accelerator group based on it's name""" 54 | result = None 55 | 56 | for group in self._groups: 57 | if group._name == name: 58 | result = group 59 | break 60 | 61 | return result 62 | 63 | def _get_group_by_type(self, group_type): 64 | """docstring for _get_group_by_type""" 65 | result = self._groups 66 | 67 | if group_type is GroupType.MAIN_MENU: 68 | result = [self._get_group_by_name('main_menu')] 69 | 70 | elif group_type is GroupType.PLUGIN_BASE: 71 | result = [self._get_group_by_name('plugin_base')] 72 | 73 | return result 74 | 75 | def check_collisions(self, keyval, modifier, group_type): 76 | """Check against collisions in specified groups matched by type""" 77 | result = [] 78 | groups = self._get_group_by_type(group_type) 79 | 80 | for group in groups: 81 | result.extend(group.get_collisions(keyval, modifier)) 82 | 83 | return result 84 | 85 | def register_group(self, group): 86 | """Register group with manager""" 87 | if not self._config.has_section(group._name): 88 | self._config.create_section(group._name) 89 | 90 | # add group name to the list 91 | if group._name not in self._group_names: 92 | self._group_names.append(group._name) 93 | 94 | # add group to internal list 95 | self._groups.append(group) 96 | 97 | # add all the methods to config file 98 | for name in group._methods: 99 | # save primary accelerator 100 | if name in group._primary: 101 | self._save_accelerator(group._name, name, group._primary[name]) 102 | 103 | else: 104 | self._save_accelerator(group._name, name) 105 | 106 | # save secondary accelerator 107 | if name in group._secondary: 108 | self._save_accelerator(group._name, name, group._secondary[name], primary=False) 109 | 110 | def get_groups(self): 111 | """Get list of unique group names""" 112 | return self._group_names 113 | 114 | def get_group_title(self, name): 115 | """Get title for specified group name""" 116 | result = '' 117 | 118 | # try to get group based on name 119 | group = self._get_group_by_name(name) 120 | 121 | if group is not None: 122 | result = group._title 123 | 124 | return result 125 | 126 | def get_methods(self, name): 127 | """Get list of methods for a specific group""" 128 | methods = [] 129 | 130 | # try to get group based on name 131 | group = self._get_group_by_name(name) 132 | 133 | if group is not None: 134 | methods = group._methods 135 | 136 | return methods 137 | 138 | def get_group_data(self, name): 139 | """Convenience method that returns title and methods in one pass""" 140 | title = '' 141 | methods = [] 142 | 143 | # try to get group based on name 144 | group = self._get_group_by_name(name) 145 | 146 | if group is not None: 147 | title = group._title 148 | methods = group._methods 149 | 150 | return title, methods 151 | 152 | def get_accelerator(self, group, name, primary=True): 153 | """Get saved accelerator""" 154 | accelerator = self._load_accelerator(group, name, primary) 155 | 156 | # no user defined accelerator, get default 157 | if accelerator is None: 158 | group = self._get_group_by_name(group) 159 | 160 | if group is not None: 161 | accelerator = group.get_accelerator(name, primary) 162 | 163 | return accelerator 164 | 165 | def schedule_groups_for_deactivation(self, groups, owner): 166 | """Set accelerator groups to be deactivated with second method call""" 167 | self._scheduled_owner = owner 168 | self._scheduled_groups = groups 169 | 170 | def deactivate_scheduled_groups(self, owner): 171 | """Deactivate scheduled accelerator groups""" 172 | result = False 173 | 174 | if self._scheduled_groups is not None\ 175 | and self._scheduled_owner is not owner: 176 | # deactivate groups 177 | for group in self._scheduled_groups: 178 | group.deactivate() 179 | 180 | # clear local list 181 | self._scheduled_groups = None 182 | 183 | # modify result 184 | result = True 185 | 186 | # in case there are no groups to deactivate, return true 187 | if self._scheduled_groups is None: 188 | result = True 189 | 190 | return result 191 | 192 | def load(self, config): 193 | """Load accelerator map""" 194 | self._config = config 195 | 196 | def save(self): 197 | """Save accelerator map""" 198 | pass 199 | -------------------------------------------------------------------------------- /sunflower/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | class Container: 6 | """Generic configuration container""" 7 | 8 | def __init__(self, data=None): 9 | self._values = {} 10 | 11 | if data is not None: 12 | self._values = data 13 | 14 | def _get_data(self): 15 | """Get data for storage""" 16 | return self._values.copy() 17 | 18 | def set(self, name, value): 19 | """Set configuration value""" 20 | self._values[name] = value 21 | 22 | def get(self, name): 23 | """Get configuration value""" 24 | return self._values[name] if name in self._values else None 25 | 26 | def has(self, name): 27 | """Check if options with specified name exists""" 28 | return name in self._values 29 | 30 | def remove(self, name): 31 | """Remove option from container""" 32 | assert name in self._values 33 | del self._values[name] 34 | 35 | def update(self, options): 36 | """Update missing options""" 37 | difference = {k: v for (k, v) in options.items() if k not in self._values} 38 | self._values.update(difference) 39 | 40 | 41 | class Config(Container): 42 | """This class provides easy way to create and edit configuration files 43 | located in project's configuration directory. 44 | 45 | It is recommended that this class is used for all purposes of storing 46 | data used by program itself and plugins! 47 | 48 | """ 49 | encoder_options = { 50 | 'skipkeys': True, 51 | 'check_circular': True, 52 | 'sort_keys': True, 53 | 'indent': 4 54 | } 55 | 56 | def __init__(self, name, config_path): 57 | Container.__init__(self) 58 | 59 | self._name = name 60 | self._sections = {} 61 | self._config_path = config_path 62 | 63 | self._encoder = json.JSONEncoder(**self.encoder_options) 64 | self._decoder = json.JSONDecoder() 65 | 66 | # try to load config file 67 | self.load() 68 | 69 | def save(self): 70 | """Save options to configuration file""" 71 | data = self._get_data() 72 | file_name = os.path.join( 73 | self._config_path, 74 | '{0}.json'.format(self._name) 75 | ) 76 | 77 | # merge sections with main values 78 | for name, section in self._sections.items(): 79 | data[name] = section._get_data() 80 | 81 | # save output to file 82 | with open(file_name, 'w') as raw_file: 83 | raw_file.write(self._encoder.encode(data)) 84 | 85 | def load(self): 86 | """Load options from configuration file""" 87 | file_name = os.path.join( 88 | self._config_path, 89 | '{0}.json'.format(self._name) 90 | ) 91 | 92 | if not os.path.exists(file_name): 93 | return 94 | 95 | try: 96 | # try loading config file 97 | with open(file_name) as raw_file: 98 | data = self._decoder.decode(raw_file.read()) 99 | 100 | except ValueError: 101 | # if error occurs, we'll just ignore it 102 | # empty config is not that scary 103 | pass 104 | 105 | else: 106 | # finish the loading 107 | for name, values in data.items(): 108 | if type(values) is dict: 109 | # section 110 | self._sections[name] = Container(values) 111 | 112 | else: 113 | # normal value 114 | self._values[name] = values 115 | 116 | def add_section(self, name, section): 117 | """Add new section to configuration""" 118 | self._sections[name] = section 119 | 120 | def create_section(self, name): 121 | """Create and return new section object""" 122 | if not name in self._sections: 123 | self._sections[name] = Container() 124 | 125 | return self._sections[name] 126 | 127 | def remove_section(self, name): 128 | """Remove section from config""" 129 | assert name in self._sections 130 | del self._sections[name] 131 | 132 | def get_sections(self): 133 | """Get list of all sections available""" 134 | return self._sections.keys() 135 | 136 | def section(self, name): 137 | """Retrieve specified section object""" 138 | assert name in self._sections 139 | return self._sections[name] 140 | 141 | def has_section(self, name): 142 | """Check for existence of section""" 143 | return name in self._sections 144 | -------------------------------------------------------------------------------- /sunflower/gui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sunflower/gui/about_window.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import sys 6 | import zipfile 7 | 8 | from sunflower import common 9 | from gi.repository import Gtk, Gdk, Pango, GLib, GdkPixbuf, Gio 10 | from collections import namedtuple 11 | 12 | 13 | Contributor = namedtuple( 14 | 'Contributor', 15 | [ 16 | 'name', # contributor's full name 17 | 'email', 18 | ]) 19 | 20 | 21 | class AboutWindow: 22 | # list of contributors 23 | contributors = [ 24 | Contributor(name='Wojciech Kluczka', email='wojtekkluczka@gmail.com'), 25 | Contributor(name='Grigory Petrov', email='grigory.v.p@gmail.com'), 26 | Contributor(name='Sebastian Gaul', email='sebastian@dev.mgvmedia.com'), 27 | Contributor(name='Arseniy Krasnov ', email='arseniy@krasnoff.org'), 28 | Contributor(name='Sevka Fedoroff', email='sevka.fedoroff@gmail.com'), 29 | Contributor(name='multiSnow', email='infinity.blick.winkel@gmail.com') 30 | ] 31 | 32 | # list of artists 33 | artists = [ 34 | Contributor(name='Andrea Pavlović', email='octogirl.design@gmail.com'), 35 | Contributor(name='Michael Kerch', email='michael@way2cu.com'), 36 | ] 37 | 38 | def __init__(self, parent): 39 | # create main window 40 | self._dialog = Gtk.AboutDialog.new() 41 | 42 | # prepare version template 43 | if parent.version['stage'] != 'f': 44 | version = '{0[major]}.{0[minor]}{0[stage]} ({0[build]})'.format(parent.version) 45 | else: 46 | version = '{0[major]}.{0[minor]} ({0[build]})'.format(parent.version) 47 | 48 | # set about dialog image 49 | image = Gtk.Image() 50 | path = os.path.join(common.get_static_assets_directory(), 'images', 'splash.png') 51 | 52 | if os.path.exists(path): 53 | image.set_from_file(path) 54 | 55 | elif os.path.isfile(sys.path[0]) and sys.path[0] != '': 56 | archive = zipfile.ZipFile(sys.path[0]) 57 | with archive.open('images/splash.png') as raw_file: 58 | buff = Gio.MemoryInputStream.new_from_bytes(GLib.Bytes.new(raw_file.read())) 59 | pixbuf = GdkPixbuf.Pixbuf.new_from_stream(buff, None) 60 | image.set_from_pixbuf(pixbuf) 61 | archive.close() 62 | 63 | # configure dialog 64 | self._dialog.set_modal(True) 65 | self._dialog.set_transient_for(parent) 66 | self._dialog.set_wmclass('Sunflower', 'Sunflower') 67 | 68 | # connect signals 69 | self._dialog.connect('activate-link', parent.goto_web) 70 | 71 | # set dialog data 72 | self._dialog.set_name(_('Sunflower')) 73 | self._dialog.set_program_name(_('Sunflower')) 74 | self._dialog.set_version(version) 75 | self._dialog.set_logo(image.get_pixbuf()) 76 | self._dialog.set_website('sunflower-fm.org') 77 | self._dialog.set_comments(_('Twin-panel file manager for Linux.')) 78 | self._dialog.set_translator_credits(_('translator-credits')) 79 | 80 | # set license 81 | self._dialog.set_copyright(_(u'Copyright \u00a9 2010-2022 by Mladen Mijatov and contributors.')) 82 | 83 | if os.path.isfile('COPYING'): 84 | license_file = open('COPYING', 'r') 85 | 86 | if license_file: 87 | license_text = license_file.read() 88 | license_file.close() 89 | 90 | self._dialog.set_license(license_text) 91 | 92 | else: 93 | self._dialog.set_license('http://www.gnu.org/licenses/old-licenses/gpl-3.0.html') 94 | 95 | # set authors 96 | self._dialog.set_authors(['Mladen Mijatov ']) 97 | self._dialog.add_credit_section(_('Contributors'), ['{0} <{1}>'.format( 98 | contributor.name, 99 | contributor.email, 100 | ) for contributor in self.contributors]) 101 | 102 | self._dialog.set_artists(['{0} <{1}>'.format( 103 | contributor.name, 104 | contributor.email, 105 | ) for contributor in self.artists]) 106 | 107 | def show(self): 108 | """Show dialog""" 109 | self._dialog.run() 110 | self._dialog.destroy() 111 | -------------------------------------------------------------------------------- /sunflower/gui/error_list.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk, Gdk 4 | 5 | 6 | class ErrorList: 7 | """Operation error list. 8 | 9 | Error list is displayed only when errors occur during operation in 10 | silent mode. 11 | 12 | """ 13 | 14 | def __init__(self, parent): 15 | # create main window 16 | self._window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) 17 | 18 | # store parameters locally, we'll need them later 19 | self._parent = parent 20 | self._error_list = [] 21 | 22 | # configure dialog 23 | self._window.set_title(_('Error list')) 24 | self._window.set_size_request(500, 400) 25 | self._window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 26 | self._window.set_resizable(True) 27 | self._window.set_skip_taskbar_hint(False) 28 | self._window.set_modal(False) 29 | self._window.set_transient_for(parent.get_window()) 30 | self._window.set_wmclass('Sunflower', 'Sunflower') 31 | self._window.set_border_width(7) 32 | 33 | self._window.connect('key-press-event', self._handle_key_press) 34 | 35 | # create user interface 36 | vbox = Gtk.VBox(False, 7) 37 | 38 | table = Gtk.Table(rows=4, columns=2, homogeneous=False) 39 | table.set_row_spacings(5) 40 | table.set_col_spacings(5) 41 | 42 | label_name = Gtk.Label(label=_('For:')) 43 | label_name.set_alignment(0, 0.5) 44 | 45 | self._entry_name = Gtk.Entry() 46 | self._entry_name.set_editable(False) 47 | 48 | label_source = Gtk.Label(label=_('Source:')) 49 | label_source.set_alignment(0, 0.5) 50 | 51 | self._entry_source = Gtk.Entry() 52 | self._entry_source.set_editable(False) 53 | 54 | label_destination = Gtk.Label(label=_('Destination:')) 55 | label_destination.set_alignment(0, 0.5) 56 | 57 | self._entry_destination = Gtk.Entry() 58 | self._entry_destination.set_editable(False) 59 | 60 | # create error list 61 | list_container = Gtk.ScrolledWindow() 62 | list_container.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 63 | list_container.set_shadow_type(Gtk.ShadowType.IN) 64 | 65 | self._store = Gtk.ListStore(str) 66 | self._list = Gtk.TreeView(model=self._store) 67 | self._list.set_headers_visible(False) 68 | 69 | cell_error = Gtk.CellRendererText() 70 | col_error = Gtk.TreeViewColumn(None, cell_error, text=0) 71 | 72 | self._list.append_column(col_error) 73 | 74 | # create controls 75 | hbox = Gtk.HBox(False, 5) 76 | 77 | button_close = Gtk.Button(stock=Gtk.STOCK_CLOSE) 78 | button_close.connect('clicked', self._close) 79 | 80 | # pack user interface 81 | list_container.add(self._list) 82 | 83 | table.attach(label_name, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.SHRINK) 84 | table.attach(self._entry_name, 1, 2, 0, 1, yoptions=Gtk.AttachOptions.SHRINK) 85 | table.attach(label_source, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.SHRINK) 86 | table.attach(self._entry_source, 1, 2, 1, 2, yoptions=Gtk.AttachOptions.SHRINK) 87 | table.attach(label_destination, 0, 1, 2, 3, xoptions=Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.SHRINK) 88 | table.attach(self._entry_destination, 1, 2, 2, 3, yoptions=Gtk.AttachOptions.SHRINK) 89 | table.attach(list_container, 0, 2, 3, 4, xoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL) 90 | 91 | hbox.pack_end(button_close, False, False, 0) 92 | 93 | vbox.pack_start(table, True, True, 0) 94 | vbox.pack_start(hbox, False, False, 0) 95 | 96 | self._window.add(vbox) 97 | 98 | # show all items 99 | self._window.show_all() 100 | 101 | def _close(self, widget=None, data=None): 102 | """Close error list window""" 103 | self._window.destroy() 104 | 105 | def _handle_key_press(self, widget, event, data=None): 106 | """Handle pressing keys""" 107 | if event.keyval == Gdk.KEY_Escape: 108 | self._close() 109 | 110 | def set_operation_name(self, operation_name): 111 | """Set operation name""" 112 | self._entry_name.set_text(operation_name) 113 | 114 | def set_source(self, source_path): 115 | """Set source path""" 116 | self._entry_source.set_text(source_path) 117 | 118 | def set_destination(self, destination_path): 119 | """Set destination path""" 120 | self._entry_destination.set_text(destination_path) 121 | 122 | def set_errors(self, error_list): 123 | """Populate error list""" 124 | for error in error_list: 125 | self._store.append((error,)) 126 | 127 | def show(self): 128 | """Show error list window""" 129 | self._window.show() 130 | -------------------------------------------------------------------------------- /sunflower/gui/history_list.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from gi.repository import Gtk, Gdk, GObject 6 | from sunflower.parameters import Parameters 7 | 8 | 9 | class Column: 10 | NAME = 0 11 | PATH = 1 12 | TIMESTAMP = 2 13 | 14 | 15 | class HistoryList(Gtk.Window): 16 | """History list is used to display complete browsing history.""" 17 | 18 | def __init__(self, parent, application): 19 | # create main window 20 | GObject.GObject.__init__(self) 21 | 22 | # store parameters locally, we'll need them later 23 | self._parent = parent 24 | self._application = application 25 | 26 | # configure dialog 27 | self.set_title(_('History')) 28 | self.set_size_request(500, 300) 29 | self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 30 | self.set_resizable(True) 31 | self.set_skip_taskbar_hint(True) 32 | self.set_modal(True) 33 | self.set_transient_for(application) 34 | self.set_wmclass('Sunflower', 'Sunflower') 35 | self.set_border_width(7) 36 | 37 | # create UI 38 | vbox = Gtk.VBox(False, 7) 39 | 40 | list_container = Gtk.ScrolledWindow() 41 | list_container.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 42 | list_container.set_shadow_type(Gtk.ShadowType.IN) 43 | 44 | self._history = Gtk.ListStore(str, str) 45 | 46 | cell_name = Gtk.CellRendererText() 47 | cell_path = Gtk.CellRendererText() 48 | 49 | col_name = Gtk.TreeViewColumn(_('Name'), cell_name, text=Column.NAME) 50 | col_path = Gtk.TreeViewColumn(_('Path'), cell_path, text=Column.PATH) 51 | 52 | self._history_list = Gtk.TreeView(self._history) 53 | self._history_list.connect('key-press-event', self._handle_key_press) 54 | self._history_list.append_column(col_name) 55 | self._history_list.append_column(col_path) 56 | 57 | # create controls 58 | hbox_controls = Gtk.HBox(False, 5) 59 | 60 | button_close = Gtk.Button(stock=Gtk.STOCK_CLOSE) 61 | button_close.connect('clicked', self._close) 62 | 63 | image_jump = Gtk.Image() 64 | image_jump.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON) 65 | button_jump = Gtk.Button() 66 | button_jump.set_image(image_jump) 67 | button_jump.set_label(_('Open')) 68 | button_jump.set_can_default(True) 69 | button_jump.connect('clicked', self._change_path, False) 70 | 71 | image_new_tab = Gtk.Image() 72 | image_new_tab.set_from_icon_name('tab-new', Gtk.IconSize.BUTTON) 73 | 74 | button_new_tab = Gtk.Button() 75 | button_new_tab.set_image(image_new_tab) 76 | button_new_tab.set_label(_('Open in tab')) 77 | button_new_tab.set_tooltip_text(_('Open selected path in new tab')) 78 | button_new_tab.connect('clicked', self._change_path, True) 79 | 80 | button_opposite = Gtk.Button(label=_('Open in opposite list')) 81 | button_opposite.set_tooltip_text(_('Open selected path in opposite list')) 82 | button_opposite.connect('clicked', self._open_in_opposite_list) 83 | 84 | # pack UI 85 | list_container.add(self._history_list) 86 | 87 | hbox_controls.pack_end(button_close, False, False, 0) 88 | hbox_controls.pack_end(button_jump, False, False, 0) 89 | hbox_controls.pack_end(button_new_tab, False, False, 0) 90 | hbox_controls.pack_end(button_opposite, False, False, 0) 91 | 92 | vbox.pack_start(list_container, True, True, 0) 93 | vbox.pack_start(hbox_controls, False, False, 0) 94 | 95 | self.add(vbox) 96 | 97 | # populate history list 98 | self._populate_list() 99 | 100 | # show all elements 101 | self.show_all() 102 | 103 | def _close(self, widget=None, data=None): 104 | """Handle clicking on close button""" 105 | self.destroy() 106 | 107 | def _change_path(self, widget=None, new_tab=False): 108 | """Change to selected path""" 109 | selection = self._history_list.get_selection() 110 | item_list, selected_iter = selection.get_selected() 111 | 112 | # if selection is valid, change to selected path 113 | if selected_iter is not None: 114 | path = item_list.get_value(selected_iter, Column.PATH) 115 | 116 | if not new_tab: 117 | # change path 118 | self._parent._handle_history_click(path=path) 119 | 120 | else: 121 | # create a new tab 122 | options = Parameters() 123 | options.set('path', path) 124 | 125 | self._application.create_tab( 126 | self._parent._notebook, 127 | self._parent.__class__, 128 | options 129 | ) 130 | 131 | # close dialog 132 | self._close() 133 | 134 | def _open_in_opposite_list(self, widget=None, data=None): 135 | """Open selected item in opposite list""" 136 | selection = self._history_list.get_selection() 137 | item_list, selected_iter = selection.get_selected() 138 | 139 | # if selection is valid, change to selected path 140 | if selected_iter is not None: 141 | path = item_list.get_value(selected_iter, Column.PATH) 142 | 143 | # open in opposite list 144 | opposite_object = self._application.get_opposite_object(self._application.get_active_object()) 145 | if hasattr(opposite_object, 'change_path'): 146 | opposite_object.change_path(path) 147 | 148 | # close dialog 149 | self._close() 150 | 151 | def _handle_key_press(self, widget, event, data=None): 152 | """Handle pressing keys in history list""" 153 | result = False 154 | 155 | if event.keyval == Gdk.KEY_Return: 156 | if event.get_state() & Gdk.ModifierType.CONTROL_MASK: 157 | # open path in new tab 158 | self._change_path(new_tab=True) 159 | 160 | else: 161 | # open path in existing tab 162 | self._change_path(new_tab=False) 163 | 164 | result = True 165 | 166 | elif event.keyval == Gdk.KEY_Escape: 167 | # close window on escape 168 | self._close() 169 | result = True 170 | 171 | return result 172 | 173 | def _populate_list(self): 174 | """Populate history list""" 175 | target_iter = None 176 | current_path = self._parent._options.get('path') 177 | 178 | # add all entries to the list 179 | for path in self._parent.history: 180 | name = os.path.basename(path) 181 | if name == '': 182 | name = path 183 | 184 | new_iter = self._history.append((name, path)) 185 | 186 | # assign row to be selected 187 | if target_iter is None or path == current_path: 188 | target_iter = new_iter 189 | 190 | # select row 191 | path = self._history.get_path(target_iter) 192 | self._history_list.set_cursor(path) 193 | -------------------------------------------------------------------------------- /sunflower/gui/preferences/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/gui/preferences/__init__.py -------------------------------------------------------------------------------- /sunflower/gui/preferences/associations.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Gdk 2 | from sunflower.gui.input_dialog import InputDialog, ApplicationInputDialog 3 | from sunflower.widgets.settings_page import SettingsPage 4 | 5 | 6 | class Column: 7 | NAME = 0 8 | COMMAND = 1 9 | 10 | 11 | class AssociationsOptions(SettingsPage): 12 | """Mime-type associations options extension class""" 13 | 14 | def __init__(self, parent, application): 15 | SettingsPage.__init__(self, parent, application, 'associations', _('Associations')) 16 | 17 | # create interface 18 | container = Gtk.ScrolledWindow() 19 | container.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) 20 | container.set_shadow_type(Gtk.ShadowType.IN) 21 | 22 | self._associations = Gtk.TreeStore(str, str) 23 | self._list = Gtk.TreeView(model=self._associations) 24 | self._list.set_rules_hint(True) 25 | self._list.set_headers_visible(False) 26 | 27 | cell_title = Gtk.CellRendererText() 28 | cell_command = Gtk.CellRendererText() 29 | 30 | col_title = Gtk.TreeViewColumn(None, cell_title, text=0) 31 | col_title.set_min_width(200) 32 | col_title.set_resizable(True) 33 | 34 | col_command = Gtk.TreeViewColumn(None, cell_command, text=1) 35 | col_command.set_resizable(True) 36 | col_command.set_expand(True) 37 | 38 | self._list.append_column(col_title) 39 | self._list.append_column(col_command) 40 | 41 | # create add menu 42 | self._add_menu = Gtk.Menu() 43 | 44 | item_add_mime_type = Gtk.MenuItem(label=_('Add mime type')) 45 | item_add_mime_type.connect('activate', self.__add_mime_type) 46 | 47 | item_add_application = Gtk.MenuItem(label=_('Add application to mime type')) 48 | item_add_application.connect('activate', self.__add_application) 49 | 50 | self._add_menu.append(item_add_mime_type) 51 | self._add_menu.append(item_add_application) 52 | 53 | self._add_menu.show_all() 54 | 55 | # create controls 56 | hbox_controls = Gtk.HBox(homogeneous=False, spacing=5) 57 | 58 | button_add = Gtk.Button(stock=Gtk.STOCK_ADD) 59 | button_add.connect('clicked', self.__button_add_clicked) 60 | 61 | # pack interface 62 | container.add(self._list) 63 | 64 | hbox_controls.pack_start(button_add, False, False, 0) 65 | 66 | self.pack_start(container, True, True, 0) 67 | self.pack_end(hbox_controls, False, False, 0) 68 | 69 | def __button_add_clicked(self, widget, data=None): 70 | """Handle clicking on add button""" 71 | self._add_menu.popup_at_widget(widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None) 72 | 73 | def __add_mime_type(self, widget, data=None): 74 | """Show dialog for adding mime type""" 75 | dialog = InputDialog(self._application) 76 | dialog.set_title(_('Add mime type')) 77 | dialog.set_label(_('Enter MIME type (eg. image/png):')) 78 | 79 | response = dialog.get_response() 80 | 81 | # add new mime type to the table 82 | if response[0] == Gtk.ResponseType.OK: 83 | mime_type = response[1] 84 | description = self._application.associations_manager.get_mime_description(mime_type) 85 | 86 | # add item to the store 87 | self._associations.append(None, (description, mime_type)) 88 | 89 | # enable save button on parent 90 | self._parent.enable_save() 91 | 92 | def __add_application(self, widget, data=None): 93 | """Show dialog for adding application to mime type""" 94 | selection = self._list.get_selection() 95 | item_list, selected_iter = selection.get_selected() 96 | 97 | if selected_iter is not None: 98 | level = item_list.iter_depth(selected_iter) 99 | 100 | if level == 0: 101 | parent = selected_iter 102 | 103 | else: 104 | parent = item_list.iter_parent(selected_iter) 105 | 106 | dialog = ApplicationInputDialog(self._application) 107 | response = dialog.get_response() 108 | 109 | # add new mime type to the table 110 | if response[0] == Gtk.ResponseType.OK: 111 | name = response[1] 112 | command = response[2] 113 | 114 | # add data to store 115 | self._associations.append(parent, (name, command)) 116 | 117 | # enable save button on parent 118 | self._parent.enable_save() 119 | 120 | else: 121 | # warn user about selection 122 | dialog = Gtk.MessageDialog( 123 | self._parent, 124 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 125 | Gtk.MessageType.INFO, 126 | Gtk.ButtonsType.OK, 127 | _( 128 | 'You need to select mime type to which application ' 129 | 'will be added. You can also select another application ' 130 | 'in which case new one will be added to its parent.' 131 | ) 132 | ) 133 | dialog.run() 134 | dialog.destroy() 135 | 136 | def _load_options(self): 137 | """Load options and update interface""" 138 | config = self._application.association_options 139 | manager = self._application.associations_manager 140 | 141 | # clear the storage 142 | self._associations.clear() 143 | 144 | # get all mime types from config file 145 | mime_types = config._get_data().keys() 146 | 147 | for mime_type in mime_types: 148 | # add mime type to the list 149 | description = manager.get_mime_description(mime_type) 150 | parent = self._associations.append(None, (description, mime_type)) 151 | 152 | # get all applications 153 | applications = config.get(mime_type) 154 | for application in applications: 155 | self._associations.append(parent, (application['name'], application['command'])) 156 | 157 | def _save_options(self): 158 | """Method called when save button is clicked""" 159 | config = self._application.association_options 160 | 161 | # iterate over mime type groups 162 | for row in self._associations: 163 | mime_type = self._associations.get_value(row.iter, Column.COMMAND) 164 | children = row.iterchildren() 165 | applications = [] 166 | 167 | # store accelerators for current group 168 | for child in children: 169 | application = { 170 | 'name': self._associations.get_value(child.iter, Column.NAME), 171 | 'command': self._associations.get_value(child.iter, Column.COMMAND) 172 | } 173 | 174 | applications.append(application) 175 | 176 | # add applications to config 177 | config.set(mime_type, applications) 178 | -------------------------------------------------------------------------------- /sunflower/gui/preferences/bookmarks.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | from sunflower.widgets.settings_page import SettingsPage 3 | 4 | 5 | class Column: 6 | NAME = 0 7 | URI = 1 8 | 9 | 10 | class BookmarksOptions(SettingsPage): 11 | """Bookmark options extension class""" 12 | 13 | def __init__(self, parent, application): 14 | SettingsPage.__init__(self, parent, application, 'bookmarks', _('Bookmarks')) 15 | 16 | # mounts checkbox 17 | self._checkbox_show_mount_points = Gtk.CheckButton(_('Show mount points in bookmarks menu')) 18 | self._checkbox_show_mount_points.connect('toggled', self._parent.enable_save) 19 | 20 | # system bookmarks checkbox 21 | self._checkbox_system_bookmarks = Gtk.CheckButton(_('Show system bookmarks')) 22 | self._checkbox_system_bookmarks.connect('toggled', self._parent.enable_save) 23 | 24 | # bookmarks checkbox 25 | self._checkbox_add_home = Gtk.CheckButton(_('Add home directory to bookmarks menu')) 26 | self._checkbox_add_home.connect('toggled', self._parent.enable_save) 27 | 28 | # create list box 29 | container = Gtk.ScrolledWindow() 30 | container.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) 31 | container.set_shadow_type(Gtk.ShadowType.IN) 32 | 33 | self._bookmarks = Gtk.ListStore(str, str) 34 | 35 | self._list = Gtk.TreeView() 36 | self._list.set_model(self._bookmarks) 37 | self._list.set_rules_hint(True) 38 | 39 | cell_title = Gtk.CellRendererText() 40 | cell_title.set_property('editable', True) 41 | cell_title.set_property('mode', Gtk.CellRendererMode.EDITABLE) 42 | cell_title.connect('edited', self._edited_bookmark, 0) 43 | 44 | cell_command = Gtk.CellRendererText() 45 | cell_command.set_property('editable', True) 46 | cell_command.set_property('mode', Gtk.CellRendererMode.EDITABLE) 47 | cell_command.connect('edited', self._edited_bookmark, 1) 48 | 49 | col_title = Gtk.TreeViewColumn(_('Title'), cell_title, text=Column.NAME) 50 | col_title.set_min_width(200) 51 | col_title.set_resizable(True) 52 | 53 | col_command = Gtk.TreeViewColumn(_('Location'), cell_command, text=Column.URI) 54 | col_command.set_resizable(True) 55 | col_command.set_expand(True) 56 | 57 | self._list.append_column(col_title) 58 | self._list.append_column(col_command) 59 | 60 | container.add(self._list) 61 | 62 | # create controls 63 | button_box = Gtk.HBox(False, 5) 64 | 65 | button_add = Gtk.Button(stock=Gtk.STOCK_ADD) 66 | button_add.connect('clicked', self._add_bookmark) 67 | 68 | button_delete = Gtk.Button(stock=Gtk.STOCK_DELETE) 69 | button_delete.connect('clicked', self._delete_bookmark) 70 | 71 | image_up = Gtk.Image() 72 | image_up.set_from_stock(Gtk.STOCK_GO_UP, Gtk.IconSize.BUTTON) 73 | 74 | button_move_up = Gtk.Button(label=None) 75 | button_move_up.add(image_up) 76 | button_move_up.set_tooltip_text(_('Move Up')) 77 | button_move_up.connect('clicked', self._move_bookmark, -1) 78 | 79 | image_down = Gtk.Image() 80 | image_down.set_from_stock(Gtk.STOCK_GO_DOWN, Gtk.IconSize.BUTTON) 81 | 82 | button_move_down = Gtk.Button(label=None) 83 | button_move_down.add(image_down) 84 | button_move_down.set_tooltip_text(_('Move Down')) 85 | button_move_down.connect('clicked', self._move_bookmark, 1) 86 | 87 | # pack ui 88 | button_box.pack_start(button_add, False, False, 0) 89 | button_box.pack_start(button_delete, False, False, 0) 90 | button_box.pack_end(button_move_down, False, False, 0) 91 | button_box.pack_end(button_move_up, False, False, 0) 92 | 93 | # pack checkboxes 94 | vbox = Gtk.VBox(False, 0) 95 | 96 | vbox.pack_start(self._checkbox_show_mount_points, False, False, 0) 97 | vbox.pack_start(self._checkbox_system_bookmarks, False, False, 0) 98 | vbox.pack_start(self._checkbox_add_home, False, False, 0) 99 | 100 | self.pack_start(vbox, False, False, 0) 101 | self.pack_start(container, True, True, 0) 102 | self.pack_start(button_box, False, False, 0) 103 | 104 | def _add_bookmark(self, widget, data=None): 105 | """Add new bookmark to the store""" 106 | if data is None: 107 | data = ('New bookmark', '') 108 | 109 | # add new data to the store 110 | self._bookmarks.append(data) 111 | 112 | # enable save button on parent 113 | self._parent.enable_save() 114 | 115 | def _edited_bookmark(self, cell, path, text, column): 116 | """Record edited text""" 117 | selected_iter = self._bookmarks.get_iter(path) 118 | self._bookmarks.set_value(selected_iter, column, text) 119 | 120 | # enable save button 121 | self._parent.enable_save() 122 | 123 | def _delete_bookmark(self, widget, data=None): 124 | """Remove selected field from store""" 125 | selection = self._list.get_selection() 126 | item_list, selected_iter = selection.get_selected() 127 | 128 | if selected_iter is not None: 129 | # remove item from the store 130 | item_list.remove(selected_iter) 131 | 132 | # enable save button if item was removed 133 | self._parent.enable_save() 134 | 135 | def _move_bookmark(self, widget, direction): 136 | """Move selected bookmark up or down""" 137 | selection = self._list.get_selection() 138 | item_list, selected_iter = selection.get_selected() 139 | 140 | if selected_iter is not None: 141 | # get iter index 142 | index = item_list.get_path(selected_iter)[0] 143 | 144 | # depending on direction, swap iters 145 | if (direction == -1 and index > 0) \ 146 | or (direction == 1 and index < len(item_list) - 1): 147 | item_list.swap(selected_iter, item_list[index + direction].iter) 148 | 149 | # enable save button if iters were swapped 150 | self._parent.enable_save() 151 | 152 | def _load_options(self): 153 | """Load options from file""" 154 | options = self._application.bookmark_options 155 | 156 | # get checkbox states 157 | self._checkbox_show_mount_points.set_active(options.get('show_mounts')) 158 | self._checkbox_add_home.set_active(options.get('add_home')) 159 | self._checkbox_system_bookmarks.set_active(options.get('system_bookmarks')) 160 | 161 | # load and parse bookmarks 162 | self._bookmarks.clear() 163 | 164 | bookmarks = options.get('bookmarks') 165 | for bookmark in bookmarks: 166 | self._bookmarks.append((bookmark['name'], bookmark['uri'])) 167 | 168 | def _save_options(self): 169 | """Save bookmarks to file""" 170 | options = self._application.bookmark_options 171 | 172 | # save checkbox state 173 | options.set('show_mounts', self._checkbox_show_mount_points.get_active()) 174 | options.set('add_home', self._checkbox_add_home.get_active()) 175 | options.set('system_bookmarks', self._checkbox_system_bookmarks.get_active()) 176 | 177 | # save bookmarks 178 | bookmarks = [] 179 | 180 | for data in self._bookmarks: 181 | bookmarks.append({ 182 | 'name': data[Column.NAME], 183 | 'uri': data[Column.URI] 184 | }) 185 | 186 | options.set('bookmarks', bookmarks) 187 | -------------------------------------------------------------------------------- /sunflower/gui/preferences/commands.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | from sunflower.widgets.settings_page import SettingsPage 3 | 4 | 5 | class Column: 6 | TITLE = 0 7 | COMMAND = 1 8 | 9 | 10 | class CommandsOptions(SettingsPage): 11 | """Commands options extension class""" 12 | 13 | def __init__(self, parent, application): 14 | SettingsPage.__init__(self, parent, application, 'commands', _('Commands')) 15 | 16 | # create list box 17 | container = Gtk.ScrolledWindow() 18 | container.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) 19 | container.set_shadow_type(Gtk.ShadowType.IN) 20 | 21 | self._commands = Gtk.ListStore(str, str) 22 | 23 | self._list = Gtk.TreeView() 24 | self._list.set_model(self._commands) 25 | self._list.set_rules_hint(True) 26 | 27 | # create and configure cell renderers 28 | cell_title = Gtk.CellRendererText() 29 | cell_title.set_property('editable', True) 30 | cell_title.set_property('mode', Gtk.CellRendererMode.EDITABLE) 31 | cell_title.connect('edited', self._edited_command, 0) 32 | 33 | cell_command = Gtk.CellRendererText() 34 | cell_command.set_property('editable', True) 35 | cell_command.set_property('mode', Gtk.CellRendererMode.EDITABLE) 36 | cell_command.connect('edited', self._edited_command, 1) 37 | 38 | # create and pack columns 39 | col_title = Gtk.TreeViewColumn(_('Title'), cell_title, text=Column.TITLE) 40 | col_title.set_min_width(200) 41 | col_title.set_resizable(True) 42 | 43 | col_command = Gtk.TreeViewColumn(_('Command'), cell_command, text=Column.COMMAND) 44 | col_command.set_resizable(True) 45 | col_command.set_expand(True) 46 | 47 | self._list.append_column(col_title) 48 | self._list.append_column(col_command) 49 | 50 | container.add(self._list) 51 | 52 | # create controls 53 | button_box = Gtk.HBox(False, 5) 54 | 55 | button_add = Gtk.Button(stock=Gtk.STOCK_ADD) 56 | button_add.connect('clicked', self._add_command) 57 | 58 | button_delete = Gtk.Button(stock=Gtk.STOCK_DELETE) 59 | button_delete.connect('clicked', self._delete_command) 60 | 61 | image_up = Gtk.Image() 62 | image_up.set_from_stock(Gtk.STOCK_GO_UP, Gtk.IconSize.BUTTON) 63 | 64 | button_move_up = Gtk.Button(label=None) 65 | button_move_up.add(image_up) 66 | button_move_up.set_tooltip_text(_('Move Up')) 67 | button_move_up.connect('clicked', self._move_command, -1) 68 | 69 | image_down = Gtk.Image() 70 | image_down.set_from_stock(Gtk.STOCK_GO_DOWN, Gtk.IconSize.BUTTON) 71 | 72 | button_move_down = Gtk.Button(label=None) 73 | button_move_down.add(image_down) 74 | button_move_down.set_tooltip_text(_('Move Down')) 75 | button_move_down.connect('clicked', self._move_command, 1) 76 | 77 | # pack ui 78 | button_box.pack_start(button_add, False, False, 0) 79 | button_box.pack_start(button_delete, False, False, 0) 80 | button_box.pack_end(button_move_down, False, False, 0) 81 | button_box.pack_end(button_move_up, False, False, 0) 82 | 83 | self.pack_start(container, True, True, 0) 84 | self.pack_start(button_box, False, False, 0) 85 | 86 | def _add_command(self, widget, data=None): 87 | """Add new command to the store""" 88 | if data is None: 89 | data = ('New command', '') 90 | 91 | # add new item to store 92 | self._commands.append(data) 93 | 94 | # enable save button on parent 95 | self._parent.enable_save() 96 | 97 | def _edited_command(self, cell, path, text, column): 98 | """Record edited text""" 99 | selected_iter = self._commands.get_iter(path) 100 | self._commands.set_value(selected_iter, column, text) 101 | 102 | # enable save button 103 | self._parent.enable_save() 104 | 105 | def _delete_command(self, widget, data=None): 106 | """Remove selected field from store""" 107 | selection = self._list.get_selection() 108 | item_list, selected_iter = selection.get_selected() 109 | 110 | if selected_iter is not None: 111 | # remove item from the list 112 | item_list.remove(selected_iter) 113 | 114 | # enable save button in case item was removed 115 | self._parent.enable_save() 116 | 117 | def _move_command(self, widget, direction): 118 | """Move selected command""" 119 | selection = self._list.get_selection() 120 | item_list, selected_iter = selection.get_selected() 121 | 122 | if selected_iter is not None: 123 | # get iter index 124 | index = item_list.get_path(selected_iter)[0] 125 | 126 | # swap iters depending on specified direction 127 | if (direction == -1 and index > 0) \ 128 | or (direction == 1 and index < len(item_list) - 1): 129 | item_list.swap(selected_iter, item_list[index + direction].iter) 130 | 131 | # it items were swapped, enable save button 132 | self._parent.enable_save() 133 | 134 | def _load_options(self): 135 | """Load options from file""" 136 | options = self._application.command_options 137 | 138 | # load and parse commands 139 | self._commands.clear() 140 | 141 | command_list = options.get('commands') 142 | 143 | for command in command_list: 144 | self._commands.append(( 145 | command['title'], 146 | command['command'] 147 | )) 148 | 149 | def _save_options(self): 150 | """Save commands to file""" 151 | options = self._application.command_options 152 | commands = [] 153 | 154 | # save commands 155 | for data in self._commands: 156 | commands.append({ 157 | 'title': data[Column.TITLE], 158 | 'command': data[Column.COMMAND] 159 | }) 160 | 161 | options.set('commands', commands) 162 | -------------------------------------------------------------------------------- /sunflower/gui/preferences/operation.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | from sunflower.widgets.settings_page import SettingsPage 3 | 4 | 5 | class OperationOptions(SettingsPage): 6 | """Operation options extension class""" 7 | 8 | def __init__(self, parent, application): 9 | SettingsPage.__init__(self, parent, application, 'operation', _('Operation')) 10 | 11 | # create frames 12 | vbox_general = Gtk.VBox(False, 0) 13 | self._create_section(_('General'), vbox_general) 14 | 15 | vbox_mounts = Gtk.VBox(False, 0) 16 | self._create_section(_('Mounts'), vbox_mounts) 17 | 18 | vbox_confirmations = Gtk.VBox(False, 0) 19 | self._create_section(_('Confirmation'), vbox_confirmations) 20 | 21 | # create components 22 | self._checkbox_trash_files = Gtk.CheckButton(_('Delete items to trashcan')) 23 | self._checkbox_reserve_size = Gtk.CheckButton(_('Reserve free space on copy/move')) 24 | self._checkbox_automount_on_start = Gtk.CheckButton(_('Automount drives on start up')) 25 | self._checkbox_automount_on_insert = Gtk.CheckButton(_('Automount removable drives when inserted')) 26 | self._checkbox_confirm_delete = Gtk.CheckButton(_('Show confirmation dialog before deleting items')) 27 | 28 | self._checkbox_trash_files.connect('toggled', self._parent.enable_save) 29 | self._checkbox_reserve_size.connect('toggled', self._parent.enable_save) 30 | self._checkbox_automount_on_start.connect('toggled', self._parent.enable_save) 31 | self._checkbox_automount_on_insert.connect('toggled', self._parent.enable_save) 32 | self._checkbox_confirm_delete.connect('toggled', self._confirm_delete_toggle) 33 | 34 | # pack user interface 35 | vbox_general.pack_start(self._checkbox_trash_files, False, False, 0) 36 | vbox_general.pack_start(self._checkbox_reserve_size, False, False, 0) 37 | 38 | vbox_mounts.pack_start(self._checkbox_automount_on_start, False, False, 0) 39 | vbox_mounts.pack_start(self._checkbox_automount_on_insert, False, False, 0) 40 | 41 | vbox_confirmations.pack_start(self._checkbox_confirm_delete, False, False, 0) 42 | 43 | def _confirm_delete_toggle(self, widget, data=None): 44 | """Make sure user really wants to disable confirmation dialog""" 45 | if not widget.get_active() and not self._checkbox_trash_files.get_active(): 46 | dialog = Gtk.MessageDialog( 47 | self._parent, 48 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 49 | Gtk.MessageType.QUESTION, 50 | Gtk.ButtonsType.YES_NO, 51 | _( 52 | 'With trashing disabled you will not be able to ' 53 | 'restore accidentally deleted items. Are you sure ' 54 | 'you want to disable confirmation dialog when ' 55 | 'deleting items?' 56 | ) 57 | ) 58 | dialog.set_default_response(Gtk.ResponseType.YES) 59 | result = dialog.run() 60 | dialog.destroy() 61 | 62 | if result == Gtk.ResponseType.NO: 63 | # user changed his mind, restore original value 64 | widget.handler_block_by_func(self._confirm_delete_toggle) 65 | widget.set_active(True) 66 | widget.handler_unblock_by_func(self._confirm_delete_toggle) 67 | 68 | else: 69 | # user really wants to disable this option 70 | self._parent.enable_save(widget, data) 71 | 72 | else: 73 | # normal operation, just notify parent 74 | self._parent.enable_save(widget, data) 75 | 76 | 77 | def _load_options(self): 78 | """Load item list options""" 79 | options = self._application.options 80 | operations = options.section('operations') 81 | confirmations = options.section('confirmations') 82 | 83 | # load options 84 | self._checkbox_trash_files.set_active(operations.get('trash_files')) 85 | self._checkbox_reserve_size.set_active(operations.get('reserve_size')) 86 | self._checkbox_automount_on_start.set_active(operations.get('automount_start')) 87 | self._checkbox_automount_on_insert.set_active(operations.get('automount_insert')) 88 | self._checkbox_confirm_delete.set_active(confirmations.get('delete_items')) 89 | 90 | def _save_options(self): 91 | """Save item list options""" 92 | options = self._application.options 93 | operations = options.section('operations') 94 | confirmations = options.section('confirmations') 95 | 96 | # save settings 97 | operations.set('trash_files', self._checkbox_trash_files.get_active()) 98 | operations.set('reserve_size', self._checkbox_reserve_size.get_active()) 99 | operations.set('automount_start', self._checkbox_automount_on_start.get_active()) 100 | operations.set('automount_insert', self._checkbox_automount_on_insert.get_active()) 101 | confirmations.set('delete_items', self._checkbox_confirm_delete.get_active()) 102 | -------------------------------------------------------------------------------- /sunflower/gui/preferences/view_and_edit.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | from sunflower.widgets.settings_page import SettingsPage 3 | 4 | 5 | class Column: 6 | ICON = 0 7 | NAME = 1 8 | COMMAND = 2 9 | 10 | 11 | class ViewEditOptions(SettingsPage): 12 | """View & Edit options extension class""" 13 | 14 | def __init__(self, parent, application): 15 | SettingsPage.__init__(self, parent, application, 'view_and_edit', _('View & Edit')) 16 | 17 | # viewer options 18 | vbox_view = Gtk.VBox(False, 0) 19 | self._create_section(_('View'), vbox_view) 20 | 21 | self._checkbox_view_word_wrap = Gtk.CheckButton(_('Wrap long lines')) 22 | self._checkbox_view_word_wrap.connect('toggled', self._parent.enable_save) 23 | 24 | # editor options 25 | vbox_edit = Gtk.VBox(False, 0) 26 | self._create_section(_('Edit'), vbox_edit) 27 | 28 | # installed application 29 | self._radio_application = Gtk.RadioButton(label=_('Use installed application')) 30 | self._radio_application.connect('toggled', self._parent.enable_save) 31 | 32 | align_application = Gtk.Alignment.new(0, 0, 1, 0) 33 | align_application.set_padding(0, 10, 15, 15) 34 | vbox_application = Gtk.VBox(False, 0) 35 | vbox_application.set_border_width(5) 36 | 37 | self._store = Gtk.ListStore(str, str, str) 38 | self._combobox_application = Gtk.ComboBox(model=self._store) 39 | self._combobox_application.connect('changed', self._parent.enable_save) 40 | 41 | cell_icon = Gtk.CellRendererPixbuf() 42 | cell_name = Gtk.CellRendererText() 43 | 44 | self._combobox_application.pack_start(cell_icon, False) 45 | self._combobox_application.pack_start(cell_name, True) 46 | 47 | self._combobox_application.add_attribute(cell_icon, 'icon-name', Column.ICON) 48 | self._combobox_application.add_attribute(cell_name, 'text', Column.NAME) 49 | 50 | # external options 51 | self._radio_external = Gtk.RadioButton(group=self._radio_application, label=_('Use external command')) 52 | self._radio_external.connect('toggled', self._parent.enable_save) 53 | 54 | align_external = Gtk.Alignment.new(0, 0, 1, 0) 55 | align_external.set_padding(0, 10, 15, 15) 56 | vbox_external = Gtk.VBox(False, 0) 57 | vbox_external.set_border_width(5) 58 | 59 | label_editor = Gtk.Label(label=_('Command line:')) 60 | label_editor.set_alignment(0, 0.5) 61 | label_editor.set_use_markup(True) 62 | self._entry_editor = Gtk.Entry() 63 | self._entry_editor.connect('changed', self._parent.enable_save) 64 | 65 | self._checkbox_terminal_command = Gtk.CheckButton(_('Execute command in terminal tab')) 66 | self._checkbox_terminal_command.connect('toggled', self._parent.enable_save) 67 | 68 | # pack ui 69 | vbox_view.pack_start(self._checkbox_view_word_wrap, False, False, 0) 70 | 71 | vbox_application.pack_start(self._combobox_application, False, False, 0) 72 | align_application.add(vbox_application) 73 | 74 | vbox_external.pack_start(label_editor, False, False, 0) 75 | vbox_external.pack_start(self._entry_editor, False, False, 0) 76 | vbox_external.pack_start(self._checkbox_terminal_command, False, False, 0) 77 | align_external.add(vbox_external) 78 | 79 | vbox_edit.pack_start(self._radio_application, False, False, 0) 80 | vbox_edit.pack_start(align_application, False, False, 0) 81 | vbox_edit.pack_start(self._radio_external, False, False, 0) 82 | vbox_edit.pack_start(align_external, False, False, 0) 83 | 84 | def _populate_list(self, selected_application): 85 | """Populate list of applications available for editing""" 86 | self._store.clear() 87 | 88 | selected_index = None 89 | application_list = self._application.associations_manager.get_application_list_for_type('text/plain') 90 | 91 | for application in application_list: 92 | # if names match store index for later use 93 | if application.name == selected_application: 94 | selected_index = len(self._store) 95 | 96 | # add application to the list 97 | self._store.append(( 98 | application.icon, 99 | application.name, 100 | application.command_line 101 | )) 102 | 103 | # make selected application active 104 | if selected_index is not None: 105 | self._combobox_application.set_active(selected_index) 106 | 107 | def _load_options(self): 108 | """Load options""" 109 | view_options = self._application.options.section('viewer') 110 | edit_options = self._application.options.section('editor') 111 | 112 | # populate application list 113 | self._populate_list(edit_options.get('application')) 114 | 115 | # select proper radio button 116 | if edit_options.get('type') == 0: 117 | self._radio_application.set_active(True) 118 | 119 | else: 120 | self._radio_external.set_active(True) 121 | 122 | # configure user interface 123 | editor_command = edit_options.get('external_command') 124 | if editor_command is not None: 125 | self._entry_editor.set_text(editor_command) 126 | 127 | self._checkbox_terminal_command.set_active(edit_options.get('terminal_command')) 128 | self._checkbox_view_word_wrap.set_active(view_options.get('word_wrap')) 129 | 130 | def _save_options(self): 131 | """Save options""" 132 | view_options = self._application.options.section('viewer') 133 | edit_options = self._application.options.section('editor') 134 | 135 | # get external command 136 | external_command = self._entry_editor.get_text() 137 | 138 | # get selected application 139 | selected_index = self._combobox_application.get_active() 140 | application_name = None 141 | application_command = None 142 | 143 | if selected_index > -1: 144 | row = self._store[selected_index] 145 | application_name = row[Column.NAME] 146 | application_command = row[Column.COMMAND] 147 | 148 | # get command based 149 | editor_type = 0 if self._radio_application.get_active() else 1 150 | command = application_command if editor_type == 0 else external_command 151 | 152 | # store options to config 153 | edit_options.set('type', editor_type) 154 | edit_options.set('default_editor', command) 155 | edit_options.set('application', application_name) 156 | edit_options.set('external_command', external_command) 157 | edit_options.set('terminal_command', self._checkbox_terminal_command.get_active()) 158 | view_options.set('word_wrap', self._checkbox_view_word_wrap.get_active()) 159 | -------------------------------------------------------------------------------- /sunflower/gui/preferences_window.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk, Gdk, GObject 4 | from sunflower.gui.preferences.display import DisplayOptions 5 | from sunflower.gui.preferences.operation import OperationOptions 6 | from sunflower.gui.preferences.item_list import ItemListOptions 7 | from sunflower.gui.preferences.terminal import TerminalOptions 8 | from sunflower.gui.preferences.view_and_edit import ViewEditOptions 9 | from sunflower.gui.preferences.toolbar import ToolbarOptions 10 | from sunflower.gui.preferences.bookmarks import BookmarksOptions 11 | from sunflower.gui.preferences.commands import CommandsOptions 12 | from sunflower.gui.preferences.plugins import PluginsOptions 13 | from sunflower.gui.preferences.accelerators import AcceleratorOptions 14 | from sunflower.gui.preferences.associations import AssociationsOptions 15 | 16 | 17 | class Column: 18 | NAME = 0 19 | WIDGET = 1 20 | 21 | 22 | class PreferencesWindow(Gtk.Window): 23 | """Container class for options editors""" 24 | 25 | def __init__(self, parent): 26 | GObject.GObject.__init__(self, type=Gtk.WindowType.TOPLEVEL) 27 | 28 | self._parent = parent 29 | 30 | # configure window 31 | self.set_title(_('Preferences')) 32 | self.set_default_size(750, 500) 33 | self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 34 | self.set_modal(True) 35 | self.set_skip_taskbar_hint(True) 36 | self.set_transient_for(parent) 37 | self.set_wmclass('Sunflower', 'Sunflower') 38 | 39 | self.connect('delete_event', self._hide) 40 | self.connect('key-press-event', self._handle_key_press) 41 | 42 | # create user interface 43 | header_bar = Gtk.HeaderBar.new() 44 | header_bar.set_show_close_button(True) 45 | header_bar.set_title(_('Preferences')) 46 | self.set_titlebar(header_bar) 47 | 48 | hbox = Gtk.HBox.new(False, 0) 49 | 50 | # create tab stack and switcher 51 | self._tabs = Gtk.Stack.new() 52 | 53 | self._labels = Gtk.StackSidebar.new() 54 | self._labels.set_stack(self._tabs) 55 | self._labels.set_size_request(150, -1) 56 | 57 | DisplayOptions(self, parent) 58 | OperationOptions(self, parent) 59 | ItemListOptions(self, parent) 60 | TerminalOptions(self, parent) 61 | ViewEditOptions(self, parent) 62 | ToolbarOptions(self, parent) 63 | BookmarksOptions(self, parent) 64 | CommandsOptions(self, parent) 65 | PluginsOptions(self, parent) 66 | AcceleratorOptions(self, parent) 67 | AssociationsOptions(self, parent) 68 | 69 | # create buttons 70 | self._button_save = Gtk.Button.new_with_label(_('Save')) 71 | self._button_save.connect('clicked', self._save_options) 72 | self._button_save.get_style_context().add_class('suggested-action') 73 | 74 | self._button_revert = Gtk.Button.new_with_label(_('Revert')) 75 | self._button_revert.connect('clicked', self._load_options) 76 | 77 | # restart label 78 | self._label_restart = Gtk.Label(label='{0}'.format(_('Program restart required!'))) 79 | self._label_restart.set_alignment(0.5, 0.5) 80 | self._label_restart.set_use_markup(True) 81 | self._label_restart.set_property('no-show-all', True) 82 | 83 | # pack buttons 84 | hbox.pack_start(self._labels, False, False, 0) 85 | hbox.pack_start(self._tabs, True, True, 0) 86 | 87 | header_bar.pack_start(self._label_restart) 88 | header_bar.pack_end(self._button_save) 89 | header_bar.pack_end(self._button_revert) 90 | 91 | self.add(hbox) 92 | 93 | def show(self, widget, tab_name=None): 94 | """Show dialog, focusing requested page, and reload options.""" 95 | self._load_options() 96 | self.show_all() 97 | if tab_name: 98 | self._tabs.set_visible_child_name(tab_name) 99 | return True 100 | 101 | def _hide(self, widget=None, data=None): 102 | """Hide dialog""" 103 | should_close = True 104 | 105 | if self._button_save.get_sensitive(): 106 | dialog = Gtk.MessageDialog( 107 | self, 108 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 109 | Gtk.MessageType.QUESTION, 110 | Gtk.ButtonsType.NONE, 111 | _("There are unsaved changes.\nDo you want to save them?") 112 | ) 113 | dialog.add_buttons( 114 | Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 115 | Gtk.STOCK_NO, Gtk.ResponseType.NO, 116 | Gtk.STOCK_YES, Gtk.ResponseType.YES, 117 | ) 118 | dialog.set_default_response(Gtk.ResponseType.YES) 119 | result = dialog.run() 120 | dialog.destroy() 121 | 122 | if result == Gtk.ResponseType.YES: 123 | self._save_options() 124 | 125 | elif result == Gtk.ResponseType.CANCEL: 126 | should_close = False 127 | 128 | if should_close: 129 | self.hide() 130 | 131 | return True # avoid destroying components 132 | 133 | def _load_options(self, widget=None, data=None): 134 | """Change interface to present current state of configuration""" 135 | # call all tabs to load their options 136 | pages = filter(lambda page: hasattr(page, '_load_options'), self._tabs.get_children()) 137 | list(map(lambda page: page._load_options(), pages)) 138 | 139 | # disable save button and hide label 140 | self._button_save.set_sensitive(False) 141 | self._button_revert.set_sensitive(False) 142 | self._label_restart.hide() 143 | 144 | def _save_options(self, widget=None, data=None): 145 | """Save options""" 146 | # call all tabs to save their options 147 | pages = filter(lambda page: hasattr(page, '_save_options'), self._tabs.get_children()) 148 | list(map(lambda page: page._save_options(), pages)) 149 | 150 | # disable save button 151 | self._button_save.set_sensitive(False) 152 | self._button_revert.set_sensitive(False) 153 | 154 | # call main window to propagate new settings 155 | self._parent.apply_settings() 156 | 157 | # write changes to configuration file 158 | self._parent.save_config() 159 | 160 | def _handle_key_press(self, widget, event, data=None): 161 | """Handle pressing keys""" 162 | if event.keyval == Gdk.KEY_Escape: 163 | self._hide() 164 | 165 | def enable_save(self, widget=None, show_restart=None): 166 | """Enable save button""" 167 | self._button_save.set_sensitive(True) 168 | self._button_revert.set_sensitive(True) 169 | 170 | # show label with message 171 | if show_restart is not None and show_restart: 172 | self._label_restart.show() 173 | 174 | def add_tab(self, name, label, tab): 175 | """Add new tab to preferences window 176 | 177 | If you are using SettingsPage class there's no need to call this 178 | method manually, class constructor will do it automatically for you! 179 | 180 | """ 181 | self._tabs.add_titled(tab, name, label) 182 | -------------------------------------------------------------------------------- /sunflower/gui/shortcuts_window.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Gdk 2 | 3 | class ShortcutsWindow(): 4 | """Shortcuts display""" 5 | 6 | def __init__(self, parent): 7 | self._parent = parent 8 | 9 | def _show(self, widget, data=None): 10 | self._window = Gtk.ShortcutsWindow() 11 | self._window.set_title(_('Keyboard shortcuts')) 12 | self._window.set_default_size(750, 500) 13 | self._window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 14 | self._window.set_modal(True) 15 | self._window.set_skip_taskbar_hint(True) 16 | self._window.set_transient_for(self._parent) 17 | 18 | manager = self._parent.accelerator_manager 19 | bookmarks = self._parent.bookmark_options.get('bookmarks') 20 | groups = manager.get_groups() 21 | groups.sort() 22 | 23 | # create rename list 24 | replace_list = {} 25 | 26 | key_name = '{0}.bookmark_home'.format('item_list') 27 | replace_list[key_name] = _('Home directory') 28 | 29 | # add bookmarks to the replace list 30 | for number in range(1, 11): 31 | key_name = '{0}.{1}_{2}'.format('item_list', 'bookmark', number) 32 | 33 | if number < len(bookmarks): 34 | # bookmark exists 35 | bookmark_value = bookmarks[number-1]['name'] 36 | 37 | else: 38 | # bookmark doesn't exist, add generic name 39 | bookmark_value = 'Bookmark #{0}'.format(number) 40 | 41 | replace_list[key_name] = bookmark_value 42 | 43 | 44 | for group_name in groups: 45 | title, methods = manager.get_group_data(group_name) 46 | 47 | method_names = sorted(methods) # iterates over dict keys 48 | 49 | section = Gtk.ShortcutsSection(title=title, section_name=group_name) 50 | section.show() 51 | 52 | group = Gtk.ShortcutsGroup() 53 | group.show() 54 | 55 | i = 0 56 | 57 | for method_name in method_names: 58 | # add all methods from the group 59 | title = methods[method_name]['title'].replace('_', '') 60 | 61 | # check if specified method name has a rename value 62 | key_name = '{0}.{1}'.format(group_name, method_name) 63 | if key_name in replace_list: 64 | title = title.format(replace_list[key_name]) 65 | 66 | # get accelerators 67 | primary = manager.get_accelerator(group_name, method_name, True) 68 | secondary = manager.get_accelerator(group_name, method_name, False) 69 | 70 | # make sure we have something to display 71 | if primary is None: 72 | primary = (0, 0) 73 | 74 | if secondary is None: 75 | secondary = (0, 0) 76 | 77 | accelerator_name = Gtk.accelerator_name(primary[0], Gdk.ModifierType(primary[1])) 78 | 79 | if (accelerator_name == ''): 80 | continue 81 | 82 | short = Gtk.ShortcutsShortcut(title=title, accelerator=accelerator_name) 83 | short.show() 84 | group.add(short) 85 | 86 | i += 1 87 | 88 | # split shortcuts into groups to fit them on screen 89 | if (i % 10 == 0): 90 | section.add(group) 91 | group = Gtk.ShortcutsGroup() 92 | group.show() 93 | 94 | section.add(group) 95 | 96 | self._window.add(section) 97 | 98 | self._window.show_all() 99 | -------------------------------------------------------------------------------- /sunflower/history.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class HistoryManager: 5 | """Class used for management and maintenance of item list history.""" 6 | 7 | def __init__(self, parent, storage_list): 8 | self._list = storage_list 9 | self._index = 0 10 | self._parent = parent 11 | 12 | def _change_to_index(self, index): 13 | """Change parent path to specified index""" 14 | new_path = self._list[index] 15 | current_path = self._parent._options.get('path') 16 | 17 | # determine if row needs to be selected 18 | selection = None 19 | if current_path.startswith(new_path): 20 | selection = os.path.basename(current_path) 21 | 22 | # change path 23 | self._parent.change_path(new_path, selection) 24 | 25 | def record(self, path): 26 | """Record new path in history""" 27 | if not path in self._list: 28 | self._list.insert(self._index, path) 29 | 30 | else: 31 | self._index = self._list.index(path) 32 | 33 | def back(self): 34 | """Go back in history one step""" 35 | if self._index >= len(self._list) - 1: 36 | return 37 | 38 | # change path 39 | self._change_to_index(self._index + 1) 40 | 41 | def forward(self): 42 | """Go forward in history one step""" 43 | if self._index < 1: 44 | return 45 | 46 | # change path 47 | self._change_to_index(self._index - 1) 48 | -------------------------------------------------------------------------------- /sunflower/icons.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from builtins import filter 3 | 4 | import os 5 | import sys 6 | import zipfile 7 | 8 | from gi.repository import Gtk, Gio, GdkPixbuf, GLib 9 | from sunflower.common import UserDirectory, get_user_directory, get_static_assets_directory 10 | 11 | 12 | class IconManager: 13 | """Icon manager class provides easy and abstract way of dealing with icons""" 14 | 15 | def __init__(self, parent): 16 | self._parent = parent 17 | self._icon_theme = Gtk.IconTheme.get_default() 18 | self._user_directories = None 19 | self._default_file = None 20 | self._default_directory = None 21 | 22 | # preload information 23 | self._prepare_icons() 24 | 25 | def _prepare_icons(self): 26 | """Load special user directories""" 27 | # set default icons for file and directory 28 | self._default_file = 'text-x-generic' 29 | self._default_directory = 'folder' 30 | 31 | # special user directories 32 | directories = [] 33 | icon_names = { 34 | UserDirectory.DESKTOP: 'user-desktop', 35 | UserDirectory.DOWNLOADS: 'folder-download', 36 | UserDirectory.TEMPLATES: 'folder-templates', 37 | UserDirectory.PUBLIC: 'folder-publicshare', 38 | UserDirectory.DOCUMENTS: 'folder-documents', 39 | UserDirectory.MUSIC: 'folder-music', 40 | UserDirectory.PICTURES: 'folder-pictures', 41 | UserDirectory.VIDEOS: 'folder-videos' 42 | } 43 | 44 | # add all directories 45 | for directory in icon_names: 46 | full_path = get_user_directory(directory) 47 | icon_name = icon_names[directory] 48 | 49 | # make sure icon exists 50 | if not self.has_icon(icon_name): 51 | icon_name = self._default_directory 52 | 53 | directories.append((full_path, icon_name)) 54 | 55 | # add user home directory 56 | if self.has_icon('user-home'): 57 | directories.append((os.path.expanduser('~'), 'user-home')) 58 | 59 | # create a dictionary 60 | self._user_directories = dict(directories) 61 | 62 | def has_icon(self, icon_name): 63 | """Check if icon with specified name exists in theme""" 64 | return self._icon_theme.has_icon(icon_name) 65 | 66 | def get_icon_sizes(self, icon_name): 67 | """Get icon sizes for specified name""" 68 | return self._icon_theme.get_icon_sizes(icon_name) 69 | 70 | def get_icon_for_file(self, filename): 71 | """Load icon for specified file""" 72 | result = self._default_file 73 | mime_type = self._parent.associations_manager.get_mime_type(filename) 74 | themed_icon = None 75 | 76 | # get icon names 77 | if mime_type is not None: 78 | themed_icon = Gio.content_type_get_icon(mime_type) 79 | 80 | # get only valid icon names 81 | if themed_icon is not None: 82 | icon_list = themed_icon.get_names() 83 | icon_list = list(filter(self.has_icon, icon_list)) 84 | 85 | if len(icon_list) > 0: 86 | result = icon_list[0] 87 | 88 | return result 89 | 90 | def get_icon_for_directory(self, path): 91 | """Get icon for specified directory""" 92 | result = self._default_directory 93 | 94 | if path in self._user_directories: 95 | result = self._user_directories[path] 96 | 97 | return result 98 | 99 | def get_mount_icon_name(self, icons): 100 | """Return existing icon name from the specified list""" 101 | result = 'drive-harddisk' 102 | 103 | # create a list of icons and filter non-existing 104 | icon_list = icons.split(' ') 105 | icon_list = list(filter(self.has_icon, icon_list)) 106 | 107 | # if list has items, grab first 108 | if len(icon_list) > 0: 109 | result = icon_list[0] 110 | 111 | return result 112 | 113 | def set_window_icon(self, window): 114 | """Set window icon""" 115 | # check system for icon 116 | if self.has_icon('sunflower'): 117 | window.set_icon(self._icon_theme.load_icon('sunflower', 256, 0)) 118 | 119 | # try loading from zip file 120 | elif os.path.isfile(sys.path[0]) and sys.path[0] != '': 121 | archive = zipfile.ZipFile(sys.path[0]) 122 | with archive.open('images/sunflower.svg') as raw_file: 123 | buff = Gio.MemoryInputStream.new_from_bytes(GLib.Bytes.new(raw_file.read())) 124 | icon = GdkPixbuf.Pixbuf.new_from_stream(buff, None) 125 | window.set_icon(icon) 126 | archive.close() 127 | 128 | # load from local path 129 | else: 130 | base_path = get_static_assets_directory() 131 | window.set_icon_from_file(os.path.join(base_path, 'images', 'sunflower.svg')) 132 | -------------------------------------------------------------------------------- /sunflower/indicator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sunflower import common 5 | from gi.repository import Gtk 6 | 7 | 8 | class Indicator(object): 9 | """This class provides access to application indicators in Gnome environments""" 10 | 11 | def __init__(self, parent): 12 | self._parent = parent 13 | self._menu = Gtk.Menu() 14 | self._create_menu_items() 15 | 16 | base_path = os.path.dirname(common.get_static_assets_directory()) 17 | 18 | self._icon = 'sunflower_64.png' 19 | self._icon_path = os.path.abspath(os.path.join(base_path, 'images')) 20 | self._indicator = None 21 | 22 | if self._parent.window_options.section('main').get('hide_on_close'): 23 | self._indicator = Gtk.StatusIcon() 24 | 25 | self._indicator.set_from_file(os.path.join(self._icon_path, self._icon)) 26 | self._indicator.connect('activate', self._status_icon_activate) 27 | self._indicator.connect('popup-menu', self._status_icon_popup_menu) 28 | 29 | def _create_menu_items(self): 30 | """Create commonly used menu items in indicator""" 31 | # show window 32 | self._menu_show = self._parent.menu_manager.create_menu_item({ 33 | 'label': _('Sh_ow main window'), 34 | 'callback': self._change_visibility, 35 | 'data': True, 36 | }) 37 | self._menu_show.hide() 38 | self._menu.append(self._menu_show) 39 | 40 | # hide window 41 | self._menu_hide = self._parent.menu_manager.create_menu_item({ 42 | 'label': _('_Hide main window'), 43 | 'callback': self._change_visibility, 44 | 'data': False, 45 | }) 46 | self._menu.append(self._menu_hide) 47 | 48 | # close window option 49 | self._menu.append(self._parent.menu_manager.create_menu_item({'type': 'separator'})) 50 | self._menu.append(self._parent.menu_manager.create_menu_item({ 51 | 'label': _('_Quit'), 52 | 'type': 'image', 53 | 'callback': self._parent._destroy, 54 | 'stock': Gtk.STOCK_QUIT, 55 | })) 56 | 57 | # separator 58 | self._separator = self._parent.menu_manager.create_menu_item({'type': 'separator'}) 59 | self._menu.append(self._separator) 60 | self._separator.hide() 61 | 62 | def _status_icon_activate(self, widget, data=None): 63 | """Toggle visibility on status icon activate""" 64 | visible = not self._parent.get_visible() 65 | self._change_visibility(widget, visible) 66 | 67 | def _status_icon_popup_menu(self, widget, button, activate_time): 68 | """Show popup menu on right click""" 69 | self._menu.popup(None, None, None, None, button, activate_time) 70 | 71 | def _change_visibility(self, widget, visible): 72 | """Change main window visibility""" 73 | self._parent.set_visible(visible) 74 | self.adjust_visibility_items(visible) 75 | 76 | def adjust_visibility_items(self, visible): 77 | """Adjust show/hide menu items""" 78 | self._menu_show.set_visible(not visible) 79 | self._menu_hide.set_visible(visible) 80 | 81 | def add_operation(self, widget, callback, data): 82 | """Add operation to operations menu""" 83 | menu_item = Gtk.MenuItem() 84 | menu_item.add(widget) 85 | 86 | if callback is not None: 87 | menu_item.connect('activate', callback, data) 88 | 89 | menu_item.show() 90 | self._separator.show() 91 | self._menu.append(menu_item) 92 | 93 | if hasattr(self._indicator, 'set_menu'): 94 | self._indicator.set_menu(self._menu) 95 | 96 | return menu_item 97 | -------------------------------------------------------------------------------- /sunflower/notifications.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import sys 5 | 6 | try: 7 | import gi 8 | gi.require_version('Notify', '0.7') 9 | from gi.repository import Notify 10 | except: 11 | Notify = None 12 | 13 | from sunflower import common 14 | 15 | 16 | class NotificationManager: 17 | """Notification manager provides OS specific notification 18 | methods to plugins and operations. 19 | """ 20 | 21 | available = Notify is not None 22 | 23 | def __init__(self, application): 24 | # initialize OS notification system 25 | if not self.available: 26 | return 27 | 28 | self._application = application 29 | 30 | Notify.init('sunflower') 31 | 32 | # decide which icon to use 33 | if self._application.icon_manager.has_icon('sunflower'): 34 | # use global icon 35 | self._default_icon = 'sunflower' 36 | 37 | else: 38 | # use local icon 39 | icon_file = os.path.join(os.path.dirname(common.get_static_assets_directory()), 'images', 'sunflower_64.png') 40 | self._default_icon = 'file://{0}'.format(icon_file) 41 | 42 | def notify(self, title, text, icon=None): 43 | """Make system notification""" 44 | if not self.available \ 45 | or not self._application.options.get('show_notifications'): 46 | return # if notifications are disabled or unavailable 47 | 48 | if icon is None: # make sure we show notification with icon 49 | icon = self._default_icon 50 | 51 | try: 52 | # create notification object 53 | notification = Notify.Notification.new(title, text, icon) 54 | 55 | # show notification 56 | notification.show() 57 | 58 | except: 59 | # we don't need to handle errors from notification daemon 60 | pass 61 | -------------------------------------------------------------------------------- /sunflower/parameters.py: -------------------------------------------------------------------------------- 1 | class Parameters: 2 | """Simple class used as storage container for various parameters""" 3 | 4 | def __init__(self, params=None): 5 | self._parameters = {} if params is None else params.copy() 6 | 7 | def get(self, name, default=None): 8 | """Get parameter""" 9 | return self._parameters[name] if name in self._parameters else default 10 | 11 | def set(self, name, value): 12 | """Set parameter""" 13 | self._parameters[name] = value 14 | 15 | def get_params(self): 16 | """Get copy of parameters""" 17 | return self._parameters.copy() 18 | 19 | def copy(self): 20 | """Return copy of parameters""" 21 | return Parameters(self.get_params()) 22 | -------------------------------------------------------------------------------- /sunflower/plugin_base/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sunflower/plugin_base/column_editor_extension.py: -------------------------------------------------------------------------------- 1 | class ColumnEditorExtension: 2 | """Class used for extending column editor in preferences window""" 3 | 4 | def __init__(self, parent, config): 5 | self._parent = parent 6 | self._parent_name = parent._name 7 | self._config = config 8 | 9 | def _save_settings(self): 10 | """Save values to config""" 11 | pass 12 | 13 | def _load_settings(self): 14 | """Load values from config""" 15 | pass 16 | 17 | def get_name(self): 18 | """Get plugin's name""" 19 | pass 20 | 21 | def get_columns(self, only_visible=False): 22 | """Get column names""" 23 | 24 | def get_size(self, column): 25 | """Get column size""" 26 | pass 27 | 28 | def get_font_size(self, column): 29 | """Get column font size""" 30 | pass 31 | 32 | def get_visible(self, column): 33 | """Get column visibility""" 34 | pass 35 | 36 | def set_size(self, column, size): 37 | """Set column size""" 38 | pass 39 | 40 | def set_visible(self, column, visible): 41 | """Set column visibility""" 42 | pass 43 | -------------------------------------------------------------------------------- /sunflower/plugin_base/column_extension.py: -------------------------------------------------------------------------------- 1 | class ColumnExtension: 2 | """Class used for extending item lists. 3 | 4 | Object of this class is created for every tab individually 5 | and for this reason class parameters are highly discouraged 6 | unless you know what you are doing. 7 | 8 | """ 9 | 10 | def __init__(self, parent, store): 11 | self._parent = parent 12 | self._store = store 13 | self._column = None 14 | 15 | def _create_column(self): 16 | """Create column 17 | 18 | For each column you create, you need to add property 'name': column.name = 'Column name'. 19 | This information will be used to store sorting and column order in 20 | configuration files. 21 | 22 | """ 23 | pass 24 | 25 | def get_column(self): 26 | """Get column object to be added to the list""" 27 | return self._column 28 | 29 | def get_sort_column(self): 30 | """Get column sort number""" 31 | return None 32 | -------------------------------------------------------------------------------- /sunflower/plugin_base/find_extension.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class FindExtension: 5 | """Base class for extending find files tool. 6 | 7 | Use this class to provide find files tool with additional 8 | options. Objects are created every time tool is created! 9 | 10 | """ 11 | 12 | def __init__(self, parent, always_on=False): 13 | self._parent = parent 14 | self._active = always_on 15 | 16 | # create and configure title widget 17 | self.title = TitleRow(self, always_on) 18 | self.title.check.connect('state-set', self.__handle_state_set) 19 | 20 | # create and configure container 21 | self.container = Gtk.VBox.new(False, 5) 22 | self.container.set_border_width(10) 23 | self.container.extension = self 24 | 25 | def __handle_state_set(self, widget, state): 26 | """Update extension active.""" 27 | self._active = state 28 | 29 | def __get_active(self): 30 | """Get state of the extension.""" 31 | return self._active 32 | 33 | def __set_active(self, value): 34 | """Set state of the extension.""" 35 | self._active = value 36 | self.title.set_active(value) 37 | 38 | def get_title(self): 39 | """Return name of the extension.""" 40 | return None 41 | 42 | def get_title_widget(self): 43 | """Return title widget for extension.""" 44 | return self.title 45 | 46 | def get_container(self): 47 | """Return widget container.""" 48 | return self.container 49 | 50 | def is_path_ok(self, provider, path): 51 | """Check is specified path fits the cirteria.""" 52 | return True 53 | 54 | active = property(__get_active, __set_active) 55 | 56 | 57 | class TitleRow(Gtk.ListBoxRow): 58 | """List box row representing extension.""" 59 | 60 | def __init__(self, extension, always_on): 61 | Gtk.ListBoxRow.__init__(self) 62 | 63 | self._extension = extension 64 | 65 | self.set_selectable(True) 66 | self.set_activatable(True) 67 | self.set_focus_on_click(True) 68 | 69 | # create interface 70 | hbox = Gtk.HBox.new(False, 10) 71 | hbox.set_border_width(5) 72 | self.add(hbox) 73 | 74 | label = Gtk.Label.new(extension.get_title()) 75 | label.set_alignment(0, 0.5) 76 | hbox.pack_start(label, True, True, 0) 77 | 78 | self.check = Gtk.Switch.new() 79 | self.check.set_sensitive(not always_on) 80 | hbox.pack_start(self.check, False, False, 0) 81 | 82 | def get_extension(self): 83 | """Return parent extension.""" 84 | return self._extension 85 | 86 | def set_active(self, value): 87 | """Set state of the extension.""" 88 | self.check.set_active(value) 89 | -------------------------------------------------------------------------------- /sunflower/plugin_base/monitor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import GObject 4 | from queue import Queue, Empty as QueueEmptyException 5 | from threading import Event 6 | 7 | 8 | class MonitorError(Exception): pass 9 | 10 | 11 | class MonitorSignals: 12 | CHANGED = 0 # file changed 13 | CHANGES_DONE = 1 # a hint that this was probably the last change in a set 14 | DELETED = 2 # file was deleted 15 | CREATED = 3 # file was created 16 | ATTRIBUTE_CHANGED = 4 # attribute was changed 17 | PRE_UNMOUNT = 5 # location will soon be unmounted 18 | UNMOUNTED = 6 # location was unmounted 19 | MOVED = 7 # file was moved 20 | EMBLEM_CHANGED = 8 # list of emblems has changed 21 | DIRECTORY_SIZE_CHANGED = 9 # calculated directory size has changed 22 | DIRECTORY_SIZE_STOPPED = 10 # directory size calculation has finished 23 | 24 | 25 | class Monitor(GObject.GObject): 26 | """File system monitor base class. 27 | 28 | Monitors are used to watch over a specific path on file system 29 | specific to provider that created the monitor. They are created and 30 | destroyed automatically on each path change and mainly used by file 31 | lists but could have other usages. 32 | 33 | This monitor class also provides custom event queue which can be 34 | used to manually emit signals. 35 | 36 | """ 37 | 38 | __gtype_name__ = 'Sunflower_Monitor' 39 | __gsignals__ = { 40 | 'changed': (GObject.SignalFlags.RUN_LAST, None, (int, GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)), 41 | } 42 | 43 | TIMEOUT = 1000 44 | 45 | def __init__(self, provider, path): 46 | GObject.GObject.__init__(self) 47 | 48 | self._path = path 49 | self._provider = provider 50 | self._monitor = None 51 | self._paused = Event() 52 | 53 | # clear initial value 54 | self._paused.clear() 55 | 56 | self._queue = Queue() 57 | self._start_interval() 58 | 59 | def _start_interval(self): 60 | """Start periodical event emission""" 61 | GObject.timeout_add(self.TIMEOUT, self._handle_interval) 62 | 63 | def _handle_interval(self): 64 | """Handle notification interval""" 65 | events = [] 66 | 67 | # get all events from the queue 68 | while True: 69 | try: 70 | # try to get another event 71 | events.append(self._queue.get(False)) 72 | 73 | except QueueEmptyException: 74 | # no more events in the queue 75 | break 76 | 77 | # emit events from a set 78 | for event in set(events): 79 | self._emit_signal(*event) 80 | 81 | # if paused break interval cycle 82 | return not self._paused.isSet() 83 | 84 | def _emit_signal(self, signal, path, other_path): 85 | """Notify connected objects that monitored path was changed. 86 | 87 | Use other_path in cases where it seems logical, like moving files. 88 | Otherwise None should be used instead. Paths needs to be relative to 89 | path specified in constructor. 90 | 91 | """ 92 | if not self._paused.is_set(): 93 | self.emit('changed', signal, path, other_path) 94 | 95 | def is_manual(self): 96 | """Check if monitor solely relies on queues""" 97 | return True 98 | 99 | def pause(self): 100 | """Pause monitoring""" 101 | self._paused.set() 102 | 103 | def resume(self): 104 | """Resume monitoring""" 105 | self._paused.clear() 106 | self._start_interval() 107 | 108 | def cancel(self): 109 | """Cancel monitoring""" 110 | self.pause() 111 | 112 | def get_queue(self): 113 | """Return monitor queue""" 114 | return self._queue 115 | 116 | def get_path(self): 117 | """Return monitor path""" 118 | return self._path 119 | -------------------------------------------------------------------------------- /sunflower/plugin_base/mount_manager_extension.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class ExtensionFeatures: 5 | SYSTEM_WIDE = 0 6 | 7 | 8 | class MountManagerExtension: 9 | """Base class for mount manager extensions. 10 | 11 | Mount manager has only one instance and is created on program startup. 12 | Methods defined in this class are called automatically by the mount manager 13 | so you need to implement them. 14 | 15 | """ 16 | 17 | # features extension supports 18 | features = () 19 | 20 | def __init__(self, parent, window): 21 | self._parent = parent 22 | self._window = window 23 | self._application = self._parent._application 24 | 25 | # create user interface 26 | self._container = Gtk.VBox(False, 5) 27 | self._controls = Gtk.HBox(False, 5) 28 | 29 | separator = Gtk.HSeparator() 30 | 31 | # pack interface 32 | self._container.pack_end(separator, False, False, 0) 33 | self._container.pack_end(self._controls, False, False, 0) 34 | 35 | def can_handle(self, uri): 36 | """Returns boolean denoting if specified URI can be handled by this extension""" 37 | return False 38 | 39 | def get_container(self): 40 | """Return container widget""" 41 | return self._container 42 | 43 | def get_information(self): 44 | """Returns information about extension""" 45 | icon = None 46 | name = None 47 | 48 | return icon, name 49 | 50 | def unmount(self, uri): 51 | """Method called by the mount manager for unmounting the selected URI""" 52 | pass 53 | 54 | def focus_object(self): 55 | """Method called by the mount manager for focusing main object""" 56 | pass 57 | 58 | @classmethod 59 | def get_features(cls): 60 | """Returns set of features supported by extension""" 61 | return cls.features 62 | -------------------------------------------------------------------------------- /sunflower/plugin_base/rename_extension.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class RenameExtension: 5 | """Base class for extending advanced rename tool. 6 | 7 | Use this class to provide advanced rename tool with additional 8 | options. Objects are created every time tool is created! 9 | 10 | """ 11 | 12 | def __init__(self, parent): 13 | self._parent = parent 14 | 15 | # create and configure container 16 | self.vbox = Gtk.VBox(False, 5) 17 | self.vbox.set_border_width(7) 18 | self.vbox.extension = self 19 | 20 | # create activity toggle 21 | self._active = False 22 | self._checkbox_active = Gtk.CheckButton(_('Use this extension')) 23 | self._checkbox_active.connect('toggled', self.__toggle_active) 24 | self._checkbox_active.show() 25 | 26 | self.vbox.pack_start(self._checkbox_active, False, False, 0) 27 | 28 | def __toggle_active(self, widget, data=None): 29 | """Toggle extension active property""" 30 | self._active = widget.get_active() 31 | self._update_parent_list() 32 | 33 | def _update_parent_list(self, widget=None, data=None): 34 | """Update parent list""" 35 | self._parent.update_list() 36 | 37 | def is_active(self): 38 | """Return boolean representing extension state""" 39 | return self._active 40 | 41 | def reset(self): 42 | """Method called before iterating through parents list""" 43 | pass 44 | 45 | def get_title(self): 46 | """Return i18n title for extension""" 47 | return None 48 | 49 | def get_container(self): 50 | """Return widget container""" 51 | return self.vbox 52 | 53 | def get_new_name(self, old_name, new_name): 54 | """Generate and return new name for specified file. 55 | 56 | If you don't make any modifications to the name make sure 57 | you return new_name instead. In cases where extension needs 58 | to file (or file contents) you can use self._parent._provider 59 | object. 60 | 61 | Parameters: 62 | old_name - original (unchanged) file name 63 | new_name - name modified by previous extensions 64 | 65 | """ 66 | return new_name 67 | -------------------------------------------------------------------------------- /sunflower/plugin_base/toolbar_factory.py: -------------------------------------------------------------------------------- 1 | class ToolbarFactory: 2 | """This factory provides methods used to create and configure widgets located 3 | on toolbar in main program window.""" 4 | 5 | def __init__(self, application): 6 | self._application = application 7 | 8 | def get_types(self): 9 | """Return dictionary of widget types this factory can create. 10 | 11 | Result needs to be dictionary with widget type as key and tuple 12 | containing icon name and description as value. 13 | Type is used to provide factory with configuration for specified item 14 | while description is user friendly representation of widget. 15 | 16 | result = { 17 | 'bookmark_button': ( 18 | _('Bookmark button'), 19 | icon_name 20 | ), 21 | } 22 | 23 | """ 24 | pass 25 | 26 | def create_widget(self, name, widget_type, transient_window=None): 27 | """Show dialog for creating a new widget. This method returns 28 | dictionary with widget specific configuration or None in case 29 | user canceled. 30 | 31 | result = { 32 | 'some_key': 'value to be stored', 33 | } 34 | 35 | """ 36 | pass 37 | 38 | def configure_widget(self, name, widget_type, config, transient_window=None): 39 | """Present blocking configuration dialog for specified widget type. 40 | 41 | Returns new config if changes were made otherwise None 42 | 43 | """ 44 | pass 45 | 46 | def get_widget(self, name, widget_type, config): 47 | """Return newly created widget based on type and configuration.""" 48 | pass 49 | -------------------------------------------------------------------------------- /sunflower/plugin_base/viewer_extension.py: -------------------------------------------------------------------------------- 1 | class ViewerExtension: 2 | """Base class used for extending viewer tool""" 3 | 4 | def __init__(self, parent): 5 | self._parent = parent 6 | 7 | def get_title(self): 8 | """Return page title""" 9 | pass 10 | 11 | def get_container(self): 12 | """Return container widget to be embedded to notebook""" 13 | pass 14 | 15 | def focus_object(self): 16 | """Focus main object in extension""" 17 | pass 18 | -------------------------------------------------------------------------------- /sunflower/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sunflower/plugins/archive_support/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/archive_support/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/archive_support/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Archive Support 3 | 4 | [Description] 5 | en=Adds support for file archives. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/archive_support/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .zip_provider import ZipProvider 4 | 5 | 6 | def register_plugin(application): 7 | """Register plugin classes with application""" 8 | application.register_provider(ZipProvider) 9 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/default_toolbar/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/bookmark_button.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from gi.repository import Gtk, GObject 4 | 5 | 6 | class Button(Gtk.ToolButton): 7 | """Bookmark toolbar button""" 8 | 9 | def __init__(self, application, name, config): 10 | GObject.GObject.__init__(self) 11 | 12 | self._name = name 13 | self._config = config 14 | self._application = application 15 | self._path = None 16 | 17 | # configure button 18 | self._set_label() 19 | self._set_icon() 20 | 21 | # show label if specified 22 | if 'show_label' in self._config: 23 | important = self._config['show_label'] in ('True', True) 24 | self.set_is_important(important) 25 | 26 | if 'path' in self._config: 27 | self._path = os.path.expanduser(self._config['path']) 28 | 29 | # connect signals 30 | self.connect('clicked', self._clicked) 31 | 32 | def _set_label(self): 33 | """Set button label""" 34 | self.set_label(self._name) 35 | self.set_tooltip_text(self._name) 36 | 37 | def _set_icon(self): 38 | """Set button icon""" 39 | icon_name = self._application.icon_manager.get_icon_for_directory(self._path) 40 | self.set_icon_name(icon_name) 41 | 42 | def _clicked(self, widget, data=None): 43 | """Handle click""" 44 | active_object = self._application.get_active_object() 45 | 46 | if hasattr(active_object, 'change_path'): 47 | active_object.change_path(self._path) 48 | 49 | 50 | class ConfigurationDialog(Gtk.Dialog): 51 | """Configuration dialog for bookmark button""" 52 | 53 | def __init__(self, application, name, config=None): 54 | Gtk.Dialog.__init__( 55 | self, 56 | parent=application, 57 | use_header_bar=True, 58 | ) 59 | 60 | self._application = application 61 | 62 | # configure dialog 63 | self.set_title(_('Configure bookmark button')) 64 | self.set_default_size(450, 10) 65 | self.set_resizable(True) 66 | self.set_skip_taskbar_hint(True) 67 | self.set_modal(True) 68 | self.set_transient_for(application) 69 | 70 | self.vbox.set_spacing(0) 71 | 72 | # interface container 73 | vbox = Gtk.VBox(False, 5) 74 | vbox.set_border_width(5) 75 | 76 | # create interface 77 | vbox_path = Gtk.VBox(False, 0) 78 | 79 | label_path = Gtk.Label(label=_('Path:')) 80 | label_path.set_alignment(0, 0.5) 81 | 82 | self._entry_path = Gtk.Entry() 83 | self._checkbox_show_label = Gtk.CheckButton(_('Show label')) 84 | 85 | # load default values 86 | if config is not None: 87 | self._entry_path.set_text(config['path']) 88 | self._checkbox_show_label.set_active(config['show_label'] == True) 89 | 90 | # create controls 91 | button_save = Gtk.Button(stock=Gtk.STOCK_SAVE) 92 | button_save.set_can_default(True) 93 | button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL) 94 | 95 | self.add_action_widget(button_cancel, Gtk.ResponseType.CANCEL) 96 | self.add_action_widget(button_save, Gtk.ResponseType.ACCEPT) 97 | 98 | self.set_default_response(Gtk.ResponseType.ACCEPT) 99 | 100 | # pack interface 101 | vbox_path.pack_start(label_path, False, False, 0) 102 | vbox_path.pack_start(self._entry_path, False, False, 0) 103 | 104 | vbox.pack_start(vbox_path, False, False, 0) 105 | vbox.pack_start(self._checkbox_show_label, False, False, 0) 106 | 107 | self.vbox.pack_start(vbox, False, False, 0) 108 | 109 | self.show_all() 110 | 111 | def get_response(self): 112 | """Return dialog response and self-destruct""" 113 | config = None 114 | 115 | # show dialog 116 | code = self.run() 117 | 118 | if code == Gtk.ResponseType.ACCEPT: 119 | config = { 120 | 'path': self._entry_path.get_text(), 121 | 'show_label': self._checkbox_show_label.get_active() 122 | } 123 | 124 | self.destroy() 125 | 126 | return config 127 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/bookmarks_button.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, GObject 2 | 3 | 4 | class Button(Gtk.ToolButton): 5 | """Toolbar control used to popup bookmarks menu""" 6 | 7 | def __init__(self, application, name, config): 8 | GObject.GObject.__init__(self) 9 | 10 | # store parameters locally 11 | self._name = name 12 | self._config = config 13 | self._application = application 14 | 15 | # configure 16 | self.set_label(_('Bookmarks')) 17 | self.set_tooltip_text(_('Bookmarks')) 18 | self.set_icon_name('go-jump') 19 | self.set_is_important(True) 20 | 21 | # connect events 22 | self.connect('clicked', self._clicked) 23 | 24 | def _clicked(self, widget, data=None): 25 | """Handle click""" 26 | self._application.show_bookmarks_menu() 27 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/home_directory_button.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .bookmark_button import Button as BookmarkButton 4 | 5 | 6 | class Button(BookmarkButton): 7 | """Home directory toolbar button""" 8 | 9 | def __init__(self, application, name, config): 10 | BookmarkButton.__init__(self, application, name, config) 11 | 12 | self._path = os.path.expanduser('~') 13 | 14 | def _set_label(self): 15 | """Set button label""" 16 | self.set_label(_('Home directory')) 17 | self.set_tooltip_text(_('Home directory')) 18 | 19 | def _set_icon(self): 20 | """Set button icon""" 21 | self.set_icon_name('user-home') 22 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/parent_directory_button.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, GObject 2 | 3 | 4 | class Button(Gtk.ToolButton): 5 | """Go to parent directory toolbar button""" 6 | 7 | def __init__(self, application, name, config): 8 | GObject.GObject.__init__(self) 9 | 10 | self._name = name 11 | self._config = config 12 | self._application = application 13 | 14 | self.set_label(_('Go to parent directory')) 15 | self.set_tooltip_text(_('Go to parent directory')) 16 | self.set_icon_name('go-up') 17 | 18 | self.connect('clicked', self._clicked) 19 | 20 | def _clicked(self, widget, data=None): 21 | """Handle button click""" 22 | active_object = self._application.get_active_object() 23 | 24 | if hasattr(active_object, '_parent_directory'): 25 | active_object._parent_directory() 26 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Default toolbar widgets 3 | 4 | [Description] 5 | en=This plugin provides basic widgets for toolbar. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from sunflower.plugin_base.toolbar_factory import ToolbarFactory 5 | from .bookmark_button import Button as BookmarkButton, ConfigurationDialog as BookmarkButton_Dialog 6 | from .bookmarks_button import Button as BookmarksButton 7 | from .home_directory_button import Button as HomeDirectoryButton 8 | from .parent_directory_button import Button as ParentDirectoryButton 9 | from .separator import Separator 10 | 11 | 12 | def register_plugin(application): 13 | """Register plugin classes with application""" 14 | application.register_toolbar_factory(DefaultToolbar) 15 | 16 | 17 | class DefaultToolbar(ToolbarFactory): 18 | """Default toolbar factory implementation for Sunflower.""" 19 | 20 | def __init__(self, application): 21 | ToolbarFactory.__init__(self, application) 22 | 23 | self._widgets = { 24 | 'parent_directory_button': { 25 | 'description': _('Parent directory button'), 26 | 'icon': Gtk.STOCK_GO_UP, 27 | 'dialog': None, 28 | 'class': ParentDirectoryButton, 29 | }, 30 | 'home_directory_button': { 31 | 'description': _('Home directory button'), 32 | 'icon': 'user-home', 33 | 'dialog': None, 34 | 'class': HomeDirectoryButton, 35 | }, 36 | 'bookmark_button': { 37 | 'description': _('Bookmark button'), 38 | 'icon': 'folder', 39 | 'dialog': BookmarkButton_Dialog, 40 | 'class': BookmarkButton, 41 | }, 42 | 'bookmarks_button': { 43 | 'description': _('Bookmarks menu'), 44 | 'icon': 'go-jump', 45 | 'dialog': None, 46 | 'class': BookmarksButton, 47 | }, 48 | 'separator': { 49 | 'description': _('Separator'), 50 | 'icon': '', 51 | 'dialog': None, 52 | 'class': Separator, 53 | }, 54 | } 55 | 56 | def get_types(self): 57 | """Return supported widget types""" 58 | widget_list = [] 59 | 60 | for key, data in self._widgets.items(): 61 | widget_list.append((key, (data['description'], data['icon']))) 62 | 63 | widget_list.sort() 64 | 65 | return dict(widget_list) 66 | 67 | def create_widget(self, name, widget_type, transient_window=None): 68 | """Show widget creation dialog""" 69 | config = {} 70 | DialogClass = self._widgets[widget_type]['dialog'] 71 | 72 | if DialogClass is not None: 73 | # create configuration dialog 74 | dialog = DialogClass(transient_window, name) 75 | 76 | # set transistent window 77 | if transient_window is not None: 78 | dialog.set_transient_for(transient_window) 79 | 80 | # get config 81 | config = dialog.get_response() 82 | 83 | return config 84 | 85 | def configure_widget(self, name, widget_type, config, transient_window=None): 86 | """Configure specified widget""" 87 | result = None 88 | DialogClass = self._widgets[widget_type]['dialog'] 89 | 90 | if DialogClass is not None: 91 | # create configuration dialog 92 | dialog = DialogClass(transient_window, name, config) 93 | 94 | # show dialog and get use input 95 | result = dialog.get_response() 96 | 97 | else: 98 | # there is no configuration dialog for this widget type 99 | dialog = Gtk.MessageDialog( 100 | transient_window, 101 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 102 | Gtk.MessageType.INFO, 103 | Gtk.ButtonsType.OK, 104 | _("This widget has no configuration dialog.") 105 | ) 106 | dialog.run() 107 | dialog.destroy() 108 | 109 | return result 110 | 111 | def get_widget(self, name, widget_type, config): 112 | """Return newly created widget based on type and configuration.""" 113 | result = None 114 | 115 | if widget_type in self._widgets \ 116 | and self._widgets[widget_type]['class'] is not None: 117 | # create widget 118 | WidgetClass = self._widgets[widget_type]['class'] 119 | result = WidgetClass(self._application, name, config) 120 | 121 | return result 122 | -------------------------------------------------------------------------------- /sunflower/plugins/default_toolbar/separator.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, GObject 2 | 3 | 4 | class Separator(Gtk.SeparatorToolItem): 5 | """Toolbar separator widget""" 6 | 7 | def __init__(self, application, name, config): 8 | GObject.GObject.__init__(self) 9 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/file_list/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/file_list/column_editor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from sunflower.plugin_base.column_editor_extension import ColumnEditorExtension 4 | 5 | 6 | class FileList_ColumnEditor(ColumnEditorExtension): 7 | """Column editor for file list plugin""" 8 | 9 | def __init__(self, parent, config): 10 | ColumnEditorExtension.__init__(self, parent, config) 11 | 12 | self._columns = [] 13 | self._visible = [] 14 | self._sizes = {} 15 | self._font_sizes = {} 16 | 17 | def __update_column_list(self): 18 | """Update list of available columns""" 19 | self._columns = [] 20 | 21 | for column in self._parent._columns: 22 | self._columns.append(column.name) 23 | 24 | def _save_settings(self): 25 | """Save values to config""" 26 | section = self._config.create_section(self._parent_name) 27 | 28 | # save column order and visibility 29 | section.set('columns', self._visible[:]) 30 | 31 | # save column sizes 32 | for name, size in self._sizes.items(): 33 | section.set('size_{0}'.format(name), size) 34 | 35 | # save font sizes 36 | for name, size in self._font_sizes.items(): 37 | section.set('font_size_{0}'.format(name), size) 38 | 39 | def _load_settings(self): 40 | """Load values from config""" 41 | section = self._config.section(self._parent_name) 42 | 43 | # update columns 44 | self.__update_column_list() 45 | 46 | if section is None: 47 | return 48 | 49 | # get list of visible columns 50 | self._visible = section.get('columns') 51 | 52 | # make sure we have list of visible columns 53 | if self._visible is None: 54 | self._visible = self._columns 55 | 56 | # get sizes 57 | for column_name in self._columns: 58 | size = section.get('size_{0}'.format(column_name)) 59 | 60 | if size is not None: 61 | self._sizes[column_name] = size 62 | 63 | # get font sizes 64 | for column_name in self._columns: 65 | size = section.get('font_size_{0}'.format(column_name)) 66 | 67 | if size is not None: 68 | self._font_sizes[column_name] = size 69 | 70 | def get_name(self): 71 | """Return name of extension""" 72 | return _('Item List'), self._parent_name 73 | 74 | def get_columns(self, only_visible=False): 75 | """Get column names""" 76 | result = self._columns[:] 77 | 78 | if only_visible: 79 | result = [column_name for column_name in result if column_name in self._visible] 80 | 81 | return result 82 | 83 | def get_size(self, column): 84 | """Get column size""" 85 | return self._sizes[column] if column in self._sizes else None 86 | 87 | def get_font_size(self, column): 88 | """Get column font size""" 89 | return self._font_sizes[column] if column in self._font_sizes else None 90 | 91 | def get_visible(self, column): 92 | """Get column visibility""" 93 | return column in self._visible 94 | 95 | def set_size(self, column, size): 96 | """Set column size""" 97 | if column in self._columns: 98 | self._sizes[column] = size 99 | 100 | def set_font_size(self, column, size): 101 | """Set column font size""" 102 | if column in self._columns: 103 | self._font_sizes[column] = size 104 | 105 | def set_visible(self, column, visible): 106 | """Set column visibility""" 107 | if visible: 108 | # column was hidden, visible now 109 | if column in self._columns: 110 | index = self._columns.index(column) 111 | self._visible.insert(index, column) 112 | 113 | else: 114 | self._visible.append(column) 115 | 116 | else: 117 | # column was visible, hidden now 118 | try: 119 | self._visible.remove(column) 120 | 121 | except ValueError: 122 | pass 123 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/gio_wrapper.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gio, GLib, GObject 2 | from sunflower.plugin_base.provider import Mode 3 | 4 | # GFile.read_bytes() has upper limit for size of 5 | # G_MAXSSIZE (9223372036854775807) which is insensibly large 6 | MAX_READ_FILE_SIZE = 4*1024*1024*1024 7 | 8 | 9 | class File: 10 | """This is a wrapper class that provides file-like object but 11 | uses Gio.File for actual operations.""" 12 | 13 | def __init__(self, path, mode): 14 | if mode == Mode.READ: 15 | self._resource = Gio.File.new_for_commandline_arg(path).read() 16 | 17 | elif mode == Mode.WRITE: 18 | if Gio.File.new_for_commandline_arg(path).query_exists(): 19 | Gio.File.new_for_commandline_arg(path).delete() # have to manually remove since flag doesn't work 20 | 21 | self._resource = Gio.File.new_for_commandline_arg(path).create( 22 | Gio.FileCreateFlags.REPLACE_DESTINATION # doesn't seem to do anything 23 | ) 24 | 25 | elif mode == Mode.APPEND: 26 | self._resource = Gio.File.new_for_commandline_arg(path).append_to() 27 | 28 | def __enter__(self): 29 | """Set opened file as runtime context""" 30 | return self._resource 31 | 32 | def __exit__(self, exc_type, exc_val, exc_tb): 33 | """Close file on exit from context""" 34 | self.close() 35 | 36 | def close(self): 37 | """Close file""" 38 | self._resource.close() 39 | 40 | def closed(self): 41 | """If file is closed""" 42 | self._resource.is_closed() 43 | 44 | def flush(self): 45 | """Flush internal buffer""" 46 | if hasattr(self._resource, 'flush'): 47 | self._resource.flush() 48 | 49 | def read(self, size=MAX_READ_FILE_SIZE): 50 | """Read at most _size_ bytes from the file""" 51 | result = self._resource.read_bytes(size) 52 | 53 | if result is True: 54 | result = "" 55 | 56 | return result.get_data() 57 | 58 | def seek(self, offset, whence=0): 59 | """Set the file's current position""" 60 | relative = (1, 0, 2)[whence] 61 | 62 | if self._resource.can_seek(): 63 | self._resource.seek(offset, relative) 64 | 65 | def tell(self): 66 | """Return file's current position""" 67 | return self._resource.tell() 68 | 69 | def truncate(self, size=None): 70 | """Truncate the file's size""" 71 | if size is None: 72 | size = self.tell() 73 | 74 | if self._resource.can_truncate(): 75 | self._resource.truncate(size) 76 | 77 | def write(self, buff): 78 | """Write string to the file""" 79 | self._resource.write(buff) 80 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/local_monitor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from gi.repository import Gio 6 | from sunflower.plugin_base.monitor import Monitor, MonitorSignals, MonitorError 7 | 8 | 9 | class LocalMonitor(Monitor): 10 | """Local file monitor based on GIO""" 11 | 12 | # signals translation table 13 | _signal_table = { 14 | Gio.FileMonitorEvent.CHANGED: MonitorSignals.CHANGED, 15 | Gio.FileMonitorEvent.CHANGES_DONE_HINT: MonitorSignals.CHANGES_DONE, 16 | Gio.FileMonitorEvent.DELETED: MonitorSignals.DELETED, 17 | Gio.FileMonitorEvent.CREATED: MonitorSignals.CREATED, 18 | Gio.FileMonitorEvent.ATTRIBUTE_CHANGED: MonitorSignals.ATTRIBUTE_CHANGED, 19 | Gio.FileMonitorEvent.PRE_UNMOUNT: MonitorSignals.PRE_UNMOUNT, 20 | Gio.FileMonitorEvent.UNMOUNTED: MonitorSignals.UNMOUNTED, 21 | Gio.FileMonitorEvent.MOVED: MonitorSignals.MOVED, 22 | } 23 | 24 | def __init__(self, provider, path): 25 | Monitor.__init__(self, provider, path) 26 | 27 | if provider.exists(self._path): 28 | try: 29 | # create file/directory monitor 30 | if (path == 'trash:///'): 31 | self._monitor = Gio.File.new_for_uri(path).monitor(Gio.FileMonitorFlags.SEND_MOVED) 32 | else: 33 | self._monitor = Gio.File.new_for_path(path).monitor(Gio.FileMonitorFlags.SEND_MOVED) 34 | 35 | except Exception as error: 36 | raise MonitorError('Error creating monitor: {0}'.format(repr(error))) 37 | 38 | else: 39 | # connect signal 40 | self._monitor.connect('changed', self._changed) 41 | 42 | else: 43 | # invalid path, raise exception 44 | raise MonitorError('Unable to create monitor. Invalid path! - {}'.format(path)) 45 | 46 | def _changed(self, monitor, path, other_path, event_type): 47 | """Handle GIO signal""" 48 | 49 | if event_type is Gio.FileMonitorEvent.MOVED: 50 | if path.get_parent().get_path() == self._path and other_path.get_parent().get_path() != self._path: 51 | # path moved to somewhere else - emit DELETED 52 | signal = MonitorSignals.DELETED 53 | 54 | else: 55 | # path renamed within same directory - emit MOVED 56 | signal = MonitorSignals.MOVED 57 | if other_path is not None: 58 | other_path = other_path.get_basename() 59 | 60 | else: 61 | signal = self._signal_table[event_type] 62 | 63 | if path is not None: 64 | path = path.get_basename() 65 | 66 | self._emit_signal(signal, path, other_path) 67 | 68 | def cancel(self): 69 | """Cancel monitoring""" 70 | if self._monitor is not None: 71 | self._monitor.cancel() 72 | 73 | def is_manual(self): 74 | """Check if monitor solely relies on queues""" 75 | return False 76 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Standard file list 3 | 4 | [Description] 5 | en=Default file list tab. This plugin is required and can not be disabled. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .file_list import FileList 4 | from .trash_list import TrashList 5 | from .gio_extension import SambaExtension, FtpExtension, DavExtension, SftpExtension 6 | from .gio_provider import NetworkProvider, TrashProvider, DavProvider, DavsProvider, Gphoto2Provider, MtpProvider 7 | from .gio_provider import SambaProvider, FtpProvider, SftpProvider, ArchiveProvider, GioProvider 8 | from .local_provider import LocalProvider 9 | 10 | 11 | def register_plugin(application): 12 | """Register plugin classes with application.""" 13 | application.register_class('file_list', _('Local file list'), FileList) 14 | application.register_class('trash_list', _('Trash can'), TrashList) 15 | 16 | # register providers 17 | application.register_provider(GioProvider) 18 | application.register_provider(SambaProvider) 19 | application.register_provider(FtpProvider) 20 | application.register_provider(SftpProvider) 21 | application.register_provider(NetworkProvider) 22 | application.register_provider(TrashProvider) 23 | application.register_provider(DavProvider) 24 | application.register_provider(DavsProvider) 25 | application.register_provider(Gphoto2Provider) 26 | application.register_provider(MtpProvider) 27 | application.register_provider(ArchiveProvider) 28 | 29 | # register mount manager extension 30 | application.register_mount_manager_extension(SambaExtension) 31 | application.register_mount_manager_extension(FtpExtension) 32 | application.register_mount_manager_extension(SftpExtension) 33 | application.register_mount_manager_extension(DavExtension) 34 | -------------------------------------------------------------------------------- /sunflower/plugins/file_list/trash_list.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | from .file_list import FileList 4 | from .gio_provider import TrashProvider 5 | from sunflower.operation import DeleteOperation 6 | 7 | 8 | class TrashList(FileList): 9 | """Trash file list plugin 10 | 11 | Generic operations related to trash management are provided with this 12 | class. By extending FileList standard features such as drag and drop are 13 | supported. 14 | 15 | """ 16 | 17 | def __init__(self, parent, notebook, options): 18 | FileList.__init__(self, parent, notebook, options) 19 | 20 | def _create_buttons(self): 21 | """Create titlebar buttons.""" 22 | options = self._parent.options 23 | 24 | # empty trash button 25 | self._empty_button = Gtk.Button.new_from_icon_name('user-trash-symbolic', Gtk.IconSize.MENU) 26 | self._empty_button.set_focus_on_click(False) 27 | self._empty_button.set_tooltip_text(_('Empty trash')) 28 | self._empty_button.connect('clicked', self.empty_trash) 29 | self._title_bar.add_control(self._empty_button) 30 | 31 | def empty_trash(self, widget=None, data=None): 32 | """Empty trash can.""" 33 | # ask user to confirm 34 | dialog = Gtk.MessageDialog( 35 | self._parent, 36 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 37 | Gtk.MessageType.QUESTION, 38 | Gtk.ButtonsType.YES_NO, 39 | _( 40 | "All items in the Trash will be permanently deleted. " 41 | "Are you sure?" 42 | ) 43 | ) 44 | dialog.set_default_response(Gtk.ResponseType.YES) 45 | result = dialog.run() 46 | dialog.destroy() 47 | 48 | # remove all items in trash 49 | if result == Gtk.ResponseType.YES: 50 | provider = self.get_provider() 51 | 52 | # create delete operation 53 | operation = DeleteOperation( 54 | self._parent, 55 | provider 56 | ) 57 | 58 | operation.set_force_delete(True) 59 | operation.set_selection(provider.list_dir(provider.get_root_path(None))) 60 | 61 | # perform removal 62 | operation.start() 63 | 64 | def change_path(self, path=None, selected=None): 65 | """Change file list path.""" 66 | if path is not None and not path.startswith('trash://'): 67 | path = 'trash:///' 68 | 69 | FileList.change_path(self, path, selected) 70 | -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/find_file_extensions/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/contents.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from sunflower.plugin_base.provider import FileType, Mode 5 | from sunflower.plugin_base.find_extension import FindExtension 6 | 7 | 8 | class ContentsFindFiles(FindExtension): 9 | """Extension for finding specified contents in files""" 10 | 11 | def __init__(self, parent): 12 | FindExtension.__init__(self, parent) 13 | 14 | # create container 15 | vbox = Gtk.VBox(False, 0) 16 | 17 | viewport = Gtk.ScrolledWindow() 18 | viewport.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 19 | viewport.set_shadow_type(Gtk.ShadowType.IN) 20 | 21 | # create entry widget 22 | label_content = Gtk.Label(label=_('Search for:')) 23 | label_content.set_alignment(0, 0.5) 24 | 25 | self._buffer = Gtk.TextBuffer() 26 | self._text_view = Gtk.TextView(buffer=self._buffer) 27 | 28 | # pack interface 29 | viewport.add(self._text_view) 30 | 31 | vbox.pack_start(label_content, False, False, 0) 32 | vbox.pack_start(viewport, True, True, 0) 33 | 34 | self.container.pack_start(vbox, True, True, 0) 35 | 36 | def get_title(self): 37 | """Return i18n title for extension""" 38 | return _('Content') 39 | 40 | def is_path_ok(self, provider, path): 41 | """Check if specified path fits the criteria""" 42 | result = False 43 | file_type = provider.get_stat(path).type 44 | 45 | if file_type is FileType.REGULAR: 46 | # get buffer 47 | text = self._buffer.get_text(*self._buffer.get_bounds(), include_hidden_chars=True) 48 | 49 | # try finding content in file 50 | try: 51 | with provider.get_file_handle(path, Mode.READ) as raw_file: # make sure file is closed afterwards 52 | result = text.encode() in raw_file.read() 53 | 54 | except IOError: 55 | pass 56 | 57 | return result 58 | 59 | -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/default.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from builtins import map 3 | 4 | import os 5 | import fnmatch 6 | 7 | from gi.repository import Gtk 8 | from sunflower.plugin_base.find_extension import FindExtension 9 | 10 | 11 | class DefaultFindFiles(FindExtension): 12 | """Default extension for find files tool""" 13 | 14 | def __init__(self, parent): 15 | FindExtension.__init__(self, parent) 16 | 17 | self.active = True 18 | self._pattern = '*' 19 | self._compare_method = fnmatch.fnmatch 20 | 21 | # prepare options 22 | plugin_options = parent._application.plugin_options 23 | self._options = plugin_options.create_section(self.__class__.__name__) 24 | 25 | # connect notify signal 26 | parent.connect('notify-start', self.__handle_notify_start) 27 | 28 | # create label showing pattern help 29 | label_help = Gtk.Label() 30 | label_help.set_alignment(0, 0) 31 | label_help.set_use_markup(True) 32 | 33 | label_help.set_markup(_( 34 | 'Pattern matching\n' 35 | '*\t\tEverything\n' 36 | '?\t\tAny single character\n' 37 | '[seq]\tAny character in seq\n' 38 | '[!seq]\tAny character not in seq' 39 | )) 40 | 41 | # create containers 42 | hbox = Gtk.HBox(True, 15) 43 | vbox_left = Gtk.VBox(False, 5) 44 | vbox_right = Gtk.VBox(False, 0) 45 | 46 | # create interface 47 | vbox_pattern = Gtk.VBox(False, 0) 48 | 49 | label_pattern = Gtk.Label(label=_('Search for:')) 50 | label_pattern.set_alignment(0, 0.5) 51 | 52 | self._entry_pattern = Gtk.ComboBoxText.new_with_entry() 53 | self._entry_pattern.connect('changed', self.__handle_pattern_change) 54 | 55 | self._checkbox_case_sensitive = Gtk.CheckButton(_('Case sensitive')) 56 | self._checkbox_case_sensitive.connect('toggled', self.__handle_case_sensitive_toggle) 57 | 58 | # pack interface 59 | vbox_pattern.pack_start(label_pattern, False, False, 0) 60 | vbox_pattern.pack_start(self._entry_pattern, False, False, 0) 61 | 62 | vbox_left.pack_start(vbox_pattern, False, False, 0) 63 | vbox_left.pack_start(self._checkbox_case_sensitive, False, False, 0) 64 | 65 | vbox_right.pack_start(label_help, True, True, 0) 66 | 67 | hbox.pack_start(vbox_left, True, True, 0) 68 | hbox.pack_start(vbox_right, True, True, 0) 69 | 70 | self.container.pack_start(hbox, True, True, 0) 71 | 72 | # load saved values 73 | self._load_history() 74 | self._pattern = self._entry_pattern.get_active_text() 75 | self._case_sensitive = False 76 | 77 | def __handle_case_sensitive_toggle(self, widget, data=None): 78 | """Handle toggling case sensitive check box""" 79 | self._compare_method = ( 80 | fnmatch.fnmatch, 81 | fnmatch.fnmatchcase 82 | )[widget.get_active()] 83 | self._case_sensitive = widget.get_active() 84 | 85 | def __handle_pattern_change(self, widget, data=None): 86 | """Handle changing pattern""" 87 | self._pattern = widget.get_child().get_text() 88 | 89 | def __handle_notify_start(self, data=None): 90 | """Handle starting search.""" 91 | entries = self._options.get('patterns') or [] 92 | 93 | # insert pattern to search history 94 | if self._pattern is not None and self._pattern not in entries: 95 | entries.insert(0, self._pattern) 96 | entries = entries[:20] 97 | 98 | # save history 99 | self._options.set('patterns', entries) 100 | 101 | def _load_history(self): 102 | """Load previously stored patterns.""" 103 | entries = self._options.get('patterns') or ['*'] 104 | 105 | for entry in entries: 106 | self._entry_pattern.append_text(entry) 107 | 108 | # select first entry 109 | self._entry_pattern.handler_block_by_func(self.__handle_pattern_change) 110 | self._entry_pattern.get_child().set_text(entries[0]) 111 | self._entry_pattern.handler_unblock_by_func(self.__handle_pattern_change) 112 | 113 | def get_title(self): 114 | """Return i18n title for extension""" 115 | return _('Basic') 116 | 117 | def is_path_ok(self, provider, path): 118 | """Check is specified path fits the cirteria""" 119 | result = False 120 | file_name = os.path.basename(path) 121 | 122 | # prepare patterns 123 | patterns = (self._pattern,) if ';' not in self._pattern else self._pattern.split(';') 124 | 125 | if not self._case_sensitive: 126 | file_name = file_name.lower() 127 | patterns = map(lambda x: x.lower(), patterns) 128 | 129 | # try to match any of the patterns 130 | for pattern in patterns: 131 | if self._compare_method(file_name, pattern): 132 | result = True 133 | break 134 | 135 | return result 136 | -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Basic find file options 3 | 4 | [Description] 5 | en=Default and most basic extensions for find files tool. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .default import DefaultFindFiles 4 | from .size import SizeFindFiles 5 | from .contents import ContentsFindFiles 6 | 7 | 8 | def register_plugin(application): 9 | """register plugin classes with application""" 10 | application.register_find_extension('default', DefaultFindFiles) 11 | application.register_find_extension('size', SizeFindFiles) 12 | application.register_find_extension('contents', ContentsFindFiles) 13 | -------------------------------------------------------------------------------- /sunflower/plugins/find_file_extensions/size.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from sunflower.plugin_base.find_extension import FindExtension 5 | 6 | 7 | class SizeFindFiles(FindExtension): 8 | """Size extension for find files tool""" 9 | 10 | def __init__(self, parent): 11 | FindExtension.__init__(self, parent) 12 | 13 | # create container 14 | table = Gtk.Table(2, 4, False) 15 | table.set_border_width(5) 16 | table.set_col_spacings(5) 17 | 18 | # create interface 19 | self._adjustment_max = Gtk.Adjustment(value=50.0, lower=0.0, upper=100000.0, step_incr=0.1, page_incr=10.0) 20 | self._adjustment_min = Gtk.Adjustment(value=0.0, lower=0.0, upper=10.0, step_incr=0.1, page_incr=10.0) 21 | 22 | label = Gtk.Label(label='{0}'.format(_('Match file size'))) 23 | label.set_alignment(0.0, 0.5) 24 | label.set_use_markup(True) 25 | 26 | label_min = Gtk.Label(label=_('Minimum:')) 27 | label_min.set_alignment(0, 0.5) 28 | label_min_unit = Gtk.Label(label=_('MB')) 29 | 30 | label_max = Gtk.Label(label=_('Maximum:')) 31 | label_max.set_alignment(0, 0.5) 32 | label_max_unit = Gtk.Label(label=_('MB')) 33 | 34 | self._entry_max = Gtk.SpinButton(adjustment=self._adjustment_max, digits=2) 35 | self._entry_min = Gtk.SpinButton(adjustment=self._adjustment_min, digits=2) 36 | self._entry_max.connect('value-changed', self._max_value_changed) 37 | self._entry_min.connect('value-changed', self._min_value_changed) 38 | self._entry_max.connect('activate', self._parent.find_files) 39 | self._entry_min.connect('activate', lambda entry: self._entry_max.grab_focus()) 40 | 41 | # pack interface 42 | table.attach(label, 0, 3, 0, 1, xoptions=Gtk.AttachOptions.FILL) 43 | 44 | table.attach(label_min, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL) 45 | table.attach(self._entry_min, 1, 2, 1, 2, xoptions=Gtk.AttachOptions.FILL) 46 | table.attach(label_min_unit, 2, 3, 1, 2, xoptions=Gtk.AttachOptions.FILL) 47 | 48 | table.attach(label_max, 0, 1, 2, 3, xoptions=Gtk.AttachOptions.FILL) 49 | table.attach(self._entry_max, 1, 2, 2, 3, xoptions=Gtk.AttachOptions.FILL) 50 | table.attach(label_max_unit, 2, 3, 2, 3, xoptions=Gtk.AttachOptions.FILL) 51 | 52 | self.container.pack_start(table, False, False, 0) 53 | 54 | def _max_value_changed(self, entry): 55 | """Assign value to adjustment handler""" 56 | self._adjustment_min.set_upper(entry.get_value()) 57 | 58 | def _min_value_changed(self, entry): 59 | """Assign value to adjustment handler""" 60 | self._adjustment_max.set_lower(entry.get_value()) 61 | 62 | def get_title(self): 63 | """Return i18n title for extension""" 64 | return _('Size') 65 | 66 | def is_path_ok(self, provider, path): 67 | """Check is specified path fits the cirteria""" 68 | size = provider.get_stat(path).size 69 | size_max = self._entry_max.get_value() * 1048576 70 | size_min = self._entry_min.get_value() * 1048576 71 | return size_min < size < size_max 72 | -------------------------------------------------------------------------------- /sunflower/plugins/gvim_viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/gvim_viewer/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/gvim_viewer/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=GVim Viewer 3 | 4 | [Description] 5 | en=Extends viewer to support viewing files with GVim. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/gvim_viewer/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import subprocess 4 | 5 | from gi.repository import Gtk 6 | from sunflower.plugin_base.viewer_extension import ViewerExtension 7 | 8 | 9 | def register_plugin(application): 10 | """Register plugin class with application""" 11 | application.register_viewer_extension(('text/plain',), GVimViewer) 12 | 13 | 14 | class GVimViewer(ViewerExtension): 15 | """Viewer extension that embeds GVim window into notebook and allows you to 16 | view files using your configuration. 17 | 18 | """ 19 | 20 | def __init__(self, parent): 21 | ViewerExtension.__init__(self, parent) 22 | 23 | self._process = None 24 | 25 | # create container 26 | self._container = Gtk.Viewport() 27 | self._container.set_shadow_type(Gtk.ShadowType.IN) 28 | 29 | # create socket for embeding GVim window 30 | self._socket = Gtk.Socket() 31 | self._socket.connect('realize', self.__socket_realized) 32 | 33 | # pack interface 34 | self._container.add(self._socket) 35 | 36 | def __socket_realized(self, widget, data=None): 37 | """Connect process when socket is realized""" 38 | socket_id = self._socket.get_id() 39 | 40 | # generate command string 41 | command = ( 42 | 'gvim', 43 | '--socketid', str(socket_id), 44 | '-R', self._parent.path 45 | ) 46 | 47 | # create new process 48 | self._process = subprocess.Popen(command) 49 | 50 | def get_title(self): 51 | """Return page title""" 52 | return _('GVim') 53 | 54 | def get_container(self): 55 | """Return container widget to be embedded to notebook""" 56 | return self._container 57 | 58 | def focus_object(self): 59 | """Focus main object in extension""" 60 | self._socket.child_focus(Gtk.DIR_TAB_FORWARD) 61 | -------------------------------------------------------------------------------- /sunflower/plugins/owner_column/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/owner_column/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/owner_column/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Owner and group column 3 | 4 | [Description] 5 | en=Add support for displaying owner and group in lists. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/owner_column/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from sunflower.plugins.file_list.file_list import Column 5 | from sunflower.plugins.file_list.plugin import FileList 6 | from sunflower.plugin_base.column_extension import ColumnExtension 7 | 8 | 9 | def register_plugin(application): 10 | """Register plugin class with application""" 11 | application.register_column_extension(FileList, OwnerColumn) 12 | application.register_column_extension(FileList, GroupColumn) 13 | 14 | 15 | class BaseColumn(ColumnExtension): 16 | """Base class for extending owner and group for item list""" 17 | 18 | def __init__(self, parent, store): 19 | ColumnExtension.__init__(self, parent, store) 20 | self._parent = parent 21 | # create column object 22 | self._create_column() 23 | 24 | def _create_column(self): 25 | """Create column""" 26 | self._cell_renderer = Gtk.CellRendererText() 27 | self._parent.set_default_font_size(self._get_column_name(), 8) 28 | 29 | self._column = Gtk.TreeViewColumn(self._get_column_title()) 30 | self._column.pack_start(self._cell_renderer, True) 31 | self._column.name = self._get_column_name() 32 | 33 | def _get_column_name(self): 34 | """Returns column name""" 35 | return None 36 | 37 | def _get_column_title(self): 38 | """Returns column title""" 39 | return None 40 | 41 | def __set_cell_data(self, column, cell, store, selected_iter, data=None): 42 | """Set column value""" 43 | pass 44 | 45 | 46 | class OwnerColumn(BaseColumn): 47 | """Adds support for displaying owner in item list""" 48 | 49 | def __set_cell_data(self, column, cell, store, selected_iter, data=None): 50 | """Set column value""" 51 | is_parent = store.get_value(selected_iter, Column.IS_PARENT_DIR) 52 | 53 | value = (store.get_value(selected_iter, Column.USER_ID), '')[is_parent] 54 | cell.set_property('text', str(value)) 55 | 56 | def _create_column(self): 57 | """Configure column""" 58 | BaseColumn._create_column(self) 59 | self._column.set_cell_data_func(self._cell_renderer, self.__set_cell_data) 60 | 61 | def _get_column_name(self): 62 | """Returns column name""" 63 | return 'owner' 64 | 65 | def _get_column_title(self): 66 | """Returns column title""" 67 | return _('Owner') 68 | 69 | def get_sort_column(self): 70 | """Return sort column""" 71 | return Column.USER_ID 72 | 73 | 74 | class GroupColumn(BaseColumn): 75 | """Adds support for displaying group in item list""" 76 | 77 | def __set_cell_data(self, column, cell, store, selected_iter, data=None): 78 | """Set column value""" 79 | is_parent = store.get_value(selected_iter, Column.IS_PARENT_DIR) 80 | 81 | value = (store.get_value(selected_iter, Column.GROUP_ID), '')[is_parent] 82 | cell.set_property('text', str(value)) 83 | 84 | def _create_column(self): 85 | """Configure column""" 86 | BaseColumn._create_column(self) 87 | self._column.set_cell_data_func(self._cell_renderer, self.__set_cell_data) 88 | 89 | def _get_column_name(self): 90 | """Returns column name""" 91 | return 'group' 92 | 93 | def _get_column_title(self): 94 | """Returns column title""" 95 | return _('Group') 96 | 97 | def get_sort_column(self): 98 | """Return sort column""" 99 | return Column.GROUP_ID 100 | -------------------------------------------------------------------------------- /sunflower/plugins/rename_extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/rename_extensions/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/rename_extensions/audio_metadata.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import string 5 | 6 | from gi.repository import Gtk 7 | from sunflower.plugin_base.rename_extension import RenameExtension 8 | 9 | try: 10 | import mutagen 11 | USE_MUTAGEN = True 12 | 13 | except ImportError: 14 | USE_MUTAGEN = False 15 | 16 | 17 | class AudioMetadataRename(RenameExtension): 18 | """Song tags rename extension""" 19 | 20 | def __init__(self, parent): 21 | RenameExtension.__init__(self, parent) 22 | 23 | self._templates = { 24 | '[a]': ('album', _('Album')), 25 | '[A]': ('artist', _('Artist')), 26 | '[T]': ('title', _('Title')), 27 | '[G]': ('genre', _('Genre')), 28 | '[D]': ('date', _('Date')), 29 | '[t]': ('tracknumber', _('Track number')), 30 | } 31 | 32 | # create template entry 33 | label_template = Gtk.Label(label=_('Template:')) 34 | label_template.set_alignment(0, 0.5) 35 | 36 | self._entry_template = Gtk.Entry() 37 | self._entry_template.set_text('[[t]] [A] - [T]') 38 | self._entry_template.connect('changed', self._update_parent_list) 39 | 40 | # create replace entry 41 | label_replace1 = Gtk.Label(label=_('Replace:')) 42 | label_replace1.set_alignment(0, 0.5) 43 | 44 | self._entry_replace = Gtk.Entry() 45 | self._entry_replace.set_text(',?/') 46 | self._entry_replace.connect('changed', self._update_parent_list) 47 | 48 | # create replace combo boxes 49 | label_replace2 = Gtk.Label(label=_('With:')) 50 | label_replace2.set_alignment(0, 0.5) 51 | 52 | self._combobox_replace = Gtk.ComboBoxText.new_with_entry() 53 | self._combobox_replace.connect('changed', self._update_parent_list) 54 | 55 | for str_rep in ('_', '-', ''): 56 | self._combobox_replace.append_text(str_rep) 57 | 58 | # create syntax 59 | label_tip = Gtk.Label() 60 | label_tip.set_alignment(0, 0) 61 | label_tip.set_use_markup(True) 62 | label_tip.set_markup('{0}\n{1}'.format(_('Template syntax'), 63 | '\n'.join(['{0}\t{1}'.format(k, v[1]) for k, v in self._templates.items()]))) 64 | 65 | # create boxes 66 | hbox = Gtk.HBox(True, 15) 67 | vbox_left = Gtk.VBox(False, 5) 68 | vbox_right = Gtk.VBox(False, 0) 69 | vbox_template = Gtk.VBox(False, 0) 70 | table_replace = Gtk.Table(2, 2, False) 71 | table_replace.set_border_width(5) 72 | 73 | frame_replace = Gtk.Frame(label=_('Character replacement')) 74 | 75 | # disable checkbox if mutagen is not available 76 | self._checkbox_active.set_sensitive(USE_MUTAGEN) 77 | 78 | # create warning label 79 | label_warning = Gtk.Label(label=_( 80 | 'In order to use this extension you need mutagen module installed!' 81 | )) 82 | label_warning.set_use_markup(True) 83 | 84 | infobar = Gtk.InfoBar() 85 | infobar.set_property('no-show-all', USE_MUTAGEN) 86 | infobar_content = infobar.get_content_area() 87 | infobar_content.add(label_warning) 88 | 89 | # pack gui 90 | vbox_template.pack_start(label_template, False, False, 0) 91 | vbox_template.pack_start(self._entry_template, False, False, 0) 92 | 93 | self.vbox.remove(self._checkbox_active) 94 | 95 | table_replace.attach(label_replace1, 0, 1, 0, 1) 96 | table_replace.attach(self._entry_replace, 1, 2, 0, 1, xoptions=Gtk.AttachOptions.FILL) 97 | table_replace.attach(label_replace2, 0, 1, 1, 2) 98 | table_replace.attach(self._combobox_replace, 1, 2, 1, 2, xoptions=Gtk.AttachOptions.FILL) 99 | 100 | frame_replace.add(table_replace) 101 | 102 | vbox_left.pack_start(self._checkbox_active, False, False, 0) 103 | vbox_left.pack_start(vbox_template, False, False, 0) 104 | vbox_left.pack_start(frame_replace, False, False, 0) 105 | 106 | vbox_right.pack_start(label_tip, False, False, 0) 107 | 108 | hbox.pack_start(vbox_left, True, True, 0) 109 | hbox.pack_start(vbox_right, True, True, 0) 110 | 111 | self.vbox.pack_start(infobar, False, False, 0) 112 | self.vbox.pack_start(hbox, False, False, 0) 113 | 114 | def get_title(self): 115 | """Return extension title""" 116 | return _('Audio Metadata') 117 | 118 | def get_new_name(self, old_name, new_name): 119 | """Get modified name""" 120 | basename, extension = os.path.splitext(new_name) 121 | path = os.path.join(self._parent._parent._parent.get_active_object().path, old_name) 122 | template = self._entry_template.get_text() 123 | tags = mutagen.File(path, easy=True) 124 | 125 | # check if filetype is supported by mutagen 126 | if tags is None: 127 | return new_name 128 | 129 | # fill template 130 | for k, v in self._templates.items(): 131 | try: 132 | template = string.replace(template, k, tags[v[0]][0]) 133 | except KeyError: 134 | template = string.replace(template, k, '') 135 | 136 | # replace unwanted characters 137 | str_rep = self._combobox_replace.get_active_text() 138 | for c in self._entry_replace.get_text(): 139 | template = string.replace(template, c, str_rep) 140 | 141 | return '{0}{1}'.format(template, extension) 142 | 143 | -------------------------------------------------------------------------------- /sunflower/plugins/rename_extensions/letter_case.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import string 5 | 6 | from gi.repository import Gtk 7 | from sunflower.plugin_base.rename_extension import RenameExtension 8 | 9 | 10 | class LetterCaseRename(RenameExtension): 11 | """Letter case rename extension support""" 12 | 13 | def __init__(self, parent): 14 | RenameExtension.__init__(self, parent) 15 | 16 | self._basename_methods = ( 17 | (_('Do nothing'), self.__do_nothing), 18 | (_('Capitalize'), self.__capitalize), 19 | (_('Upper case'), self.__upper), 20 | (_('Lower case'), self.__lower), 21 | (_('Swap case'), self.__swap), 22 | ) 23 | 24 | self._extension_methods = ( 25 | (_('Do nothing'), self.__do_nothing), 26 | (_('Upper case'), self.__upper), 27 | (_('Lower case'), self.__lower), 28 | ) 29 | 30 | # create labels 31 | label_basename = Gtk.Label(label=_('Item name:')) 32 | label_basename.set_alignment(0, 0.5) 33 | 34 | label_extension = Gtk.Label(label=_('Extension:')) 35 | label_extension.set_alignment(0, 0.5) 36 | 37 | # create combo boxes 38 | self._combo_basename = Gtk.ComboBoxText() 39 | self._combo_basename.connect('changed', self._update_parent_list) 40 | 41 | self._combo_extension = Gtk.ComboBoxText() 42 | self._combo_extension.connect('changed', self._update_parent_list) 43 | 44 | # fill comboboxes 45 | for method in self._basename_methods: 46 | self._combo_basename.append_text(method[0]) 47 | 48 | for method in self._extension_methods: 49 | self._combo_extension.append_text(method[0]) 50 | 51 | self._combo_basename.set_active(0) 52 | self._combo_extension.set_active(0) 53 | 54 | # pack gui 55 | table = Gtk.Table(2, 2, False) 56 | table.set_col_spacing(0, 5) 57 | table.set_row_spacings(5) 58 | 59 | table.attach(label_basename, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.FILL) 60 | table.attach(label_extension, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL) 61 | 62 | table.attach(self._combo_basename, 1, 2, 0, 1, xoptions=Gtk.AttachOptions.FILL) 63 | table.attach(self._combo_extension, 1, 2, 1, 2, xoptions=Gtk.AttachOptions.FILL) 64 | 65 | self.vbox.pack_start(table, False, False, 0) 66 | 67 | def __do_nothing(self, name): 68 | """Return the same string""" 69 | return name 70 | 71 | def __capitalize(self, name): 72 | """Return capitalized string""" 73 | return string.capwords(name) 74 | 75 | def __upper(self, name): 76 | """Return upper case string""" 77 | return name.upper() 78 | 79 | def __lower(self, name): 80 | """Return lower case string""" 81 | return name.lower() 82 | 83 | def __swap(self, name): 84 | """Swap case in string""" 85 | return name.swapcase() 86 | 87 | def get_title(self): 88 | """Return extension title""" 89 | return _('Letter Case') 90 | 91 | def get_new_name(self, old_name, new_name): 92 | """Get modified name""" 93 | basename, extension = os.path.splitext(new_name) 94 | new_basename = self._basename_methods[self._combo_basename.get_active()][1](basename) 95 | new_extension = self._extension_methods[self._combo_extension.get_active()][1](extension) 96 | return "{0}{1}".format(new_basename, new_extension) 97 | -------------------------------------------------------------------------------- /sunflower/plugins/rename_extensions/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Basic rename options 3 | 4 | [Description] 5 | en=Default and most basic extensions for advanced rename tool. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/rename_extensions/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .default import DefaultRename 4 | from .letter_case import LetterCaseRename 5 | from .audio_metadata import AudioMetadataRename 6 | 7 | 8 | def register_plugin(application): 9 | """Register plugin classes with application""" 10 | application.register_rename_extension('default', DefaultRename) 11 | application.register_rename_extension('letter_case', LetterCaseRename) 12 | application.register_rename_extension('audio_metadata', AudioMetadataRename) 13 | -------------------------------------------------------------------------------- /sunflower/plugins/sessions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/sessions/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/sessions/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=Sessions 3 | pl=Sesje 4 | 5 | [Description] 6 | en=This plugin enables you to define and manage automatically updated work sessions. 7 | 8 | [Version] 9 | number= 10 | 11 | [Author] 12 | name=Sunflower developers 13 | contact= 14 | site=https://sunflower-fm.org 15 | 16 | -------------------------------------------------------------------------------- /sunflower/plugins/system_terminal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/plugins/system_terminal/__init__.py -------------------------------------------------------------------------------- /sunflower/plugins/system_terminal/plugin.conf: -------------------------------------------------------------------------------- 1 | [Name] 2 | en=System terminal 3 | 4 | [Description] 5 | en=Provides system terminal tab. This plugin is required and can not be disabled. 6 | 7 | [Version] 8 | number= 9 | 10 | [Author] 11 | name=Sunflower developers 12 | contact= 13 | site=https://sunflower-fm.org 14 | 15 | -------------------------------------------------------------------------------- /sunflower/plugins/system_terminal/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | import shlex 6 | import subprocess 7 | 8 | from gi.repository import GLib, Vte 9 | 10 | from sunflower.parameters import Parameters 11 | from sunflower.plugin_base.terminal import Terminal, TerminalType 12 | 13 | 14 | def register_plugin(application): 15 | """Register plugin classes with application""" 16 | application.register_class('system_terminal', _('System terminal'), SystemTerminal) 17 | 18 | 19 | class SystemTerminal(Terminal): 20 | """System terminal plugin""" 21 | 22 | def __init__(self, parent, notebook, options): 23 | Terminal.__init__(self, parent, notebook, options) 24 | 25 | # variable to store process id 26 | self._pid = None 27 | 28 | # make sure we open in a good path 29 | self.path = self._options.get('path', os.path.expanduser('~')) 30 | self._close_on_child_exit = self._options.get('close_with_child', True) 31 | self._terminal_type = self._parent.options.section('terminal').get('type') 32 | 33 | if self._terminal_type == TerminalType.VTE: 34 | # we need TERM environment variable set 35 | if not 'TERM' in os.environ: 36 | os.environ['TERM'] = 'xterm-color' 37 | os.environ['COLORTERM'] = 'gnome-terminal' 38 | 39 | # fork default shell 40 | self._terminal.connect('child-exited', self.__child_exited) 41 | self._terminal.connect('realize', self.__terminal_realized) 42 | 43 | elif self._terminal_type == TerminalType.EXTERNAL: 44 | # connect signals 45 | self._terminal.connect('realize', self.__socket_realized) 46 | self._terminal.connect('plug-removed', self.__child_exited) 47 | 48 | # disable controls 49 | self._menu_button.set_sensitive(False) 50 | 51 | shell_command = self._options.get('shell_command', os.environ['SHELL']) 52 | 53 | # change titles 54 | self._change_tab_text(_('Terminal')) 55 | self._title_bar.set_title(_('Terminal')) 56 | self._title_bar.set_subtitle(shell_command) 57 | 58 | self.show_all() 59 | 60 | def __socket_realized(self, widget, data=None): 61 | """Connect process when socket is realized""" 62 | socket_id = self._terminal.get_id() 63 | shell_command = self._options.get('shell_command', None) 64 | command_version = 'command' if shell_command is None else 'command2' 65 | arguments = self._options.get('arguments', []) 66 | 67 | # append additional parameter if we need to wait for command to finish 68 | if not self._options.get('close_with_child'): 69 | arguments.extend(('&&', 'read')) 70 | 71 | arguments_string = ' '.join(arguments) 72 | 73 | # parse command 74 | terminal_command = self._parent.options.section('terminal').get(command_version) 75 | terminal_command = shlex.split(terminal_command.format(socket_id, arguments_string)) 76 | 77 | # execute process 78 | process = subprocess.Popen(terminal_command, cwd=self.path) 79 | self._pid = process.pid 80 | 81 | def __terminal_realized(self, widget, data=None): 82 | """Event called once terminal emulator is realized""" 83 | shell_command = self._options.get('shell_command', os.environ['SHELL']) 84 | 85 | command = { 86 | 'pty_flags': Vte.PtyFlags.DEFAULT, 87 | 'working_directory': self.path, 88 | 'argv': self._options.get('arguments', [shell_command]), 89 | 'envv': [], 90 | 'spawn_flags': GLib.SpawnFlags.DO_NOT_REAP_CHILD, 91 | 'child_setup': None, 92 | 'child_setup_data': None 93 | } 94 | 95 | # since VTE 0.38 fork_command_full has been renamed spawn_sync 96 | if hasattr(self._terminal, 'fork_command_full'): 97 | (result, self._pid) = self._terminal.fork_command_full(**command) 98 | else: 99 | (result, self._pid) = self._terminal.spawn_sync(**command) 100 | 101 | def __child_exited(self, widget, data=None): 102 | """Handle child process termination""" 103 | already_closing = self._notebook.page_num(self) == -1 104 | 105 | if not already_closing and self._close_on_child_exit or self._terminal_type == TerminalType.EXTERNAL: 106 | self._close_tab() 107 | 108 | return True 109 | 110 | def __update_path_from_pid(self): 111 | """Update terminal path from child process""" 112 | try: 113 | if self._pid is not None and os.path.isdir('/proc/{0}'.format(self._pid)): 114 | self.path = os.readlink('/proc/{0}/cwd'.format(self._pid)) 115 | self._options.set('path', self.path) 116 | except: 117 | pass 118 | 119 | def _close_tab(self, widget=None, data=None): 120 | """Provide additional functionality""" 121 | if self._notebook.get_n_pages() == 1: 122 | DefaultList = self._parent.plugin_classes['file_list'] 123 | options = Parameters() 124 | options.set('path', self.path) 125 | 126 | self._parent.create_tab(self._notebook, DefaultList, options) 127 | 128 | return Terminal._close_tab(self, widget, data) 129 | 130 | def _handle_tab_close(self): 131 | """Clean up before closing tab""" 132 | Terminal._handle_tab_close(self) 133 | self.__update_path_from_pid() 134 | 135 | def _create_file_list(self, widget=None, data=None): 136 | """Create file list in parent notebook""" 137 | self.__update_path_from_pid() 138 | DefaultList = self._parent.plugin_classes['file_list'] 139 | options = Parameters() 140 | options.set('path', self.path) 141 | self._parent.create_tab(self._notebook, DefaultList, options) 142 | return True 143 | -------------------------------------------------------------------------------- /sunflower/queue.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from threading import Lock 5 | from queue import Queue, Empty 6 | 7 | 8 | class OperationQueue: 9 | """Generic, multi-name, operation queueing support class.""" 10 | _queue_list = {} 11 | _active_list = {} 12 | _list_store = None 13 | _lock = Lock() 14 | 15 | COLUMN_TEXT = 0 16 | COLUMN_TYPE = 1 17 | 18 | TYPE_QUEUE = 0 19 | TYPE_NONE = 1 20 | TYPE_NEW = 2 21 | TYPE_SEPARATOR = 3 22 | 23 | @classmethod 24 | def __update_list(cls): 25 | """Update list store to contain all the queues.""" 26 | # clear options 27 | cls._list_store.clear() 28 | 29 | # add no queue option 30 | cls._list_store.append((_('None'), cls.TYPE_NONE)) 31 | 32 | # create default queue 33 | default_name = _('Default') 34 | if default_name not in cls._queue_list: 35 | cls._lock.acquire() 36 | cls._queue_list[default_name] = Queue() 37 | cls._active_list[default_name] = False 38 | cls._lock.release() 39 | 40 | # add queues 41 | for name in cls._queue_list.keys(): 42 | cls._list_store.append((name, cls.TYPE_QUEUE)) 43 | 44 | # add option for new queue 45 | cls._list_store.append((None, cls.TYPE_SEPARATOR)) 46 | cls._list_store.append((_('New queue'), cls.TYPE_NEW)) 47 | 48 | @classmethod 49 | def add(cls, name, event): 50 | """Add operation to queue.""" 51 | # make sure queue exists 52 | if name not in cls._queue_list: 53 | cls._lock.acquire() 54 | cls._queue_list[name] = Queue() 55 | cls._active_list[name] = False 56 | cls._lock.release() 57 | cls.__update_list() 58 | 59 | # add operation to specified queue 60 | cls._queue_list[name].put(event, False) 61 | 62 | # start operation immediately if queue is empty 63 | if not cls._active_list[name]: 64 | cls._lock.acquire() 65 | cls._active_list[name] = True 66 | cls._lock.release() 67 | cls.start_next(name) 68 | 69 | @classmethod 70 | def start_next(cls, name): 71 | """Start next operation in specified queue.""" 72 | if name not in cls._queue_list: 73 | return 74 | 75 | # get operation event and clear it 76 | try: 77 | event = cls._queue_list[name].get(False) 78 | 79 | except Empty: 80 | # last operation finished, mark as inactive 81 | cls._lock.acquire() 82 | cls._active_list[name] = False 83 | cls._lock.release() 84 | 85 | else: 86 | event.set() 87 | 88 | @classmethod 89 | def get_list(cls): 90 | """Return list of available queues.""" 91 | return cls._queue_list.keys() 92 | 93 | @classmethod 94 | def get_model(cls): 95 | """Return model to be used with different widgets.""" 96 | if cls._list_store is None: 97 | cls._list_store = Gtk.ListStore(str, int) 98 | cls.__update_list() 99 | 100 | return cls._list_store 101 | 102 | @classmethod 103 | def get_name_from_iter(cls, selected_iter): 104 | """Get queue name from specified iter.""" 105 | result = None 106 | 107 | if selected_iter is not None: 108 | selection_type = cls._list_store.get_value(selected_iter, cls.COLUMN_TYPE) 109 | 110 | if selection_type is cls.TYPE_QUEUE: 111 | result = cls._list_store.get_value(selected_iter, cls.COLUMN_TEXT) 112 | 113 | return result 114 | 115 | @classmethod 116 | def handle_separator_check(cls, model, current_iter, data=None): 117 | """Test if specified iter is a separator.""" 118 | return model.get_value(current_iter, cls.COLUMN_TYPE) == cls.TYPE_SEPARATOR 119 | 120 | @classmethod 121 | def handle_queue_select(cls, widget, dialog): 122 | """Handle changing operation queue or adding a new one.""" 123 | selected_iter = widget.get_active_iter() 124 | if selected_iter is None: 125 | return False 126 | 127 | model = widget.get_model() 128 | option_type = model.get_value(selected_iter, cls.COLUMN_TYPE) 129 | 130 | # we handle only new option selection 131 | if option_type != cls.TYPE_NEW: 132 | return False 133 | 134 | # import locally to avoid circular imports 135 | from sunflower.gui.input_dialog import InputDialog 136 | 137 | # create dialog 138 | dialog = InputDialog(dialog) 139 | dialog.set_title(_('New operation queue')) 140 | dialog.set_label(_('Enter name for new operation queue:')) 141 | 142 | # get response from the user 143 | response = dialog.get_response() 144 | 145 | if response[0] != Gtk.ResponseType.OK: 146 | widget.set_active(0) 147 | return False 148 | 149 | # make sure queue doesn't already exist 150 | if response[1] in cls._queue_list: 151 | dialog = Gtk.MessageDialog( 152 | dialog, 153 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 154 | Gtk.MessageType.ERROR, 155 | Gtk.ButtonsType.OK, 156 | _('Operation queue with specified name already exists.') 157 | ) 158 | dialog.run() 159 | dialog.destroy() 160 | return False 161 | 162 | # select newly added queue 163 | cls._lock.acquire() 164 | cls._queue_list[response[1]] = Queue() 165 | cls._active_list[response[1]] = False 166 | cls._lock.release() 167 | cls.__update_list() 168 | 169 | queue_index = 0 170 | for index, row in enumerate(cls._list_store): 171 | if row[0] == response[1]: 172 | queue_index = index 173 | break 174 | 175 | widget.set_active(queue_index) 176 | 177 | return True 178 | -------------------------------------------------------------------------------- /sunflower/toolbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk 4 | from sunflower.gui.input_dialog import CreateToolbarWidgetDialog 5 | 6 | 7 | class ToolbarManager: 8 | """Manager for toolbar widget factories""" 9 | 10 | def __init__(self, application): 11 | self._application = application 12 | 13 | self._config = None 14 | self._widget_types = {} 15 | self._factory_cache = {} 16 | self._factories = [] 17 | 18 | self._toolbar = Gtk.Toolbar() 19 | 20 | def get_toolbar(self): 21 | """Return toolbar widget""" 22 | return self._toolbar 23 | 24 | def get_description(self, widget_type): 25 | """Get widget description for specified type""" 26 | result = None 27 | 28 | data = self.get_widget_data(widget_type) 29 | if data is not None: 30 | result = data[0] 31 | 32 | return result 33 | 34 | def get_icon(self, widget_type): 35 | """Get icon name for specified widget type""" 36 | result = None 37 | 38 | data = self.get_widget_data(widget_type) 39 | if data is not None: 40 | result = data[1] 41 | 42 | return result 43 | 44 | def get_widget_data(self, widget_type): 45 | """Get data for specified widget type""" 46 | result = None 47 | 48 | if widget_type in self._widget_types: 49 | result = self._widget_types[widget_type] 50 | 51 | return result 52 | 53 | def load_config(self, config): 54 | """Set config parser for toolbar""" 55 | self._config = config 56 | 57 | def create_widgets(self): 58 | """Create widgets for toolbar""" 59 | # remove existing widgets 60 | self._toolbar.foreach(lambda item: self._toolbar.remove(item)) 61 | 62 | # create new widgets 63 | for item in self._config.get('items'): 64 | widget_name = item['name'] 65 | widget_type = item['type'] 66 | 67 | # skip creating widget if there's no factory for specified type 68 | if not widget_type in self._factory_cache: continue 69 | 70 | # get factory from cache 71 | factory = self._factory_cache[widget_type] 72 | 73 | # get config 74 | config = item['config'] 75 | widget = factory.get_widget(widget_name, widget_type, config) 76 | 77 | if widget is not None: 78 | widget.show() 79 | self._toolbar.add(widget) 80 | 81 | def register_factory(self, FactoryClass): 82 | """Register and create new factory""" 83 | factory = FactoryClass(self._application) 84 | 85 | # add factory to local storage 86 | self._factories.append(factory) 87 | 88 | # get widget list 89 | widgets = factory.get_types() 90 | 91 | # update types and factory cache 92 | if widgets is not None: 93 | self._widget_types.update(widgets) 94 | 95 | for widget_type in widgets.keys(): 96 | self._factory_cache[widget_type] = factory 97 | 98 | def show_create_widget_dialog(self, window=None): 99 | """Show dialog with type selection and name input""" 100 | result = False 101 | dialog = CreateToolbarWidgetDialog(self._application) 102 | 103 | # update dialog type list 104 | dialog.update_type_list(self._widget_types) 105 | 106 | # set transient window if specified 107 | if window is not None: 108 | dialog.set_transient_for(window) 109 | 110 | # get user response 111 | response, name, widget_type = dialog.get_response() 112 | 113 | if response == Gtk.ResponseType.ACCEPT: 114 | if None in (name, widget_type) or name == '': 115 | # user didn't input all the data 116 | dialog = Gtk.MessageDialog( 117 | window, 118 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 119 | Gtk.MessageType.ERROR, 120 | Gtk.ButtonsType.OK, 121 | _( 122 | "Error adding widget. You need to enter unique " 123 | "name and select widget type." 124 | ) 125 | ) 126 | dialog.run() 127 | dialog.destroy() 128 | 129 | else: 130 | # get factory from cache 131 | factory = self._factory_cache[widget_type] 132 | 133 | # present configuration dialog 134 | config = factory.create_widget(name, widget_type, window) 135 | 136 | # save config 137 | if config is not None: 138 | result = { 139 | 'name': name, 140 | 'type': widget_type, 141 | 'config': config, 142 | } 143 | 144 | return result 145 | 146 | def show_configure_widget_dialog(self, name, widget_type, widget_config, window=None): 147 | """Show blocking configuration dialog for specified widget""" 148 | if not widget_type in self._factory_cache: 149 | # there is no factory for specified type, show error and return 150 | dialog = Gtk.MessageDialog( 151 | window, 152 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 153 | Gtk.MessageType.ERROR, 154 | Gtk.ButtonsType.OK, 155 | _( 156 | "Plugin used to create selected toolbar widget is not active " 157 | "or not present. In order to edit this entry you need to activate " 158 | "plugin used to create it." 159 | ) 160 | ) 161 | dialog.run() 162 | dialog.destroy() 163 | 164 | return False 165 | 166 | # get factory 167 | factory = self._factory_cache[widget_type] 168 | 169 | # load config 170 | config = factory.configure_widget(name, widget_type, widget_config, window) 171 | if config: 172 | return config 173 | 174 | return {} 175 | 176 | def apply_settings(self): 177 | """Apply toolbar settings""" 178 | style = ( 179 | Gtk.ToolbarStyle.ICONS, 180 | Gtk.ToolbarStyle.TEXT, 181 | Gtk.ToolbarStyle.BOTH, 182 | Gtk.ToolbarStyle.BOTH_HORIZ, 183 | )[self._config.get('style')] 184 | self._toolbar.set_style(style) 185 | 186 | icon_size = ( 187 | Gtk.IconSize.SMALL_TOOLBAR, 188 | Gtk.IconSize.LARGE_TOOLBAR, 189 | Gtk.IconSize.DND, 190 | Gtk.IconSize.DIALOG, 191 | )[self._config.get('icon_size')] 192 | self._toolbar.set_icon_size(icon_size) 193 | -------------------------------------------------------------------------------- /sunflower/tools/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sunflower/tools/disk_usage.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from threading import Thread, Event, Lock 6 | from sunflower.plugin_base.monitor import MonitorSignals 7 | 8 | 9 | class DiskUsage: 10 | """Dedicated object for counting disk usage for specified directories.""" 11 | 12 | def __init__(self, application): 13 | self._stop_events = {} 14 | self._sizes = {} 15 | self._counts = {} 16 | self._lock = Lock() 17 | 18 | def __update_totals(self, parent_id, path, total_count, total_size): 19 | """Update global dictionaries with new statistics.""" 20 | key = (parent_id, path) 21 | 22 | self._lock.acquire() 23 | self._sizes[key] = total_size 24 | self._counts[key] = total_count 25 | self._lock.release() 26 | 27 | def __calculate_usage(self, parent_id, monitor_queue, provider, path, stop_event): 28 | """Threaded method used for calculating disk usage.""" 29 | total_count = 0 30 | total_size = 0 31 | scan_list = [] 32 | 33 | # add initial path for scanning 34 | scan_list.append(path) 35 | 36 | # loop through all paths and calculate 37 | while not stop_event.is_set(): 38 | # get path for scanning 39 | try: 40 | scan_path = scan_list.pop(0) 41 | 42 | except IndexError: 43 | # no more directories to traverse 44 | self.__update_totals(parent_id, path, total_count, total_size) 45 | monitor_queue.put((MonitorSignals.DIRECTORY_SIZE_CHANGED, path, None), False) 46 | stop_event.set() 47 | continue 48 | 49 | # get list of items in specified directory 50 | try: 51 | item_list = provider.list_dir(scan_path, relative_to=path) 52 | 53 | except OSError: 54 | # silently ignore errors 55 | continue 56 | 57 | else: 58 | relative_path = os.path.join(path, scan_path) 59 | 60 | for item in item_list: 61 | if provider.is_dir(item, relative_to=relative_path) \ 62 | and not provider.is_link(item, relative_to=relative_path): 63 | # queue up new directory to check 64 | scan_list.append(os.path.join(scan_path, item)) 65 | 66 | else: 67 | # update total statistics 68 | stat = provider.get_stat(item, relative_to=relative_path, extended=False, follow=False) 69 | total_count += 1 70 | total_size += stat.size 71 | 72 | # update monitor only once in a while 73 | if total_count % 50 == 0: 74 | self.__update_totals(parent_id, path, total_count, total_size) 75 | monitor_queue.put((MonitorSignals.DIRECTORY_SIZE_CHANGED, path, None), False) 76 | 77 | # notify monitor we are done 78 | monitor_queue.put((MonitorSignals.DIRECTORY_SIZE_STOPPED, path, None), False) 79 | 80 | def get(self, parent_object, path): 81 | """Get statistics for specified path.""" 82 | key = (id(parent_object), path) 83 | result = (0, 0) 84 | 85 | if key in self._sizes: 86 | result = ( 87 | self._counts[key], 88 | self._sizes[key] 89 | ) 90 | 91 | return result 92 | 93 | def calculate(self, parent_object, monitor_queue, provider, path): 94 | """Calculate disk usage for specified path.""" 95 | key = (id(parent_object), path) 96 | 97 | # if for some strange reason calculation is requested again 98 | if key in self._stop_events: 99 | return False 100 | 101 | # store event to allow stopping thread early 102 | stop_event = Event() 103 | self._stop_events[key] = stop_event 104 | 105 | # start calculation in new thread 106 | Thread(target=self.__calculate_usage, args=(key[0], monitor_queue, provider, path, stop_event)).start() 107 | 108 | return True 109 | 110 | def cancel(self, parent_object, path): 111 | """Cancel disk usage calculation for specified path requested by parent_object.""" 112 | key = (id(parent_object), path) 113 | 114 | if key in self._stop_events: 115 | # stop calculation thread 116 | self._stop_events[key].set() 117 | 118 | # remove data 119 | del self._stop_events[key] 120 | del self._sizes[key] 121 | del self._counts[key] 122 | 123 | def cancel_all(self): 124 | """Cancel all calculation threads.""" 125 | # stop all calculation threads 126 | for stop_event in self._stop_events.values(): 127 | stop_event.set() 128 | 129 | # clear lists 130 | self._stop_events = {} 131 | self._sizes = {} 132 | self._counts = {} 133 | 134 | def cancel_all_for_object(self, parent_object): 135 | """Cancel all calculation threads for specified parent object.""" 136 | parent_id = id(parent_object) 137 | 138 | for key in list(self._stop_events): 139 | if key[0] != parent_id: 140 | continue 141 | 142 | # stop calculation thread 143 | self._stop_events[key].set() 144 | 145 | # remove data 146 | del self._stop_events[key] 147 | del self._sizes[key] 148 | del self._counts[key] 149 | 150 | def cancel_all_for_path(self, parent_path): 151 | """Cancel all threads calculating disk usage for child paths.""" 152 | for key in self._stop_events.keys(): 153 | if not key[1].startswith(parent_path): 154 | # stop calculation thread 155 | self._stop_events[key].set() 156 | 157 | # remove data 158 | del self._stop_events[key] 159 | del self._sizes[key] 160 | del self._counts[key] 161 | -------------------------------------------------------------------------------- /sunflower/tools/version_check.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gi.repository import Gtk, Gdk, GObject 4 | from urllib.request import urlopen 5 | from json import JSONDecoder 6 | from threading import Thread 7 | 8 | 9 | class VersionCheck: 10 | """Small class used for checking and displaying current and 11 | latest version of software detected by getting a file from 12 | project hosting site. 13 | 14 | """ 15 | 16 | URL = 'https://api.github.com/repos/MeanEYE/Sunflower/releases' 17 | 18 | def __init__(self, application): 19 | self._dialog = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) 20 | self._application = application 21 | 22 | # configure window 23 | self._dialog.set_title(_('Version check')) 24 | self._dialog.set_wmclass('Sunflower', 'Sunflower') 25 | self._dialog.set_border_width(7) 26 | self._dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 27 | self._dialog.set_resizable(False) 28 | self._dialog.set_skip_taskbar_hint(True) 29 | self._dialog.set_modal(True) 30 | self._dialog.set_transient_for(application) 31 | self._dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) 32 | self._dialog.connect('key-press-event', self._handle_key_press) 33 | 34 | # create user interface 35 | vbox = Gtk.VBox(False, 5) 36 | hbox = Gtk.HBox(False, 0) 37 | table = Gtk.Table(2, 2) 38 | 39 | table.set_row_spacings(5) 40 | table.set_col_spacings(5) 41 | 42 | label_current = Gtk.Label(label=_('Current:')) 43 | label_current.set_alignment(0, 0.5) 44 | 45 | label_latest = Gtk.Label(label=_('Latest:')) 46 | label_latest.set_alignment(0, 0.5) 47 | 48 | self._entry_current = Gtk.Entry() 49 | self._entry_current.set_editable(False) 50 | 51 | self._entry_latest = Gtk.Entry() 52 | self._entry_latest.set_editable(False) 53 | 54 | separator = Gtk.HSeparator() 55 | 56 | # create controls 57 | button_close = Gtk.Button(stock=Gtk.STOCK_CLOSE) 58 | button_close.connect('clicked', lambda widget: self._dialog.hide()) 59 | 60 | # pack user interface 61 | self._dialog.add(vbox) 62 | 63 | vbox.pack_start(table, True, True, 0) 64 | vbox.pack_start(separator, True, True, 0) 65 | vbox.pack_start(hbox, True, True, 0) 66 | 67 | hbox.pack_end(button_close, False, False, 0) 68 | 69 | table.attach(label_current, 0, 1, 0, 1) 70 | table.attach(label_latest, 0, 1, 1, 2) 71 | table.attach(self._entry_current, 1, 2, 0, 1) 72 | table.attach(self._entry_latest, 1, 2, 1, 2) 73 | 74 | vbox.show_all() 75 | 76 | def __threaded_check(self): 77 | """Method called in separate thread""" 78 | try: 79 | # get data from web 80 | url_handler = urlopen(self.URL) 81 | encoding = url_handler.headers.get_content_charset() 82 | data = url_handler.read().decode(encoding) 83 | 84 | finally: 85 | decoder = JSONDecoder() 86 | releases = decoder.decode(data) 87 | 88 | GObject.idle_add(self._entry_latest.set_text, releases[0]['tag_name']) 89 | 90 | def _handle_key_press(self, widget, event, data=None): 91 | """Handle pressing keys""" 92 | if event.keyval == Gdk.KEY_Escape: 93 | self._dialog.hide() 94 | 95 | def check(self): 96 | """Check for new version online""" 97 | version = self._application.version 98 | 99 | # prepare template 100 | if version['stage'] != 'f': 101 | template = '{0[major]}.{0[minor]}{0[stage]}-{0[build]}' 102 | else: 103 | template = '{0[major]}.{0[minor]}-{0[build]}' 104 | 105 | # populate version values 106 | self._entry_current.set_text(template.format(version)) 107 | self._entry_latest.set_text(_('Checking...')) 108 | 109 | # show dialog 110 | self._dialog.show() 111 | 112 | # start new thread and check for new version 113 | thread = Thread(target=self.__threaded_check) 114 | thread.start() 115 | 116 | -------------------------------------------------------------------------------- /sunflower/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/sunflower/widgets/__init__.py -------------------------------------------------------------------------------- /sunflower/widgets/breadcrumbs.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from gi.repository import Gtk, GObject, Gio 6 | from sunflower.common import decode_file_name 7 | 8 | 9 | class Breadcrumbs: 10 | """Linked list of buttons navigating to different paths.""" 11 | 12 | def __init__(self, parent): 13 | self.container = Gtk.ScrolledWindow.new() 14 | self.container.set_overlay_scrolling(True) 15 | self.container.set_placement(Gtk.CornerType.TOP_RIGHT) 16 | self.container.get_hscrollbar().hide() 17 | 18 | self.box = Gtk.HBox.new(False, 0) 19 | self.container.add_with_viewport(self.box) 20 | 21 | # change the look of container 22 | self.container.set_focus_on_click(False) 23 | self.container.get_style_context().add_class('sunflower-breadcrumbs') 24 | 25 | self._path = None 26 | self._parent = parent 27 | self._updating = False 28 | self._group = None 29 | 30 | self.container.show_all() 31 | 32 | def __fragment_click(self, widget, data=None): 33 | """Handle clicking on path fragment.""" 34 | if self._updating: 35 | return 36 | 37 | # ignore non active buttons 38 | if not widget.props.active: 39 | return 40 | 41 | # change path 42 | file_list = self._parent._parent 43 | if hasattr(file_list, 'change_path'): 44 | file_list.change_path(widget.path) 45 | 46 | def _focus_fragment(self, fragment, is_new=True): 47 | """Set fragment as active and present it to user.""" 48 | allocation = fragment.get_allocation() 49 | adjustment = self.container.get_hadjustment() 50 | container_size = self.container.get_allocation().width 51 | 52 | if is_new: 53 | position = adjustment.get_upper() 54 | elif allocation.x + allocation.width > container_size: 55 | position = allocation.x + allocation.width + 20 56 | else: 57 | position = 0 58 | 59 | adjustment.set_value(position) 60 | fragment.set_active(True) 61 | 62 | def set_state(self, state): 63 | """Set widget state.""" 64 | self._state = state 65 | 66 | def refresh(self, path): 67 | """Update buttons on directory change.""" 68 | provider = self._parent._parent.get_provider() 69 | 70 | # prevent signal dead-loops 71 | self._updating = True 72 | 73 | if self._path is not None and self._path.startswith(path): 74 | # path is a subset, update highlight and exit 75 | for control in self.box.get_children(): 76 | if control.path == path: 77 | self._focus_fragment(control, is_new=False) 78 | break 79 | 80 | else: 81 | # prepare for parsing 82 | self._path = path 83 | self.box.foreach(self.box.remove) 84 | 85 | # split root element from others 86 | root_element = provider.get_root_path(path) 87 | root_name = provider.get_root_name(path) 88 | root_icon = provider.get_root_symbolic_icon(path) 89 | other_elements = path[len(root_element):] 90 | 91 | # make sure our path doesn't begin with slash 92 | if other_elements.startswith(os.path.sep): 93 | other_elements = other_elements[1:] 94 | 95 | # split elements 96 | elements = other_elements.split(os.path.sep) 97 | elements.insert(0, root_element) 98 | 99 | # create controls 100 | control = None 101 | current_path = None 102 | for element in elements: 103 | current_path = os.path.join(current_path, element) if current_path is not None else element 104 | control = Fragment( 105 | decode_file_name(element) if control is not None else root_name, 106 | current_path, 107 | self.__fragment_click, 108 | control, 109 | None if control is not None else root_icon # icon 110 | ) 111 | self.box.pack_start(control, False, False, 0) 112 | 113 | if control is not None: 114 | GObject.idle_add(self._focus_fragment, control) 115 | 116 | # prevent signal dead-loops 117 | self._updating = False 118 | 119 | 120 | class Fragment(Gtk.HBox): 121 | """Simple path fragment containing necessary widgets.""" 122 | 123 | def __init__(self, text, path, click_handler, previous, icon=None): 124 | Gtk.HBox.__init__(self) 125 | 126 | self.path = path 127 | self.click_handler = click_handler 128 | 129 | # create separator label 130 | if previous is not None: 131 | label = Gtk.Label.new('/') 132 | self.pack_start(label, False, False, 0) 133 | 134 | # create button 135 | self._button = Gtk.RadioButton.new() 136 | self._button.set_focus_on_click(False) 137 | self._button.set_mode(False) 138 | self._button.connect('clicked', self.click_handler) 139 | self._button.path = path 140 | 141 | if icon is None: 142 | self._button.set_label(text) 143 | 144 | else: 145 | if isinstance(icon, Gio.ThemedIcon): 146 | image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) 147 | else: 148 | image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON) 149 | label = Gtk.Label.new(text) 150 | label.set_alignment(0, 0.5) 151 | 152 | hbox = Gtk.HBox.new(False, 2) 153 | hbox.pack_start(image, False, False, 0) 154 | hbox.pack_start(label, False, False, 0) 155 | 156 | self._button.add(hbox) 157 | 158 | if previous is not None: 159 | self._button.join_group(previous._button) 160 | 161 | self.pack_start(self._button, False, False, 0) 162 | 163 | self.show_all() 164 | 165 | def set_active(self, active): 166 | """Set button active state.""" 167 | self._button.handler_block_by_func(self.click_handler) 168 | self._button.set_active(active) 169 | self._button.handler_unblock_by_func(self.click_handler) 170 | -------------------------------------------------------------------------------- /sunflower/widgets/command_row.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class CommandRow(Gtk.ListBoxRow): 5 | """List item which is used for displaying items in commands menu.""" 6 | 7 | def __init__(self, name, command): 8 | Gtk.ListBoxRow.__init__(self) 9 | 10 | self._command = command 11 | 12 | self.set_selectable(True) 13 | self.set_activatable(True) 14 | self.set_focus_on_click(True) 15 | 16 | # create interface 17 | box = Gtk.EventBox.new() 18 | box.set_border_width(5) 19 | self.add(box) 20 | 21 | label = Gtk.Label.new(name) 22 | label.set_alignment(0, 0.5) 23 | box.add(label) 24 | 25 | self.show_all() 26 | 27 | def _get_command(self): 28 | """Return command for execution.""" 29 | return self._command 30 | 31 | command = property(_get_command) 32 | -------------------------------------------------------------------------------- /sunflower/widgets/completion_entry.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import re 5 | 6 | from gi.repository import Gtk, GObject 7 | 8 | 9 | 10 | class PathCompletionEntry(Gtk.Entry): 11 | """Entry with path completion""" 12 | number_split = re.compile('([0-9]+)') 13 | 14 | def __init__(self, application): 15 | GObject.GObject.__init__(self) 16 | 17 | # store application locally for later use 18 | self._application = application 19 | self._network_path_completion = self._application.options.get('network_path_completion') 20 | 21 | # create suggestion list 22 | self._store = Gtk.ListStore(str) 23 | self._store.set_sort_column_id(0, Gtk.SortType.ASCENDING) 24 | self._store.set_sort_func(0, self._sort_list) 25 | 26 | # create entry field with completion 27 | self._completion = Gtk.EntryCompletion() 28 | self._completion.set_model(self._store) 29 | self._completion.set_text_column(0) 30 | self._completion.set_inline_completion(True) 31 | self._completion.set_inline_selection(True) 32 | 33 | # configure entry 34 | self.set_completion(self._completion) 35 | 36 | # TODO: Add delayed populate to avoid spamming 37 | self.connect('changed', self._populate_list) 38 | 39 | def _populate_list(self, widget, data=None): 40 | """Populate a list of file names from entered path.""" 41 | self._store.clear() 42 | original_path = widget.get_text() 43 | directory = os.path.dirname(original_path) 44 | 45 | # separate protocol from path 46 | if '://' not in original_path: 47 | scheme = 'file' 48 | 49 | else: 50 | scheme = original_path.split('://', 1)[0] 51 | 52 | # get associated provider 53 | Provider = self._application.get_provider_by_protocol(scheme) 54 | can_lookup = Provider.is_local or self._network_path_completion 55 | 56 | if Provider is not None and can_lookup: 57 | provider = Provider(self._application) 58 | 59 | # make sure path exists 60 | if not provider.exists(directory): 61 | return 62 | 63 | # populate list 64 | for path in provider.list_dir(directory): 65 | if provider.is_dir(path, relative_to=directory): 66 | self._store.append((os.path.join(directory, path),)) 67 | 68 | def _sort_list(self, item_list, iter1, iter2, data=None): 69 | """Compare two items for sorting process.""" 70 | value1 = item_list.get_value(iter1, 0) 71 | value2 = item_list.get_value(iter2, 0) 72 | 73 | value1 = value1.lower() 74 | value1 = [int(part) if part.isdigit() else part for part in self.number_split.split(value1)] 75 | 76 | if value2 is not None: 77 | value2 = value2.lower() 78 | value2 = [int(part) if part.isdigit() else part for part in self.number_split.split(value2)] 79 | 80 | return (value1 > value2) - (value1 < value2) 81 | -------------------------------------------------------------------------------- /sunflower/widgets/context_menu.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class ContextMenu: 5 | """Interface which shows options and information related to current path.""" 6 | 7 | def __init__(self, parent, relative_to): 8 | self._parent = parent 9 | 10 | # create popover interface 11 | self._popover = Gtk.Popover.new() 12 | self._popover.set_relative_to(relative_to) 13 | self._popover.set_position(Gtk.PositionType.BOTTOM) 14 | 15 | # create widget container 16 | self._container = Gtk.VBox.new(False, 10) 17 | self._container.set_border_width(10) 18 | 19 | # show all widgets inside of container 20 | self._container.show_all() 21 | 22 | # pack interface 23 | self._popover.add(self._container) 24 | 25 | def add_control(self, control, fill=False, spacing=0): 26 | """Add specified control to the context menu.""" 27 | control.show_all() 28 | self._container.pack_start(control, fill, False, spacing) 29 | 30 | def show(self): 31 | """Show context menu for current directory.""" 32 | self._popover.popup() 33 | -------------------------------------------------------------------------------- /sunflower/widgets/emblems_renderer.py: -------------------------------------------------------------------------------- 1 | import cairo 2 | 3 | from gi.repository import Gtk, Gdk, GObject 4 | 5 | 6 | class CellRendererEmblems(Gtk.CellRenderer): 7 | """Cell renderer that accepts list of icon names.""" 8 | __gproperties__ = { 9 | 'emblems': ( 10 | GObject.TYPE_PYOBJECT, 11 | 'Emblem list', 12 | 'List of icon names to display', 13 | GObject.PARAM_READWRITE 14 | ), 15 | 'is-link': ( 16 | GObject.TYPE_BOOLEAN, 17 | 'Link indicator', 18 | 'Denotes if item is a link or regular file', 19 | False, 20 | GObject.PARAM_READWRITE 21 | ) 22 | } 23 | 24 | def __init__(self): 25 | Gtk.CellRenderer.__init__(self) 26 | 27 | self.emblems = None 28 | self.is_link = None 29 | self.icon_size = 16 30 | self.spacing = 2 31 | self.padding = 1 32 | 33 | def do_set_property(self, prop, value): 34 | """Set renderer property.""" 35 | if prop.name == 'emblems': 36 | self.emblems = value 37 | 38 | elif prop.name == 'is-link': 39 | self.is_link = value 40 | 41 | else: 42 | setattr(self, prop.name, value) 43 | 44 | def do_get_property(self, prop): 45 | """Get renderer property.""" 46 | if prop.name == 'emblems': 47 | result = self.emblems 48 | elif prop.name == 'is-link': 49 | result = self.is_link 50 | else: 51 | result = getattr(self, prop.name) 52 | 53 | return result 54 | 55 | def do_render(self, context, widget, background_area, cell_area, flags): 56 | """Render emblems on tree view.""" 57 | if not self.is_link and (self.emblems is None or len(self.emblems) == 0): 58 | return 59 | 60 | # cache constants locally 61 | icon_size = self.icon_size 62 | spacing = self.spacing 63 | emblems = self.emblems or () 64 | icon_theme = Gtk.IconTheme.get_default() 65 | 66 | # add symbolic link emblem if needed 67 | if self.is_link: 68 | emblems = ('emblem-symbolic-link',) + emblems 69 | 70 | # position of next icon 71 | pos_x = cell_area.x + cell_area.width 72 | pos_y = cell_area.y + ((cell_area.height - icon_size) / 2) 73 | 74 | # draw all the icons 75 | for emblem in emblems: 76 | # load icon from the theme 77 | pixbuf = icon_theme.load_icon(emblem, 16, 0) 78 | 79 | # move position of next icon 80 | pos_x -= icon_size + spacing 81 | 82 | # draw icon 83 | Gdk.cairo_set_source_pixbuf(context, pixbuf, pos_x, pos_y) 84 | context.paint() 85 | 86 | def do_get_size(self, widget, cell_area=None): 87 | """Calculate size taken by emblems.""" 88 | count = 5 # optimum size, we can still render more or less emblems 89 | 90 | width = self.icon_size * count + (self.spacing * (count - 1)) 91 | height = self.icon_size 92 | result = (0, 0, width + 2 * self.padding, height + 2 * self.padding) 93 | 94 | return result 95 | -------------------------------------------------------------------------------- /sunflower/widgets/settings_page.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, GObject 2 | 3 | 4 | class SettingsPage(Gtk.ScrolledWindow): 5 | """Abstract class used to build pages in preferences window.""" 6 | 7 | def __init__(self, parent, application, name, title): 8 | Gtk.ScrolledWindow.__init__(self) 9 | 10 | self._parent = parent 11 | self._application = application 12 | self._page_name = name 13 | self._page_title = title 14 | 15 | # configure main container 16 | self._box = Gtk.VBox.new(False, 0) 17 | self._box.set_spacing(15) 18 | self._box.set_border_width(15) 19 | 20 | # add page to preferences window 21 | self.add(self._box) 22 | self._parent.add_tab(self._page_name, self._page_title, self) 23 | 24 | def _create_section(self, title, container): 25 | """Create widget section with title.""" 26 | box = Gtk.VBox.new(False, 0) 27 | 28 | # create section title 29 | label_title = Gtk.Label.new('{}'.format(title)) 30 | label_title.set_alignment(0, 0.5) 31 | label_title.set_use_markup(True) 32 | box.pack_start(label_title, True, False, 0) 33 | box.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, False, 0) 34 | 35 | # pack container 36 | box.pack_start(container, True, False, 0) 37 | container.set_border_width(10) 38 | self._box.pack_start(box, False, False, 0) 39 | 40 | def _create_radio_section(self, title, container, group=None): 41 | """Create section which contains radio button and return radio button.""" 42 | box = Gtk.VBox.new(False, 0) 43 | 44 | # create section title 45 | label_title = Gtk.Label.new('{}'.format(title)) 46 | label_title.set_alignment(0, 0.5) 47 | label_title.set_use_markup(True) 48 | radio_title = Gtk.RadioButton.new_from_widget(group) 49 | radio_title.add(label_title) 50 | box.pack_start(radio_title, True, False, 0) 51 | box.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, False, 0) 52 | 53 | # pack container 54 | box.pack_start(container, True, False, 0) 55 | container.set_border_width(10) 56 | self._box.pack_start(box, False, False, 0) 57 | 58 | return radio_title 59 | 60 | def _load_options(self): 61 | """Load options and update interface""" 62 | pass 63 | 64 | def _save_options(self): 65 | """Method called when save button is clicked""" 66 | pass 67 | 68 | def pack_start(self, *args, **kwargs): 69 | """Pack things in container.""" 70 | self._box.pack_start(*args, **kwargs) 71 | 72 | def pack_end(self, *args, **kwargs): 73 | """Pack things in container.""" 74 | self._box.pack_end(*args, **kwargs) 75 | -------------------------------------------------------------------------------- /sunflower/widgets/status_bar.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, GObject 2 | 3 | 4 | class StatusBar(Gtk.HBox): 5 | """Plugin status bar""" 6 | 7 | def __init__(self): 8 | GObject.GObject.__init__(self, homogeneous=False, spacing=15) 9 | 10 | self.set_border_width(4) 11 | self.set_property('no-show-all', True) 12 | 13 | self._icons = {} 14 | self._labels = {} 15 | 16 | # create default label 17 | self._label = Gtk.Label() 18 | self._label.set_use_markup(True) 19 | self._label.set_alignment(0, 0.5) 20 | self._label.show() 21 | 22 | # pack interface 23 | self.pack_end(self._label, True, True, 0) 24 | 25 | def set_text(self, text, group=None): 26 | """Set default label text""" 27 | if group is None: 28 | # set default label 29 | self._label.set_markup(text) 30 | 31 | elif group in self._labels: 32 | # set specified group label 33 | self._labels[group].set_markup(text) 34 | 35 | def add_group_with_icon(self, name, icon_name, value='', tooltip=None): 36 | """Add status bar group with icon""" 37 | icon = Gtk.Image() 38 | icon.set_from_icon_name(icon_name, Gtk.IconSize.MENU) 39 | icon.show() 40 | 41 | label = Gtk.Label(label=value) 42 | label.set_use_markup(True) 43 | label.set_alignment(0, 0.5) 44 | label.show() 45 | 46 | # configure tooltip 47 | if tooltip is not None: 48 | label.set_tooltip_text(tooltip) 49 | icon.set_tooltip_text(tooltip) 50 | 51 | # pack group 52 | hbox = Gtk.HBox(False, 3) 53 | hbox.show() 54 | 55 | hbox.pack_start(icon, False, False, 0) 56 | hbox.pack_start(label, False, False, 0) 57 | 58 | self.pack_start(hbox, False, False, 0) 59 | 60 | # add group to local cache 61 | self._labels[name] = label 62 | self._icons[name] = icon 63 | -------------------------------------------------------------------------------- /sunflower/widgets/tab_label.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Pango, Gdk 2 | 3 | 4 | class TabLabel: 5 | """Tab label wrapper class""" 6 | 7 | MAX_CHARS=20 8 | 9 | def __init__(self, application, parent): 10 | self._container = Gtk.EventBox.new() 11 | 12 | self._application = application 13 | self._parent = parent 14 | 15 | # initialize tab events 16 | self._container.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) 17 | self._container.connect('button-release-event', self._button_release_event) 18 | self._container.set_visible_window(False) 19 | 20 | # create interface 21 | self._hbox = Gtk.HBox(homogeneous=False, spacing=0) 22 | self._container.add(self._hbox) 23 | 24 | self._label = Gtk.Label.new() 25 | self._label.set_single_line_mode(True) 26 | 27 | self._lock_image = Gtk.Image() 28 | self._lock_image.set_property('no-show-all', True) 29 | self._lock_image.set_from_icon_name('changes-prevent-symbolic', Gtk.IconSize.MENU) 30 | 31 | self._button = Gtk.Button.new_from_icon_name('window-close-symbolic', Gtk.IconSize.MENU) 32 | self._button.set_focus_on_click(False) 33 | self._button.connect('clicked', self._close_tab) 34 | self._button.set_property('no-show-all', True) 35 | self._button.get_style_context().add_class('sunflower-close-tab') 36 | self._button.get_style_context().add_class('flat') 37 | 38 | # pack interface 39 | self._hbox.pack_start(self._lock_image, False, False, 0) 40 | self._hbox.pack_start(self._label, True, True, 0) 41 | self._hbox.pack_start(self._button, False, False, 0) 42 | 43 | # show controls 44 | if self._application.options.get('tab_close_button'): 45 | self._button.show() 46 | self._hbox.set_spacing(3) 47 | 48 | self._container.show_all() 49 | 50 | def _close_tab(self, widget=None, mode=None): 51 | """Handle clicking on close button""" 52 | if mode == 'all': 53 | self._application.close_all_tabs(self._parent._notebook) 54 | 55 | elif mode == 'other': 56 | self._application.close_all_tabs(self._parent._notebook, self._parent) 57 | 58 | else: 59 | self._parent._close_tab() 60 | 61 | def _toggle_lock_tab(self, widget=None, data=None): 62 | """Toggle tab lock state.""" 63 | if self._parent.is_tab_locked(): 64 | self._parent.unlock_tab() 65 | 66 | else: 67 | self._parent.lock_tab() 68 | 69 | def _show_menu(self): 70 | """Show tab menu.""" 71 | menu_manager = self._application.menu_manager 72 | menu_items = ( 73 | { 74 | 'label': _('Unlock') if self._parent.is_tab_locked() else _('Lock'), 75 | 'callback': self._toggle_lock_tab, 76 | }, 77 | { 78 | 'label': _('Duplicate tab'), 79 | 'callback': self._parent._duplicate_tab, 80 | }, 81 | { 82 | 'label': _('Move to opposite panel'), 83 | 'callback': self._parent._move_tab, 84 | }, 85 | { 86 | 'type': 'separator' 87 | }, 88 | { 89 | 'label': _('Close Tab'), 90 | 'type': 'image', 91 | 'stock': Gtk.STOCK_CLOSE, 92 | 'callback': self._close_tab, 93 | }, 94 | { 95 | 'label': _('Close All'), 96 | 'data': 'all', 97 | 'callback': self._close_tab, 98 | }, 99 | { 100 | 'label': _('Close Other Tabs'), 101 | 'data': 'other', 102 | 'callback': self._close_tab, 103 | }, 104 | ) 105 | 106 | # create menu 107 | menu = Gtk.Menu() 108 | 109 | for item in menu_items: 110 | item = menu_manager.create_menu_item(item) 111 | menu.append(item) 112 | 113 | menu.popup_at_pointer() 114 | menu.show_all() 115 | 116 | def _button_release_event(self, widget, event, data=None): 117 | """ 118 | Handle clicking on the tab itself, when middle button is pressed 119 | the tab is closed. 120 | """ 121 | result = False 122 | 123 | if event.button == 2: 124 | self._close_tab() 125 | result = True 126 | 127 | elif event.button == 3: 128 | self._show_menu() 129 | result = False 130 | 131 | return result 132 | 133 | def set_text(self, text): 134 | """Set label text""" 135 | if len(text)>self.MAX_CHARS: 136 | self._label.set_width_chars(self.MAX_CHARS) 137 | self._label.set_ellipsize(Pango.EllipsizeMode.END) 138 | else: 139 | self._label.set_width_chars(-1) 140 | self._label.set_ellipsize(Pango.EllipsizeMode.NONE) 141 | self._label.set_text(text) 142 | 143 | def lock_tab(self): 144 | """Set label state to locked""" 145 | self._lock_image.show() 146 | 147 | def unlock_tab(self): 148 | """Delete * from label""" 149 | self._lock_image.hide() 150 | 151 | def get_container(self): 152 | """Return container to be added to notebook""" 153 | return self._container 154 | 155 | def apply_settings(self): 156 | """Apply global settings to tab label""" 157 | if self._application.options.get('tab_close_button'): 158 | self._button.show() 159 | self._hbox.set_spacing(3) 160 | 161 | else: 162 | self._button.hide() 163 | self._hbox.set_spacing(0) 164 | -------------------------------------------------------------------------------- /sunflower/widgets/thumbnail_view.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gi 3 | 4 | from gi.repository import Gtk, Gdk, GObject, GdkPixbuf 5 | 6 | try: 7 | # try to import module 8 | gi.require_version('GnomeDesktop', '3.0') 9 | from gi.repository import GnomeDesktop 10 | USE_FACTORY = True 11 | except: 12 | USE_FACTORY = False 13 | 14 | 15 | class ThumbnailView: 16 | """Load and display images from Gnome thumbnail factory storage. 17 | 18 | Idea is to create one object and then update thumbnail image as 19 | needed. This class *WILL* try to create thumbnails as well as load 20 | them cached. 21 | 22 | """ 23 | 24 | def __init__(self, parent, size=None): 25 | self.popover = Gtk.Popover.new() 26 | 27 | self.popover.set_modal(False) 28 | self.popover.set_transitions_enabled(False) 29 | self.popover.set_position(Gtk.PositionType.LEFT) 30 | 31 | # create image preview 32 | self._image = Gtk.Image() 33 | self._image.show() 34 | self.popover.add(self._image) 35 | 36 | # store parameters locally 37 | self._parent = parent 38 | self._thumbnail_size = size 39 | 40 | # create thumbnail factory 41 | if USE_FACTORY: 42 | # set default thumbnail size 43 | if self._thumbnail_size is None: 44 | self._thumbnail_size = GnomeDesktop.DesktopThumbnailSize.NORMAL 45 | 46 | # create a factory 47 | self._factory = GnomeDesktop.DesktopThumbnailFactory.new(self._thumbnail_size) 48 | 49 | else: 50 | self._factory = None 51 | 52 | def hide(self): 53 | """Hide tooltip.""" 54 | self.popover.hide() 55 | 56 | def can_have_thumbnail(self, uri): 57 | """Check if specified URI can have thumbnail""" 58 | if not USE_FACTORY: 59 | return False 60 | 61 | mime_type = self._parent._parent.associations_manager.get_mime_type(uri) 62 | return self._factory.can_thumbnail(uri, mime_type, 0) 63 | 64 | def get_thumbnail(self, uri): 65 | """Return thumbnail pixbuf for specified URI""" 66 | if not USE_FACTORY: 67 | return None 68 | 69 | result = None 70 | mime_type = self._parent._parent.associations_manager.get_mime_type(uri) 71 | 72 | # check for existing thumbnail 73 | thumbnail_file = self._factory.lookup(uri, 0) 74 | if thumbnail_file and os.path.isfile(thumbnail_file): 75 | result = GdkPixbuf.Pixbuf.new_from_file(thumbnail_file) 76 | 77 | # create thumbnail 78 | elif self.can_have_thumbnail(uri): 79 | result = self._factory.generate_thumbnail(uri, mime_type) 80 | 81 | if result is not None: 82 | self._factory.save_thumbnail(result, uri, 0) 83 | 84 | return result 85 | 86 | def show_thumbnail(self, uri, widget, position): 87 | """Show thumbnail for specified image""" 88 | thumbnail = self.get_thumbnail(uri) 89 | 90 | if thumbnail is not None: 91 | self._image.set_from_pixbuf(thumbnail) 92 | else: 93 | self._image.set_from_icon_name('gtk-missing-image', Gtk.IconSize.DIALOG) 94 | 95 | self.popover.set_relative_to(widget) 96 | self.popover.set_pointing_to(position) 97 | self.popover.show() 98 | -------------------------------------------------------------------------------- /sunflower/widgets/title_bar.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import math 4 | 5 | from gi.repository import Gtk, Pango, Gdk 6 | from sunflower.widgets.breadcrumbs import Breadcrumbs 7 | from sunflower.widgets.context_menu import ContextMenu 8 | from sunflower.common import decode_file_name 9 | 10 | 11 | class Mode: 12 | NORMAL = 0 13 | SUPER_USER = 1 14 | 15 | 16 | class TitleBar: 17 | """Tab titlebar class. 18 | 19 | This class provides many different features, including tab specific 20 | controls, menus and coloring. 21 | 22 | """ 23 | 24 | def __init__(self, application, parent): 25 | self._application = application 26 | self._parent = parent 27 | 28 | self._control_count = 0 29 | self._state = Gtk.StateType.NORMAL 30 | self._mode = Mode.NORMAL 31 | self.context_menu = None 32 | self._breadcrumbs = None 33 | self._title_label = None 34 | self._subtitle_label = None 35 | 36 | # get options 37 | options = self._application.options 38 | 39 | self._superuser_notification = options.get('superuser_notification') 40 | self._button_relief = options.get('button_relief') or 0 41 | 42 | # create container box 43 | self._container = Gtk.HBox.new(False, 5) 44 | self._container.get_style_context().add_class('sunflower-title-bar') 45 | 46 | self._container_controls = Gtk.HBox.new(False, 0) 47 | self._container_controls.get_style_context().add_class('linked') 48 | 49 | # top folder icon as default 50 | self._icon = Gtk.Image.new() 51 | 52 | self._button_menu = Gtk.Button.new() 53 | self._button_menu.add(self._icon) 54 | self._button_menu.set_focus_on_click(False) 55 | self._button_menu.set_tooltip_text(_('Context menu')) 56 | self._button_menu.connect('clicked', self.show_context_menu) 57 | self._button_menu.get_style_context().add_class('sunflower-context-menu') 58 | 59 | # create context menu 60 | self.context_menu = ContextMenu(self, self._button_menu) 61 | 62 | # create spinner control if it exists 63 | self._spinner = Gtk.Spinner() 64 | self._spinner.set_property('no-show-all', True) 65 | 66 | # pack interface 67 | self._container.pack_start(self._button_menu, False, False, 0) 68 | self._container.pack_end(self._container_controls, False, False, 0) 69 | self._container.pack_end(self._spinner, False, False, 0) 70 | 71 | self._spinner_counter = 0 72 | 73 | def create_breadcrumbs(self): 74 | """Create breadcrumbs as main control.""" 75 | self._breadcrumbs = Breadcrumbs(self) 76 | self._container.pack_start(self._breadcrumbs.container, True, True, 0) 77 | 78 | def create_title(self): 79 | """Create title as main control.""" 80 | vbox = Gtk.VBox.new(False, 0) 81 | 82 | # create main tab title 83 | self._title_label = Gtk.Label.new() 84 | self._title_label.set_alignment(0, 0.5) 85 | self._title_label.set_use_markup(True) 86 | self._title_label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) 87 | 88 | # create smaller subtitle 89 | font = Pango.FontDescription('8') 90 | self._subtitle_label = Gtk.Label.new() 91 | self._subtitle_label.set_alignment(0, 0.5) 92 | self._subtitle_label.set_use_markup(False) 93 | self._subtitle_label.modify_font(font) 94 | 95 | # pack interface 96 | vbox.pack_start(self._title_label, True, True, 0) 97 | vbox.pack_start(self._subtitle_label, False, False, 0) 98 | self._container.pack_start(vbox, True, True, 0) 99 | 100 | def add_control(self, widget): 101 | """Add control to button bar.""" 102 | self._control_count += 1 103 | self._container_controls.pack_end(widget, False, False, 0) 104 | 105 | def set_state(self, state): 106 | """Set GTK control state for title bar.""" 107 | self._state = state 108 | 109 | # apply style class to container 110 | if state == Gtk.StateType.SELECTED: 111 | self._container.get_style_context().add_class('selected') 112 | else: 113 | self._container.get_style_context().remove_class('selected') 114 | 115 | def set_mode(self, mode): 116 | """Set title bar mode""" 117 | self._mode = mode 118 | 119 | if self._mode == Mode.SUPER_USER: 120 | self._container.get_style_context().add_class('superuser') 121 | 122 | def set_title(self, path): 123 | """Set title text""" 124 | if self._breadcrumbs is not None: 125 | self._breadcrumbs.refresh(path) 126 | else: 127 | self._title_label.set_markup(decode_file_name(path).replace('&', '&')) 128 | 129 | def set_subtitle(self, text): 130 | """Set subtitle text""" 131 | self._subtitle_label.set_text(text.replace('&', '&')) 132 | 133 | def set_icon_from_name(self, icon_name): 134 | """Set icon from specified name""" 135 | self._icon.set_from_icon_name(icon_name, Gtk.IconSize.LARGE_TOOLBAR) 136 | 137 | def get_container(self): 138 | """Return title bar container""" 139 | return self._container 140 | 141 | def show_context_menu(self, widget=None, data=None): 142 | """Show title bar menu""" 143 | self.context_menu.show() 144 | return True 145 | 146 | def show_spinner(self): 147 | """Show spinner widget""" 148 | if self._spinner is None: 149 | return 150 | 151 | self._spinner_counter += 1 152 | if self._spinner_counter == 1: 153 | self._spinner.start() 154 | self._spinner.show() 155 | 156 | def hide_spinner(self): 157 | """Hide spinner widget""" 158 | if self._spinner is None: 159 | return 160 | 161 | self._spinner_counter -= 1 162 | if self._spinner_counter <= 0: 163 | self._spinner_counter = 0 164 | self._spinner.stop() 165 | self._spinner.hide() 166 | 167 | def apply_settings(self): 168 | """Method called when system applies new settings""" 169 | self._superuser_notification = self._application.options.get('superuser_notification') 170 | self._button_relief = self._application.options.get('button_relief') or 0 171 | 172 | # apply button relief 173 | relief = (Gtk.ReliefStyle.NONE, Gtk.ReliefStyle.NORMAL)[self._button_relief] 174 | for control in self._container.get_children(): 175 | if issubclass(control.__class__, Gtk.Button): 176 | control.set_relief(relief) 177 | -------------------------------------------------------------------------------- /translations/be/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/be/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/bg/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/bg/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/ca/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/ca/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/cs/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/cs/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/cs_CZ/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/cs_CZ/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/de/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/de/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/de_DE/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/de_DE/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/el/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/el/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/en_AU/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/en_AU/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/es/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/es/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/es_AR/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/es_AR/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/fr/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/fr/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/hu/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/hu/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/it_IT/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/it_IT/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/ja_JP/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/ja_JP/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/lt/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/lt/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/lv/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/lv/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/nl/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/nl/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/nl_BE/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/nl_BE/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/pl/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/pl/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/pl_PL/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/pl_PL/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/pt_BR/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/pt_BR/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/ru/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/ru/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/ru_RU/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/ru_RU/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/sk/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/sk/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/sr/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/sr/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/sv/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/sv/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/tr/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/tr/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/uk/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/uk/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/uk_UA/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/uk_UA/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/zh_CN.GB2312/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/zh_CN.GB2312/LC_MESSAGES/sunflower.mo -------------------------------------------------------------------------------- /translations/zh_TW/LC_MESSAGES/sunflower.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeanEYE/Sunflower/0bac3b585a237f0f7e48098ef38fda24bdf7bef7/translations/zh_TW/LC_MESSAGES/sunflower.mo --------------------------------------------------------------------------------