Compare commits

..

69 Commits

Author SHA1 Message Date
c204e4e094 Bump liberty and termo
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2025-04-23 22:00:17 +02:00
16e0ff2188 Make README.adoc a bit less confusing
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-19 15:29:58 +01:00
2d6855445f Update the screenshot
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-19 15:24:53 +01:00
531f18d827 GUI: add basic configuration
It is simply not feasible to write the text file by hand on Windows.
2024-12-19 14:38:19 +01:00
862cde36ae CMakeLists.txt: quote more paths 2024-12-07 22:50:53 +01:00
661dc85d45 Fix macOS build
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-04 18:10:35 +01:00
2fe846f09f Fix running tests in Windows builds
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
2024-04-10 12:48:05 +02:00
33426992ec Bump liberty, moving the Win64 toolchain file
All checks were successful
Arch Linux AUR Success
2024-04-09 17:04:45 +02:00
197d071160 Enable cross-compiled tests
All checks were successful
Arch Linux AUR Success
2024-04-09 13:04:32 +02:00
fafac22d60 Bump liberty
All checks were successful
Arch Linux AUR Success
2024-02-10 05:46:13 +01:00
58f7ba55b3 CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:09:31 +02:00
d2cfc2ee81 Deduplicate CMake scripts 2023-07-24 11:47:33 +02:00
5a9a446b9c Fix build on OpenIndiana 2023-07-24 08:24:52 +02:00
39e2fc5142 Find ncursesw on OpenIndiana 2023-07-24 08:07:26 +02:00
f94bb77091 Reflect the recent renaming of tabfile in scripts 2023-07-07 12:15:37 +02:00
d3cfb12e16 README.adoc: update package information 2023-07-01 22:00:54 +02:00
9aac2511d3 Win64Depends.sh: only extract what we need
In case the packages directory has been preloaded or symlinked.
2023-06-26 20:57:56 +02:00
8af337c83c CMakeLists.txt: fix dependencies after renaming 2023-06-22 00:23:29 +02:00
951208c15b Test SVG and desktop file validity 2023-06-15 16:24:16 +02:00
74d9acecb5 Bump termo 2023-06-14 16:24:27 +02:00
d1ce97010e Update the manual page 2023-06-11 18:14:46 +02:00
e7be281b58 Reflect upstream URL rename 2023-06-11 18:12:12 +02:00
2aa6390146 Update translations 2023-06-11 18:08:03 +02:00
c77d994dc4 Rename tools, make them installable 2023-06-11 18:08:03 +02:00
238e7a2bb9 Merge TUI and GUI binaries, using a new name
The appropriate interface will be chosen automatically.
2023-06-11 18:08:03 +02:00
7bcbc04b04 Bump liberty
This improves the still-imperfect fallback manpage output.
2023-06-11 10:11:54 +02:00
59d7c4af17 Improve manpage AsciiDoc compatibility 2023-06-10 15:05:12 +02:00
2ed1c005c9 Import liberty for its manpage generator
It cannot parse the page well, so it's just a convenience fallback.
2023-06-10 12:12:42 +02:00
26e73711b1 Improve GLib version compatibility 2023-06-10 12:12:41 +02:00
d13b4a793d Prevent undefined behaviour 2022-09-24 18:42:59 +02:00
0570a4d050 sdtui: measure the "search" prompt properly 2022-09-24 18:42:30 +02:00
e28e576fdb sdtui: improve tab bar overflow behaviour 2022-09-24 11:15:25 +02:00
ae9952387a sdgui: don't highlight when cursor not on widget 2022-09-12 04:00:26 +02:00
27dcf87a64 sdgui: load dictionaries asynchronously
This is a must when loading huge dictionaries,
where not even parallelization helps much.
2022-09-04 13:16:40 +02:00
7ef502759e sdgui: clean up DnD 2022-09-04 12:03:26 +02:00
ded899933c sdgui: make Ctrl+click put hovered word in search 2022-09-03 22:00:53 +02:00
832842bf81 sdgui: don't reload on size-allocate
We could annoyingly get these events on window de/focus.
2022-09-03 18:17:04 +02:00
49072f9d01 sdgui: fix context menu item sensitivity 2022-09-03 17:19:12 +02:00
b0f1d3d6ea Build with AsciiDoc as well as Asciidoctor
And bump copyright years.
2022-08-24 00:55:32 +02:00
4073749d3b Synchronize cross-compilation scripts 2022-08-11 14:15:20 +02:00
2e684d2f4e Clean up cross-compilation 2022-08-10 16:04:10 +02:00
57739ff81e Update README 2022-08-05 00:08:54 +02:00
2ff01f9fdb sdgui: support text selection in the view
This is generally an improvement over the initial GtkLabel approach:
 - Multiple definition lines can be selected at once.
 - The widget doesn't keep a selection caret around (which means
   it can't be controlled from the keyboard, a conscious trade-off).
 - Text doesn't needlessly go to PRIMARY immediately during selection,
   making it somewhat possible lift the self-exception for
   the PRIMARY selection watch.

Closes #2
2022-08-04 05:23:09 +02:00
5ed881d25b czech-wordnet.pl: improve portability
Perl runs `` commands in the shell only if they contain
shell metacharacters.
2022-08-01 23:25:15 +02:00
259d0ac860 sdgui: support touch screen dragging for the view 2022-07-24 23:15:18 +02:00
aa985514a6 sdgui: fix up the smooth scrolling commit 2022-07-24 23:15:18 +02:00
6f5e32386e tabfile: fix mismatching popen/fclose 2022-07-24 23:15:18 +02:00
92556d5269 README: mention gettext utilities as a dependency 2022-07-24 23:15:10 +02:00
2c69937ef5 sdgui: use smooth scrolling 2022-07-24 21:36:57 +02:00
4e4ba67025 README: mention icoutils 2022-07-24 21:15:24 +02:00
13ddec0274 Add a missing gtk_drag_finish() call 2022-02-17 11:02:35 +01:00
c899ceff10 sdgui: avoid focusing tab headers by mouse
Our tabs have dummy contents, which causes some complications.
2022-02-11 08:20:10 +01:00
4d95f46d36 Bump termo 2021-11-04 14:23:40 +01:00
0d0ac40f96 sdtui: handle input field overflows reasonably 2021-11-04 11:43:18 +01:00
181df7fbae czech-wordnet.pl: fix to work on non-GNU systems 2021-11-04 08:33:28 +01:00
b36f185426 Fix various macOS-related/found issues
Even though this software isn't very useful on that system either,
due to its lack of PRIMARY.
2021-11-02 17:04:37 +01:00
726ecd83ac Win64Depends.sh: describe a more precise fix 2021-11-01 09:28:18 +01:00
dd5e90a324 CMakeLists.txt: update icon cache after cleanup
docs/iconcache.txt says the format is machine-independent.

It does not seem to result in any noticeable improvement.
2021-11-01 08:42:41 +01:00
ac6ac4f845 Win32CleanupAdwaita.sh: make note of a corner case
I've actually tried to replicate that behaviour earlier,
and it kept around way too many icons.
2021-11-01 02:40:41 +01:00
093baaa034 sdgui: make M-Tab go to the last chosen dictionary 2021-10-30 10:45:32 +02:00
ecab86966f sdtui: make M-Tab go to the last chosen dictionary 2021-10-30 10:18:20 +02:00
e383a50af7 Add clang-format configuration, clean up 2021-10-30 03:02:33 +02:00
ed26259e6d sdgui: fix M-0 going one item beyond our intent 2021-10-28 21:42:49 +02:00
8c80aa9da2 CMakeLists.txt: configure NSIS
- Install a link to sdgui to the Start Menu.
 - Associate the .ifo file extension with sdgui.
 - Change the installation directory name to contain a space,
   rather than a dash, since that appears to be the norm.
   It's also copied over to the display name.

And thus, the GUI has become somewhat usable on Windows.
2021-10-27 22:52:29 +02:00
9aa26e2807 CMakeLists.txt: think of older versions
At minimum it documents the novelty of the command argument.
2021-10-27 19:54:02 +02:00
f7528a05a3 CMakeLists.txt: refactor icon generation 2021-10-27 19:23:47 +02:00
46e9b7b584 sdgui: rasterize some icons, including for Windows 2021-10-27 09:48:57 +02:00
0eb935ad9f sdgui: set the window title explicitly 2021-10-27 09:46:54 +02:00
c75de30765 sdgui: create and install a program icon 2021-10-27 09:46:17 +02:00
38 changed files with 2435 additions and 987 deletions

32
.clang-format Normal file
View File

@@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignEscapedNewlines: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
MaxEmptyLinesToKeep: 2
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# Must be kept synchronized with gi18n.h and make-template.sh.
WhitespaceSensitiveMacros: ['_', 'Q_', 'N_', 'C_', 'NC_']
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2

12
.gitignore vendored
View File

@@ -3,9 +3,9 @@
# Qt Creator files
/CMakeLists.txt.user*
/sdtui.cflags
/sdtui.cxxflags
/sdtui.config
/sdtui.files
/sdtui.creator*
/sdtui.includes
/tdv.cflags
/tdv.cxxflags
/tdv.config
/tdv.files
/tdv.creator*
/tdv.includes

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "termo"]
path = termo
url = https://git.janouch.name/p/termo.git
[submodule "liberty"]
path = liberty
url = https://git.janouch.name/p/liberty.git

View File

@@ -1,15 +1,18 @@
cmake_minimum_required (VERSION 3.0)
project (sdtui VERSION 0.1.0 LANGUAGES C)
cmake_minimum_required (VERSION 3.0...3.27)
project (tdv VERSION 0.1.0 LANGUAGES C)
# Moar warnings
# Adjust warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99")
set (CMAKE_C_FLAGS_DEBUG
"${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wno-missing-field-initializers")
set (ignores "-Wno-missing-field-initializers -Wno-cast-function-type")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 ${ignores}")
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra")
endif ()
add_definitions (-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_38)
# For custom modules
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
set (CMAKE_MODULE_PATH
"${PROJECT_SOURCE_DIR}/cmake;${PROJECT_SOURCE_DIR}/liberty/cmake")
# Cross-compilation for Windows, as a proof-of-concept pulled in from logdiag
if (WIN32)
@@ -17,18 +20,23 @@ if (WIN32)
message (FATAL_ERROR "Win32 must be cross-compiled to build sensibly")
endif ()
set (WIN32_DEPENDS_PATH ${PROJECT_SOURCE_DIR}/win32-depends)
list (APPEND CMAKE_PREFIX_PATH ${WIN32_DEPENDS_PATH})
list (APPEND CMAKE_INCLUDE_PATH ${WIN32_DEPENDS_PATH}/lib)
set (win32_deps_root "${PROJECT_SOURCE_DIR}")
set (win32_deps_prefix "${win32_deps_root}/mingw64")
list (APPEND CMAKE_PREFIX_PATH "${win32_deps_prefix}")
list (APPEND CMAKE_INCLUDE_PATH "${win32_deps_prefix}/lib")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mms-bitfields")
if (CMAKE_CROSSCOMPILING)
list (APPEND CMAKE_FIND_ROOT_PATH ${WIN32_DEPENDS_PATH})
endif (CMAKE_CROSSCOMPILING)
list (APPEND CMAKE_FIND_ROOT_PATH ${win32_deps_prefix})
endif ()
set (ENV{PKG_CONFIG_LIBDIR}
"${WIN32_DEPENDS_PATH}/share/pkgconfig:${WIN32_DEPENDS_PATH}/lib/pkgconfig")
endif (WIN32)
# Relativize prefixes, and bar pkg-config from looking up host libraries
set (ENV{PKG_CONFIG_SYSROOT_DIR} "${win32_deps_root}")
set (win32_deps_pcpath
"${win32_deps_prefix}/share/pkgconfig:${win32_deps_prefix}/lib/pkgconfig")
set (ENV{PKG_CONFIG_PATH} "${win32_deps_pcpath}")
set (ENV{PKG_CONFIG_LIBDIR} "${win32_deps_pcpath}")
endif ()
# Dependencies
find_package (ZLIB REQUIRED)
@@ -66,12 +74,11 @@ endif ()
find_package (Termo QUIET NO_MODULE)
option (USE_SYSTEM_TERMO
"Don't compile our own termo library, use the system one" ${Termo_FOUND})
if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found")
endif ()
else ()
elseif (NOT WIN32)
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
add_subdirectory (termo EXCLUDE_FROM_ALL)
file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
@@ -88,7 +95,6 @@ endif ()
pkg_check_modules (xcb xcb xcb-xfixes)
option (WITH_X11 "Compile with X11 selection support using XCB" ${xcb_FOUND})
if (WITH_X11)
if (NOT xcb_FOUND)
message (FATAL_ERROR "XCB not found")
@@ -100,8 +106,16 @@ endif ()
pkg_check_modules (gtk gtk+-3.0)
option (WITH_GUI "Build an alternative GTK+ UI" ${gtk_FOUND})
if (WITH_GUI)
if (NOT gtk_FOUND)
message (FATAL_ERROR "GTK+ not found")
endif ()
link_directories (${dependencies_LIBRARY_DIRS})
include_directories (${gtk_INCLUDE_DIRS})
link_directories (${gtk_LIBRARY_DIRS})
endif ()
link_directories (${dependencies_LIBRARY_DIRS} ${icu_LIBRARY_DIRS})
include_directories (${ZLIB_INCLUDE_DIRS} ${icu_INCLUDE_DIRS}
${dependencies_INCLUDE_DIRS} ${Ncursesw_INCLUDE_DIRS}
${Termo_INCLUDE_DIRS})
@@ -113,27 +127,47 @@ CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
# Localization
find_package (Gettext REQUIRED)
file (GLOB project_PO_FILES ${PROJECT_SOURCE_DIR}/po/*.po)
file (GLOB project_PO_FILES "${PROJECT_SOURCE_DIR}/po/*.po")
GETTEXT_CREATE_TRANSLATIONS (
${PROJECT_SOURCE_DIR}/po/${PROJECT_NAME}.pot
"${PROJECT_SOURCE_DIR}/po/${PROJECT_NAME}.pot"
ALL ${project_PO_FILES})
# Documentation
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "asciidoctor not found")
find_program (A2X_EXECUTABLE a2x)
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
message (WARNING "Neither asciidoctor nor a2x were found, "
"falling back to a substandard manual page generator")
endif ()
foreach (page "${PROJECT_NAME}.1")
set (page_output "${PROJECT_BINARY_DIR}/${page}")
list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${PROJECT_VERSION}
"${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
-o "${page_output}"
DEPENDS "docs/${page}.adoc"
COMMENT "Generating man page for ${page}" VERBATIM)
if (ASCIIDOCTOR_EXECUTABLE)
add_custom_command (OUTPUT "${page_output}"
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${PROJECT_VERSION}
-o "${page_output}"
"${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
DEPENDS "docs/${page}.adoc"
COMMENT "Generating man page for ${page}" VERBATIM)
elseif (A2X_EXECUTABLE)
add_custom_command (OUTPUT "${page_output}"
COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
-a release-version=${PROJECT_VERSION}
-D "${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
DEPENDS "docs/${page}.adoc"
COMMENT "Generating man page for ${page}" VERBATIM)
else ()
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
add_custom_command (OUTPUT "${page_output}"
COMMAND env LC_ALL=C asciidoc-release-version=${PROJECT_VERSION}
awk -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
> "${page_output}"
DEPENDS "docs/${page}.adoc" ${ASCIIMAN}
COMMENT "Generating man page for ${page}" VERBATIM)
endif ()
endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
@@ -147,7 +181,7 @@ if (WIN32)
endif (WIN32)
set (project_common_headers
${PROJECT_BINARY_DIR}/config.h
"${PROJECT_BINARY_DIR}/config.h"
src/dictzip-input-stream.h
src/stardict.h
src/stardict-private.h
@@ -164,47 +198,80 @@ add_library (stardict OBJECT
set (project_common_sources $<TARGET_OBJECTS:stardict>)
# Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h)
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
configure_file ("${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h")
include_directories ("${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}")
# Primary target source files
# Build the main executable and link it
set (project_libraries
${project_common_libraries})
set (project_sources
${project_common_sources}
src/${PROJECT_NAME}.c)
set (project_headers
${project_common_headers})
# Build the main executable and link it
add_definitions (-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_38)
if (NOT WIN32)
add_executable (${PROJECT_NAME}
${project_sources} ${project_headers} ${project_common_sources})
target_link_libraries (${PROJECT_NAME} ${project_common_libraries}
${Ncursesw_LIBRARIES} termo-static)
if (WITH_X11)
target_link_libraries (${PROJECT_NAME} ${xcb_LIBRARIES})
endif ()
endif (NOT WIN32)
# The same for the alternative GTK+ UI
if (WITH_GUI)
add_executable (sdgui WIN32
src/sdgui.c
src/stardict-view.c
${project_common_sources})
target_include_directories (sdgui PUBLIC ${gtk_INCLUDE_DIRS})
target_link_libraries (sdgui ${project_common_libraries} ${gtk_LIBRARIES})
include (IconUtils)
# The largest size is mainly for an appropriately sized Windows icon
set (icon_base "${PROJECT_BINARY_DIR}/icons")
set (icon_png_list)
foreach (icon_size 16 32 48 256)
icon_to_png (${PROJECT_NAME} "${PROJECT_SOURCE_DIR}/${PROJECT_NAME}.svg"
${icon_size} "${icon_base}" icon_png)
list (APPEND icon_png_list "${icon_png}")
endforeach ()
add_custom_target (icons ALL DEPENDS ${icon_png_list})
endif ()
if (WIN32)
list (REMOVE_ITEM icon_png_list "${icon_png}")
set (icon_ico "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ico")
icon_for_win32 ("${icon_ico}" "${icon_png_list}" "${icon_png}")
set (resource_file "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.rc")
list (APPEND project_sources "${resource_file}")
add_custom_command (OUTPUT "${resource_file}"
COMMAND ${CMAKE_COMMAND} -E echo "1 ICON \"${PROJECT_NAME}.ico\""
> "${resource_file}" VERBATIM)
set_property (SOURCE "${resource_file}"
APPEND PROPERTY OBJECT_DEPENDS "${icon_ico}")
else ()
list (APPEND project_libraries ${Ncursesw_LIBRARIES} ${Termo_LIBRARIES})
list (APPEND project_sources
src/${PROJECT_NAME}-tui.c)
endif ()
if (WITH_X11)
list (APPEND project_libraries ${xcb_LIBRARIES})
endif ()
if (WITH_GUI)
list (APPEND project_libraries ${gtk_LIBRARIES})
list (APPEND project_sources
src/${PROJECT_NAME}-gui.c
src/stardict-view.c)
add_executable (${PROJECT_NAME} WIN32 ${project_sources} ${project_headers})
else ()
add_executable (${PROJECT_NAME} ${project_sources} ${project_headers})
endif ()
target_link_libraries (${PROJECT_NAME} ${project_libraries})
# Tools
set (tools tabfile add-pronunciation query-tool transform)
set (tools tdv-tabfile tdv-add-pronunciation tdv-query-tool tdv-transform)
foreach (tool ${tools})
add_executable (${tool} EXCLUDE_FROM_ALL
src/${tool}.c ${project_common_sources})
target_link_libraries (${tool} ${project_common_libraries})
endforeach ()
add_custom_target (tools DEPENDS ${tools})
option (WITH_TOOLS "Build and install some StarDict tools" ${UNIX})
if (WITH_TOOLS)
add_custom_target (tools ALL DEPENDS ${tools})
endif ()
# Example dictionaries
file (GLOB dicts_scripts "${PROJECT_SOURCE_DIR}/dicts/*.*")
@@ -214,7 +281,7 @@ foreach (dict_script ${dicts_scripts})
list (APPEND dicts_targets "dicts-${dict_name}")
add_custom_target (dicts-${dict_name}
COMMAND sh -c "PATH=.:$PATH \"$0\"" "${dict_script}"
DEPENDS tabfile
DEPENDS tdv-tabfile
COMMENT "Generating sample dictionary ${dict_name}"
VERBATIM)
endforeach ()
@@ -227,11 +294,17 @@ if (NOT WIN32)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
if (WITH_TOOLS)
install (TARGETS ${tools} DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
if (WITH_GUI)
install (TARGETS sdgui DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES sdgui.desktop
install (FILES ${PROJECT_NAME}.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
install (DIRECTORY ${icon_base}
DESTINATION ${CMAKE_INSTALL_DATADIR})
install (FILES ${PROJECT_NAME}.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install (FILES sdgui.xml
install (FILES ${PROJECT_NAME}.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages)
endif ()
@@ -242,74 +315,108 @@ if (NOT WIN32)
endforeach ()
elseif (WITH_GUI)
# This rather crude filter has been mostly copied over from logdiag
install (TARGETS sdgui DESTINATION .)
install (TARGETS ${PROJECT_NAME} DESTINATION .)
install (DIRECTORY
${WIN32_DEPENDS_PATH}/bin/
${win32_deps_prefix}/bin/
DESTINATION .
FILES_MATCHING PATTERN "*.dll")
install (DIRECTORY
${WIN32_DEPENDS_PATH}/etc/
${win32_deps_prefix}/etc/
DESTINATION etc)
install (DIRECTORY
${WIN32_DEPENDS_PATH}/lib/gdk-pixbuf-2.0
${win32_deps_prefix}/lib/gdk-pixbuf-2.0
DESTINATION lib
FILES_MATCHING PATTERN "*" PATTERN "*.a" EXCLUDE)
install (DIRECTORY
${WIN32_DEPENDS_PATH}/share/glib-2.0/schemas
${win32_deps_prefix}/share/glib-2.0/schemas
DESTINATION share/glib-2.0)
install (DIRECTORY
${WIN32_DEPENDS_PATH}/share/icons/Adwaita
${win32_deps_prefix}/share/icons/Adwaita
DESTINATION share/icons OPTIONAL)
install (FILES
${WIN32_DEPENDS_PATH}/share/icons/hicolor/index.theme
${win32_deps_prefix}/share/icons/hicolor/index.theme
DESTINATION share/icons/hicolor)
install (DIRECTORY "${icon_base}" DESTINATION share)
install (SCRIPT cmake/Win32Cleanup.cmake)
find_program (GTK_UPDATE_ICON_CACHE_EXECUTABLE gtk-update-icon-cache)
if (NOT GTK_UPDATE_ICON_CACHE_EXECUTABLE)
message (FATAL_ERROR "gtk-update-icon-cache not found")
endif ()
install (CODE "execute_process (COMMAND
sh \"${PROJECT_SOURCE_DIR}/cmake/Win32CleanupAdwaita.sh\"
WORKING_DIRECTORY \${CMAKE_INSTALL_PREFIX})")
install (CODE " # This may speed up program start-up a little bit
execute_process (COMMAND \"${GTK_UPDATE_ICON_CACHE_EXECUTABLE}\"
\"\${CMAKE_INSTALL_PREFIX}/share/icons/Adwaita\")")
endif ()
# Do some unit tests
option (BUILD_TESTING "Build tests" OFF)
set (project_tests stardict)
if (BUILD_TESTING)
enable_testing ()
find_program (xmlwf_EXECUTABLE xmlwf)
find_program (xmllint_EXECUTABLE xmllint)
foreach (xml sdgui.xml)
foreach (xml ${PROJECT_NAME}.xml ${PROJECT_NAME}.svg)
if (xmlwf_EXECUTABLE)
add_test (test-xmlwf-${xml} ${xmlwf_EXECUTABLE}
${PROJECT_SOURCE_DIR}/${xml})
"${PROJECT_SOURCE_DIR}/${xml}")
endif ()
if (xmllint_EXECUTABLE)
add_test (test-xmllint-${xml} ${xmllint_EXECUTABLE} --noout
${PROJECT_SOURCE_DIR}/${xml})
"${PROJECT_SOURCE_DIR}/${xml}")
endif ()
endforeach ()
foreach (name ${project_tests})
find_program (dfv_EXECUTABLE desktop-file-validate)
if (dfv_EXECUTABLE)
foreach (df ${PROJECT_NAME}.desktop)
add_test (test-dfv-${df} ${dfv_EXECUTABLE}
"${PROJECT_SOURCE_DIR}/${df}")
endforeach ()
endif ()
foreach (name stardict)
add_executable (test-${name}
src/test-${name}.c ${project_common_sources})
target_link_libraries (test-${name} ${project_common_libraries})
add_test (test-${name} test-${name})
add_test (NAME test-${name} COMMAND test-${name})
endforeach ()
endif ()
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "StarDict TUI and GUI")
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Translation dictionary viewer")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME} ${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
# XXX: It is still possible to install multiple copies, making commands collide.
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "${PROJECT_NAME}")
set (CPACK_NSIS_INSTALLED_ICON_NAME ${PROJECT_NAME}.exe)
set (CPACK_PACKAGE_EXECUTABLES ${PROJECT_NAME} ${PROJECT_NAME})
set (CPACK_NSIS_EXECUTABLES_DIRECTORY .)
set (CPACK_NSIS_EXTRA_INSTALL_COMMANDS [[
WriteRegStr HKCR '.ifo' '' 'tdv.Dictionary'
WriteRegStr HKCR 'tdv.Dictionary' '' 'StarDict Dictionary'
WriteRegStr HKCR 'tdv.Dictionary\\shell\\open\\command' '' '\"$INSTDIR\\tdv.exe\" \"%1\"'
System::Call 'shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)'
]])
set (CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS [[
DeleteRegKey HKCR 'tdv.Dictionary'
System::Call 'shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)'
]])
include (CPack)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2013 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@@ -1,41 +1,49 @@
StarDict Terminal and Graphical UI
==================================
Translation dictionary viewer
=============================
'sdtui' aims to provide an easy way of viewing translation as well as other
kinds of dictionaries in your terminal, and is inspired by the dictionary
component of PC Translator. I wasn't successful in finding any free software
of this kind, GUI or not, and thus decided to write my own.
'tdv' aims to provide an easy way of viewing translation as well as other kinds
of StarDict dictionaries, and is inspired by the dictionary component
of PC Translator. I was unsuccessful in finding any free software of this kind,
and thus decided to write my own.
The program offers both a terminal user interface, and a GTK+ 3 based UI.
The styling of the latter will follow your theme, and may be customized
from 'gtk.css'.
The project is covered by a permissive license, unlike vast majority of other
similar projects, and can serve as a base for implementing other dictionary
software.
image::sdtui.png[align="center"]
As a recent addition, there is now an alternative GTK+ 3 based frontend as well,
called 'sdgui'. It shares its dictionary list with 'sdtui', but styling will
follow your theme, and may be customized from 'gtk.css'.
Screenshot
----------
image::tdv.png[align="center"]
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/tdv-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Documentation
-------------
See the link:docs/sdtui.1.adoc[man page] for information about usage.
See the link:docs/tdv.1.adoc[man page] for information about usage.
The rest of this README will concern itself with externalities.
Building and Running
--------------------
Build dependencies: CMake, pkg-config, asciidoctor +
Runtime dependencies: ncursesw, zlib, ICU, termo (included), glib-2.0 >= 2.38,
pango, xcb, xcb-xfixes (the latter two optional for the TUI),
gtk+-3.0 (for the alternative graphical UI)
Build-only dependencies:
CMake, pkg-config, gettext utilities, asciidoctor or asciidoc +
Optional build-only dependencies:
librsvg (for the GUI), icoutils (for the GUI, when targetting Windows) +
Runtime dependencies:
ncursesw, zlib, ICU, termo (included), glib-2.0 >= 2.38, pango +
Optional runtime dependencies:
xcb, xcb-xfixes (the first two for the TUI), gtk+-3.0 (for the GUI)
$ git clone --recursive https://git.janouch.name/p/sdtui.git
$ mkdir sdtui/build
$ cd sdtui/build
$ git clone --recursive https://git.janouch.name/p/tdv.git
$ mkdir tdv/build
$ cd tdv/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
-DWITH_X11=ON -DWITH_GUI=ON
$ make
@@ -47,22 +55,22 @@ To install the application, you can do either the usual:
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i sdtui-*.deb
# dpkg -i tdv-*.deb
Having the program installed, simply run it with a StarDict '.ifo' file as
an argument. It is, however, preferable to
link:docs/sdtui.1.adoc#_configuration[configure it] to load your dictionaries
link:docs/tdv.1.adoc#_configuration[configure it] to load your dictionaries
automatically.
Windows
~~~~~~~
With the help of Mingw-w64 and WINE, 'sdgui' will successfully cross-compile
With the help of Mingw-w64 and WINE, 'tdv' will successfully cross-compile
for Windows. It isn't particularly usable on that system, if only because
selection watching is a very X11/Wayland-specific feature. Beware that build
dependencies take up almost a gigabyte of disk space.
$ sh cmake/Win64Depends.sh
$ cmake -DCMAKE_TOOLCHAIN_FILE=cmake/Win64CrossToolchain.cmake \
$ sh -e cmake/Win64Depends.sh
$ cmake -DCMAKE_TOOLCHAIN_FILE=liberty/cmake/toolchains/MinGW-w64-x64.cmake \
-DCMAKE_BUILD_TYPE=Release -B build
$ cmake --build build -- package
@@ -75,30 +83,24 @@ types are plain text, Pango markup, and XDXF (the visual format works better).
The `make dicts` command will build some examples from freely available sources:
- GNU/FDL Czech-English dictionary
- Czech foreign words
- Czech foreign words (the site's export is broken as of 2022/08, no response)
- Czech WordNet 1.9 PDT (synonyms, hypernyms, hyponyms)
You can use the included 'transform' tool to convert already existing StarDict
dictionaries that are nearly good as they are. Remember that you can change
the `sametypesequence` of the resulting '.ifo' file to another format, or run
'dictzip' on '.dict' files to make them compact.
You can use the included 'tdv-transform' tool to convert already existing
StarDict dictionaries that are nearly good as they are. Remember that you can
change the `sametypesequence` of the resulting '.ifo' file to another format,
or run 'dictzip' on '.dict' files to make them compact.
https://mega.co.nz/#!axtD0QRK!sbtBgizksyfkPqKvKEgr8GQ11rsWhtqyRgUUV0B7pwg[CZ <--> EN/DE/PL/RU dictionaries]
Further Development
-------------------
While I've been successfully using 'sdtui' for many years now, some work has to
be done yet before the software can be considered fit for inclusion in regular
Linux and/or BSD distributions:
- The tab bar and the text input field don't handle overflows well in the TUI.
- Lacking configuration, standard StarDict locations should be scanned.
Given all issues with the file format, it might be better to start anew.
Lacking configuration, standard StarDict locations should be scanned.
We should try harder to display arbitrary dictionaries sensibly.
Contributing and Support
------------------------
Use https://git.janouch.name/p/sdtui to report any bugs, request features,
Use https://git.janouch.name/p/tdv to report any bugs, request features,
or submit pull requests. `git send-email` is tolerated. If you want to discuss
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.

View File

@@ -1,17 +0,0 @@
# Public Domain
find_package (PkgConfig REQUIRED)
pkg_check_modules (Ncursesw QUIET ncursesw)
# OpenBSD doesn't provide a pkg-config file
set (required_vars Ncursesw_LIBRARIES)
if (NOT Ncursesw_FOUND)
find_library (Ncursesw_LIBRARIES NAMES ncursesw)
find_path (Ncursesw_INCLUDE_DIRS ncurses.h)
list (APPEND required_vars Ncursesw_INCLUDE_DIRS)
endif (NOT Ncursesw_FOUND)
include (FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS (Ncursesw DEFAULT_MSG ${required_vars})
mark_as_advanced (Ncursesw_LIBRARIES Ncursesw_INCLUDE_DIRS)

View File

@@ -1,6 +1,6 @@
#!/bin/sh -e
# Removes unused icons from the Adwaita theme, it could be even more aggressive,
# since it keeps around lots of sizes and all the GTK+ stock icons
# Removes unused icons from the Adwaita theme. It could be even more aggressive,
# since it keeps around lots of sizes and all the GTK+ stock icons.
export LC_ALL=C
find share/icons/Adwaita -type f | awk 'BEGIN {
while (("grep -aho \"[a-z][a-z-]*\" *.dll *.exe" | getline) > 0)
@@ -12,8 +12,9 @@ find share/icons/Adwaita -type f | awk 'BEGIN {
sub(/[.].+$/, "", base)
# Try matching while cutting off suffixes
# Disregarding the not-much-used GTK_ICON_LOOKUP_GENERIC_FALLBACK
while (!(keep = good[base]) &&
sub(/-(ltr|rtl|symbolic)$/, "", base)) {}
if (!keep)
print
}' | xargs rm
}' | xargs rm --

View File

@@ -1,15 +0,0 @@
set (CMAKE_SYSTEM_NAME "Windows")
set (CMAKE_SYSTEM_PROCESSOR "x86_64")
set (CMAKE_C_COMPILER "x86_64-w64-mingw32-gcc")
set (CMAKE_CXX_COMPILER "x86_64-w64-mingw32-g++")
set (CMAKE_RC_COMPILER "x86_64-w64-mingw32-windres")
# Not needed to crosscompile an installation package
#set (CMAKE_CROSSCOMPILING_EMULATOR "wine64")
set (CMAKE_FIND_ROOT_PATH "/usr/x86_64-w64-mingw32")
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

View File

@@ -1,5 +1,5 @@
#!/bin/sh -e
# Win64Depends.sh: download dependencies from MSYS2 for cross-compilation
# Win64Depends.sh: download dependencies from MSYS2 for cross-compilation.
# Dependencies: AWK, sed, sha256sum, cURL, bsdtar, wine64
repository=https://repo.msys2.org/mingw/mingw64/
@@ -27,7 +27,8 @@ fetch() {
} BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
while IFS= read -r name
do
status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
@@ -44,9 +45,9 @@ extract() {
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
for i in packages/*
do bsdtar -xf "$i" --strip-components 1 mingw64
done
while IFS= read -r name
do bsdtar -xf "packages/$name" --strip-components 1
done < db.want
}
configure() {
@@ -54,16 +55,15 @@ configure() {
glib-compile-schemas share/glib-2.0/schemas
wine64 bin/gdk-pixbuf-query-loaders.exe \
> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
# pkgconf has a command line option for this, but CMake can't pass it
sed -i "s|^prefix=/mingw64|prefix=$(pwd)|" {share,lib}/pkgconfig/*.pc
}
mkdir -p win32-depends
cd win32-depends
# This directory name matches the prefix in .pc files, so we don't need to
# modify them (pkgconf has --prefix-variable, but CMake can't pass that option).
mkdir -p mingw64
cd mingw64
dbsync
fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-icu \
mingw-w64-x86_64-libwinpthread-git # because we don't do "provides"?
mingw-w64-x86_64-libwinpthread-git # Because we don't do "provides"?
verify
extract
configure
@@ -71,4 +71,4 @@ configure
status Success
# XXX: Why is this override needed to run some GLib-based things under wine64?
export XDG_DATA_DIRS=$(pwd)/share
unset XDG_DATA_DIRS

View File

@@ -8,6 +8,7 @@
#define GETTEXT_DIRNAME "${CMAKE_INSTALL_PREFIX}/share/locale"
#cmakedefine WITH_X11
#cmakedefine WITH_GUI
#cmakedefine HAVE_RESIZETERM
#endif // ! CONFIG_H

View File

@@ -4,11 +4,14 @@
use warnings;
use strict;
# GNU Gzip can unpack a ZIP file, but not the BSD one, and unzip can't use stdin
my $zipcat = qx/(command -v bsdtar)/ ? 'bsdtar -xOf-' : 'zcat';
my $base = 'https://lindat.cz/repository/xmlui';
my $path = 'handle/11858/00-097C-0000-0001-4880-3';
open(my $doc, '-|',
"curl -Lo- '$base/bitstream/$path/Czech_WordNet_1.9_PDT.zip'"
. ' | zcat | iconv -f latin2 -t UTF-8') or die $!;
. " | $zipcat | iconv -f latin2 -t UTF-8") or die $!;
# https://nlp.fi.muni.cz/trac/deb2/wiki/WordNetFormat but not quite;
# for terminology see https://wordnet.princeton.edu/documentation/wngloss7wn
@@ -52,7 +55,7 @@ while (my ($id, $synset) = each %synsets) {
# Output synsets exploded to individual words, with expanded relationships
close($doc) or die $?;
open(my $tabfile, '|-', 'tabfile', 'czech-wordnet',
open(my $tabfile, '|-', 'tdv-tabfile', 'czech-wordnet',
'--book-name=Czech WordNet 1.9 PDT', "--website=$base/$path",
'--date=2011-01-24', '--collation=cs_CZ') or die $!;

View File

@@ -2,14 +2,14 @@
# GNU/FDL German-Czech dictionary, see https://gnu.nemeckoceskyslovnik.cz
# Sometimes the domain doesn't resolve, and the contents are close to useless
[ -n "$WANT_BAD_DICTS" ] || exit
[ -n "$WANT_BAD_DICTS" ] || exit 0
curl -Lo- 'https://gnu.nemeckoceskyslovnik.cz/index.php?id=6&sablona=export&format=zcu' | \
grep -v ^# | sed 's/\\//g' | perl -CSD -F\\t -le '
sub tabesc { shift =~ s/\\/\\\\/gr =~ s/\n/\\n/gr =~ s/\t/\\t/gr }
sub w {
my ($name, $dict, $collation) = @_;
open(my $f, "|-", "tabfile", "--pango", "--collation=$collation",
open(my $f, "|-", "tdv-tabfile", "--pango", "--collation=$collation",
"--website=https://gnu.nemeckoceskyslovnik.cz",
"gnu-fdl-$name") or die $!;
print $f tabesc($keyword) . "\t" . tabesc(join("\n", @$defs))

View File

@@ -5,7 +5,7 @@ zcat | grep -v ^# | sed 's/\\//g' | perl -CSD -F\\t -le '
sub tabesc { shift =~ s/\\/\\\\/gr =~ s/\n/\\n/gr =~ s/\t/\\t/gr }
sub w {
my ($name, $dict, $collation) = @_;
open(my $f, "|-", "tabfile", "--pango", "--collation=$collation",
open(my $f, "|-", "tdv-tabfile", "--pango", "--collation=$collation",
"--website=https://www.svobodneslovniky.cz",
"gnu-fdl-$name") or die $!;
print $f tabesc($keyword) . "\t" . tabesc(join("\n", @$defs))

View File

@@ -4,7 +4,7 @@
curl -Lo- https://slovnik-cizich-slov.abz.cz/export.php | \
iconv -f latin2 -t UTF-8 | perl -CSD -F\\\| -le '
print "$_\t" . $F[2] =~ s/\\/\\\\/gr =~ s/; /\\n/gr for split(", ", $F[0])
' | sort -u | tabfile slovnik-cizich-slov \
' | sort -u | tdv-tabfile slovnik-cizich-slov \
--book-name="Slovník cizích slov" \
--website=https://slovnik-cizich-slov.abz.cz \
--date="$(date +%F)" \

View File

@@ -1,21 +1,21 @@
sdtui(1)
========
tdv(1)
======
:doctype: manpage
:manmanual: sdtui Manual
:mansource: sdtui {release-version}
:manmanual: tdv Manual
:mansource: tdv {release-version}
Name
----
sdtui - StarDict terminal UI
tdv - Translation dictionary viewer
Synopsis
--------
*sdtui* [_OPTION_]... [_DICTIONARY_.ifo]...
*tdv* [_OPTION_]... [_DICTIONARY_.ifo]...
Description
-----------
*sdtui* is a StarDict dictionary viewer, custom-tailored for translation
dictionaries, with a simple curses-based terminal UI.
*tdv* is a StarDict dictionary viewer, custom-tailored for translation
dictionaries, with a simple curses-based terminal UI, and a GTK+ graphical UI.
Without any command line arguments, the program expects to find a list of
dictionaries to load on start-up in its configuration file. The _.ifo_ files
@@ -24,6 +24,9 @@ database files.
Options
-------
*--gui*::
Launch the GUI even when run from a terminal.
*-h*, *--help*::
Display a help message and exit.
@@ -32,7 +35,7 @@ Options
Configuration
-------------
You can start your _sdtui.conf_ file with the following snippet:
You can start your _tdv.conf_ file with the following snippet:
[Settings]
center-search = true # Ensure visibility of preceding entries?
@@ -48,10 +51,13 @@ Luckily, some compositors, such as Sway, synchronize selections with Xwayland.
To set up automatically loaded dictionaries, use the following scheme:
// AsciiDoc would otherwise like to process tildes as a long subscript.
:tilde: ~
[subs="normal"]
[Dictionaries]
_name 1_ = __~/path/to/dict.ifo__
_name 2_ = __~/another/dict.ifo__
_name 1_ = __{tilde}/path/to/dict.ifo__
_name 2_ = __{tilde}/another/dict.ifo__
The left-hand side keys define their appearance in the tab bar.
@@ -72,21 +78,21 @@ Extensions
----------
Because the StarDict file format is a bit of a clusterfuck with regard to
collation of dictionary entries, this software introduces an additional,
optional "collation" field into the '.ifo' file. When *sdtui* discovers this
optional "collation" field into the _.ifo_ file. When *tdv* discovers this
field while reading a dictionary, it automatically reorders the index according
to that locale (e.g., "cs_CZ"). This operation may take a little while,
in the order of seconds.
Files
-----
*sdtui* follows the XDG Base Directory Specification.
*tdv* follows the XDG Base Directory Specification.
_~/.config/sdtui/sdtui.conf_::
_~/.config/tdv/tdv.conf_::
The configuration file.
Reporting bugs
--------------
Use https://git.janouch.name/p/sdtui to report bugs, request features,
Use https://git.janouch.name/p/tdv to report bugs, request features,
or submit pull requests.
See also

1
liberty Submodule

Submodule liberty added at 0f20cce9c8

102
po/cs.po
View File

@@ -1,84 +1,95 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2013 Přemysl Eric Janouch
# This file is distributed under the same license as the sdtui package.
# This file is distributed under the same license as the tdv package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: sdtui 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/sdtui/issues\n"
"POT-Creation-Date: 2021-10-11 21:10+0200\n"
"PO-Revision-Date: 2016-09-28 16:15+0200\n"
"Project-Id-Version: tdv 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/tdv/issues\n"
"POT-Creation-Date: 2023-06-11 17:47+0200\n"
"PO-Revision-Date: 2023-06-11 17:53+0200\n"
"Last-Translator: Přemysl Eric Janouch <p@janouch.name>\n"
"Language-Team: Czech <translation-team-cs@lists.sourceforge.net>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.9\n"
"X-Generator: Poedit 3.3.1\n"
#: ../src/sdgtk.c:304
msgid "- StarDict GTK+ UI"
msgstr ""
#: ../src/sdtui.c:726
#: ../src/tdv-tui.c:572
msgid "Cannot load configuration"
msgstr "Nemohu načíst konfiguraci"
#: ../src/sdtui.c:2497
#: ../src/tdv.c:77
msgid "Error"
msgstr "Chyba"
#: ../src/sdtui.c:745
#: ../src/tdv-tui.c:592
msgid "Error loading dictionary"
msgstr "Chyba při načítání slovníku"
#: ../src/sdgtk.c:299
msgid "FILE..."
msgstr ""
#: ../src/tdv.c:60
msgid "Launch the GUI even when run from a terminal"
msgstr "Spustit GUI i při běhu z terminálu"
#: ../src/sdgtk.c:355
msgid "Follow selection"
msgstr ""
#: ../src/sdtui.c:750
msgid ""
"No dictionaries found either in the configuration or on the command line"
#: ../src/tdv-gui.c:467 ../src/tdv-tui.c:597
msgid "No dictionaries found either in the configuration or on the command line"
msgstr "V konfiguraci ani na příkazové řádce nebyly nalezeny žádné slovníky"
#: ../src/sdtui.c:2476
#: ../src/tdv-gui.c:367
msgid "Open dictionary"
msgstr "Otevřít slovník"
#: ../src/tdv.c:55
msgid "Output version information and exit"
msgstr "Vypíše informace o verzi a ukončí se"
#: ../src/sdtui.c:700
#: ../src/tdv-tui.c:555
msgid "Search"
msgstr "Hledat"
#: ../src/sdtui.c:1114
#: ../src/tdv-tui.c:1068
msgid "Terminal UI for StarDict dictionaries"
msgstr "Terminálové UI pro stardictové slovníky"
#: ../src/sdtui.c:1117
#: ../src/tdv-tui.c:1071
msgid "Type to search"
msgstr "Zadejte vyhledávaný výraz"
#: ../src/sdgtk.c:289 ../src/sdtui.c:2481
#: ../src/tdv.c:39
msgid "Warning"
msgstr "Varování"
#: ../src/sdtui.c:2095
#: ../src/tdv-tui.c:2071
#, c-format
msgid "X11 connection failed (error code %d)"
msgstr ""
msgstr "Spojení s X11 selhalo (chybový kód %d)"
#: ../src/sdtui.c:2241
#: ../src/tdv-tui.c:2217
#, c-format
msgid "X11 request error (%d, major %d, minor %d)"
msgstr ""
msgstr "Chyba X11 požadavku (%d, major %d, minor %d)"
#: ../src/sdtui.c:2489
msgid "[dictionary.ifo...] - StarDict terminal UI"
msgstr "[slovník.ifo...] - terminálové UI pro StarDict"
#: ../src/tdv.c:67
msgid "[dictionary.ifo...] - Translation dictionary viewer"
msgstr "[slovník.ifo...] - Prohlížeč překladových slovníků"
#: ../src/tdv-gui.c:369
msgid "_Cancel"
msgstr "_Storno"
#: ../src/tdv-gui.c:509
msgid "_Follow selection"
msgstr "_Sledovat výběr"
#: ../src/tdv-gui.c:370
msgid "_Open"
msgstr "_Otevřít"
#: ../src/tdv-gui.c:504
msgid "_Open..."
msgstr "_Otevřít..."
#: ../src/stardict.c:850
msgid "cannot find .dict file"
@@ -88,19 +99,19 @@ msgstr "nemohu najít .dict soubor"
msgid "cannot find .idx file"
msgstr "nemohu najít .idx soubor"
#: ../src/sdtui.c:320
#: ../src/tdv-tui.c:258
msgid "error in entry"
msgstr "chyba v záznamu"
#: ../src/sdgtk.c:289 ../src/sdtui.c:2481
#: ../src/tdv.c:39
msgid "failed to set the locale"
msgstr "selhalo nastavení locale"
#: ../src/stardict.c:330
#: ../src/stardict.c:328
msgid "index file size not specified"
msgstr "nebyla určena velikost rejstříku"
#: ../src/stardict.c:1153 ../src/stardict.c:1178
#: ../src/stardict.c:1157 ../src/stardict.c:1182
msgid "invalid data entry"
msgstr "neplatná datová položka"
@@ -112,7 +123,7 @@ msgstr "neplatné kódování, musí být validní UTF-8"
msgid "invalid header format"
msgstr "neplatný formát hlavičky"
#: ../src/stardict.c:339
#: ../src/stardict.c:337
msgid "invalid index offset bits"
msgstr "neplatný počet bitů pro offset v rejstříku"
@@ -124,11 +135,11 @@ msgstr "neplatné číslo"
msgid "invalid version"
msgstr "neplatná verze"
#: ../src/stardict.c:318
#: ../src/stardict.c:316
msgid "no book name specified"
msgstr "nebyl určen název knihy"
#: ../src/sdtui.c:415
#: ../src/stardict-view.c:96 ../src/tdv-tui.c:340
msgid "no usable field found"
msgstr "nenalezeno žádné použitelné pole"
@@ -136,7 +147,7 @@ msgstr "nenalezeno žádné použitelné pole"
msgid "option format error"
msgstr "chyba v zápisu volby"
#: ../src/sdtui.c:2497
#: ../src/tdv.c:77
msgid "option parsing failed"
msgstr "zpracování přepínačů selhalo"
@@ -148,6 +159,9 @@ msgstr "neznámý klíč, ignoruji"
msgid "version not specified"
msgstr "nebyla určena verze"
#: ../src/stardict.c:324
#: ../src/stardict.c:322
msgid "word count not specified"
msgstr "nebyl určen počet slov"
#~ msgid "FILE..."
#~ msgstr "SOUBOR..."

View File

@@ -1,14 +1,14 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Přemysl Eric Janouch
# This file is distributed under the same license as the sdtui package.
# This file is distributed under the same license as the tdv package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: sdtui 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/sdtui/issues\n"
"POT-Creation-Date: 2021-10-11 21:10+0200\n"
"Project-Id-Version: tdv 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/tdv/issues\n"
"POT-Creation-Date: 2023-06-11 17:47+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,85 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../src/sdgtk.c:289 ../src/sdtui.c:2481
msgid "Warning"
msgstr ""
#: ../src/sdgtk.c:289 ../src/sdtui.c:2481
msgid "failed to set the locale"
msgstr ""
#: ../src/sdgtk.c:299
msgid "FILE..."
msgstr ""
#: ../src/sdgtk.c:304
msgid "- StarDict GTK+ UI"
msgstr ""
#: ../src/sdgtk.c:355
msgid "Follow selection"
msgstr ""
#: ../src/sdtui.c:320
msgid "error in entry"
msgstr ""
#: ../src/sdtui.c:415
#: ../src/stardict-view.c:96 ../src/tdv-tui.c:340
msgid "no usable field found"
msgstr ""
#: ../src/sdtui.c:700
msgid "Search"
msgstr ""
#: ../src/sdtui.c:726
msgid "Cannot load configuration"
msgstr ""
#: ../src/sdtui.c:745
msgid "Error loading dictionary"
msgstr ""
#: ../src/sdtui.c:750
msgid ""
"No dictionaries found either in the configuration or on the command line"
msgstr ""
#: ../src/sdtui.c:1114
msgid "Terminal UI for StarDict dictionaries"
msgstr ""
#: ../src/sdtui.c:1117
msgid "Type to search"
msgstr ""
#: ../src/sdtui.c:2095
#, c-format
msgid "X11 connection failed (error code %d)"
msgstr ""
#: ../src/sdtui.c:2241
#, c-format
msgid "X11 request error (%d, major %d, minor %d)"
msgstr ""
#: ../src/sdtui.c:2476
msgid "Output version information and exit"
msgstr ""
#: ../src/sdtui.c:2489
msgid "[dictionary.ifo...] - StarDict terminal UI"
msgstr ""
#: ../src/sdtui.c:2497
msgid "Error"
msgstr ""
#: ../src/sdtui.c:2497
msgid "option parsing failed"
msgstr ""
#: ../src/stardict.c:89
msgid "invalid header format"
msgstr ""
@@ -124,19 +49,19 @@ msgstr ""
msgid "option format error"
msgstr ""
#: ../src/stardict.c:318
#: ../src/stardict.c:316
msgid "no book name specified"
msgstr ""
#: ../src/stardict.c:324
#: ../src/stardict.c:322
msgid "word count not specified"
msgstr ""
#: ../src/stardict.c:330
#: ../src/stardict.c:328
msgid "index file size not specified"
msgstr ""
#: ../src/stardict.c:339
#: ../src/stardict.c:337
msgid "invalid index offset bits"
msgstr ""
@@ -148,6 +73,93 @@ msgstr ""
msgid "cannot find .dict file"
msgstr ""
#: ../src/stardict.c:1153 ../src/stardict.c:1178
#: ../src/stardict.c:1157 ../src/stardict.c:1182
msgid "invalid data entry"
msgstr ""
#: ../src/tdv-gui.c:367
msgid "Open dictionary"
msgstr ""
#: ../src/tdv-gui.c:369
msgid "_Cancel"
msgstr ""
#: ../src/tdv-gui.c:370
msgid "_Open"
msgstr ""
#: ../src/tdv-gui.c:467 ../src/tdv-tui.c:597
msgid ""
"No dictionaries found either in the configuration or on the command line"
msgstr ""
#: ../src/tdv-gui.c:504
msgid "_Open..."
msgstr ""
#: ../src/tdv-gui.c:509
msgid "_Follow selection"
msgstr ""
#: ../src/tdv-tui.c:258
msgid "error in entry"
msgstr ""
#: ../src/tdv-tui.c:555
msgid "Search"
msgstr ""
#: ../src/tdv-tui.c:572
msgid "Cannot load configuration"
msgstr ""
#: ../src/tdv-tui.c:592
msgid "Error loading dictionary"
msgstr ""
#: ../src/tdv-tui.c:1068
msgid "Terminal UI for StarDict dictionaries"
msgstr ""
#: ../src/tdv-tui.c:1071
msgid "Type to search"
msgstr ""
#: ../src/tdv-tui.c:2071
#, c-format
msgid "X11 connection failed (error code %d)"
msgstr ""
#: ../src/tdv-tui.c:2217
#, c-format
msgid "X11 request error (%d, major %d, minor %d)"
msgstr ""
#: ../src/tdv.c:39
msgid "Warning"
msgstr ""
#: ../src/tdv.c:39
msgid "failed to set the locale"
msgstr ""
#: ../src/tdv.c:55
msgid "Output version information and exit"
msgstr ""
#: ../src/tdv.c:60
msgid "Launch the GUI even when run from a terminal"
msgstr ""
#: ../src/tdv.c:67
msgid "[dictionary.ifo...] - Translation dictionary viewer"
msgstr ""
#: ../src/tdv.c:77
msgid "Error"
msgstr ""
#: ../src/tdv.c:77
msgid "option parsing failed"
msgstr ""

BIN
sdtui.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,500 +0,0 @@
/*
* StarDict GTK+ UI
*
* Copyright (c) 2020 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <locale.h>
#include <stdlib.h>
#include "config.h"
#include "stardict.h"
#include "utils.h"
#include "stardict-view.h"
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *hamburger; ///< Hamburger menu
GtkWidget *entry; ///< Search entry widget
GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary
GPtrArray *dictionaries; ///< All open dictionaries
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
init (gchar **filenames)
{
for (gsize i = 0; filenames[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_strdup (filenames[i]);
g_ptr_array_add (g.dictionaries, dict);
}
}
// TODO: try to deduplicate, similar to app_load_config_values()
static gboolean
init_from_key_file (GKeyFile *kf, GError **error)
{
const gchar *dictionaries = "Dictionaries";
gchar **names = g_key_file_get_keys (kf, dictionaries, NULL, NULL);
if (!names)
return TRUE;
for (gsize i = 0; names[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->name = names[i];
g_ptr_array_add (g.dictionaries, dict);
}
g_free (names);
for (gsize i = 0; i < g.dictionaries->len; i++)
{
Dictionary *dict = g_ptr_array_index (g.dictionaries, i);
gchar *path =
g_key_file_get_string (kf, dictionaries, dict->name, error);
if (!path)
return FALSE;
// Try to resolve relative paths and expand tildes
if (!(dict->filename =
resolve_filename (path, resolve_relative_config_filename)))
dict->filename = path;
else
g_free (path);
}
return TRUE;
}
static gboolean
init_from_config (GError **error)
{
GKeyFile *key_file = load_project_config_file (error);
if (!key_file)
return FALSE;
gboolean result = init_from_key_file (key_file, error);
g_key_file_free (key_file);
return result;
}
static void
search (Dictionary *dict)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
StardictIterator *iterator =
stardict_dict_search (dict->dict, input_utf8, NULL);
stardict_view_set_position (STARDICT_VIEW (g.view),
dict->dict, stardict_iterator_get_offset (iterator));
g_object_unref (iterator);
stardict_view_set_matched (STARDICT_VIEW (g.view), input_utf8);
}
static void
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
search (g_ptr_array_index (g.dictionaries, g.dictionary));
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gchar *trimmed = g_strstrip (g_strdup (text));
gtk_entry_set_text (GTK_ENTRY (g.entry), trimmed);
g_free (trimmed);
g_signal_emit_by_name (g.entry,
"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
}
static void
on_selection (GtkClipboard *clipboard, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (g.watch_selection
&& !gtk_window_has_toplevel_focus (GTK_WINDOW (g.window))
&& event->owner_change.owner != NULL)
gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
}
static void
on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
{
g.watch_selection = gtk_check_menu_item_get_active (item);
}
static void
on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
guint page_num, G_GNUC_UNUSED gpointer data)
{
g.dictionary = page_num;
search (g_ptr_array_index (g.dictionaries, g.dictionary));
}
static gboolean
accelerate_hamburger (GdkEvent *event)
{
gchar *accelerator = NULL;
g_object_get (gtk_widget_get_settings (g.window), "gtk-menu-bar-accel",
&accelerator, NULL);
if (!accelerator)
return FALSE;
guint key = 0;
GdkModifierType mods = 0;
gtk_accelerator_parse (accelerator, &key, &mods);
g_free (accelerator);
guint mask = gtk_accelerator_get_default_mod_mask ();
if (!key || event->key.keyval != key || (event->key.state & mask) != mods)
return FALSE;
gtk_button_clicked (GTK_BUTTON (g.hamburger));
return TRUE;
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
// The "activate" signal of the GtkMenuButton cannot be used
// from a real accelerator, due to "no trigger event for menu popup".
if (accelerate_hamburger (event))
return TRUE;
GtkNotebook *notebook = GTK_NOTEBOOK (g.notebook);
guint mods = event->key.state & gtk_accelerator_get_default_mod_mask ();
if (mods == GDK_CONTROL_MASK)
{
// Can't use gtk_widget_add_accelerator() to change-current-page(-1/+1)
// because that signal has arguments, which cannot be passed.
gint current = gtk_notebook_get_current_page (notebook);
if (event->key.keyval == GDK_KEY_Page_Up)
return gtk_notebook_set_current_page (notebook, --current), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return gtk_notebook_set_current_page (notebook,
++current % gtk_notebook_get_n_pages (notebook)), TRUE;
}
if (mods == GDK_MOD1_MASK)
{
if (event->key.keyval >= GDK_KEY_0
&& event->key.keyval <= GDK_KEY_9)
{
gint n = event->key.keyval - GDK_KEY_0;
gtk_notebook_set_current_page (notebook, n ? (n - 1) : 10);
return TRUE;
}
}
if (mods == 0)
{
StardictView *view = STARDICT_VIEW (g.view);
if (event->key.keyval == GDK_KEY_Page_Up)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, -0.5), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, +0.5), TRUE;
if (event->key.keyval == GDK_KEY_Up)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, -1), TRUE;
if (event->key.keyval == GDK_KEY_Down)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, +1), TRUE;
}
return FALSE;
}
static void
init_tabs (void)
{
for (gsize i = 0; i < g.dictionaries->len; i++)
{
Dictionary *dict = g_ptr_array_index (g.dictionaries, i);
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_append_page (GTK_NOTEBOOK (g.notebook), dummy, label);
}
gtk_widget_show_all (g.notebook);
gtk_widget_grab_focus (g.entry);
}
static void
show_error_dialog (GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (g.window), 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
g_error_free (error);
}
static gboolean
reload_dictionaries (GPtrArray *new_dictionaries)
{
GError *error = NULL;
if (!load_dictionaries (new_dictionaries, &error))
{
show_error_dialog (error);
return FALSE;
}
while (gtk_notebook_get_n_pages (GTK_NOTEBOOK (g.notebook)))
gtk_notebook_remove_page (GTK_NOTEBOOK (g.notebook), -1);
g.dictionary = -1;
stardict_view_set_position (STARDICT_VIEW (g.view), NULL, 0);
g_ptr_array_free (g.dictionaries, TRUE);
g.dictionaries = new_dictionaries;
init_tabs ();
return TRUE;
}
static void
on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
// The default is local-only. Paths are returned absolute.
GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open dictionary"),
GTK_WINDOW (g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT, NULL);
GtkFileFilter *filter = gtk_file_filter_new ();
gtk_file_filter_add_pattern (filter, "*.ifo");
gtk_file_filter_set_name (filter, "*.ifo");
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
gtk_file_chooser_add_filter (chooser, filter);
gtk_file_chooser_set_select_multiple (chooser, TRUE);
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
{
GSList *paths = gtk_file_chooser_get_filenames (chooser);
for (GSList *iter = paths; iter; iter = iter->next)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = iter->data;
g_ptr_array_add (new_dictionaries, dict);
}
g_slist_free (paths);
}
gtk_widget_destroy (dialog);
if (!new_dictionaries->len || !reload_dictionaries (new_dictionaries))
g_ptr_array_free (new_dictionaries, TRUE);
}
static void
on_drag_data_received (G_GNUC_UNUSED GtkWidget *widget,
G_GNUC_UNUSED GdkDragContext *context, G_GNUC_UNUSED gint x,
G_GNUC_UNUSED gint y, GtkSelectionData *data, G_GNUC_UNUSED guint info,
G_GNUC_UNUSED guint time, G_GNUC_UNUSED gpointer user_data)
{
GError *error = NULL;
gchar **dropped_uris = gtk_selection_data_get_uris (data);
if (!dropped_uris)
return;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
for (gsize i = 0; !error && dropped_uris[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_filename_from_uri (dropped_uris[i], NULL, &error);
g_ptr_array_add (new_dictionaries, dict);
}
g_strfreev (dropped_uris);
if (error)
show_error_dialog (error);
else if (new_dictionaries->len && reload_dictionaries (new_dictionaries))
return;
g_ptr_array_free (new_dictionaries, TRUE);
}
static void
on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
gtk_main_quit ();
}
static void
die_with_dialog (const gchar *message)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
exit (EXIT_FAILURE);
}
int
main (int argc, char *argv[])
{
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
gchar **filenames = NULL;
GOptionEntry option_entries[] =
{
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
NULL, N_("[FILE]...")},
{},
};
GError *error = NULL;
gtk_init_with_args (&argc, &argv, N_("- StarDict GTK+ UI"),
option_entries, GETTEXT_PACKAGE, &error);
if (error)
{
g_warning ("%s", error->message);
g_error_free (error);
return 1;
}
g.dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (filenames)
init (filenames);
else if (!init_from_config (&error) && error)
die_with_dialog (error->message);
g_strfreev (filenames);
if (!g.dictionaries->len)
die_with_dialog (_("No dictionaries found either in "
"the configuration or on the command line"));
if (!load_dictionaries (g.dictionaries, &error))
die_with_dialog (error->message);
// Some Adwaita stupidity, plus defaults for our own widget.
// All the named colours have been there since GNOME 3.4
// (see gnome-extra-themes git history, Adwaita used to live there).
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
// `gsettings set org.gnome.desktop.interface gtk-key-theme "Emacs"`
// isn't quite what I want, and note that ^U works by default
"@binding-set Readline {"
"bind '<Control>H' { 'delete-from-cursor' (chars, -1) };"
"bind '<Control>W' { 'delete-from-cursor' (word-ends, -1) }; }"
"entry { -gtk-key-bindings: Readline; border-radius: 0; }"
"stardict-view { padding: 0 .25em; }"
"stardict-view.odd {"
"background: @theme_base_color; "
"color: @theme_text_color; }"
"stardict-view.odd:backdrop {"
"background: @theme_unfocused_base_color; "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view.even {"
"background: mix(@theme_base_color, @theme_text_color, 0.03); "
"color: @theme_text_color; }"
"stardict-view.even:backdrop {"
"background: mix(@theme_unfocused_base_color, "
"@theme_fg_color, 0.03); "
"color: @theme_fg_color; /* should be more faded than 'text' */ }";
GdkScreen *screen = gdk_screen_get_default ();
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GtkWidget *item_open = gtk_menu_item_new_with_mnemonic (_("_Open..."));
g_signal_connect (item_open, "activate", G_CALLBACK (on_open), NULL);
g.watch_selection = TRUE;
GtkWidget *item_selection =
gtk_check_menu_item_new_with_mnemonic (_("_Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item_selection), g.watch_selection);
g_signal_connect (item_selection, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_widget_set_halign (menu, GTK_ALIGN_END);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_open);
#ifndef WIN32
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_selection);
#endif // ! WIN32
gtk_widget_show_all (menu);
g.hamburger = gtk_menu_button_new ();
gtk_button_set_relief (GTK_BUTTON (g.hamburger), GTK_RELIEF_NONE);
gtk_button_set_image (GTK_BUTTON (g.hamburger), gtk_image_new_from_icon_name
("open-menu-symbolic", GTK_ICON_SIZE_BUTTON));
gtk_menu_button_set_popup (GTK_MENU_BUTTON (g.hamburger), menu);
gtk_widget_show (g.hamburger);
g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL);
gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), g.hamburger, GTK_PACK_END);
g.entry = gtk_search_entry_new ();
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view);
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_default_size (GTK_WINDOW (g.window), 300, 600);
g_signal_connect (g.window, "destroy",
G_CALLBACK (on_destroy), NULL);
g_signal_connect (g.window, "key-press-event",
G_CALLBACK (on_key_press), NULL);
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
gtk_container_add (GTK_CONTAINER (g.window), superbox);
gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
gtk_container_add (GTK_CONTAINER (superbox), g.entry);
gtk_container_add (GTK_CONTAINER (superbox),
gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
g.view = stardict_view_new ();
gtk_box_pack_end (GTK_BOX (superbox), g.view, TRUE, TRUE, 0);
init_tabs ();
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
gtk_drag_dest_set (g.view,
GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
gtk_drag_dest_add_uri_targets (g.view);
g_signal_connect (g.view, "drag-data-received",
G_CALLBACK (on_drag_data_received), NULL);
gtk_widget_show_all (g.window);
gtk_main ();
return 0;
}

View File

@@ -1,7 +1,7 @@
/*
* StarDict GTK+ UI - dictionary view component
*
* Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -135,27 +135,107 @@ view_entry_get_padding (GtkStyleContext *style)
return padding;
}
static gint
view_entry_draw (ViewEntry *ve, cairo_t *cr, gint full_width,
GtkStyleContext *style)
{
gint word_y = 0, defn_y = 0,
height = view_entry_height (ve, &word_y, &defn_y);
typedef struct view_entry_render_ctx ViewEntryRenderCtx;
gtk_render_background (style, cr, 0, 0, full_width, height);
gtk_render_frame (style, cr, 0, 0, full_width, height);
// TODO: see if we can't think of a cleaner way of doing this
struct view_entry_render_ctx
{
GtkStyleContext *style;
cairo_t *cr;
int width;
int height;
// Forwarded from StardictView
PangoLayout *selection_layout;
int selection_begin;
int selection_end;
PangoLayout *hover_layout;
int hover_begin;
int hover_end;
};
static PangoLayout *
view_entry_adjust_layout (ViewEntryRenderCtx *ctx, PangoLayout *layout)
{
if (layout != ctx->hover_layout)
return g_object_ref (layout);
layout = pango_layout_copy (layout);
PangoAttrList *attrs = pango_layout_get_attributes (layout);
attrs = attrs
? pango_attr_list_copy (attrs)
: pango_attr_list_new ();
PangoAttribute *u = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
u->start_index = ctx->hover_begin;
u->end_index = ctx->hover_end;
pango_attr_list_change (attrs, u);
PangoAttribute *uc = pango_attr_underline_color_new (0, 0, 0xffff);
uc->start_index = ctx->hover_begin;
uc->end_index = ctx->hover_end;
pango_attr_list_change (attrs, uc);
PangoAttribute *c = pango_attr_foreground_new (0, 0, 0xffff);
c->start_index = ctx->hover_begin;
c->end_index = ctx->hover_end;
pango_attr_list_change (attrs, c);
pango_layout_set_attributes (layout, attrs);
pango_attr_list_unref (attrs);
return layout;
}
static void
view_entry_render (ViewEntryRenderCtx *ctx, gdouble x, gdouble y,
PangoLayout *layout)
{
PangoLayout *adjusted = view_entry_adjust_layout (ctx, layout);
gtk_render_layout (ctx->style, ctx->cr, x, y, adjusted);
if (layout != ctx->selection_layout)
goto out;
gtk_style_context_save (ctx->style);
gtk_style_context_set_state (ctx->style, GTK_STATE_FLAG_SELECTED);
cairo_save (ctx->cr);
int ranges[2] = { MIN (ctx->selection_begin, ctx->selection_end),
MAX (ctx->selection_begin, ctx->selection_end) };
cairo_region_t *region
= gdk_pango_layout_get_clip_region (adjusted, x, y, ranges, 1);
gdk_cairo_region (ctx->cr, region);
cairo_clip (ctx->cr);
cairo_region_destroy (region);
gtk_render_background (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height);
gtk_render_layout (ctx->style, ctx->cr, x, y, adjusted);
cairo_restore (ctx->cr);
gtk_style_context_restore (ctx->style);
out:
g_object_unref (adjusted);
}
static gint
view_entry_draw (ViewEntry *ve, ViewEntryRenderCtx *ctx)
{
gint word_y = 0, defn_y = 0;
ctx->height = view_entry_height (ve, &word_y, &defn_y);
gtk_render_background (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height);
gtk_render_frame (ctx->style, ctx->cr, 0, 0, ctx->width, ctx->height);
// Top/bottom and left/right-dependent padding will not work, too much code
GtkBorder padding = view_entry_get_padding (style);
GtkBorder padding = view_entry_get_padding (ctx->style);
gtk_style_context_save (style);
gtk_style_context_add_class (style, GTK_STYLE_CLASS_RIGHT);
gtk_render_layout (style, cr,
full_width / 2 + padding.left, defn_y, ve->definition_layout);
gtk_style_context_restore (style);
gtk_style_context_save (ctx->style);
gtk_style_context_add_class (ctx->style, GTK_STYLE_CLASS_RIGHT);
view_entry_render (ctx, ctx->width / 2 + padding.left, defn_y,
ve->definition_layout);
gtk_style_context_restore (ctx->style);
gtk_style_context_save (style);
gtk_style_context_add_class (style, GTK_STYLE_CLASS_LEFT);
gtk_style_context_save (ctx->style);
gtk_style_context_add_class (ctx->style, GTK_STYLE_CLASS_LEFT);
PangoLayoutIter *iter = pango_layout_get_iter (ve->definition_layout);
do
{
@@ -164,13 +244,13 @@ view_entry_draw (ViewEntry *ve, cairo_t *cr, gint full_width,
PangoRectangle logical = {};
pango_layout_iter_get_line_extents (iter, NULL, &logical);
gtk_render_layout (style, cr,
padding.left, word_y + PANGO_PIXELS (logical.y), ve->word_layout);
view_entry_render (ctx, padding.left, word_y + PANGO_PIXELS (logical.y),
ve->word_layout);
}
while (pango_layout_iter_next_line (iter));
pango_layout_iter_free (iter);
gtk_style_context_restore (style);
return height;
gtk_style_context_restore (ctx->style);
return ctx->height;
}
static void
@@ -223,10 +303,17 @@ struct _StardictView
gchar *matched; ///< Highlight common word part of this
gint top_offset; ///< Pixel offset into the entry
// TODO: think about making it, e.g., a pair of (ViewEntry *, guint)
// NOTE: this is the index of a Pango paragraph (a virtual entity)
guint selected; ///< Offset to the selected definition
gdouble drag_last_offset; ///< Last offset when dragging
GList *entries; ///< ViewEntry-s within the view
GtkGesture *selection_gesture; ///< Selection gesture
GWeakRef selection; ///< Selected PangoLayout, if any
int selection_begin; ///< Start index within `selection`
int selection_end; ///< End index within `selection`
GWeakRef hover; ///< Hovered PangoLayout, if any
int hover_begin; ///< Word start index within `hover`
int hover_end; ///< Word end index within `hover`
};
static ViewEntry *
@@ -238,6 +325,23 @@ make_entry (StardictView *self, StardictIterator *iterator)
return ve;
}
static void
reset_hover (StardictView *self)
{
GtkWidget *widget = GTK_WIDGET (self);
PangoLayout *hover = g_weak_ref_get (&self->hover);
if (hover)
{
g_object_unref (hover);
g_weak_ref_set (&self->hover, NULL);
self->hover_begin = self->hover_end = -1;
gtk_widget_queue_draw (widget);
}
if (gtk_widget_get_realized (widget))
gdk_window_set_cursor (gtk_widget_get_window (widget), NULL);
}
static void
adjust_for_height (StardictView *self)
{
@@ -269,6 +373,14 @@ adjust_for_height (StardictView *self)
}
g_object_unref (iterator);
// Also handling this for adjust_for_offset(), which calls this.
PangoLayout *selection = g_weak_ref_get (&self->selection);
if (selection)
g_object_unref (selection);
else
self->selection_begin = self->selection_end = -1;
reset_hover (self);
self->entries = g_list_concat (self->entries, g_list_reverse (append));
gtk_widget_queue_draw (widget);
}
@@ -324,6 +436,10 @@ reload (StardictView *self)
self->entries = NULL;
gtk_widget_queue_draw (widget);
// For consistency, and the check in make_context_menu()
self->selection_begin = self->selection_end = -1;
reset_hover (self);
if (gtk_widget_get_realized (widget) && self->dict)
adjust_for_height (self);
}
@@ -338,10 +454,133 @@ natural_row_size (GtkWidget *widget)
return height;
}
// --- Figuring out where stuff is----------------------------------------------
/// Figure out which layout is at given widget coordinates, and translate them.
static PangoLayout *
layout_at (StardictView *self, int *x, int *y)
{
GtkWidget *widget = GTK_WIDGET (self);
int width = gtk_widget_get_allocated_width (widget);
// The algorithm here is a simplification of stardict_view_draw().
GtkStyleContext *style = gtk_widget_get_style_context (widget);
GtkBorder padding = view_entry_get_padding (style);
gint offset = -self->top_offset;
for (GList *iter = self->entries; iter; iter = iter->next)
{
ViewEntry *ve = iter->data;
if (G_UNLIKELY (*y < offset))
break;
gint top_y = offset, word_y = 0, defn_y = 0;
offset += view_entry_height (ve, &word_y, &defn_y);
if (*y >= offset)
continue;
if (*x >= width / 2)
{
*x -= width / 2 + padding.left;
*y -= top_y + defn_y;
return ve->definition_layout;
}
else
{
*x -= padding.left;
*y -= top_y + word_y;
return ve->word_layout;
}
}
return NULL;
}
/// Figure out a layout's coordinates.
static gboolean
layout_coords (StardictView *self, PangoLayout *layout, int *x, int *y)
{
GtkWidget *widget = GTK_WIDGET (self);
int width = gtk_widget_get_allocated_width (widget);
// The algorithm here is a simplification of stardict_view_draw().
GtkStyleContext *style = gtk_widget_get_style_context (widget);
GtkBorder padding = view_entry_get_padding (style);
gint offset = -self->top_offset;
for (GList *iter = self->entries; iter; iter = iter->next)
{
ViewEntry *ve = iter->data;
gint top_y = offset, word_y = 0, defn_y = 0;
offset += view_entry_height (ve, &word_y, &defn_y);
if (layout == ve->definition_layout)
{
*x = width / 2 + padding.left;
*y = top_y + defn_y;
return TRUE;
}
if (layout == ve->word_layout)
{
*x = padding.left;
*y = top_y + word_y;
return TRUE;
}
}
return FALSE;
}
static int
layout_index_at (PangoLayout *layout, int x, int y)
{
int index = 0, trailing = 0;
(void) pango_layout_xy_to_index (layout,
x * PANGO_SCALE,
y * PANGO_SCALE,
&index,
&trailing);
const char *text = pango_layout_get_text (layout) + index;
while (trailing--)
{
int len = g_utf8_next_char (text) - text;
text += len;
index += len;
}
return index;
}
static PangoLayout *
locate_word_at (StardictView *self, int x, int y, int *beginpos, int *endpos)
{
*beginpos = -1;
*endpos = -1;
PangoLayout *layout = layout_at (self, &x, &y);
if (!layout)
return NULL;
const char *text = pango_layout_get_text (layout), *p = NULL;
const char *begin = text + layout_index_at (layout, x, y), *end = begin;
while ((p = g_utf8_find_prev_char (text, begin))
&& !g_unichar_isspace (g_utf8_get_char (p)))
begin = p;
gunichar c;
while ((c = g_utf8_get_char (end)) && !g_unichar_isspace (c))
end = g_utf8_next_char (end);
*beginpos = begin - text;
*endpos = end - text;
return layout;
}
// --- Boilerplate -------------------------------------------------------------
G_DEFINE_TYPE (StardictView, stardict_view, GTK_TYPE_WIDGET)
enum {
SEND,
LAST_SIGNAL,
};
static guint view_signals[LAST_SIGNAL];
static void
stardict_view_finalize (GObject *gobject)
{
@@ -351,6 +590,9 @@ stardict_view_finalize (GObject *gobject)
g_list_free_full (self->entries, (GDestroyNotify) view_entry_destroy);
self->entries = NULL;
g_object_unref (self->selection_gesture);
g_weak_ref_clear (&self->selection);
g_free (self->matched);
self->matched = NULL;
@@ -395,13 +637,12 @@ stardict_view_realize (GtkWidget *widget)
// but it merely seems to involve more work.
.wclass = GDK_INPUT_OUTPUT,
.visual = gtk_widget_get_visual (widget),
// GDK_SMOOTH_SCROLL_MASK is useless, will stop sending UP/DOWN
.event_mask = gtk_widget_get_events (widget) | GDK_SCROLL_MASK,
.event_mask = gtk_widget_get_events (widget) | GDK_SCROLL_MASK
| GDK_SMOOTH_SCROLL_MASK | GDK_BUTTON_PRESS_MASK
| GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK,
};
// We need this window to receive input events at all.
// TODO: see if we don't want GDK_WA_CURSOR for setting a text cursor
GdkWindow *window = gdk_window_new (gtk_widget_get_parent_window (widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
@@ -411,6 +652,67 @@ stardict_view_realize (GtkWidget *widget)
gtk_widget_set_realized (widget, TRUE);
}
static void
reset_hover_for_event (StardictView *self, guint state, int x, int y)
{
reset_hover (self);
if ((state &= gtk_accelerator_get_default_mod_mask ()) != GDK_CONTROL_MASK)
return;
GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self));
if (x < 0
|| y < 0
|| x >= gdk_window_get_width (window)
|| y >= gdk_window_get_height (window))
return;
g_weak_ref_set (&self->hover,
locate_word_at (self, x, y, &self->hover_begin, &self->hover_end));
gtk_widget_queue_draw (GTK_WIDGET (self));
GdkCursor *cursor = gdk_cursor_new_from_name
(gdk_window_get_display (window), "pointer");
gdk_window_set_cursor (window, cursor);
g_object_unref (cursor);
}
static void
on_keymap_state_changed (G_GNUC_UNUSED GdkKeymap *keymap, StardictView *self)
{
GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self));
GdkSeat *seat = gdk_display_get_default_seat (display);
GdkDevice *pointer = gdk_seat_get_pointer (seat);
int x = -1, y = -1;
GdkModifierType state = 0;
GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self));
gdk_window_get_device_position (window, pointer, &x, &y, &state);
reset_hover_for_event (self, state, x, y);
}
static void
stardict_view_map (GtkWidget *widget)
{
GTK_WIDGET_CLASS (stardict_view_parent_class)->map (widget);
GdkWindow *window = gtk_widget_get_window (widget);
GdkDisplay *display = gdk_window_get_display (window);
GdkKeymap *keymap = gdk_keymap_get_for_display (display);
g_signal_connect (keymap, "state-changed",
G_CALLBACK (on_keymap_state_changed), widget);
}
static void
stardict_view_unmap (GtkWidget *widget)
{
GdkWindow *window = gtk_widget_get_window (widget);
GdkDisplay *display = gdk_window_get_display (window);
GdkKeymap *keymap = gdk_keymap_get_for_display (display);
g_signal_handlers_disconnect_by_data (keymap, widget);
GTK_WIDGET_CLASS (stardict_view_parent_class)->unmap (widget);
}
static gboolean
stardict_view_draw (GtkWidget *widget, cairo_t *cr)
{
@@ -425,6 +727,21 @@ stardict_view_draw (GtkWidget *widget, cairo_t *cr)
gtk_render_frame (style, cr,
0, 0, allocation.width, allocation.height);
ViewEntryRenderCtx ctx =
{
.style = style,
.cr = cr,
.width = allocation.width,
.height = 0,
.selection_layout = g_weak_ref_get (&self->selection),
.selection_begin = self->selection_begin,
.selection_end = self->selection_end,
.hover_layout = g_weak_ref_get (&self->hover),
.hover_begin = self->hover_begin,
.hover_end = self->hover_end,
};
gint offset = -self->top_offset;
gint i = self->top_position;
for (GList *iter = self->entries; iter; iter = iter->next)
@@ -442,11 +759,13 @@ stardict_view_draw (GtkWidget *widget, cairo_t *cr)
cairo_save (cr);
cairo_translate (cr, 0, offset);
// TODO: later exclude clipped entries, but it's not that important
offset += view_entry_draw (iter->data, cr, allocation.width, style);
offset += view_entry_draw (iter->data, &ctx);
cairo_restore (cr);
gtk_style_context_restore (style);
}
g_clear_object (&ctx.selection_layout);
g_clear_object (&ctx.hover_layout);
return TRUE;
}
@@ -456,7 +775,28 @@ stardict_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
GTK_WIDGET_CLASS (stardict_view_parent_class)
->size_allocate (widget, allocation);
reload (STARDICT_VIEW (widget));
StardictView *self = STARDICT_VIEW (widget);
if (!gtk_widget_get_realized (widget) || !self->dict)
return;
PangoLayout *selection = g_weak_ref_get (&self->selection), **origin = NULL;
for (GList *iter = self->entries; iter; iter = iter->next)
{
ViewEntry *ve = iter->data;
if (selection && selection == ve->word_layout)
origin = &ve->word_layout;
if (selection && selection == ve->definition_layout)
origin = &ve->definition_layout;
}
if (selection)
g_object_unref (selection);
for (GList *iter = self->entries; iter; iter = iter->next)
view_entry_rebuild_layouts (iter->data, widget);
if (origin)
g_weak_ref_set (&self->selection, *origin);
adjust_for_offset (self);
}
static void
@@ -487,37 +827,339 @@ stardict_view_scroll_event (GtkWidget *widget, GdkEventScroll *event)
stardict_view_scroll (self, GTK_SCROLL_STEPS, +3);
return TRUE;
case GDK_SCROLL_SMOOTH:
self->top_offset += event->delta_y;
adjust_for_offset (self);
{
// On GDK/Wayland, the mouse wheel will typically create 1.5 deltas,
// after dividing a 15 degree click angle from libinput by 10.
// (Noticed on Arch + Sway, cannot reproduce on Ubuntu 22.04.)
// On X11, as libinput(4) indicates, the delta will always be 1.0.
double delta = CLAMP (event->delta_y, -1, +1);
stardict_view_scroll (self, GTK_SCROLL_STEPS, 3 * delta);
return TRUE;
}
default:
return FALSE;
}
}
static void
publish_selection (StardictView *self, GdkAtom target)
{
PangoLayout *layout = g_weak_ref_get (&self->selection);
if (!layout)
return;
// Unlike GtkLabel, we don't place the selection in PRIMARY immediately.
const char *text = pango_layout_get_text (layout);
int len = strlen (text),
s1 = MIN (self->selection_begin, self->selection_end),
s2 = MAX (self->selection_begin, self->selection_end);
if (s1 != s2 && s1 >= 0 && s1 <= len && s2 >= 0 && s2 <= len)
gtk_clipboard_set_text (gtk_clipboard_get (target), text + s1, s2 - s1);
g_object_unref (layout);
}
static void
select_word_at (StardictView *self, int x, int y)
{
g_weak_ref_set (&self->selection, locate_word_at (self,
x, y, &self->selection_begin, &self->selection_end));
gtk_widget_queue_draw (GTK_WIDGET (self));
publish_selection (self, GDK_SELECTION_PRIMARY);
}
static void
select_all_at (StardictView *self, int x, int y)
{
PangoLayout *layout = layout_at (self, &x, &y);
if (!layout)
return;
g_weak_ref_set (&self->selection, layout);
self->selection_begin = 0;
self->selection_end = strlen (pango_layout_get_text (layout));
gtk_widget_queue_draw (GTK_WIDGET (self));
publish_selection (self, GDK_SELECTION_PRIMARY);
}
static void
on_copy_activate (G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
{
publish_selection (STARDICT_VIEW (user_data), GDK_SELECTION_CLIPBOARD);
}
static gboolean
destroy_widget_idle_source_func (GtkWidget *widget)
{
// The whole menu is deactivated /before/ any item is activated,
// and a destroyed child item will not activate.
gtk_widget_destroy (widget);
return FALSE;
}
static GtkMenu *
make_context_menu (StardictView *self)
{
GtkWidget *copy = gtk_menu_item_new_with_mnemonic ("_Copy");
gtk_widget_set_sensitive (copy,
self->selection_begin != self->selection_end);
g_signal_connect_data (copy, "activate",
G_CALLBACK (on_copy_activate), g_object_ref (self),
(GClosureNotify) g_object_unref, 0);
GtkWidget *menu = gtk_menu_new ();
gtk_menu_shell_append (GTK_MENU_SHELL (menu), copy);
// As per GTK+ 3 Common Questions, 1.5.
g_object_ref_sink (menu);
g_signal_connect_swapped (menu, "deactivate",
G_CALLBACK (g_idle_add), destroy_widget_idle_source_func);
g_signal_connect (menu, "destroy",
G_CALLBACK (g_object_unref), NULL);
gtk_widget_show_all (menu);
return GTK_MENU (menu);
}
static gboolean
stardict_view_button_press_event (GtkWidget *widget, GdkEventButton *event)
{
StardictView *self = STARDICT_VIEW (widget);
if (gdk_event_triggers_context_menu ((const GdkEvent *) event))
{
gtk_menu_popup_at_pointer (make_context_menu (self),
(const GdkEvent *) event);
return GDK_EVENT_STOP;
}
if (event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
{
gtk_event_controller_reset (
GTK_EVENT_CONTROLLER (self->selection_gesture));
select_word_at (self, event->x, event->y);
return GDK_EVENT_STOP;
}
if (event->type == GDK_3BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
{
gtk_event_controller_reset (
GTK_EVENT_CONTROLLER (self->selection_gesture));
select_all_at (self, event->x, event->y);
return GDK_EVENT_STOP;
}
return GTK_WIDGET_CLASS (stardict_view_parent_class)
->button_press_event (widget, event);
}
static gboolean
stardict_view_motion_notify_event (GtkWidget *widget, GdkEventMotion *event)
{
StardictView *self = STARDICT_VIEW (widget);
reset_hover_for_event (self, event->state, event->x, event->y);
return GTK_WIDGET_CLASS (stardict_view_parent_class)
->motion_notify_event (widget, event);
}
static gboolean
stardict_view_leave_notify_event
(GtkWidget *widget, G_GNUC_UNUSED GdkEventCrossing *event)
{
reset_hover (STARDICT_VIEW (widget));
return GDK_EVENT_PROPAGATE;
}
static void
on_drag_begin (GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x,
G_GNUC_UNUSED gdouble start_y, gpointer user_data)
{
GtkGesture *gesture = GTK_GESTURE (drag);
GdkEventSequence *sequence
= gtk_gesture_get_last_updated_sequence (gesture);
GdkModifierType state = 0;
const GdkEvent *last_event = gtk_gesture_get_last_event (gesture, sequence);
(void) gdk_event_get_state (last_event, &state);
if (state & gtk_accelerator_get_default_mod_mask ())
gtk_gesture_set_sequence_state (gesture, sequence,
GTK_EVENT_SEQUENCE_DENIED);
else
{
gtk_gesture_set_sequence_state (gesture, sequence,
GTK_EVENT_SEQUENCE_CLAIMED);
STARDICT_VIEW (user_data)->drag_last_offset = 0;
}
}
static void
on_drag_update (G_GNUC_UNUSED GtkGestureDrag *drag,
G_GNUC_UNUSED gdouble offset_x, gdouble offset_y, gpointer user_data)
{
StardictView *self = STARDICT_VIEW (user_data);
self->top_offset += self->drag_last_offset - offset_y;
adjust_for_offset (self);
self->drag_last_offset = offset_y;
}
static gboolean
send_hover (StardictView *self)
{
PangoLayout *layout = g_weak_ref_get (&self->hover);
if (!layout)
return FALSE;
const char *text = pango_layout_get_text (layout);
int len = strlen (text),
s1 = MIN (self->hover_begin, self->hover_end),
s2 = MAX (self->hover_begin, self->hover_end);
if (s1 != s2 && s1 >= 0 && s1 <= len && s2 >= 0 && s2 <= len)
{
gchar *word = g_strndup (text + s1, s2 - s1);
g_signal_emit (self, view_signals[SEND], 0, word);
g_free (word);
}
g_object_unref (layout);
return TRUE;
}
static void
on_select_begin (GtkGestureDrag *drag, gdouble start_x, gdouble start_y,
gpointer user_data)
{
// We probably don't need to check modifiers and mouse position again.
StardictView *self = STARDICT_VIEW (user_data);
GtkGesture *gesture = GTK_GESTURE (drag);
if (send_hover (self))
{
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
// Despite our two gestures not being grouped up, claiming one doesn't
// deny the other, and :exclusive isn't the opposite of :touch-only.
// A non-NULL sequence indicates a touch event.
if (gtk_gesture_get_last_updated_sequence (gesture))
{
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
g_weak_ref_set (&self->selection, NULL);
self->selection_begin = -1;
self->selection_end = -1;
gtk_widget_queue_draw (GTK_WIDGET (self));
int layout_x = start_x, layout_y = start_y;
PangoLayout *layout = layout_at (self, &layout_x, &layout_y);
if (!layout)
{
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
g_weak_ref_set (&self->selection, layout);
self->selection_end = self->selection_begin
= layout_index_at (layout, layout_x, layout_y);
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
}
static void
on_select_update (GtkGestureDrag *drag, gdouble offset_x, gdouble offset_y,
gpointer user_data)
{
GtkGesture *gesture = GTK_GESTURE (drag);
StardictView *self = STARDICT_VIEW (user_data);
PangoLayout *layout = g_weak_ref_get (&self->selection);
if (!layout)
{
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
double start_x = 0, start_y = 0;
(void) gtk_gesture_drag_get_start_point (drag, &start_x, &start_y);
int x = 0, y = 0;
if (!layout_coords (self, layout, &x, &y))
{
g_warning ("internal error: weakly referenced layout not found");
gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
goto out;
}
self->selection_end = layout_index_at (layout,
start_x + offset_x - x,
start_y + offset_y - y);
gtk_widget_queue_draw (GTK_WIDGET (self));
out:
g_object_unref (layout);
}
static void
on_select_end (G_GNUC_UNUSED GtkGestureDrag *drag,
G_GNUC_UNUSED gdouble offset_x, G_GNUC_UNUSED gdouble offset_y,
gpointer user_data)
{
publish_selection (STARDICT_VIEW (user_data), GDK_SELECTION_PRIMARY);
}
static void
stardict_view_class_init (StardictViewClass *klass)
{
view_signals[SEND] = g_signal_new ("send",
G_TYPE_FROM_CLASS (klass), 0, 0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_STRING);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = stardict_view_finalize;
// TODO: handle mouse events for text selection
// See https://wiki.gnome.org/HowDoI/CustomWidgets for some guidelines.
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
widget_class->get_preferred_height = stardict_view_get_preferred_height;
widget_class->get_preferred_width = stardict_view_get_preferred_width;
widget_class->realize = stardict_view_realize;
widget_class->map = stardict_view_map;
widget_class->unmap = stardict_view_unmap;
widget_class->draw = stardict_view_draw;
widget_class->size_allocate = stardict_view_size_allocate;
widget_class->screen_changed = stardict_view_screen_changed;
widget_class->scroll_event = stardict_view_scroll_event;
widget_class->button_press_event = stardict_view_button_press_event;
widget_class->motion_notify_event = stardict_view_motion_notify_event;
widget_class->leave_notify_event = stardict_view_leave_notify_event;
gtk_widget_class_set_css_name (widget_class, "stardict-view");
}
static void
stardict_view_init (G_GNUC_UNUSED StardictView *self)
stardict_view_init (StardictView *self)
{
g_weak_ref_init (&self->selection, NULL);
self->selection_begin = -1;
self->selection_end = -1;
GtkGesture *drag = gtk_gesture_drag_new (GTK_WIDGET (self));
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (drag), TRUE);
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (drag),
GTK_PHASE_TARGET);
g_object_set_data_full (G_OBJECT (self), "stardict-view-drag-gesture",
drag, g_object_unref);
g_signal_connect (drag, "drag-begin",
G_CALLBACK (on_drag_begin), self);
g_signal_connect (drag, "drag-update",
G_CALLBACK (on_drag_update), self);
self->selection_gesture = gtk_gesture_drag_new (GTK_WIDGET (self));
gtk_gesture_single_set_exclusive (
GTK_GESTURE_SINGLE (self->selection_gesture), TRUE);
gtk_event_controller_set_propagation_phase (
GTK_EVENT_CONTROLLER (self->selection_gesture), GTK_PHASE_TARGET);
g_signal_connect (self->selection_gesture, "drag-begin",
G_CALLBACK (on_select_begin), self);
g_signal_connect (self->selection_gesture, "drag-update",
G_CALLBACK (on_select_update), self);
g_signal_connect (self->selection_gesture, "drag-end",
G_CALLBACK (on_select_end), self);
}
// --- Public ------------------------------------------------------------------

View File

@@ -309,26 +309,24 @@ load_ifo (StardictInfo *sti, const gchar *path, GError **error)
goto error;
}
ret_val = TRUE;
// FIXME check for zeros, don't assume that 0 means for "not set"
// FIXME check for zeros, don't assume that 0 means "not set"
if (!sti->book_name || !*sti->book_name)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("no book name specified"));
ret_val = FALSE;
goto error;
}
if (!sti->word_count)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("word count not specified"));
ret_val = FALSE;
goto error;
}
if (!sti->idx_filesize)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("index file size not specified"));
ret_val = FALSE;
goto error;
}
if (!sti->idx_offset_bits)
@@ -338,9 +336,11 @@ load_ifo (StardictInfo *sti, const gchar *path, GError **error)
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s: %lu", path, _("invalid index offset bits"),
sti->idx_offset_bits);
ret_val = FALSE;
goto error;
}
ret_val = TRUE;
error:
if (!ret_val)
{
@@ -357,6 +357,20 @@ error:
return ret_val;
}
/// Read an .ifo file.
/// @return StardictInfo *. Deallocate with stardict_info_free();
StardictInfo *
stardict_info_new (const gchar *filename, GError **error)
{
StardictInfo *ifo = g_new (StardictInfo, 1);
if (!load_ifo (ifo, filename, error))
{
g_free (ifo);
return NULL;
}
return ifo;
}
/// List all dictionary files located in a path.
/// @return GList<StardictInfo *>. Deallocate the list with:
/// @code
@@ -377,12 +391,10 @@ stardict_list_dictionaries (const gchar *path)
continue;
gchar *filename = g_build_filename (path, name, NULL);
StardictInfo *ifo = g_new (StardictInfo, 1);
if (load_ifo (ifo, filename, NULL))
dicts = g_list_append (dicts, ifo);
else
g_free (ifo);
StardictInfo *ifo = stardict_info_new (filename, NULL);
g_free (filename);
if (ifo)
dicts = g_list_append (dicts, ifo);
}
g_dir_close (dir);
g_pattern_spec_free (ps);
@@ -1014,6 +1026,10 @@ stardict_longest_common_collation_prefix (StardictDict *sd,
u_strFromUTF8 (NULL, 0, &uc2_len, s2, -1, &error);
error = U_ZERO_ERROR;
// Prevent undefined behaviour with VLAs.
if (!uc1_len || !uc2_len)
return 0;
UChar uc1[uc1_len];
UChar uc2[uc2_len];
u_strFromUTF8 (uc1, uc1_len, NULL, s1, -1, &error);

View File

@@ -108,6 +108,7 @@ GQuark stardict_error_quark (void);
// --- Dictionary information --------------------------------------------------
StardictInfo *stardict_info_new (const gchar *filename, GError **error);
const gchar *stardict_info_get_path (StardictInfo *sdi) G_GNUC_PURE;
const gchar *stardict_info_get_book_name (StardictInfo *sdi) G_GNUC_PURE;
gsize stardict_info_get_word_count (StardictInfo *sd) G_GNUC_PURE;

875
src/tdv-gui.c Normal file
View File

@@ -0,0 +1,875 @@
/*
* StarDict GTK+ UI
*
* Copyright (c) 2020 - 2024, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <stdlib.h>
#include "config.h"
#include "stardict.h"
#include "utils.h"
#include "stardict-view.h"
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *hamburger; ///< Hamburger menu
GtkWidget *entry; ///< Search entry widget
GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary
gint last; ///< The last dictionary index
GPtrArray *dictionaries; ///< All open dictionaries
gboolean loading; ///< Dictionaries are being loaded
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_from_filenames (GPtrArray *out, gchar **filenames)
{
for (gsize i = 0; filenames[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_strdup (filenames[i]);
g_ptr_array_add (out, dict);
}
}
// TODO: try to deduplicate, similar to app_load_config_values()
static gboolean
load_from_key_file (GPtrArray *out, GKeyFile *kf, GError **error)
{
const gchar *dictionaries = "Dictionaries";
gchar **names = g_key_file_get_keys (kf, dictionaries, NULL, NULL);
if (!names)
return TRUE;
for (gsize i = 0; names[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->name = names[i];
g_ptr_array_add (out, dict);
}
g_free (names);
for (gsize i = 0; i < out->len; i++)
{
Dictionary *dict = g_ptr_array_index (out, i);
gchar *path =
g_key_file_get_string (kf, dictionaries, dict->name, error);
if (!path)
return FALSE;
// Try to resolve relative paths and expand tildes
if (!(dict->filename =
resolve_filename (path, resolve_relative_config_filename)))
dict->filename = path;
else
g_free (path);
}
return TRUE;
}
static gboolean
load_from_config (GPtrArray *out, GError **error)
{
GKeyFile *key_file = load_project_config_file (error);
if (!key_file)
return FALSE;
gboolean result = load_from_key_file (out, key_file, error);
g_key_file_free (key_file);
return result;
}
static void
search (Dictionary *dict)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
if (!dict->dict)
return;
StardictIterator *iterator =
stardict_dict_search (dict->dict, input_utf8, NULL);
stardict_view_set_position (STARDICT_VIEW (g.view),
dict->dict, stardict_iterator_get_offset (iterator));
g_object_unref (iterator);
stardict_view_set_matched (STARDICT_VIEW (g.view), input_utf8);
}
static void
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
search (g_ptr_array_index (g.dictionaries, g.dictionary));
}
static void
on_send (G_GNUC_UNUSED StardictView *view,
const char *word, G_GNUC_UNUSED gpointer data)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
gtk_entry_buffer_set_text (buf, word, -1);
gtk_editable_select_region (GTK_EDITABLE (g.entry), 0, -1);
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gchar *trimmed = g_strstrip (g_strdup (text));
gtk_entry_set_text (GTK_ENTRY (g.entry), trimmed);
g_free (trimmed);
g_signal_emit_by_name (g.entry,
"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
}
static void
on_selection (GtkClipboard *clipboard, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (g.watch_selection
&& !gtk_window_has_toplevel_focus (GTK_WINDOW (g.window))
&& event->owner_change.owner != NULL)
gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
}
static void
on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
{
g.watch_selection = gtk_check_menu_item_get_active (item);
}
static void
on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
guint page_num, G_GNUC_UNUSED gpointer data)
{
g.last = g.dictionary;
g.dictionary = page_num;
search (g_ptr_array_index (g.dictionaries, g.dictionary));
// Hack: Make right-clicking notebook arrows also re-focus the entry.
GdkEvent *event = gtk_get_current_event ();
if (event && event->type == GDK_BUTTON_PRESS)
gtk_widget_grab_focus (g.entry);
}
static gboolean
accelerate_hamburger (GdkEvent *event)
{
gchar *accelerator = NULL;
g_object_get (gtk_widget_get_settings (g.window), "gtk-menu-bar-accel",
&accelerator, NULL);
if (!accelerator)
return FALSE;
guint key = 0;
GdkModifierType mods = 0;
gtk_accelerator_parse (accelerator, &key, &mods);
g_free (accelerator);
guint mask = gtk_accelerator_get_default_mod_mask ();
if (!key || event->key.keyval != key || (event->key.state & mask) != mods)
return FALSE;
gtk_button_clicked (GTK_BUTTON (g.hamburger));
return TRUE;
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
// The "activate" signal of the GtkMenuButton cannot be used
// from a real accelerator, due to "no trigger event for menu popup".
if (accelerate_hamburger (event))
return TRUE;
GtkNotebook *notebook = GTK_NOTEBOOK (g.notebook);
guint mods = event->key.state & gtk_accelerator_get_default_mod_mask ();
if (mods == GDK_CONTROL_MASK)
{
// Can't use gtk_widget_add_accelerator() to change-current-page(-1/+1)
// because that signal has arguments, which cannot be passed.
gint current = gtk_notebook_get_current_page (notebook);
if (event->key.keyval == GDK_KEY_Page_Up)
return gtk_notebook_set_current_page (notebook, --current), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return gtk_notebook_set_current_page (notebook,
++current % gtk_notebook_get_n_pages (notebook)), TRUE;
}
if (mods == GDK_MOD1_MASK)
{
if (event->key.keyval >= GDK_KEY_0
&& event->key.keyval <= GDK_KEY_9)
{
gint n = event->key.keyval - GDK_KEY_0;
gtk_notebook_set_current_page (notebook, (n ? n : 10) - 1);
return TRUE;
}
if (event->key.keyval == GDK_KEY_Tab)
{
gtk_notebook_set_current_page (notebook, g.last);
return TRUE;
}
}
if (mods == 0)
{
StardictView *view = STARDICT_VIEW (g.view);
if (event->key.keyval == GDK_KEY_Page_Up)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, -0.5), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, +0.5), TRUE;
if (event->key.keyval == GDK_KEY_Up)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, -1), TRUE;
if (event->key.keyval == GDK_KEY_Down)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, +1), TRUE;
}
return FALSE;
}
static gboolean
on_tab_focus (G_GNUC_UNUSED GtkWidget *widget,
G_GNUC_UNUSED GtkDirectionType direction, G_GNUC_UNUSED gpointer user_data)
{
// Hack: Make it so that tab headers don't retain newly gained focus
// when clicked, re-focus the entry instead.
GdkEvent *event = gtk_get_current_event ();
if (!event || event->type != GDK_BUTTON_PRESS
|| event->button.button != GDK_BUTTON_PRIMARY)
return FALSE;
gtk_widget_grab_focus (g.entry);
return TRUE;
}
static void
init_tabs (void)
{
for (gsize i = g.dictionaries->len; i--; )
{
Dictionary *dict = g_ptr_array_index (g.dictionaries, i);
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
g_signal_connect (dummy, "focus", G_CALLBACK (on_tab_focus), NULL);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_insert_page (GTK_NOTEBOOK (g.notebook), dummy, label, 0);
}
gtk_widget_show_all (g.notebook);
gtk_widget_grab_focus (g.entry);
}
static void
show_error_dialog (GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (g.window), 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
g_error_free (error);
}
// --- Loading -----------------------------------------------------------------
static void
on_new_dictionaries_loaded (G_GNUC_UNUSED GObject* source_object,
GAsyncResult* res, G_GNUC_UNUSED gpointer user_data)
{
g.loading = FALSE;
GError *error = NULL;
GPtrArray *new_dictionaries =
g_task_propagate_pointer (G_TASK (res), &error);
if (!new_dictionaries)
{
show_error_dialog (error);
return;
}
while (gtk_notebook_get_n_pages (GTK_NOTEBOOK (g.notebook)))
gtk_notebook_remove_page (GTK_NOTEBOOK (g.notebook), -1);
g.dictionary = -1;
if (g.dictionaries)
g_ptr_array_free (g.dictionaries, TRUE);
stardict_view_set_position (STARDICT_VIEW (g.view), NULL, 0);
g.dictionaries = new_dictionaries;
init_tabs ();
}
static void
on_reload_dictionaries_task (GTask *task, G_GNUC_UNUSED gpointer source_object,
gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
GError *error = NULL;
if (load_dictionaries (task_data, &error))
{
g_task_return_pointer (task,
g_ptr_array_ref (task_data), (GDestroyNotify) g_ptr_array_unref);
}
else
g_task_return_error (task, error);
}
static gboolean
reload_dictionaries (GPtrArray *new_dictionaries, GError **error)
{
// TODO: We could cancel that task.
if (g.loading)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"already loading dictionaries");
return FALSE;
}
// TODO: Some other kind of indication.
// Note that "action widgets" aren't visible without GtkNotebook tabs.
g.loading = TRUE;
GTask *task = g_task_new (NULL, NULL, on_new_dictionaries_loaded, NULL);
g_task_set_name (task, __func__);
g_task_set_task_data (task,
new_dictionaries, (GDestroyNotify) g_ptr_array_unref);
g_task_run_in_thread (task, on_reload_dictionaries_task);
g_object_unref (task);
return TRUE;
}
static GtkWidget *
new_open_dialog (void)
{
// The default is local-only. Paths are returned absolute.
GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open dictionary"),
GTK_WINDOW (g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT, NULL);
GtkFileFilter *filter = gtk_file_filter_new ();
gtk_file_filter_add_pattern (filter, "*.ifo");
gtk_file_filter_set_name (filter, "*.ifo");
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
gtk_file_chooser_add_filter (chooser, filter);
gtk_file_chooser_set_select_multiple (chooser, TRUE);
return dialog;
}
static void
on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
{
GSList *paths = gtk_file_chooser_get_filenames (chooser);
for (GSList *iter = paths; iter; iter = iter->next)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = iter->data;
g_ptr_array_add (new_dictionaries, dict);
}
g_slist_free (paths);
}
gtk_widget_destroy (dialog);
GError *error = NULL;
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
if (error)
show_error_dialog (error);
}
static void
on_drag_data_received (G_GNUC_UNUSED GtkWidget *widget, GdkDragContext *context,
G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, GtkSelectionData *data,
G_GNUC_UNUSED guint info, guint time, G_GNUC_UNUSED gpointer user_data)
{
GError *error = NULL;
gchar **dropped_uris = gtk_selection_data_get_uris (data);
if (!dropped_uris)
return;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
for (gsize i = 0; !error && dropped_uris[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_filename_from_uri (dropped_uris[i], NULL, &error);
g_ptr_array_add (new_dictionaries, dict);
}
g_strfreev (dropped_uris);
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
gtk_drag_finish (context, error == NULL, FALSE, time);
if (error)
show_error_dialog (error);
}
// --- Settings ----------------------------------------------------------------
typedef struct settings_data SettingsData;
enum
{
SETTINGS_COLUMN_NAME,
SETTINGS_COLUMN_PATH,
SETTINGS_COLUMN_COUNT
};
struct settings_data
{
GKeyFile *key_file; ///< Configuration file
GtkTreeModel *model; ///< GtkListStore
};
static void
settings_load (SettingsData *data)
{
// We want to keep original comments, as well as any other data.
GError *error = NULL;
data->key_file = load_project_config_file (&error);
if (!data->key_file)
{
if (error)
show_error_dialog (error);
data->key_file = g_key_file_new ();
}
GtkListStore *list_store = gtk_list_store_new (SETTINGS_COLUMN_COUNT,
G_TYPE_STRING, G_TYPE_STRING);
data->model = GTK_TREE_MODEL (list_store);
const gchar *dictionaries = "Dictionaries";
gchar **names =
g_key_file_get_keys (data->key_file, dictionaries, NULL, NULL);
if (!names)
return;
for (gsize i = 0; names[i]; i++)
{
gchar *path = g_key_file_get_string (data->key_file,
dictionaries, names[i], NULL);
if (!path)
continue;
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, names[i], SETTINGS_COLUMN_PATH, path, -1);
g_free (path);
}
g_strfreev (names);
}
static void
settings_save (SettingsData *data)
{
const gchar *dictionaries = "Dictionaries";
g_key_file_remove_group (data->key_file, dictionaries, NULL);
GtkTreeIter iter = { 0 };
gboolean valid = gtk_tree_model_get_iter_first (data->model, &iter);
while (valid)
{
gchar *name = NULL, *path = NULL;
gtk_tree_model_get (data->model, &iter,
SETTINGS_COLUMN_NAME, &name, SETTINGS_COLUMN_PATH, &path, -1);
if (name && path)
g_key_file_set_string (data->key_file, dictionaries, name, path);
g_free (name);
g_free (path);
valid = gtk_tree_model_iter_next (data->model, &iter);
}
GError *e = NULL;
if (!save_project_config_file (data->key_file, &e))
show_error_dialog (e);
}
static void
on_settings_name_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_NAME, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_path_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_PATH, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_add (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GSList *paths = NULL;
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
paths = gtk_file_chooser_get_filenames (chooser);
gtk_widget_destroy (dialog);
// When the dialog is aborted, we simply add an empty list.
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
const gchar *home = g_get_home_dir ();
for (GSList *iter = paths; iter; iter = iter->next)
{
GError *error = NULL;
StardictInfo *ifo = stardict_info_new (iter->data, &error);
g_free (iter->data);
if (!ifo)
{
show_error_dialog (error);
continue;
}
// We also expand tildes, even on Windows, so no problem there.
const gchar *path = stardict_info_get_path (ifo);
gchar *tildified = g_str_has_prefix (stardict_info_get_path (ifo), home)
? g_strdup_printf ("~%s", path + strlen (home))
: g_strdup (path);
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, stardict_info_get_book_name (ifo),
SETTINGS_COLUMN_PATH, tildified, -1);
g_free (tildified);
stardict_info_free (ifo);
}
g_slist_free (paths);
}
static void
on_settings_remove (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
GtkTreeSelection *selection = gtk_tree_view_get_selection (tree_view);
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
GList *selected = gtk_tree_selection_get_selected_rows (selection, &model);
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = iter->data;
iter->data = gtk_tree_row_reference_new (model, path);
gtk_tree_path_free (path);
}
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = gtk_tree_row_reference_get_path (iter->data);
if (path)
{
GtkTreeIter tree_iter = { 0 };
if (gtk_tree_model_get_iter (model, &tree_iter, path))
gtk_list_store_remove (list_store, &tree_iter);
gtk_tree_path_free (path);
}
}
g_list_free_full (selected, (GDestroyNotify) gtk_tree_row_reference_free);
}
static void
on_settings_selection_changed
(GtkTreeSelection* selection, gpointer user_data)
{
GtkWidget *remove = GTK_WIDGET (user_data);
gtk_widget_set_sensitive (remove,
gtk_tree_selection_count_selected_rows (selection) > 0);
}
static void
on_settings (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
SettingsData sd = {};
settings_load (&sd);
GtkWidget *treeview = gtk_tree_view_new_with_model (sd.model);
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (treeview), TRUE);
g_object_unref (sd.model);
GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_name_edited), sd.model);
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes
(_("Name"), renderer, "text", SETTINGS_COLUMN_NAME, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_path_edited), sd.model);
column = gtk_tree_view_column_new_with_attributes
(_("Path"), renderer, "text", SETTINGS_COLUMN_PATH, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
GtkWidget *scrolled = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type
(GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_ETCHED_IN);
gtk_container_add (GTK_CONTAINER (scrolled), treeview);
GtkWidget *dialog = gtk_dialog_new_with_buttons (_("Settings"),
GTK_WINDOW (g.window),
GTK_DIALOG_MODAL,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Save"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 400);
GtkWidget *remove = gtk_button_new_with_mnemonic (_("_Remove"));
gtk_widget_set_sensitive (remove, FALSE);
g_signal_connect (remove, "clicked",
G_CALLBACK (on_settings_remove), treeview);
GtkWidget *add = gtk_button_new_with_mnemonic (_("_Add..."));
g_signal_connect (add, "clicked",
G_CALLBACK (on_settings_add), treeview);
GtkTreeSelection *selection =
gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
g_signal_connect (selection, "changed",
G_CALLBACK (on_settings_selection_changed), remove);
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start (GTK_BOX (box),
gtk_label_new (_("Here you can configure the default dictionaries.")),
FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), remove, FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), add, FALSE, FALSE, 0);
GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
g_object_set (content_area, "margin", 12, NULL);
gtk_box_pack_start (GTK_BOX (content_area), box, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (content_area), scrolled, TRUE, TRUE, 12);
gtk_widget_show_all (dialog);
switch (gtk_dialog_run (GTK_DIALOG (dialog)))
{
case GTK_RESPONSE_NONE:
break;
case GTK_RESPONSE_ACCEPT:
settings_save (&sd);
// Fall through
default:
gtk_widget_destroy (dialog);
}
g_key_file_free (sd.key_file);
}
// --- Main --------------------------------------------------------------------
static void
on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
gtk_main_quit ();
}
static void
die_with_dialog (const gchar *message)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
exit (EXIT_FAILURE);
}
int
gui_main (char *argv[])
{
// Just like with GtkApplication, argv has been parsed by the option group.
gtk_init (NULL, NULL);
gtk_window_set_default_icon_name (PROJECT_NAME);
GError *error = NULL;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (argv[0])
load_from_filenames (new_dictionaries, argv);
else if (!load_from_config (new_dictionaries, &error) && error)
die_with_dialog (error->message);
if (!new_dictionaries->len)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
_("No dictionaries found either in "
"the configuration or on the command line"));
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
// This is better than nothing.
// Our GtkNotebook action widget would be invisible without any tabs.
on_settings (NULL, NULL);
exit (EXIT_SUCCESS);
}
// Some Adwaita stupidity, plus defaults for our own widget.
// All the named colours have been there since GNOME 3.4
// (see gnome-extra-themes git history, Adwaita used to live there).
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
// `gsettings set org.gnome.desktop.interface gtk-key-theme "Emacs"`
// isn't quite what I want, and note that ^U works by default
"@binding-set Readline {"
"bind '<Control>H' { 'delete-from-cursor' (chars, -1) };"
"bind '<Control>W' { 'delete-from-cursor' (word-ends, -1) }; }"
"entry { -gtk-key-bindings: Readline; border-radius: 0; }"
"stardict-view { padding: 0 .25em; }"
"stardict-view.odd {"
"background: @theme_base_color; "
"color: @theme_text_color; }"
"stardict-view.odd:backdrop {"
"background: @theme_unfocused_base_color; "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view.even {"
"background: mix(@theme_base_color, @theme_text_color, 0.03); "
"color: @theme_text_color; }"
"stardict-view.even:backdrop {"
"background: mix(@theme_unfocused_base_color, "
"@theme_fg_color, 0.03); "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view:selected {"
"background-color: @theme_selected_bg_color; "
"color: @theme_selected_fg_color; }";
GdkScreen *screen = gdk_screen_get_default ();
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GtkWidget *item_open = gtk_menu_item_new_with_mnemonic (_("_Open..."));
g_signal_connect (item_open, "activate", G_CALLBACK (on_open), NULL);
GtkWidget *item_settings = gtk_menu_item_new_with_mnemonic (_("_Settings"));
g_signal_connect (item_settings, "activate",
G_CALLBACK (on_settings), NULL);
g.watch_selection = TRUE;
GtkWidget *item_selection =
gtk_check_menu_item_new_with_mnemonic (_("_Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item_selection), g.watch_selection);
g_signal_connect (item_selection, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_widget_set_halign (menu, GTK_ALIGN_END);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_open);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_settings);
#ifndef G_OS_WIN32
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_selection);
#endif // ! G_OS_WIN32
gtk_widget_show_all (menu);
g.hamburger = gtk_menu_button_new ();
gtk_button_set_relief (GTK_BUTTON (g.hamburger), GTK_RELIEF_NONE);
gtk_button_set_image (GTK_BUTTON (g.hamburger), gtk_image_new_from_icon_name
("open-menu-symbolic", GTK_ICON_SIZE_BUTTON));
gtk_menu_button_set_popup (GTK_MENU_BUTTON (g.hamburger), menu);
gtk_widget_show (g.hamburger);
g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL);
gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), g.hamburger, GTK_PACK_END);
g.entry = gtk_search_entry_new ();
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view);
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (g.window), PROJECT_NAME);
gtk_window_set_default_size (GTK_WINDOW (g.window), 300, 600);
g_signal_connect (g.window, "destroy",
G_CALLBACK (on_destroy), NULL);
g_signal_connect (g.window, "key-press-event",
G_CALLBACK (on_key_press), NULL);
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
gtk_container_add (GTK_CONTAINER (g.window), superbox);
gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
gtk_container_add (GTK_CONTAINER (superbox), g.entry);
gtk_container_add (GTK_CONTAINER (superbox),
gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
g.view = stardict_view_new ();
gtk_box_pack_end (GTK_BOX (superbox), g.view, TRUE, TRUE, 0);
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
gtk_drag_dest_set (g.view,
GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
gtk_drag_dest_add_uri_targets (g.view);
g_signal_connect (g.view, "drag-data-received",
G_CALLBACK (on_drag_data_received), NULL);
g_signal_connect (g.view, "send",
G_CALLBACK (on_send), NULL);
if (!reload_dictionaries (new_dictionaries, &error))
die_with_dialog (error->message);
gtk_widget_show_all (g.window);
gtk_main ();
return 0;
}

View File

@@ -6,7 +6,7 @@
* finalised with an empty line. Newlines are escaped with `\n',
* backslashes with `\\'.
*
* So far only the `m', `g`, and `x` fields are supported, as in sdtui.
* So far only the `m', `g`, and `x` fields are supported, as in tdv.
*
* Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
*

View File

@@ -218,6 +218,6 @@ main (int argc, char *argv[])
fatal ("Error: failed to write the dictionary: %s\n", error->message);
generator_free (generator);
fclose (fsorted);
pclose (fsorted);
return 0;
}

View File

@@ -3,7 +3,7 @@
*
* The external filter needs to process NUL-separated textual entries.
*
* Example: transform input.ifo output -- perl -p0e s/bullshit/soykaf/g
* Example: tdv-transform input.ifo output -- perl -p0e s/bullshit/soykaf/g
*
* Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
*

View File

@@ -1,7 +1,7 @@
/*
* StarDict terminal UI
*
* Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2013 - 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -18,16 +18,15 @@
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <stdarg.h>
#include <limits.h>
#include <string.h>
#include <glib.h>
#include <glib-unix.h>
#include <glib/gi18n.h>
#include <gio/gio.h>
#include <pango/pango.h>
#include <glib/gi18n.h>
#include <unistd.h>
#include <poll.h>
@@ -37,9 +36,7 @@
#include <termo.h> // input
#include <ncurses.h> // output
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
#include "config.h"
#include "stardict.h"
@@ -67,7 +64,7 @@ unichar_width (gunichar ch)
void
update_curses_terminal_size (void)
{
#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
struct winsize size;
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
{
@@ -154,20 +151,24 @@ struct application
GPtrArray * dictionaries; ///< All loaded AppDictionaries
StardictDict * dict; ///< The current dictionary
StardictDict * last; ///< The last dictionary
guint show_help : 1; ///< Whether help can be shown
guint center_search : 1; ///< Whether to center the search
guint underline_last : 1; ///< Underline the last definition
guint hl_prefix : 1; ///< Highlight the common prefix
guint watch_x11_sel : 1; ///< Requested X11 selection watcher
guint dict_offset; ///< Scroll position of the tab bar
guint32 top_position; ///< Index of the topmost dict. entry
guint top_offset; ///< Offset into the top entry
guint selected; ///< Offset to the selected definition
GPtrArray * entries; ///< ViewEntry-s within the view
gchar * search_label; ///< Text of the "Search" label
gsize search_label_width; ///< Visible width of "search_label"
GArray * input; ///< The current search input
guint input_pos; ///< Cursor position within input
guint input_offset; ///< Render offset in codepoints
gboolean input_confirmed; ///< Input has been confirmed
gfloat division; ///< Position of the division column
@@ -205,6 +206,10 @@ app_char_width (Application *app, gunichar c)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#if !GLIB_CHECK_VERSION(2, 68, 0)
#define g_memdup2 g_memdup
#endif
/// Splits the entry and adds it to a pointer array.
static void
view_entry_split_add (ViewEntry *ve, const gchar *text, const chtype *attrs)
@@ -485,6 +490,17 @@ app_init_attrs (Application *self)
#undef XX
}
static gsize
app_utf8_width (Application *self, const char *utf8)
{
gsize width = 0;
gunichar *ucs4 = g_utf8_to_ucs4_fast (utf8, -1, NULL);
for (gunichar *it = ucs4; *it; it++)
width += app_char_width (self, *it);
g_free (ucs4);
return width;
}
static gboolean
app_load_dictionaries (Application *self, GError **e)
{
@@ -499,11 +515,7 @@ app_load_dictionaries (Application *self, GError **e)
gchar *tmp = g_strdup_printf (" %s ", dict->super.name);
g_free (dict->super.name);
dict->super.name = tmp;
gunichar *ucs4 = g_utf8_to_ucs4_fast (dict->super.name, -1, NULL);
for (gunichar *it = ucs4; *it; it++)
dict->name_width += app_char_width (self, *it);
g_free (ucs4);
dict->name_width = app_utf8_width (self, dict->super.name);
}
return TRUE;
}
@@ -519,6 +531,15 @@ app_init (Application *self, char **filenames)
self->tk = NULL;
self->tk_timer = 0;
const char *charset = NULL;
self->locale_is_utf8 = g_get_charset (&charset);
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
self->ucs4_to_locale = g_iconv_open (charset, "UTF-32LE");
#else // G_BYTE_ORDER != G_LITTLE_ENDIAN
self->ucs4_to_locale = g_iconv_open (charset, "UTF-32BE");
#endif // G_BYTE_ORDER != G_LITTLE_ENDIAN
self->focused = TRUE;
self->show_help = TRUE;
self->center_search = TRUE;
self->underline_last = TRUE;
@@ -532,22 +553,14 @@ app_init (Application *self, char **filenames)
((GDestroyNotify) view_entry_free);
self->search_label = g_strdup_printf ("%s: ", _("Search"));
self->search_label_width = app_utf8_width (self, self->search_label);
self->input = g_array_new (TRUE, FALSE, sizeof (gunichar));
self->input_pos = 0;
self->input_pos = self->input_offset = 0;
self->input_confirmed = FALSE;
self->division = 0.5;
const char *charset;
self->locale_is_utf8 = g_get_charset (&charset);
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
self->ucs4_to_locale = g_iconv_open (charset, "UTF-32LE");
#else // G_BYTE_ORDER != G_LITTLE_ENDIAN
self->ucs4_to_locale = g_iconv_open (charset, "UTF-32BE");
#endif // G_BYTE_ORDER != G_LITTLE_ENDIAN
self->focused = TRUE;
app_init_attrs (self);
self->dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
@@ -710,7 +723,7 @@ row_buffer_append_length (RowBuffer *self,
// XXX: this is very crude as it disrespects combining marks
gunichar c =
app_is_character_in_locale (self->app, ucs4[i]) ? ucs4[i] : '?';
struct row_char rc = { c, attrs, unichar_width (c) };
RowChar rc = { c, attrs, unichar_width (c) };
g_array_append_val (self->chars, rc);
self->total_width += rc.width;
}
@@ -796,6 +809,20 @@ row_buffer_ellipsis (RowBuffer *self, int target, chtype attrs)
}
}
static void
row_buffer_align (RowBuffer *self, int target, chtype attrs)
{
if (target >= 0 && self->total_width > target)
row_buffer_ellipsis (self, target, attrs);
while (self->total_width < target)
{
struct row_char rc = { ' ', attrs, 1 };
g_array_append_val (self->chars, rc);
self->total_width += rc.width;
}
}
static void
row_buffer_print (RowBuffer *self, gunichar *ucs4, size_t len, chtype attrs)
{
@@ -838,21 +865,82 @@ row_buffer_flush (RowBuffer *self)
static void
row_buffer_finish (RowBuffer *self, int width, chtype attrs)
{
if (width >= 0 && self->total_width > width)
row_buffer_ellipsis (self, width, attrs);
while (self->total_width < width)
{
struct row_char rc = { ' ', attrs, 1 };
g_array_append_val (self->chars, rc);
self->total_width += rc.width;
}
row_buffer_align (self, width, attrs);
row_buffer_flush (self);
row_buffer_free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gint
app_input_width (Application *self, guint begin, guint end)
{
gint width = 0;
for (guint i = begin; i < end; i++)
width += app_char_width (self,
g_array_index (self->input, gunichar, i));
return width;
}
static guint
app_scroll_back_input (Application *self, guint from, gint target)
{
guint last_spacing = from;
while (from--)
{
gint width = app_input_width (self, from, from + 1);
if (target < width)
break;
if (width)
{
last_spacing = from;
target -= width;
}
}
return last_spacing;
}
static guint
app_adjust_input_offset (Application *self, gint space)
{
gint to_cursor =
app_input_width (self, 0, self->input_pos);
gint at_cursor =
app_input_width (self, self->input_pos, self->input_pos + 1);
gint past_cursor =
app_input_width (self, self->input_pos + 1, self->input->len);
// 1. If everything fits, no scrolling is desired, and no arrows present
if (to_cursor + at_cursor + past_cursor <= space)
return 0;
// TODO: try to prevent 2. and 3. from fighting with each other
// 2. If everything up to and including the cursor, plus right arrow fits,
// start at the beginning
if (to_cursor + at_cursor + 1 /* right arrow */ <= space)
return 0;
// 3. If everything from the cursor to the right fits, fill the line,
// but keep one extra space for a trailing caret
gint reserved = self->input_pos != self->input->len;
gint from_cursor_with_trailing_caret = at_cursor + past_cursor + reserved;
if (1 /* left arrow */ + from_cursor_with_trailing_caret <= space)
return app_scroll_back_input (self, self->input->len, space - 1 - 1);
// At this point, we know there will be arrows on both sides
space -= 2;
// 4. If the cursor has moved too much to either side, follow it
if (self->input_pos < self->input_offset
|| app_input_width (self, self->input_offset, self->input_pos + 1) > space)
return app_scroll_back_input (self, self->input_pos, space / 2);
// 5. Otherwise, don't fiddle with the offset at all, it's not necessary
return self->input_offset;
}
/// Render the top bar.
static void
app_redraw_top (Application *self)
@@ -860,38 +948,69 @@ app_redraw_top (Application *self)
RowBuffer buf = row_buffer_make (self);
row_buffer_append (&buf, APP_TITLE, APP_ATTR (HEADER) | A_BOLD);
// Trivially ensure the current dictionary's tab is visible.
int available = COLS - buf.total_width, width = 0;
self->dict_offset = 0;
for (guint i = 0; i < self->dictionaries->len; i++)
{
Dictionary *dict = g_ptr_array_index (self->dictionaries, i);
row_buffer_append (&buf, dict->name,
AppDictionary *dict = g_ptr_array_index (self->dictionaries, i);
width += dict->name_width;
if (self->dict != dict->super.dict)
continue;
gboolean is_last = i + 1 == self->dictionaries->len;
while (self->dict_offset < i
&& (available < width || (available == width && !is_last)))
{
dict = g_ptr_array_index (self->dictionaries, self->dict_offset++);
width -= dict->name_width;
}
break;
}
for (guint i = self->dict_offset; i < self->dictionaries->len; i++)
{
AppDictionary *dict = g_ptr_array_index (self->dictionaries, i);
row_buffer_append (&buf, dict->super.name,
APP_ATTR_IF (self->dictionaries->len > 1
&& self->dict == dict->dict, ACTIVE, HEADER));
&& self->dict == dict->super.dict, ACTIVE, HEADER));
}
move (0, 0);
row_buffer_finish (&buf, COLS, APP_ATTR (HEADER));
buf = row_buffer_make (self);
row_buffer_append (&buf, self->search_label, APP_ATTR (SEARCH));
gsize indent = buf.total_width;
gint indent = buf.total_width;
int word_attrs = APP_ATTR (SEARCH);
if (self->input_confirmed)
word_attrs |= A_BOLD;
gchar *input_utf8 = g_ucs4_to_utf8
((gunichar *) self->input->data, -1, NULL, NULL, NULL);
self->input_offset = app_adjust_input_offset (self, COLS - indent);
if (self->input_offset)
{
row_buffer_append (&buf, "<", word_attrs ^ A_BOLD);
indent++;
}
gchar *input_utf8 = g_ucs4_to_utf8 ((gunichar *) self->input->data
+ self->input_offset, -1, NULL, NULL, NULL);
g_return_if_fail (input_utf8 != NULL);
row_buffer_append (&buf, input_utf8, word_attrs);
g_free (input_utf8);
gint overflow = buf.total_width - COLS;
if (overflow > 0)
{
row_buffer_pop_cells (&buf, overflow + 1 /* right arrow */);
row_buffer_align (&buf, COLS - 1 /* right arrow */, APP_ATTR (SEARCH));
row_buffer_append (&buf, ">", word_attrs ^ A_BOLD);
}
row_buffer_finish (&buf, COLS, APP_ATTR (SEARCH));
gint offset = app_input_width (self, self->input_offset, self->input_pos);
guint offset, i;
for (offset = i = 0; i < self->input_pos; i++)
offset += app_char_width (self,
g_array_index (self->input, gunichar, i));
move (1, MIN ((gint) (indent + offset), COLS - 1));
move (1, MIN (indent + offset, COLS - 1));
refresh ();
}
@@ -947,7 +1066,7 @@ app_show_help (Application *self)
{
PROJECT_NAME " " PROJECT_VERSION,
_("Terminal UI for StarDict dictionaries"),
"Copyright (c) 2013 - 2021, Přemysl Eric Janouch",
"Copyright (c) 2013 - 2022, Přemysl Eric Janouch",
"",
_("Type to search")
};
@@ -1271,7 +1390,7 @@ app_set_input (Application *self, const gchar *text, gsize text_len)
g_array_free (self->input, TRUE);
self->input = g_array_new (TRUE, FALSE, sizeof (gunichar));
self->input_pos = 0;
self->input_pos = self->input_offset = 0;
gunichar *p = output;
gboolean last_was_space = false;
@@ -1330,6 +1449,18 @@ app_get_current_definition (Application *self)
return NULL;
}
static void
app_goto_dictionary_directly (Application *self, StardictDict *dict)
{
if (dict == self->dict)
return;
self->last = self->dict;
self->dict = dict;
app_search_for_entry (self);
app_redraw_top (self);
}
/// Switch to a different dictionary by number.
static gboolean
app_goto_dictionary (Application *self, guint n)
@@ -1338,9 +1469,7 @@ app_goto_dictionary (Application *self, guint n)
return FALSE;
Dictionary *dict = g_ptr_array_index (self->dictionaries, n);
self->dict = dict->dict;
app_search_for_entry (self);
app_redraw_top (self);
app_goto_dictionary_directly (self, dict->dict);
return TRUE;
}
@@ -1403,6 +1532,7 @@ enum user_action
USER_ACTION_GOTO_PAGE_NEXT,
USER_ACTION_GOTO_DICTIONARY_PREVIOUS,
USER_ACTION_GOTO_DICTIONARY_NEXT,
USER_ACTION_GOTO_DICTIONARY_LAST,
USER_ACTION_FLIP,
@@ -1505,6 +1635,12 @@ app_process_user_action (Application *self, UserAction action)
if (!app_goto_dictionary_delta (self, +1))
beep ();
return TRUE;
case USER_ACTION_GOTO_DICTIONARY_LAST:
if (!self->last)
beep ();
else
app_goto_dictionary_directly (self, self->last);
return TRUE;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1678,6 +1814,7 @@ app_process_keysym (Application *self, termo_key_t *event)
{
[TERMO_SYM_LEFT] = USER_ACTION_MOVE_SPLITTER_LEFT,
[TERMO_SYM_RIGHT] = USER_ACTION_MOVE_SPLITTER_RIGHT,
[TERMO_SYM_TAB] = USER_ACTION_GOTO_DICTIONARY_LAST,
};
static ActionMap actions_ctrl =
{
@@ -1765,7 +1902,7 @@ app_process_key (Application *self, termo_key_t *event)
{
if (self->input->len != 0)
g_array_remove_range (self->input, 0, self->input->len);
self->input_pos = 0;
self->input_pos = self->input_offset = 0;
self->input_confirmed = FALSE;
}
@@ -1785,7 +1922,7 @@ app_process_left_mouse_click (Application *self, int line, int column)
if (column < indent)
return;
for (guint i = 0; i < self->dictionaries->len; i++)
for (guint i = self->dict_offset; i < self->dictionaries->len; i++)
{
AppDictionary *dict = g_ptr_array_index (self->dictionaries, i);
if (column < (indent += dict->name_width))
@@ -1797,17 +1934,21 @@ app_process_left_mouse_click (Application *self, int line, int column)
}
else if (line == 1)
{
// FIXME: this is only an approximation
gsize label_len = g_utf8_strlen (self->search_label, -1);
gint pos = column - label_len;
gint pos = column - self->search_label_width;
if (pos >= 0)
{
guint offset, i;
for (offset = i = 0; i < self->input->len; i++)
// On clicking the left arrow, go to that invisible character
// behind the arrow (skiping over non-spacing suffixes)
guint i = self->input_offset;
if (i && !pos--)
{
size_t width = app_char_width
(self, g_array_index (self->input, gunichar, i));
if ((offset += width) > (guint) pos)
while (i-- && !app_input_width (self, i, i + 1))
;
}
for (gint occupied = 0; i < self->input->len; i++)
{
size_t width = app_input_width (self, i, i + 1);
if ((occupied += width) > pos)
break;
}
@@ -2296,53 +2437,10 @@ log_handler (const gchar *domain, GLogLevelFlags level,
}
int
main (int argc, char *argv[])
tui_main (char *argv[])
{
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
if (glib_check_version (2, 36, 0))
g_type_init ();
G_GNUC_END_IGNORE_DEPRECATIONS
gboolean show_version = FALSE;
GOptionEntry entries[] =
{
{ "version", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &show_version,
N_("Output version information and exit"), NULL },
{ NULL }
};
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
GError *error = NULL;
GOptionContext *ctx = g_option_context_new
(N_("[dictionary.ifo...] - StarDict terminal UI"));
GOptionGroup *group = g_option_group_new ("", "", "", NULL, NULL);
g_option_group_add_entries (group, entries);
g_option_group_set_translation_domain (group, GETTEXT_PACKAGE);
g_option_context_add_group (ctx, group);
g_option_context_set_translation_domain (ctx, GETTEXT_PACKAGE);
if (!g_option_context_parse (ctx, &argc, &argv, &error))
{
g_printerr ("%s: %s: %s\n", _("Error"), _("option parsing failed"),
error->message);
exit (EXIT_FAILURE);
}
g_option_context_free (ctx);
if (show_version)
{
g_print (PROJECT_NAME " " PROJECT_VERSION "\n");
exit (EXIT_SUCCESS);
}
Application app;
app_init (&app, argv + 1);
app_init (&app, argv);
app_init_terminal (&app);
app_redraw (&app);
@@ -2386,4 +2484,3 @@ G_GNUC_END_IGNORE_DEPRECATIONS
return 0;
}

98
src/tdv.c Normal file
View File

@@ -0,0 +1,98 @@
/*
* Translation dictionary viewer
*
* Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include "config.h"
#include <glib.h>
#include <glib/gi18n.h>
#ifdef WITH_GUI
#include <gtk/gtk.h>
#endif
#include <locale.h>
#ifndef G_OS_WIN32
#include <unistd.h>
#endif
int tui_main (char *[]);
int gui_main (char *[]);
int
main (int argc, char *argv[])
{
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
gboolean show_version = FALSE;
#ifdef WITH_GUI
# ifndef G_OS_WIN32
gboolean gui = FALSE;
# endif
#endif
GOptionEntry entries[] =
{
{ "version", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &show_version,
N_("Output version information and exit"), NULL },
#ifdef WITH_GUI
# ifndef G_OS_WIN32
{ "gui", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &gui,
N_("Launch the GUI even when run from a terminal"), NULL },
# endif
#endif
{ },
};
GOptionContext *ctx = g_option_context_new
(N_("[dictionary.ifo...] - Translation dictionary viewer"));
g_option_context_add_main_entries (ctx, entries, GETTEXT_PACKAGE);
#ifdef WITH_GUI
g_option_context_add_group (ctx, gtk_get_option_group (FALSE));
#endif
g_option_context_set_translation_domain (ctx, GETTEXT_PACKAGE);
GError *error = NULL;
if (!g_option_context_parse (ctx, &argc, &argv, &error))
{
g_printerr ("%s: %s: %s\n", _("Error"), _("option parsing failed"),
error->message);
exit (EXIT_FAILURE);
}
g_option_context_free (ctx);
if (show_version)
{
g_print (PROJECT_NAME " " PROJECT_VERSION "\n");
exit (EXIT_SUCCESS);
}
#ifdef WITH_GUI
# ifndef G_OS_WIN32
if (gui || !isatty (STDIN_FILENO))
# endif
return gui_main (argv + 1);
#endif
#ifndef G_OS_WIN32
return tui_main (argv + 1);
#endif
}

View File

@@ -16,6 +16,11 @@
*
*/
// getpwnam_r, _SC_GETPW_R_SIZE_MAX
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#include <glib.h>
#include <glib/gprintf.h>
#include <gio/gio.h>
@@ -217,7 +222,7 @@ load_project_config_file (GError **error)
// which is completely undocumented
g_key_file_load_from_dirs (key_file,
PROJECT_NAME G_DIR_SEPARATOR_S PROJECT_NAME ".conf",
paths, NULL, 0, &e);
paths, NULL, G_KEY_FILE_KEEP_COMMENTS, &e);
g_free (paths);
if (!e)
return key_file;
@@ -231,6 +236,25 @@ load_project_config_file (GError **error)
return NULL;
}
gboolean
save_project_config_file (GKeyFile *key_file, GError **error)
{
gchar *dirname =
g_build_filename (g_get_user_config_dir (), PROJECT_NAME, NULL);
(void) g_mkdir_with_parents (dirname, 0755);
gchar *path = g_build_filename (dirname, PROJECT_NAME ".conf", NULL);
g_free (dirname);
gsize length = 0;
gchar *data = g_key_file_to_data (key_file, &length, error);
if (!data)
return FALSE;
gboolean result = g_file_set_contents (path, data, length, error);
g_free (data);
return result;
}
// --- Loading -----------------------------------------------------------------
void
@@ -269,7 +293,6 @@ load_dictionaries_sequentially (GPtrArray *dictionaries, GError **e)
}
// Parallelize dictionary loading if possible, because of collation reindexing
#if GLIB_CHECK_VERSION (2, 36, 0)
static void
load_worker (gpointer data, gpointer user_data)
{
@@ -304,10 +327,3 @@ load_dictionaries (GPtrArray *dictionaries, GError **e)
g_async_queue_unref (error_queue);
return result;
}
#else // GLib < 2.36
gboolean
load_dictionaries (GPtrArray *dictionaries, GError **e)
{
return load_dictionaries_sequentially (dictionaries, e);
}
#endif // GLib < 2.36

View File

@@ -54,6 +54,7 @@ gchar *resolve_relative_config_filename (const gchar *filename);
gchar *resolve_filename
(const gchar *filename, gchar *(*relative_cb) (const char *));
GKeyFile *load_project_config_file (GError **error);
gboolean save_project_config_file (GKeyFile *key_file, GError **error);
// --- Loading -----------------------------------------------------------------

View File

@@ -1,8 +1,9 @@
[Desktop Entry]
Type=Application
Name=sdgui
GenericName=StarDict GUI
Exec=sdgui %F
Name=tdv
GenericName=Translation dictionary viewer
Icon=tdv
Exec=tdv %F
StartupNotify=true
MimeType=application/x-stardict-ifo;
Categories=Office;Dictionary;GTK;

BIN
tdv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

51
tdv.svg Normal file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
color-interpolation="linearRGB"
color-interpolation-filters="linearRGB">
<defs>
<rect id="text" width="13" height="3" fill="#d0d0d0" />
<filter id="shadow" x="-25%" y="-25%" width="150%" height="150%">
<feFlood flood-color="#000000" flood-opacity=".5" result="flood" />
<feComposite in="SourceGraphic" in2="flood" operator="in" />
<feGaussianBlur stdDeviation="1.25" />
<feOffset dx="1" dy="1" result="offset" />
<feComposite in="SourceGraphic" in2="offset" />
</filter>
<mask id="hole">
<rect x="-25%" y="-25%" width="150%" height="150%" fill="#ffffff" />
<circle r="10.5" />
</mask>
</defs>
<rect x="5" y="1" width="38" height="46" ry="2"
fill="#ffffff" stroke="#606060" stroke-width="1.25" />
<use xlink:href="#text" x="9" y="5" />
<use xlink:href="#text" x="9" y="10" />
<use xlink:href="#text" x="9" y="15" />
<use xlink:href="#text" x="9" y="20" />
<use xlink:href="#text" x="9" y="25" />
<use xlink:href="#text" x="9" y="30" />
<use xlink:href="#text" x="9" y="35" />
<use xlink:href="#text" x="9" y="40" />
<use xlink:href="#text" x="26" y="5" />
<use xlink:href="#text" x="26" y="10" />
<use xlink:href="#text" x="26" y="15" />
<use xlink:href="#text" x="26" y="20" />
<use xlink:href="#text" x="26" y="25" />
<use xlink:href="#text" x="26" y="30" />
<use xlink:href="#text" x="26" y="35" />
<use xlink:href="#text" x="26" y="40" />
<circle cx="21" cy="19" r="9" fill="#ffffff" fill-opacity=".5"
stroke-width="2" stroke="#000000" filter="url(#shadow)" />
<g filter="url(#shadow)">
<rect x="-1.75" y="0" width="3.5" height="22.5"
transform="translate(21 19) rotate(-30)" mask="url(#hole)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

2
termo

Submodule termo updated: 94a77a10d8...f9a102456f